
Elasticsearch 9.5 的 logsdb_columnar 模式,把日志文本纯列式存放,且只保存一份。上一篇文章我们用它做了极致压缩,这次换个角度:不建索引,靠硬扫描实时搜日志,到底能有多快?
我在家里一台 5 年前的老 PC(CPU:2021年 AMD 5700G 家用 CPU,Zen3 架构,8核心16线程,内存 32GB DDR4, NVME SSD)上横向对比了 7.17、8.19、9.5 三个版本的实时扫描性能。结论先放这里:
ES 9.5 + ES|QL:每秒扫描 343 万行日志,比 7.17 时代的 DSL 硬扫快了 8 倍以上,同样比传统 DSL 查询语言成绩最好的 9.5 DSL 快 2.5倍
什么是日志硬扫描?
grep 是最经典的日志硬扫:不建任何索引,逐行暴力匹配。Splunk、Loki 等系统也走类似路线------存储便宜、写入极快,代价是查询时全靠扫描算力硬顶。
大量低优先级的日志场景适合这个思路:数据量巨大、成本敏感、但真正需要搜索的次数并不多。为一堆几乎不会被查的日志建全文索引,是典型的"为小概率事件付大额保费"。
Elasticsearch 如何实现不建索引也能实时扫描?
答案是 Runtime Field :这个特性在 ES 7.11 版本就开始支持,查询时用脚本动态获取字段值,再对它做匹配。message 日志文本字段设置 "index": false,不建倒排索引,实时搜索脚本获取原始文本有两个来源:
_source(行存) :一个文档的所有字段打包存在一起。读取一条日志文档的完整内容很快,但只想读 message 一个字段时,也得把整行解压出来。脚本写法:params._source.message。
doc value(列存) :同一字段的所有值连续存放,不同字段分开。只扫 message 一列时,磁盘 IO 和 CPU 缓存都极其友好。脚本写法:doc["message"].value。
另外还有 synthetic source :不真正存 _source,查询时从 doc value 反向 "合成" 出来 ------ 省一份存储,但读取时多一步重组开销。logsdb 模式默认就用它。
DSL 与 ES|QL:两代查询引擎的分野
DSL 面向高并发搜索场景设计:每个查询被限制在有限资源内(单个 shard 的查询基本跑在一个核心上),保证成百上千个并发请求互不拖垮。
ES|QL 面向分析型查询和计算:单个查询就可以吃满全部 CPU 核心,引擎按列式数据组织计算,天生匹配列存。对 "一次扫几百万行" 的分析任务,这才是正确的引擎形态。
当然,硬扫描不等于无脑全扫。时间范围和标签过滤永远是第一道闸门,先把扫描范围切小,再谈扫描速度。
测试设计
测试数据集和之前一篇文章一样,采用 2022 年 CCF 国际 AIOps 挑战赛 数据集中的一个日志目录,2022-03-20-cloudbed2。这是一套基于微服务电商 Demo(hipstershop)的标题一可观测性数据,日志形态非常接近真实生产的应用日志。
| 项 | 值 |
|---|---|
| 原始 CSV | 4,856,187,767 字节 = 4631 MB |
| zip 压缩后 | 431,613,887 字节 = 411.6 MB(压掉 91.1%) |
| 日志条数 | 16,410,236 |
| 平均单条 | 295.9 字节 |
原始数据样例:
sql
`
1. log_id,timestamp,cmdb_id,log_name,value
3. KN43pn8BmS57GQLkQUdP,1647761110,cartservice-1,log_cartservice-service_application,etCartAsync called with userId=3af80013-c2c1-4ae6-86d0-1d9d308e6f5b
5. M943pn8BmS57GQLkQUdP,1647761111,cartservice-1,log_cartservice-service_application, Executed endpoint 'gRPC - /hipstershop.CartService/GetCart'
7. Ot43pn8BmS57GQLkQUdP,1647761111,cartservice-1,log_cartservice-service_application,mptyCartAsync called with userId=e25fe877-95f8-4a44-ae29-ae6c3eca824a
`AI写代码
字段映射
| CSV 列 | ES 字段 | 处理 | 低基数字段不同值数量 |
|---|---|---|---|
log_id |
_id |
20 个字符,与 ES 自动生成的 ID 形态一致,不重复写入,交给 ES 自动生成,自动生成_id 比业务指定id具有更好的性能 | |
timestamp |
@timestamp |
秒级 epoch,×1000 转毫秒 | |
cmdb_id |
host.name |
keyword |
31个不同值 |
log_name |
logname |
keyword |
22个不同值 |
value |
message |
match_only_text |
查询模式:时间范围过滤 + message 中匹配一段卡号样式的字符串(*4432-8015-6152-0454*),分别测 10 分钟、1 小时、1 天三个时间窗口。
| 时间窗 | 区间内文档数 | 命中 |
|---|---|---|
| 10 分钟 | 180,563 | 137 |
| 1 小时 | 1,104,256 | 825 |
| 1 天 | 6,972,239 | 5,195 |
DSL 查询示例(doc value 版):
bash
`
1. GET logs-aiops-22/_search?request_cache=false
2. {
3. "runtime_mappings": {
4. "message_rt": {
5. "type": "keyword",
6. "script": {
7. "source": """
8. if (!doc["message"].empty) {
9. emit(doc["message"].value);
10. }
11. """
12. }
13. }
14. },
15. "query": {
16. "bool": {
17. "filter": [
18. { "range": { "@timestamp": {
19. "gte": "2022-03-20T08:18:27.000Z",
20. "lte": "2022-03-21T08:18:27.000Z" } } },
21. { "wildcard": { "message_rt": { "value": "*4432-8015-6152-0454*" } } }
22. ]
23. }
24. },
25. "fields": ["@timestamp", "message_rt"],
26. "_source": false
27. }
`AI写代码
ES|QL 查询则简洁得多:
sql
`
1. FROM logs-aiops-22
2. | WHERE @timestamp >= "2022-03-20T08:18:27.000Z"
3. AND @timestamp <= "2022-03-20T08:28:27.000Z"
4. AND CONTAINS(message, "4432-8015-6152-0454")
5. | LIMIT 10000
`AI写代码
测试结果
7.17:行存快,列存慢
| 数据来源 | 10 分钟 | 1 小时 | 1 天 | 每秒速率 |
|---|---|---|---|---|
DSL + _source(行存) |
477ms | 2703ms | 16778ms | ~41 万条 |
DSL + doc value |
1143ms | 7024ms | 超时 | ~16 万条 |
7.17 时代 doc value 的列存实现对大文本扫描很不友好,1 天窗口直接超时。
8.19:logsdb 登场,但 doc value 依然不快
| 数据来源 | 10 分钟 | 1 小时 | 1 天 | 每秒速率 |
|---|---|---|---|---|
DSL + _source(行存) |
312ms | 1878ms | 11824ms | ~48 万条 |
DSL + synthetic source |
582ms | 3488ms | 22077ms | ~32 万条 |
DSL + doc value |
839ms | 5249ms | 超时 | ~21 万条 |
行存直读仍然最快;synthetic source 因为多了一步"从列存合成行"的开销,比直读 _source 慢约 35%。
9.5:logsdb_columnar 模式,doc value 彻底翻身
| 查询方式 | 10 分钟 | 1 小时 | 1 天 | 每秒速率 |
|---|---|---|---|---|
DSL + synthetic source |
488ms | 2849ms | 16712ms | ~42 万条 |
DSL + doc value |
152ms | 889ms | 5121ms | ~136 万条 |
ESQL + doc value |
60ms | 322ms | 746ms | ~343 万条 |
数据背后的三个结论
1. 为什么直读 _source 一直比 synthetic source 快? 行存把整条日志连续存放,顺序读取零重组;synthetic source 每读一条都要从多个列存字段里拼装还原,天然多一层开销。
2. 为什么 9.5 的 doc value 突然快了 6 倍? logsdb columnar 模式重写了列式存储布局:message 只存一份、连续紧凑排列,扫描一列就是顺序读一段磁盘,CPU 预取和缓存命中率大幅提升。列存终于发挥出了列存该有的样子。
3. 为什么 ES|QL 又比最快的 DSL 快 2.5 倍?
ES|QL 不是 DSL 的语法糖,而是一台天生为列式数据设计的现代分析引擎:
- 列式向量化执行:数据按列成块(block)流经算子,而不是一条文档一条文档地处理,可以充分利用 SIMD 指令,一条 CPU 指令同时处理多个数据;
- 全核心并行:DSL 单查询基本困在一个核心里,ES|QL 单个查询就能跑满整机所有 CPU 核心,硬件利用率被最大化;
- 存储与引擎同构:columnar 存储 + 列式引擎,数据从磁盘到 CPU 全程不需要行列转换。
应用建议
- 成本优先的日志场景:直接上 9.5 logsdb columnar 纯列式模式,并且不建倒排索引,存储省到极致;
- 实时扫描优先使用 ES|QL,DSL runtime field 只作兼容手段;
- 永远做好时间和标签过滤,控制扫描范围,别让算力浪费在无关数据上;
- 适用边界要清楚:这套打法适合"存得多、查得少"的成本敏感型日志。对安全分析、业务搜索这类需要毫秒级高并发响应的场景,老老实实建索引------用存储换计算,依然是最划算的买卖。