首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >ES存日志很贵?我用ES 9.5把日志从4.5G压到412M 压缩比11.2倍,日志硬扫每秒128万行!

ES存日志很贵?我用ES 9.5把日志从4.5G压到412M 压缩比11.2倍,日志硬扫每秒128万行!

作者头像
点火三周
修改2026-08-11 17:59:14
修改2026-08-11 17:59:14
1110
举报
文章被收录于专栏:Elastic Stack专栏Elastic Stack专栏

这是一次用真实生产形态日志数据做的实测:1641 万条日志,4631 MB 原始文本大小,ES 落盘后 412.5 MB,压缩比 11.2 倍,只剩原始体积的 8.91%。而同一份数据用 zip 压缩是 411.6 MB。区别是:zip 那份要先解压才能看一眼,而 Elasticsearch 那份可以按时间范围过滤、可以聚合、可以全量扫描——它是在线的、活的数据。

"ES 做日志成本高"这个印象,来自它的出身。Elasticsearch 是搜索引擎,一条文档进来会同时落到四套磁盘结构里:倒排索引(让全文检索快)、_source(原始 JSON 一整块存下来)、doc values(列式存储,给排序和聚合用)、BKD树(数值和日期的范围查询)。同一份字段值被写了好几遍。对搜索场景这是合理的代价,对日志场景——你几乎从不需要原始 JSON、也几乎从不做相关性排序——其中很大一部分是纯浪费。

index.mode: logsdb 和 9.5 新增的 index.mode: logsdb_columnar 就是在拆掉这些冗余。这篇文章用 5 组对照测试算清 ES 9.5 是如何实现这么极致的压缩的。

一、ES 的日志优化之旅

过去三年,ES 在日志存储上的改动不是一次大重构,而是一层一层叠上来的。按时间粗略排一下:

  • 列式存储(doc values) 是一切的基础。_source 是一行一个 JSON blob,doc values 是把每个字段拉成一整列。同一列里的值形态相似,压缩算法才有发挥空间。
  • 索引排序。如果日志按到达顺序落盘,几十台主机的时间戳交错在一起,列里的相邻值毫无规律,delta 编码无从下手。先按 host.name 再按 @timestamp 排序之后,同一台主机的日志连成一片,时间戳单调递增,主机名整段重复——delta、GCD、RLE 这些编码才能真正生效。
  • 合成 source(synthetic source)。写入时干脆不存 _source 那个 JSON blob;查询需要返回原文时,从各字段的 doc values 里重新拼出来。
  • ZSTD。8.16 起 best_compression 从 DEFLATE 换成 ZSTD,存储更小的同时写入吞吐还更高。
  • 专用 doc values codec。8.13/8.14 引入了一套针对时序/日志数据形态的编码,其中包括对排序后连续相同值的 run-length 编码,logsdb 自 8.15 起继承。

logsdb 在 8.17 GA,9.0 起对 logs-*-* 数据流默认启用。9.5 又带来了 logsdb_columnar——一个更彻底的形态:字段只存一次、只以 doc values 形式存在。

二、数据集与测试方法

测试数据集

采用 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 字节

原始数据样例:

代码语言:csv
复制
log_id,timestamp,cmdb_id,log_name,value

KN43pn8BmS57GQLkQUdP,1647761110,cartservice-1,log_cartservice-service_application,etCartAsync called with userId=3af80013-c2c1-4ae6-86d0-1d9d308e6f5b

M943pn8BmS57GQLkQUdP,1647761111,cartservice-1,log_cartservice-service_application,     Executed endpoint 'gRPC - /hipstershop.CartService/GetCart'

Ot43pn8BmS57GQLkQUdP,1647761111,cartservice-1,log_cartservice-service_application,mptyCartAsync called with userId=e25fe877-95f8-4a44-ae29-ae6c3eca824a

字段映射

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

在ES说明文档中,logsdb 如果存在 host.name 则会按照 @timestamp+ host.name 联合排序,一般一个host产生的日志有比较大的相似性,聚集在一起存储可以获得比较高的压缩率

两个 keyword 字段都是极低基数——31 个服务实例、22 类日志源。message 则是每条几乎都不一样的长文本,是全部存储的主体。这个反差是理解后面所有数字的钥匙。

五组配置

配置

index.mode

_source

message 倒排

对照目的

1

logsdb

stored(无 license)

关闭

与配置2 对照:synthetic source 值多少钱

2

logsdb

synthetic

关闭

基准最优配置

3

logsdb

synthetic

开启

与配置2 对照:倒排索引的成本

4

logsdb_columnar

