引言
随着数字化进程的持续深化,可观测性一直是近年来非常火热的话题,在可观测的三大支柱 Log/Trace/Metric 中,日志(Log)数据一般是最为常见的,企业迈向可观测性的第一步,也往往始于日志数据的采集上云。日志完成收集后,最直接的需求就是从海量日志数据中检索分析出有价值的信息。随着日志数据量的不断增长,数据种类不断增多并日益朝着非结构化、多场景、多模态等方向演进,传统的日志搜索方式已经越来越难以满足不同场景下多样化、个性化的分析需求。
日志数据的查询分析需求是多样化的
日志(Log)数据作为可观测场景中最基础的数据类型之一,具备以下特点 :
- 不可变:日志数据一旦产生就不会被再次修改,是对事件原始信息的忠实记录,往往结构不太固定。
- 数据随机:比如异常事件日志、用户行为日志,一般天然就是随机的、难以预测的。
- 来源多样:日志数据种类繁多,不同来源的数据难以具有统一的 Schema。
- 业务复杂:不同的业务参与方对数据的理解不同,写日志过程中难以预见到后期具体的分析需求。
这些因素导致日志数据在采集过程中往往并不存在一个理想的数据模型可以用来预处理日志数据,因此更常见的做法是直接采集存储原始的日志数据,这可以称为是一种 Schema-on-Read 的模式,或者是所谓的寿司原则(The Sushi Principle:Raw data is better than cooked, since you can cook it in as many different ways as you like)。
这种直接存储原始数据的做法,意味着在分析的时候往往需要动态实时的对数据进行处理(比如 json 处理、正则提取、数学计算等等);而且由于不同分析人员对数据特征缺乏先验知识,一般也需要对数据先进行一定的探索式分析。
也就是说,在日志查询分析的过程,既需要能够展现非结构化的文档结构,又需要丰富的算子进行实时处理,同时最好还能够便捷的支持级联式、探索式的分析方式。
理想的日志查询语法应该是什么样的
日志数据分析通常可以分为两大类场景:
一类是查询类场景,或者说是搜索场景、纯过滤场景,即按照特定的条件过滤掉不需要的日志,并针对符合条件的日志直接输出日志原文。
一类是分析类场景,主要包括聚合分析(比如 sum、sort)、关联分析(比如多个表 join),需要对数据进行更复杂的计算,输出结果一般是表格模式。
这里我们重点关注纯查询过滤的场景,在 SLS 中既可以用传统的搜索语法(如 Key:XXX ),也可以在标准 SQL 中使用 where 语句(如 * | select * where Key like '%XXX%'),两种方式各有优点,却也都有着各自的局限性。
对于查询语法来说,天然就是为过滤搜索场景而生的,但是可惜表达能力有限,只能支持关键词匹配,以及多个条件的 And/Not/Or 的逻辑组合,无法支持更为复杂的处理逻辑。
而对于 SQL 语法来说,优点是表达能力强,但 SQL 是表格模型,不便于查看原始日志结果(因为要将字段对齐,输出结果中对于不存在的列就会填充大量 null),而且对于 select * 这样的语句,也只能输出开启字段索引的字段。
详细对比如下:
纯查询场景的挑战 | 搜索查询语法 | 标准SQL语法 |
需要复杂的处理逻辑 | 弱,主要就是支持关键词匹配 | 强,具备丰富的处理函数和算子,如正则匹配、json提取 |
输出内容是非结构化的 | 强,输出的是原文,便于查看 | 弱,输出的是表格模式,不存在的字段全部要补空值,不利于查看 |
翻页逻辑 | 简单,控制台可以直接点,API传递offset+lines即可 | 较复杂,需要在SQL中通过limit x,y的方式,并且要指定排序方式 |
查看结果的时间分布 | 简单,histogram柱状图直观展示出不同时间的分布 | 较复杂,需要在SQL中按照时间分组求和,再按时间排序,然后再画线图查看 |
结果中输出所有原文字段 | 输出的是原文,天然包含所有字段 | 较麻烦,select * 只能输出建了字段索引的列 |
获取部分字段 | 不支持 | select指定列即可 |
计算出新的列 | 不支持 | select中可以计算新的列 |
多级级联处理能力 | 无法表达 | 可以通过with语句、SQL嵌套,但写起来较为复杂 |
既然两种方式各有所长,那么我们是否可以结合这两种方式的优点,支持一种新的查询语法,既能遵从文档模型(直接输出日志原文、不按表格模式、不要求所有输出列有索引),又能支持各种好用的 SQL 算子,同时还能够支持一种更便捷的级联处理(而不需要复杂的多层嵌套)呢?
SPL 管道式查询语言
SPL 基本语法如下:
<data-source> | <spl-expr> ... | <spl-expr> ...
其中 <data-source> 是数据源,对于日志查询的场景,指的就是索引查询语句。<spl-expr> 是 SPL 指令,支持正则取值、字段分裂、字段投影、数值计算等多种丰富的操作,具体参考 SPL 指令介绍[2]。
从语法定义上可以看到,SPL 是支持多个 SPL 指令组成管道级联的。对于日志查询的场景来说,在索引查询语句之后,可以根据需要通过管道符不断追加 SPL 指令,从而获得类似 Unix 管道处理文本数据的体验,对日志进行灵活的探索式分析。
SPL 能做什么?
筛选字段获得更精确的视图
在查询日志的时候,往往是带着某个目的去检索,这个时候一般是只关心其中的部分字段。这时就可以使用 SPL 中的 project 指令,只保留自己关心的字段。(或者使用 project-away 指令,移除不需要看到的字段)
实时计算出新的字段
使用 Extend 指令,可以基于已有字段加工提取出新的字段,可以使用丰富的函数(这些大部分是和 SQL 语法通用的)进行标量处理。
Status:200 | extend urlParam=split_part(Uri, '/', 3)
同时也可以根据多个字段计算出新的字段,比如计算两个数字字段的差值。(注意字段默认是被视为 varchar,进行数字类型计算的时候要先通过 cast 转换类型)
Status:200 | extend timeRange = cast(BeginTime as bigint) - cast(EndTime as bigint)
并且也可以在后续管道中,再对这个计算后的值进行 where 判断过滤:
Status:200
| where UserAgent like '%Chrome%'
| extend timeRange = cast(BeginTime as bigint) - cast(EndTime as bigint)
| where timeRange > 86400
自由的展开半结构化数据
SPL 提供了 parse-json、parse-csv 这样的指令,可以将 json、csv 类型的字段,直接完全展开出为独立的字段,之后就可以直接对这些字段进行操作。省去了书写字段提取函数的开销,在交互式查询场景中这种写法是更为便捷的。
SPL 之前已经在扫描查询模式上全地域支持,详见扫描查询[3]。扫描查询可以不依赖索引,直接扫描原始日志数据计算。下图中这个例子,就是在原始日志数据上,通过 SPL 管道完成了模糊过滤、json 展开、字段提取等多种操作。
当前扫描模式 SPL 难以处理大规模数据
扫描模式具备很好的灵活性,但最大的问题是性能不足,特别是面对大规模数据时难以在有限时间内处理完。现有的扫描查询限制单次最多扫描 10 万行,超出限制后需要控制台手动点击触发下一次扫描(或者 SDK 触发下一次调用)。
由于性能受限,导致现有的 SPL 查询在使用上存在以下问题:
- 对于过滤结果较为稀疏的查询,由于单次扫描的原始数据量太少,很难在有限时间内扫描到结果。
- 查询界面的直方图展示的是索引过滤后的结果(以及扫描进度),而无法展示出 SPL 条件过滤后的最终结果分布。
- 无法支持针对最终过滤后的结果随机翻页,只能按照已经扫描的原文的 offset 进行连续翻页扫描。
这些约束,导致扫描模式下的 SPL,面对具备较大规模的日志数据,使用体验较差,也就很难发挥出实际用处。
极致优化,高性能 SPL 模式
计算下推,并行化加速
首先要在架构上解决水平扩展的问题。原有的架构下,因为存储节点不具备复杂表达式的计算能力,只能将原始数据全量拉取到计算节点处理,大数据量的读取、传输、序列化是很大的瓶颈。
在查询场景下,实际单次请求每次需要的最终结果行数是比较少的(一般单次请求 100 行以内,超出后通过翻页请求获取),关键在于当 SPL 语句中包含 where 条件的时候,就存在从大量数据中计算 where 条件过滤的过程。为了能够处理大规模数据并减少传输开销,我们就需要将 where 条件的计算下推到各个 shard 所在的存储节点上处理。相应的,也就必须要求存储节点具备对 SPL 中丰富算子的高效处理能力。
为此我们在存储节点上,引入 C++ 向量化计算引擎,在存储节点上读取了原始的数据后,直接原地就可以进行高效的过滤计算。只有对满足 where 条件的日志,才需要进行剩余的 SPL 计算并输出最终结果。
计算下推之后,整个的处理能力就可以随着 shard 数目水平扩展,同时也大幅减少了存储节点和计算节点之间的数据传输、网络序列化开销。
向量化计算,多级火箭加速
计算下推解决了按 shard 水平扩展的问题,接下来我们还要进一步的大幅提升每个 shard 上的处理能力。
扫描模式的 SPL,最大性能瓶颈还是在于直接扫描读取原始的行数据。这样读放大会比较严重,IO 效率很低。正如使用 SQL 分析能力时需要开启字段索引(并开启统计),这些字段的数据就可以被高效的读取和计算,那 SPL 同样也可以基于字段索引来进行高性能的数据 IO,然后再基于 SIMD 向量化技术进行高性能计算,同时在过程中尽可能减少额外计算量。
以图中的 SPL 为例,在下推到存储节点后,会经过“多级火箭”进行层层加速:
- 按照查询时间范围过滤(当数据量非常大时,建议选择必要的时间范围进行分析)。
- 处理第一级管道 Status:200 ,关键词索引条件过滤(这个是最快的,有索引过滤条件尽量写上过滤条件)。
- 处理 SPL 中的 where 过滤条件,基于字段索引(并开启统计),高效读取对应的数据。
- 向量化高性能计算,获得过滤结果,然后再计算剩余的 SPL 部分,得到最终结果
- 同时在计算过程中,如果发现过滤结果行数已经满足要求,则尽量提前终止(特别对于高命中率的情形,可以尽量减少不必要的计算)。
经过这些优化之后,高性能 SPL 的执行性能相比扫描模式,得到了质的飞跃。
高性能 SPL 的性能表现
我们以单个 shard 处理 1 亿行数据为例,来评估高性能 SPL 的性能表现。在线上真实环境创建一个 Logstore,10 个 shard,查询时间范围内有 10 亿数据。(服务访问日志数据)
选取如下几个典型的场景:
场景 1:通过字符串函数处理后过滤
SPL 语句:* | where split_part(Uri, '#', 2) = 'XXX'
场景 2:短语查询、模糊查询
SPL 语句:* | where Content like '%XXX%'
场景 3:json提取子字段,然后再过滤
SPL 语句: * | where json_extract_scalar(Params, 'Schema') = 'XXX'
在上述语句中选择不同的比较参数,构造出不同的命中率的场景(比如命中率 1%,指的是原始 10 亿条数据中,有 1000 万条满足 where 条件的结果数据),并请求前 20 条满足条件数据(对应 GetLogs 接口的 API 参数是 offset=0, lines=20),测试平均耗时。
命中率 | 场景1 耗时 | 场景2 耗时 | 场景3 耗时 |
1% | 52 ms | 73 ms | 89 ms |
0.1% | 65 ms | 94 ms | 126 ms |
0.01% | 160 ms | 206 ms | 586 ms |
0.001% | 1301 ms | 2185 ms | 3074 ms |
0.0001% | 2826 ms | 3963 ms | 6783 ms |
可以看出:
- 当命中率较高时,不同场景下都有很好的性能表现,甚至可以接近关键词索引查询。
- 当命中率很低时,由于要实时计算大量数据,需要更长一些的执行时间,具体实际性能表现和数据字段的长度、语句中算子复杂度、命中结果在原始数据的分布位置等因素都有关。
- 整体来看,高性能 SPL 对于数十亿级别的日志量级,可以在数秒内完成计算。
控制台交互升级,展示过滤后结果的直方图
高性能模式 SPL,由于计算性能有了大幅提升,因此控制台展示 histogram,直接展示的是整个 SPL 语句过滤后的结果分布。(意味着整个范围内的数据也进行了全量的计算)
举个例子,原始日志有 1000 万条,SPL 语句是 Status:200 | where Category like '%xx%',符合 Status:200 条件的日志是 10 万条,这其中再符合 where Category like '%xx%' 条件的日志是 1000 条,则查询界面上 histogram 柱状图展示的是这最终的 1000 条日志随时间的分布情况。
相应的,和纯索引查询模式下的交互完全相同,高性能模式 SPL 支持随机翻页,也支持点击柱状图直接跳转到对应区间的查询结果。
API 调用简化,统一的 offset 语义
在高性能 SPL 模式下,调用 GetLogs 通过 SPL 语句查询日志时,offset 直接表示的就是过滤后的结果偏移量,从而大大简化了 API 调用方式。也就是说,使用上,和纯索引查询完全统一。直接按照过滤后最终结果的 offset 来翻页即可。
使用说明
如何开启高性能 SPL?
无须显式指定运行模式。当 SPL 语句中所有参与 where 条件计算的列,全都已经创建了字段索引(并开启了统计),则自动按照高性能模式执行;否则以扫描模式执行。
是否计费?
高性能 SPL 模式,查询本身不产生任何额外费用。
🔔 注意:如果没有完全命中索引列导致走的是扫描模式 SPL(并且当前 Logstore 是按功能计费模式),则按照查询过程中的扫描原始日志数据量计费。
最佳实践
尽可能增加索引查询语句预过滤
如果有关键词索引过滤条件,尽可能使用,放在多级 SPL 管道的第一级。索引查询的效率总是最高的。
复杂过滤场景,建议使用 SPL 代替 SQL
特别是对于模糊匹配、短语匹配、正则匹配、json 提取以及更复杂的各种纯过滤场景,以前只能使用 SQL 语法(* | select * where XXX),现在建议替换为 SPL 语法(* | where XXX)。可以能更好的输出日志原文(而不是表格模式),更便捷的看到过滤后的结果柱状图分布,以及更简洁的输入体验。
更多功能,敬请期待
SPL 也能支持聚合操作
目前 SPL 仅支持纯查询过滤场景下的使用,接下来在日志查询场景下,SPL 语法会进一步支持排序、聚合等操作(聚合后按照表格模式输出),从而使得 SPL 的多级管道级联处理能力更强大、更完善,能够更好的对日志进行更灵活的查询分析。
总结
企业的日志数据上云后,从海量日志中搜索出想要的信息,是一项最基本的需求。SLS 推出 SPL 查询语法,支持类似 Unix 管道的级联语法,并支持 SQL 的各种丰富的函数。同时,基于计算下推、向量化计算等优化,支持高性能模式 SPL 查询,可以在数秒内处理亿级数据,并且支持 SPL 过滤后最终结果的分布直方图、随机翻页等特性,具备和纯索引查询模式类似的体验。对于模糊、短语、正则、json 提取以及各种复杂过滤场景,推荐使用 SPL 语句来进行查询。
高性能模式 SPL 目前正在按区域逐步发布中,有任何使用上的问题或者需求,可以通过工单或者直接在 SLS 的钉钉群咨询。SLS 会一直持续不断的优化,提供更强大、更好用的可观测存储分析引擎。