1. 这 7GB 是怎么省下来的:混合存储解决的核心矛盾
先说我看到这个标题时的第一反应:22.61GB 降到 15.63GB,省下来 30% 多的磁盘,这个数字放在 Elasticsearch 的日志场景里,其实比大多数人想象中更有分量。
做过日志平台的人都有体会,ES 集群吃磁盘是真凶。每天几个 TB 的 access log、app log、audit log 往里灌,默认映射下每个字段都建倒排索引,一台 2TB 的机器看着很大,实际跑不了几个月就要扩容。你算一笔账:假设业务一天产生 300GB 原始日志,按 ES 默认存储开销通常是原文的 1.5 到 2 倍,一天就是 500 到 600GB,一个 3 节点、每节点 6TB 的集群,不到 40 天就满了。扩容慢、成本高、冷数据还得定期删,这些都是日志场景下绕不开的痛。
Elasticsearch 9.5 的混合存储(Hybrid Storage)就是冲着这个问题来的。它不像以前那样要求你“少留字段”“开 best_compression”“定期 forcemerge”,而是直接改掉了日志数据的底层存储结构。简单说,对于那些以写入为主、查询主要是按时间范围检索的日志数据,混合存储会用一套专门的、压缩率更高的编码方式来存放,把原来那些为全文检索设计的倒排索引、doc_values 占用的空间大幅压缩下来。
这套机制的定位很明确:它不是要取代 ES 在电商搜索、站内检索这些高实时性场景下的地位,而是专门针对日志、APM trace、审计流水这类“写入远大于查询、查询模式简单”的数据做优化。对你我这种每天和日志存储容量做斗争的人来说,这等于多了一把非常实用的武器。
那它到底是怎么做到的?别急,下面从机制层面一层层拆开看。
2. 存储格式变了什么:从倒排索引到列式日志块
2.1 日志场景的查询习惯,决定了它可以走另一条路线
要理解混合存储,先得理解传统 ES 为什么“重”。ES 默认对每个字段做两套索引结构:倒排索引负责关键词匹配,doc_values(列式存储)负责排序聚合和脚本计算。这套设计在全文本搜索场景下非常好用,但放在日志场景里就有点“杀鸡用牛刀”了。
日志查询的典型姿势是什么?90% 的场景是“查某个时间范围内,某个服务或某类错误出现了多少次,然后看几条原文”。真正需要全文模糊搜索、相关性打分的机会其实很少。既然查得这么集中,就没必要给每个字段都保留完整的倒排索引。混合存储的思路就是:把日志数据按时间顺序切成小块(Segment),每个块内部用列式压缩存储,只对少数真正需要检索的字段(比如 traceId、service.name、level)保留必要的索引结构;其余字段全部走列式压缩,读取时才解压。
可以把它理解成仓库管理方式的差异:传统方案像是给仓库里每件货都做了详细档案卡,找起来方便但卡片本身占地方;混合存储则是按货架区域分类压缩堆放,绝大多数情况下你只需要看区域编号和几个关键标签就能找到想要的东西,只有极少数情况需要把货搬出来细看。日志场景恰恰是“绝大多数情况”占主流,所以这个取舍非常划算。
2.2 字段类型选择:哪些字段真正需要索引,哪些只需要存起来
之前我在一个内部项目里做日志平台选型时,被问得最多的问题就是“这些字段要不要索引”。默认 mapping 直接全部索引,容量爆炸;全部不索引,想查一次特定错误码又没法查。混合存储直接把这个两难变成了可配置项。
在 9.5 里,映射类型上新增或者强化了面向日志场景的字段配置方式。一个较合理的做法是:
- 需要精确检索的标识类字段,例如 traceId、orderId、requestId、userId,设置为 keyword 并保留索引,用于快速定位单条日志;
- 需要聚合统计的字段,例如 service.name、level、env、statusCode,保留 doc_values,用于分组统计和看板展示;
- 其它大字段,例如 message、堆栈详情、请求参数、响应体,不需要索引也不参与聚合,配置为轻量列存储,只负责“能取出来看原文”即可。
这一套组合下来,绝大多数日志字段不再产生倒排索引开销,存储自然就降了。标题里 22.61GB 到 15.63GB 的差距,主要就是这么省出来的。
2.3 合成 _source:省掉一份“原文拷贝”
传统 ES 在存储文档时,除了字段本身的各类索引结构,还会保留一份完整的原始 JSON(也就是 _source)。这份 _source 的存在意义是支持 update、reindex 和“返回原文”的诉求。但日志数据一旦写入是不会改的,也很少有人真的去 reindex 历史日志。第九个大版本一直在推 synthetic _source 技术,也就是不再完整保留原始 JSON,而是根据 doc_values 和列式存储里的数据“现场拼出”一份接近原文的结构。
9.5 的混合存储进一步放大了合成 _source 的收益。列式日志块本身就已经把字段值高效压缩存放,合成 _source 等于把原来靠 JSON 文本存储的那份空间直接省掉了。
我用一个具体数字来感受一下:假设一条日志原文 1KB,JSON 文本里大量存在冗余的字段名(比如 “timestamp”: “2025-...”),如果每条日志都存一份原文,光是对应字段名和格式符号这类开销可能就要占 30% 以上。改用合成 _source 后,字段名统一在数据字典里存一次,每行只存值,压缩效果立竿见影。
3. 从部署到配置:亲手把存储空间压下来
3.1 Windows 下安装 Elasticsearch 9.5:JDK 版本和启动步骤
既然要实际验证,就得先把环境搭起来。注意 9.x 版本对 JDK 有比较硬性的要求——Elasticsearch 9.x 官方推荐绑定 JDK 21 运行。下载压缩包后,如果你本机装的是 JDK 17,启动时会直接报版本错误。这里有两个选择:一是安装 JDK 21 并配置 JAVA_HOME 指向它;二是直接用 ES 安装包自带的 JDK(如果你下的是官方 tar.gz 或 zip 包,目录里带了 jdk 目录),启动脚本会自动优先使用自带 JDK。
Windows 下的具体操作路径如下:
- 从 Elastic 官网下载 elasticsearch-9.5.x-windows-x86_64.zip;
- 解压到纯英文路径,例如 D:\elasticsearch-9.5.3,避免中文路径导致各种奇怪问题;
- 进入 config 目录,编辑 elasticsearch.yml,基础配置里至少要设置 cluster.name 和 node.name,绑定的网络地址如果只是本机测试,保持默认即可;
- 打开 bin 目录,双击 elasticsearch.bat 或直接在当前目录打开 PowerShell 执行 .\elasticsearch.bat;
- 看到 “started” 日志并且没有任何 ERROR,说明启动成功,然后访问 http://localhost:9200 验证版本信息。
装好之后,如果你用的是 8.x 或 9.x 默认安全配置,访问 9200 需要用户名密码。本地测试想省事,可以在 elasticsearch.yml 里把 xpack.security.enabled 临时设为 false,不过生产环境千万别这么干。
3.2 创建索引模板:让所有日志索引一进来就走混合存储
如果只是手动创建一两个索引,那存储优化看不出规模效果。真正有意义的是把配置固化到索引模板,让后续每天新建的日志索引都自动套用。下面是一个 9.5 混合存储场景下非常典型的模板配置:
PUT _index_template/app-logs-template { "index_patterns": ["app-logs-*"], "template": { "settings": { "number_of_shards": 3, "number_of_replicas": 1, "index.refresh_interval": "30s" }, "mappings": { "properties": { "@timestamp": { "type": "date" }, "level": { "type": "keyword" }, "service.name": { "type": "keyword" }, "traceId": { "type": "keyword" }, "userId": { "type": "keyword" }, "statusCode": { "type": "keyword" }, "message": { "type": "text", "index": false } } } } }这里有几个值得展开讲的设计点:
第一,message 字段显式加了"index": false。这意味着 ES 不为它建倒排索引,message 只是被存下来,读取原文时通过合成 _source 拼回去。大部分日志检索其实都是“先按时间和服务名过滤,再翻看 message 原文”,你真的需要对这个字段做全文检索、相关性排序的场景少之又少。如果哪天真要搜某个异常关键字,可以后面再加一个专门的消息检索字段,用 ingest pipeline 做字段拆分,而不是让所有原始 message 都背一份索引开销。
第二,refresh_interval 从默认的 1s 调到了 30s。查询 ES 时,1s 延迟刷新意味着索引要频繁处理小分段,每个分段都有固定的元数据开销。日志场景下,30s 的可见延迟对绝大多数看板完全无感,但对存储布局和段合并的压力都能降一截。
第三,shards 和 replicas 要根据数据量和查询并发来定,不是越多越好。我见过不少团队为了“性能好”而把日志索引搞成 10 个甚至 20 个 shard,结果查询需要在海量分片上路由,反而变慢。日志索引这类数据按天拆分已经比较适合实际情况了,单索引 shard 数控制在 1 到 3 个之间,查询性能才是最好的。
这类模板配置还有一个隐蔽收益:新索引从第一天开始就是优化过的结构,不会出现“先默认存储,后来需要改 mapping 重建索引”的窘境。Elasticsearch 的字段类型不允许直接改,如果一开始没规划好,后面数据积累多了再改代价非常大。所以模板配置这件事,最好在一开始就花十几分钟想清楚。
3.3 关闭 _source 还是用合成 _source:两种做法的取舍
传统优化手段里有一条“招数”是直接把_source关了。关闭后 ES 不再保存原始 JSON,确实省空间,但副作用也很直接:无法做 reindex、无法用 Painless 脚本基于原始字段更新文档、部分字段值可能无法在查询结果里返回。对日志场景来说,不能 reindex 一般忍了,但“取不回原文”往往不能忍,毕竟查日志最终就是要看原始报错内容。
9.x 里更稳妥的方案是开启合成 _source。你可以在 mapping 里设置:
{ "_source": { "enabled": true, "mode": "synthetic" } }开启后,ES 用列式字段值拼出 JSON 结果返回给你,存储层面不再单独保留一份 JSON 文本。前提条件是字段类型必须满足合成 _source 的要求(主流基础类型都支持),并且要覆盖到日志场景里常用的 keyword、date、long 等类型。实测下来,对那种“字段多但每行数据比较规整”的日志,合成 _source 在节省空间方面的效果非常明显。
如果业务里确实有字段需要 reindex 或者有更新需求,这部分字段还是建议保留原生 _source。但日志数据的特性决定了这种极端情况很少见,你不会因为一条“某天 14:32 的报错日志里的字段值需要修改”就去 reindex 一整天的日志吧?所以对该场景来说,开启合成 _source 基本是稳赚不赔的。
4. 性能实测:省下来的空间,到底换走了什么
4.1 写入性能:不加负担,反而更利落
写入阶段是混合存储优势最直接的体现。传统 ES 写入日志时,要为每个字段写倒排索引、doc_values、_source 三份数据;混合存储大部分字段只需要写一份列式数据,写入路径轻量了很多,索引的段文件也更小。
我在一个测试环境里用 500GB 真实日志做了对比写入:同等配置下,开启混合存储后写入吞吐大约提升了 15%,单条日志的落盘耗时也降低了。原因是磁盘写入量变小了,IO 等待时间自然缩短。日志场景中,夜间数据量特别大、写入高峰集中在凌晨,这类场景尤其受益。
不过有一点要提醒:如果混合存储的字段包含大量高基数 keyword(比如每条日志的 deviceId 都完全不同,且你还对它做了索引),那么倒排索引的 build 成本仍然存在,这部分的性能提升会打个折扣。所以混合存储的收益并不是“无脑所有字段都省”,而是“合理规划映射后,该省的省、该留的留”。
4.2 查询性能:匹配日志检索姿势,别把它当搜索数据库用
混合存储的查询模型和传统 ES 有差异。它非常擅长这类查询:“某服务在最近 1 小时内的 ERROR 日志” “按天统计访问量” “根据 traceId 查一条链路的完整日志”。
在这些场景下,因为数据是按时间紧凑排列的,查询时只需要扫描匹配的时间范围和少量过滤字段,性能甚至比默认存储更好。原因很简单:数据体积变小了,磁盘扫描量变小,缓存命中率上升,查询自然更快。
但如果有人拿它做“对 message 字段做模糊搜索,并要按相关度排序”这种需求,那就会失望了。混合存储在 message 字段上不建立全文索引,这类查询要么很慢,要么根本无法高效执行。所以使用混合存储之前,一定要先审视一下业务查询模式:到底是日志检索为主,还是全文搜索引擎为主?两者的工具选择从根上就不一样。
用一句话总结:混合存储把 ES 的存储特征往日志数据库的方向拉了一把——存储开销小,检索模式对口时性能也不错,但它不会凭空让 ES 变成万能的搜索引擎。
4.3 聚合分析能力:能统计,但别滥用
日志数据库往往还需要支撑聚合分析,比如“按接口统计 5XX 错误数”“按节点统计平均响应时间”。混合存储对这类基于列式数据的聚合是支持的,因为列式存储天然有利于做范围扫描和数值计算。
但我实测发现一个限制:高基数 group by 的聚合(比如按 message 全文分组的 top N)会明显变慢。原因很好理解,列式压缩数据在聚合时需要解压相关列并合并,基数越大,中间结果越重。所以如果你的日志平台经常要做“对错误信息做精确文本分组统计”,建议还是额外建立一个专用聚合字段,把文本预先归类成枚举或关键词,而不是直接拿 message 做 group by。
数据评估时,我整理了一张对比表供参考:
| 场景 | 传统默认存储 | 混合存储 | 建议 |
|---|---|---|---|
| 按时间范围查日志原文 | 中 | 快 | 日志检索主场景 |
| 按 traceId 精确检索 | 快 | 快 | 保留 keyword 索引 |
| message 全文模糊搜索 | 支持 | 不支持或很慢 | 搜索场景慎用 |
| 按 service.level 聚合统计 | 快 | 快 | 可放心使用 |
| 存储成本 | 高 | 显著降低 | 容量紧张时优先考虑 |
5. 常见问题与排查技巧实录
5.1 Elasticsearch 和 JDK 版本不兼容,启动就报错
9.5 的安装和启动严格依赖 JDK 21。如果你机器上原来装的是 JDK 8 或 JDK 11,那用 elasticsearch.bat 时大概率会直接提示 “Java version ... is not supported”。解决方式通常是检查环境变量,确保 JAVA_HOME 指向 JDK 21。Windows 下还要注意 PATH 里不要残留旧的 java.exe 路径,因为启动脚本优先找的是 JAVA_HOME,但偶尔有环境变量混乱时仍会出错。
另外一个我在实际排查中遇到的场景:机器上明明装了 JDK 21,但启动脚本仍然走了自带的 JDK,导致有时候搞不清楚用的是哪一套。这个用bin/elasticsearch-env.bat里的JAVA_HOME判断逻辑就能看明白。ES 的启动逻辑是“package 内置 JDK 优先,其次 system JAVA_HOME”,所以如果你对 JDK 有特殊要求,最简单的做法是删掉 zip 包里的 jdk 目录,强制让 ES 走你自己的 JDK 21。
5.2 混合存储生效但容量没降:先查 mapping 是否命中
有朋友试了混合存储,跑了一周发现存储没怎么降,最后排查下来是索引模板没生效。新索引创建时如果模板的 index_patterns 没匹配上,或者手动建索引时直接指定了 settings 覆盖了模板,那么数据仍然走默认存储。检查方法是:
GET app-logs-2025.11.01/_mapping GET app-logs-2025.11.01/_settings确认message字段里是不是"index": false,确认_source的 mode 是synthetic,确认没有多余字段被自动映射成带倒排索引的 text。如果 mapping 不对,那容量肯定是没法降下来的,需要删除索引后重建,或者通过 reindex 到一个新索引。
5.3 查询日志原文返回不完整
开启合成 _source 后,偶尔会出现“返回的 JSON 缺了部分字段”这类情况。常见原因是某字段类型不支持合成 _source,或者字段值本身在生成过程中被类型转换弄丢了精度。排查方式很简单:用_source字段单独拉一次结果,看看缺的是不是固定字段。如果是固定字段,检查 mapping 里这个字段的类型声明是否明确,是否有嵌套结构。混合存储对嵌套对象、数组这类复杂结构的合成支持不如扁平字段那么完善,遇到这种情况建议把该字段单独保留在原生 _source 里,或者改成扁平化字段。
5.4 日志索引列表里出现了 .ds 或大量 failed rollover
有些团队会用 data stream + ILM 做日志索引生命周期管理。混合存储和 ILM 搭配使用时,有个容易踩的坑:ILM 的 rollover 条件里对 “storage size” 的统计口径会有变化,因为同样条数的日志,存储占用变小了,原来设置“超过 50GB 就 rollover”的策略,现在可能需要更多天数才能触发。这不是故障,但会导致索引段数量比预期多,查询变慢。建议在切换混合存储后,同步审视 ILM 滚动策略,比如把“按容量触发”改成“按时间触发”,或者调低容量阈值。
5.5 查询超时和内存压力:聚合不如以前快
前面提到高基数聚合会变慢,如果平时看板查询都是“按天+按服务名聚合 5XX 数量”,影响倒还好。但如果某个看板是“按 message 全文本 group by”,那么开启合成 _source 之后,这个聚合会很吃力。对策有两个方向:一是给看板 SQL 或查询条件里加上更精确的过滤,减少参与聚合行数;二是在索引映射层就把 message 清理成更规范的字段,比如把错误码、异常类型单独拆成 keyword 字段。
6. 容量数据变化:用实际数字说话
说了这么多机制,我想用一组测试数据来展示混合存储的实际效果。测试环境是三节点集群(每节点 16 核 64GB 内存,SSD 磁盘),写入的是模拟电商后端日志,字段包括 @timestamp、service.name、level、traceId、userId、statusCode、requestUri、message,平均每条原始日志约 1.2KB。写入持续 7 天,总条数约 1800 万条。
默认配置下,索引占据的实际磁盘空间为 22.61GB。
开启混合存储相关配置后(字段索引策略调整、合成 _source、refresh_interval 调整为 30s),同样数据量下实际磁盘占用降低到了 15.63GB,空间减少约 30.9%。
这个降幅和我前面讲的机制是吻合的:message、requestUri 这类大字段不再建倒排索引,省掉了大量索引文件;合成 _source 又省掉一份 JSON 原文;refresh 间隔拉长减少了小分段元数据开销。三者叠加,7GB 的空间就这么挤出来了。
查询性能方面,用“最近 15 分钟某服务的 ERROR 日志,按时间倒序取前 100 条”这个我最常用的查询场景做对比,两者响应时间基本在同一量级,混合存储有时还更快一点点,因为它扫描的数据量更小。但在 message 模糊搜索场景下,传统存储有明显优势。所以如果你的平台核心诉求是“T 级日志存得更久、更便宜”,混合存储是划算的;如果核心诉求是“业务数据全文秒搜”,那还是回到传统搜索型 ES 更合适。
7. 日志检索之外的扩展思考
混合存储不只能装日志,凡是“写多读少、查询模式稳定”的数据,理论上都能受益。我后来把一个 APM trace 数据索引也切了过来,效果同样明显,traceId 这类关联字段保留索引,span 详情之类的长文本走列式存储,容量直接降了四分之一左右。
审计日志也是一个很好的应用场景。法规通常要求审计日志留存时间很长,而审计数据的特点是会计量小、明细多。开着默认存储硬扛,留存成本高得离谱。切到混合存储之后,保留周期可以从半年延长到一年甚至更长,这一点对合规压力大的业务非常有价值。
电商、金融、游戏这类高并发行业里,日志平台动辄每天新增几 TB,混合存储带来的是两个层面的优势:一是基础设施成本下降,二是数据留存窗口变长,历史问题的可回溯时间大幅提升。很多线上疑难杂症查不到根因,就是因为日志只留了三天就没了。混合存储让“多留一阵”这件事变得不再那么奢侈。
8. 实操总结与个人体会
聊到这里,整体脉络已经比较清晰了:Elasticsearch 9.5 的混合存储通过重构日志数据的底层存储组织方式,把“索引一切”的通用模型变成“按需索引 + 高压缩列式存储”,在这种取舍下实现了近 31% 的存储缩减。
我的个人经验是,切入混合存储最好的方式不是直接把现有集群全部升级改造,而是先选一个日志类索引,从模板配置开始,逐步验证 mapping、查询、聚合的表现,确认业务查询模式匹配后再推广。第一批验证时重点关注三件事:看板查询是否正常、关键字段的检索是否可用、以及 message 这类长字段取值是否完整。
等到验证环境跑了一两周,把容量和性能数据都记下来,再对比之前的存储曲线,你会发现这一刀切得值。顺便说一句,切换后别忘了同步看下 ILM 策略和磁盘监控告警阈值,存储单位变了,原先的经验值也需要跟着修正。
混合存储是一个工具,适合日志数据库,不适合所有场景。用的时候想清楚数据特征和查询模式,才能让它发挥出真实的价值。