synthetic columnar

开启

与配置3 对照:两种 index.mode 的差异

5

logsdb_columnar

synthetic columnar

关闭

与配置2 对照:两种 index.mode 的差异

全部为 1 primary / 0 replica,data stream 写入,日志场景统一禁用 sequence number(index.disable_sequence_numbers: true,9.4+)。

配置2 的索引模板(其余配置只改 index.modemessageindex 参数):

代码语言:json
复制
PUT _index_template/aiops-log-stream-template
{
  "index_patterns": ["logs-aiops-*"],
  "data_stream": {},
  "priority": 200,
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 0,
      "index.mode": "logsdb",
      "index.disable_sequence_numbers": true
    },
    "mappings": {
      "properties": {
        "@timestamp": { "type": "date" },
        "host.name":  { "type": "keyword" },
        "logname":    { "type": "keyword" },
        "message":    { "type": "match_only_text", "index": "false" }
      }
    }
  }
}

两条口径说明

  1. 横向对比统一使用 force merge 到 1 段之后的落盘大小。各配置的自然段数从 8 段到 36 段不等,段数会带来 0.4%~3.3% 的体积差异。不统一口径的话,比出来的是段数噪声而不是配置差异。

配置

自然段数 / 落盘

segments=1 落盘

段数带来的膨胀

1

36 段 / 522.8 MB

506.2 MB

+3.3%

2

8 段 / 414.2 MB

412.5 MB

+0.4%

3

18 段 / 747.7 MB

732.1 MB

+2.1%

4

10 段 / 760.4 MB

744.6 MB

+2.1%

5

10 段 / 437.2 MB

423.4 MB

+3.3%

| ) |

顺带一个可以直接用的结论:数据合并得越充分,当然压缩越好。 但是从上面的实测数据看影响也没有那么大。

  1. ES _disk_usage API 可以用来分析一个索引每个字段存储的明细, 本测试的明细都是 segments=1 下通过这个API分析获得的。

三、总览结果

按落盘从小到大排:

配置

说明

segments=1

占原始

压缩比

vs zip

字节/条

2

logsdb / synthetic / 无倒排

412.5 MB

8.91%

11.23×

1.002×

26.4

5

columnar / synthetic / 无倒排

423.4 MB

9.14%

10.94×

1.029×

27.1

1

logsdb / stored / 无倒排

506.2 MB

10.93%

9.15×

1.230×

32.3

3

logsdb / synthetic / 有倒排

732.1 MB

15.81%

6.33×

1.779×

46.8

4

columnar / synthetic / 有倒排

744.6 MB

16.08%

6.22×

1.809×

47.6

最优解 412.5 MB,与 zip 归档的 411.6 MB 几乎完全相等。

这张表还给出了另一件更有实操价值的东西:三个控制开关的量级差了一个数量级以上。

控制开关

影响

占索引比例

message 建不建倒排索引

±320 MB

±77%

synthetic source 开不开

±94 MB

±19%

logsdb 还是 logsdb_columnar

±11 MB

±2.6%

如果你只想记住一件事:先去看你的 message 字段需不需要倒排索引。

下面按这个顺序逐个拆开。

四、最节省空间的抉择:message 到底要不要建倒排索引

两组独立对照,结果高度一致:

对照组

无倒排

有倒排

增量

logsdb(配置2 → 3)

412.5 MB

732.1 MB

+319.6 MB(+77.5%)

logsdb_columnar(配置5 → 4)

423.4 MB

744.6 MB

+321.2 MB(+75.9%)

_disk_usage 给出的字段级明细给出了更刺眼的对比:

大小

message倒排索引

320.9 MB

message列式正文(doc values)

272.7 MB

倒排索引比它索引的正文本身还大 18%。

这里值得多说一句机制。message 用的已经是 match_only_text 而不是 text——这个类型专为日志设计,砍掉了词频(BM25 打分用的,日志场景没意义:你不关心 "timeout" 在这行里出现了两次还是七次)、位置信息(短语精确匹配用的,日志上很少用到),只保留"哪些文档里有这个词"。官方口径是这一刀能把文本字段的倒排索引砍掉约 40%。

砍掉 40% 之后,剩下的仍然有 320.9 MB。

原因是倒排索引的成本主要由 term 字典的基数决定,而不是由存了多少额外信息决定。这份日志的 message 里内嵌了 JSON,token 里全是 UUID、信用卡号、金额、纳秒时间戳——每一个都是只出现一两次的高基数字符串。字典里塞满了这种"只用一次就扔"的词。对机器生成的日志来说,倒排索引的性价比天生就不高。

但这 320 MB 买到的东西是实打实的: matchmatch_phrase、Kibana 里的自由文本搜索、KQL 全文查询,全都依赖它。关掉之后这些能力直接消失。

所以这不是一个"免费优化",而是一次明确的能力交换:

  • 重要日志(核心交易链路、审计日志、告警相关):付这 77% 的存储,换亚秒级的全文检索能力。值。
  • 大量的次要日志、冷日志(Debug 级别、访问日志、查询频次很低但必须留存的合规日志):这 77% 可能是整个日志平台最大的一笔浪费。

那么关掉之后,还能查吗?

五、关掉倒排之后,硬扫日志到底能不能用?

关闭倒排后,字符串检索只能靠查询时候的 runtime field 全量扫描。听起来很暴力,实测数据比预期好得多。

测试查询:在指定时间窗内,扫描 message 找出包含某个卡号的日志。

代码语言:json
复制
GET logs-aiops-22/_search
{
  "runtime_mappings": {
    "message_rt": {
      "type": "keyword",
      "script": { "source": "emit(doc['message'].value)" }
    }
  },
  "query": {
    "bool": { "filter": [
      { "range": { "@timestamp": { "gte": "2022-03-20T08:18:27Z", "lte": "2022-03-20T08:28:27Z" }}},
      { "wildcard": { "message_rt": { "value": "*4432-8015-6152-0454*" }}}
    ]}
  },
  "fields": ["@timestamp", "message_rt"],
  "_source": false
}

实测结果(单分片):

时间窗

区间内文档数

命中

配置2-logsdb

配置5-logsdb_columnar

加速

10 分钟

180,563

137

554 ms

151 ms

3.67×

1 小时

1,104,256

825

3,323 ms

871 ms

3.82×

1 天

6,972,239

5,195

20,762 ms

5,430 ms

3.82×

注意:多分片可以有更多的并发搜索,可以调用更多的核心进行扫描

换算成吞吐:

文档吞吐

逻辑数据吞吐

配置2 - logsdb

约 33万 条/秒

约 95 MB/s

配置5 - logsdb_columnar

128万 条/秒

360 MB/s

注:测试CPU AMD 5700G 3.8-4.6G 内存DDR4 32GB,单分片一个段扫描所以只用到一个核心

这个能力落到什么场景?

它依赖时间窗过滤把扫描范围先缩小。 上面每一次查询都带 @timestamp 的 range 过滤,这不是可选项,而是这套方案成立的基础。日志天然按时间排序、天然按时间分索引,所以这个前提在日志场景里几乎总是满足的。

在这个前提下,它足够覆盖排障中最高频的那个动作:"我知道大概是昨天下午三点前后,要找这个订单号/卡号/traceID 出现在哪些日志里。" 哪怕 10 亿级数据、20 分片,几十秒返回。

它不能替代什么:Kibana 里那种边打字边出结果的交互式检索(要亚秒级)、需要相关性排序的场景。

所以完整的决策是分层的:核心链路日志付倒排索引的钱,换交互式检索;海量的次要日志、冷日志关掉倒排,用时间窗 + 实时扫描兜底。前者可能只占日志总量的 10%,后者占 90% —— 而 90% 那部分可以节省巨大的存储成本。

六、logsdb vs logsdb_columnar:本数据集差异只有 2%,但不代表所有

两种 index mode 差多少?

对照

logsdb

logsdb_columnar

差异

message 无倒排(配置2 vs 5)

412.5 MB

423.4 MB

+10.9 MB(+2.6%)

message 有倒排(配置3 vs 4)

732.1 MB

744.6 MB

+12.5 MB(+1.7%)

只差 2%,而且在这份数据上是 columnar 略大。 它和很多人的预期相反。差异来自两种模式对字段的编码方式不同,最明显的几处:

字段

logsdb-配置2

logsdb_columnar-配置5

host.name

0.99 MB

9.02 MB

logname

8.55 MB

13.63 MB

_id

124.98 MB

122.96 MB

message

274.41 MB

272.90 MB

_field_names

0.75 MB

不存在

stored_fields(全局)

61.3 MB

0 字节

看得很清楚:columnar 在 message _id 上确有小幅优势,但在两个低基数 keyword 字段上多花了 13 MB,把优势吃掉了还倒欠。 logsdb 对这类"低基数 + 与索引排序强相关"的列有一套非常高效的编码(host.name 的 doc values 只占几百字节),logsdb_columnar 当前的编码格式没有这项优化。但是如果 keyword 扩展到更多的列,并且数据形态不利于 logsdb 的压缩算法,那么 logsdb_columnar 的压缩会超过 logsdb。有待后续的测试验证。

这里也顺便看到了 logsdb_columnar "真列存"的硬证据:整个索引的 stored fields 是 0 字节_field_names 这个元数据字段直接消失,连 _id 都从 stored fields 迁移到了 doc values。它不只是营销口径上的列存。

需要说明的是,logsdb_columnar 在 9.5 是 技术预览版。它的设计目标写在官方 release notes 里也很清楚:为将来更快的分析型查询提供基础结构。至少在这份窄 schema 的日志数据上,选它的理由不是"更省空间"。 而是在上一节那个取出 message 硬扫时速度是 logsdb 的 3.8 倍!

七、logsdb_columnar 真正的收益:扫描快 3.8 倍,以及一个被忽略的实现差异

第五节的表里,logsdb_columnar 的日志硬扫的速度是 logsdb 的 3.8 倍。这个差距不是来自"列存天生扫得快"这种笼统理由,而是来自一个具体的、我在测试中才发现的实现差异。

先看两个配置的查询写法:

  • 配置5-logsdb_columnardoc['message'].value——直接读列式 doc values
  • 配置2-logsdbparams._source.message——触发 合成 source 重建

为什么 logsdb 不也用 doc['message']因为它会报错,跑不起来。

这就是那个实现差异。两种模式下 合成 source 的落地方式不一样:

logsdb 沿用经典文档模型。 match_only_text 字段本身没有 doc values,合成 source 靠一个 mapper 生成的隐藏字段 message._original 来重建原文——从 _disk_usage 里能看到它实实在在占了 274.1 MB 的 doc values。但这个字段是实现细节,不进入用户可见的 field lookup,所以 doc['message._original'] 解析不到;而 message 本体作为 text 字段又没有 fielddata,doc['message'] 同样不可用。

两条路都堵死,只剩 params._source。而走这条路,每一条文档都要完整地跑一遍:从 doc values 取值 → 组装成 JSON → 脚本侧再解析出来。 3.8 倍的差距就在这个往返里。

logsdb_columnar 的前提是"每个字段只存一次,且必须有 doc values"。 所以 message 的列存副本直接挂在字段本名上,doc['message'] 天然可用,完全不需要 source 重建。

小结一下 logsdb_columnar 在这份数据上的取舍:用 +2.6% 的存储,换 3.8 倍的暴力扫描吞吐。 如果你的场景是"大量冷日志 + 偶尔全量捞一次",这笔交换是划算的。

八、商业版功能 合成 source 能提升多少?

前面所有的最优数字都建立在 合成 source 上,这是一项商业功能。针对这一功能也做了对照测试。

配置1:index.mode: logsdb + index.mapping.source.mode: stored(老老实实存 _source)+ message 关倒排。

落盘

占原始

压缩比

配置2 - 有合成 source

412.5 MB

8.91%

11.23×

配置1 - 无合成 source

506.2 MB

10.93%

9.15×

差 93.7 MB,也就是 18.5%

九、未来展望:还能再省多少?_id 占了 30%!

配置2 的字段构成(414.8 MB):

字段

大小

占比

message._original(正文列存)

274.1 MB

66.1%

_id

124.9 MB

30.1%

logname

8.5 MB

2.1%

@timestamp

4.8 MB

1.2%

host.name

1.0 MB

0.2%

其它元数据

< 1 MB

_id 一个字段吃掉了 30%。 logsdb_columnar 那边是 29.0%,同一个量级。

换算成单条:约 8 字节/文档。注意本次已经采用了 ES 自动生成的 ID——这是日志场景的正确做法,用户指定 _id 会同时废掉 合成 source 优化和分片路由优化。即便如此,20 个字符的 flake ID 压缩后仍然要 8 字节,因为它天然不可压缩,而且必须建倒排索引来支持按 ID 取文档和去重。

这已经不是用户侧能优化的东西了,解法只能来自引擎。而这件事官方已经在做:TSDB 上已经落地了从 doc values 实时重建 _id 的方案(ES PR #144026),彻底消除了 _id 的存储字节;官方也明确表示正在为 LogsDB 探索同一方向。Elastic 自己的表述是,_id_seq_no 对小文档而言可能占到索引一半以上——本次实测的 30% 和这个判断是吻合的。

如果这条路走通,按当前数据推演(注意:这是推演,不是实测):

配置2 实测

消除 _id 后推演

落盘

414.8 MB

约 290 MB

占原始日志

8.91%

6.26%

压缩比

11.2×

16.0×

vs zip 归档

1.00×

0.70×

也就是比 zip 归档还小 30%,而且可以直接查

十、结语

回到开头那个成见。

"ES 做日志太贵"——这句话只在几年可以理解,因为那个时候 ES 还是一个通用搜索引擎,没有为日志专门优化。一条日志进来写四套结构,其中大部分对日志场景是冗余的。

现在把同一份日志灌进 ES 9.5,落盘和 zip 归档相当,而且能按时间过滤、能聚合、能在几十秒内扫完十亿条。

虽然 ES 现在已经优化的不错了,仍然有继续改进的地方:_id 白白占掉 30%,logsdb_columnar 在低基数 keyword 字段上比 logsdb 多花 13 MB。

给一份可以直接抄的决策顺序:

  1. 先看 message 需不需要倒排索引。 这是 ±77% 的开关,比其它所有选择加起来都重要。核心链路日志保留,海量次要日志关掉。
  2. 有 license 默认打开合成 source 再省 18.5%。
  3. logsdb 还是 logsdb_columnar******?** 存储上只差 2%,别在这里纠结。如果你的负载是大量冷日志的暴力扫描, logsdb_columnar 3.8 倍于 logsdb 的扫描速度值得试。
  4. force merge。 合并越充分压缩越好,但是效果并不会特别显著,本次实测段数差异带来 0.4%~3.3% 的体积波动。

附录

各配置测试索引的完整字段存储明细(ES _disk_usage API)

POST .ds-logs-aiops-22-000001/_disk_usage?run_expensive_tasks=true

配置2(logsdb / 无倒排,1段,414.8 MB)

字段

总计

倒排

stored

doc values

message._original

274.1 MB

274.1 MB

_id

124.9 MB

63.5 MB

61.3 MB

logname

8.5 MB

2.7 MB

5.7 MB

@timestamp

4.8 MB

4.8 MB

host.name

1018.6 KB

1018.3 KB

278 B

_field_names

770.8 KB

770.8 KB

message._original.counts

250.4 KB

250.4 KB

配置5(logsdb_columnar / 无倒排,1段,423.4 MB)

字段

总计

倒排

stored

doc values

message

272.6 MB

0

272.6 MB

_id

122.9 MB

61.0 MB

0

61.9 MB

logname

13.3 MB

0

13.3 MB

host.name

8.7 MB

0

8.7 MB

@timestamp

4.8 MB

0

4.8 MB

*.counts × 3

751.2 KB

0

751.2 KB

配置3(logsdb / 有倒排,1段,732.1 MB)

字段

总计

倒排

stored

doc values

message

320.8 MB

320.8 MB

message._original

272.6 MB

272.6 MB

_id

121.5 MB

61.5 MB

59.9 MB

logname

8.5 MB

2.7 MB

5.8 MB

@timestamp

4.8 MB

4.8 MB

host.name

1018.6 KB

1018.3 KB

278 B

配置4(logsdb_columnar / 有倒排,1段,744.6 MB)

字段

总计

倒排

stored

doc values

message

593.3 MB

320.7 MB

0

272.6 MB

_id

122.1 MB

60.2 MB

0

61.8 MB

logname

13.3 MB

0

13.3 MB

host.name

8.7 MB

0

8.7 MB

@timestamp

4.8 MB

0

4.8 MB

配置1(logsdb / stored source / 无倒排,1段,522.8 MB)

字段

总计

倒排

stored

doc values

_source

396.6 MB

396.6 MB

_id

85.4 MB

56.3 MB

29.0 MB

_seq_no

26.9 MB

26.9 MB ⚠️

@timestamp

9.5 MB

9.5 MB

logname

1.9 MB

1.2 MB

726.5 KB

host.name

1.1 MB

1.1 MB

11.1 KB

_field_names

704 KB

704 KB

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、ES 的日志优化之旅
  • 二、数据集与测试方法
    • 测试数据集
    • 字段映射
    • 五组配置
    • 两条口径说明
  • 三、总览结果
  • 四、最节省空间的抉择:message 到底要不要建倒排索引
  • 五、关掉倒排之后,硬扫日志到底能不能用?
  • 六、logsdb vs logsdb_columnar:本数据集差异只有 2%,但不代表所有
  • 七、logsdb_columnar 真正的收益:扫描快 3.8 倍,以及一个被忽略的实现差异
  • 八、商业版功能 合成 source 能提升多少?
  • 九、未来展望:还能再省多少?_id 占了 30%!
  • 十、结语
  • 附录
    • 各配置测试索引的完整字段存储明细(ES _disk_usage API)
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档