前两天帮朋友整理一套日志集群,被存储占用吓了一跳:光一个月的日志索引,堆在本地磁盘上就占了 22.61GB。这个体量放在几十 TB 的机器上看着不算大,但如果你每天写入的日志是这个体量的 50 倍、100 倍,半年不清理,整台机器都会被拖垮。后来我把这套集群切换到 Elasticsearch 9.5 的混合存储方案,ILM 策略跑完一轮后,存储占用降到了 15.63GB,整体瘦身 30.9%。
这篇文章会把整个思路、具体配置和踩坑记录完整写出来。不管你是正在用 ES 做日志平台,还是刚准备把应用日志集中接入 Elasticsearch,这篇文章都值得你花几分钟看完。尤其是“日志数据库”这种大量写入、少量查询、长期保留的数据场景,混合存储几乎是目前性价比最高的解法。
1. 22.61GB 的日志索引:存储成本从哪来
1.1 日志库到底在存什么
很多人第一次接触 Elasticsearch 时,会把它当成一个“更快的关系型数据库”,但日志场景下它的价值完全不同。你要处理的不是用户下单、库存扣减这类强一致业务数据,而是 Nginx 访问日志、Java 应用日志、数据库慢查询日志、安全审计日志。这些日志的特征非常一致:写入量极大、字段结构重复度高、单条记录没有修改需求、越老的日志查询频率越低。
在这个背景下,Elasticsearch 索引里存储的内容主要有三块:倒排索引、doc values 列存、_source原始文档。前两块用于加速搜索和聚合,第三块是“备份原样 JSON”。大多数日志检索不会真的需要_source里的完整字段,但默认情况下 ES 会老实把整条 JSON 存一份,这部分空间浪费非常明显。我最初拿到的那套集群,_source贡献的存储占比在 40% 以上,这是 22.61GB 高居不下的第一个原因。
另外,日志场景往往不会只保留一份数据。很多团队为了保证查询可用性,给所有索引都配置了 2 个副本。主分片加副本,一份数据实际写了 3 份,磁盘占用直接翻两倍。再加上分片数设置不合理、segment 长期不合并、冷数据没有及时迁移,22.61GB 就是这么一点点涨上来的。
1.2 日志数据的“热与冷”分布很极端
日志数据和业务数据的访问模式有一个非常大的差异:热度极度不均衡。当天的日志触发告警、排障、看板统计时会被频繁查询,热度极高;一周前的日志偶尔会被拉出来排查线上问题;一个月以前的日志,可能半年都没人碰一次。
但传统做法是,不管日志多老,全都放在同样的热磁盘上,采用同样的分片策略、同样的副本数、同样的压缩方式。这就像把所有衣服不分季节全挂在卧室衣柜里,爆满不说,想找当季的衣服反而更难。
所以日志场景最需要的不是“更强的查询能力”,而是一套能把数据按照年龄和访问热度,自动分配到不同存储介质上的机制。越新的数据放越快、越贵的本地磁盘;越老的数据越要往便宜、容量大的存储上放。这正是 Elasticsearch 混合存储要做的事。很多团队最开始不愿意引入这套机制,觉得配置 ILM、快照仓库、分层节点很麻烦,但等到磁盘告警邮件每周都来的时候再改,成本只会更高。
1.3 选定 ES 9.5 作为落地方案的原因
我这次使用的是 Elasticsearch 9.5,选择这个版本主要考虑几点:一是新版本对可搜索快照的稳定性明显比早期版本好,冷数据直接挂在对象存储上查询不会动不动就超时;二是索引生命周期管理(ILM)的默认行为更符合日志场景,滚动、迁移、删除的调度准确率高了很多;三是 9.x 系列在 Windows 和 Linux 上的部署都足够友好,不少团队还在 Windows 环境下做内网日志平台,新版解压直接跑、JDK 版本匹配也省心不少。
当然,版本从来不应该是第一决策因素。你现有的集群如果是 7.x 甚至 6.x,方案原理完全一致,升级之后很多配置项也能继承。我之所以强调 9.5,是因为整套配置写出来之后,在 9.5.3 这个版本上实测跑通了。后续你自己搭环境时,至少知道这套写法和版本不会有明显冲突。
2. 混合存储方案的底层逻辑
2.1 你首先要知道的分层架构:Hot / Warm / Cold / Frozen
混合存储这一概念,在 Elasticsearch 里的落地形态是分层的节点架构。官方把这套机制称为 index tiering,一共四个温度层:Hot、Warm、Cold、Frozen。名字很直白,但对刚接触的人来说还是容易混淆,我用一个生活化类比解释一下。
Hot 层相当于你工位上的笔记本,数据当天写当天查,性能拉满,配置好的 SSD 放在这层;Warm 层相当于你背后的文件柜,数据不是每天用了,但还是希望打开文件柜就能翻到,对应的是一块大容量机械盘或者混合盘;Cold 层相当于仓库里的货架,数据很少动了,但要在几分钟内能拿出来,放对象存储或者归档型存储都行;Frozen 层就是仓库最里面的纸箱,几乎永远不翻,但只要你知道箱子里有什么,还是能通过索引查到,不必把整个箱子搬到工位上来。
ES 的节点通过node.roles来标识自己能承担哪层角色。比如一个节点配置了data_hot,它就只接收 Hot 层数据;配置了data_cold,ILM 把历史索引迁移到 Cold 层时,就会选择这类节点落地。实际部署时,节点可以兼职多种角色,但我建议你做日志平台时尽量把角色拆开,尤其是 Hot 层节点不要兼 Cold 层角色,否则数据迁移会变成在原地“倒腾”,省不了空间。
2.2 可搜索快照:把索引“一半放本地点一半放对象存储”
混合存储能够实现存储瘦身,核心功臣是可搜索快照(searchable snapshot)。传统快照是给你一个“备份文件”,要恢复数据必须先把索引完整还原回本地,才能搜索。可搜索快照不一样,它把索引的核心数据封装成快照存到对象存储上,本地只挂载一份“文件的目录和访问入口”,查询时可以直接去对象存储里读数据,也可以把常用数据缓存到本地磁盘。
打个比方,传统方案是你要看一个档案,必须把整箱档案从仓库搬回办公室,看完再送回去;可搜索快照是你不用搬箱子,直接在仓库里翻档案,办公室桌上只放一张索引卡,记着每份材料在哪一层哪个位置。如果经常查某几份,你再把它们从箱子里拿出来放在最近的抽屉里,这就是本地缓存。
正是因为可搜索快照的存在,Cold 和 Frozen 层的数据不再需要占用完整的本地副本空间。本地磁盘上只需要保留索引元数据和查询缓存,绝大多数 segment 数据都躺在对象存储里。日志这种老数据查询频率极低的场景,用这个方案能把本地磁盘占用“压缩”掉一大半。S3、对象存储、MinIO 都支持,社区里也有不少团队直接用 MinIO 内网搭建低成本冷存储池。
2.3 ILM 就是这套体系的“自动转运中枢”
分层节点和可搜索快照单独使用,能解决空间问题,但无法解决“管数据”的问题。你不可能每天晚上手动去检查哪些索引该迁移、哪些该删除,这时候将由 ILM(Index Lifecycle Management)承担自动调度职责。
ILM 的基本单位是一个 Policy,里面按时间或者按索引大小配置了几个阶段:hot、warm、cold、frozen、delete。每个阶段可以定义 min_age(进入该阶段所需的最短时间),以及具体动作。比如,索引创建后进入 Hot 阶段,当索引滚动了 7 天或体积超过 30GB,就自动滚动到新索引;到了 3 天,迁移到 Warm 层并做 force merge;到了 30 天,转成可搜索快照进入 Cold 层;到了 180 天,直接删除。
这套机制跑起来之后,数据在存储层之间的移动完全不需要人工干预。你只需要保证每个阶段有对应的节点角色、有足够的快照仓库容量,剩下的交给 ILM 调度器。这个能力在日志场景的价值极高,因为它把“存储成本控制”从一次性动作变成了持续运行的自愈机制。
3. 实操记录:从 22.61GB 瘦身到 15.63GB
3.1 先摸清现状:哪些索引占大头
动手之前,我习惯先跑一条命令把索引体积排序摸清底数:
GET _cat/indices?v&s=store.size:desc&h=index,docs.count,store.size,pri.store.size当时输出里最显眼的是logs-nginx-*和logs-app-*两组索引,logs-nginx-*的单日索引在 700MB 到 900MB 之间,logs-app-*单日索引在 300MB 到 500MB 之间。我选了其中一个月的索引做样本,统计出来整段数据的本地总体积正好是 22.61GB。
我还顺手看了一眼分片数和副本数:每个索引 3 个主分片、2 个副本,这意味着同样的数据在集群里实际写了 3 份。对日志这种“按时间滚动写入”的数据来说,单日索引的体量根本不需要 3 个主分片,分片越多反而让 segment 越碎,查询和写入都要做更多多余工作。于是优化方案里排在最前的不是压缩,而是先减少这种“结构性浪费”。
3.2 索引模板与压缩参数调整
第一步,我先定义一个新的索引模板,把后续新建日志索引的默认分片数从 3 降到 1,副本数从 2 降到 1,同时开启best_compression压缩编码器。best_compression用多一点的 CPU 占用换取更高的压缩率,日志场景的写入瓶颈往往不在 CPU 而在磁盘 IO,这个交换非常划算。
PUT _index_template/logs-nginx-template { "index_patterns": ["logs-nginx-*"], "template": { "settings": { "number_of_shards": 1, "number_of_replicas": 1, "codec": "best_compression", "refresh_interval": "30s", "index.lifecycle.name": "logs-lifecycle", "index.lifecycle.rollover_alias": "logs-nginx" } } }这里refresh_interval也值得单独说一句。日志索引默认刷新间隔是 1 秒,意味着每秒都会生成新 segment,刷得越快 segment 越多,后续 merge 压力越大。我把刷新间隔调整到 30 秒,写入吞吐几乎没有影响,因为日志查询本来就有分钟级延迟容忍度,但磁盘上的小文件数量下降了一大截。
接着,在 mappings 里显式开启 synthetic_source。ES 9.x 在部分场景下默认会启用合成源,但为了可控性和迁移一致性,我建议你在模板中直接写明确:
{ "mappings": { "_source": { "mode": "synthetic" }, "properties": { "@timestamp": { "type": "date" }, "remote_addr": { "type": "ip" }, "request": { "type": "keyword" }, "status": { "type": "long" }, "message": { "type": "text", "fields": { "keyword": { "type": "keyword" } } } } } }合成_source的意思是,ES 不再为每一条文档保存一份完整的原始 JSON,而是利用字段的 doc values 和倒排索引在查询时动态还原_source。对日志数据来说,绝大多数字段写入后不会被修改,合成源的效果和普通源几乎没有差别,但 _source 的存储占用能大幅下降。唯一要注意的是,text 类型字段必须配置一个 keyword 子字段,否则合成源会报错。
3.3 配置 ILM 策略让历史数据自动迁移
模板就位后,我写了一个 ILM Policy,这是整个混合存储方案的调度大脑。策略目标非常明确:热数据留在 Hot 层;3 天后的数据进 Warm 层做 force merge;30 天后的数据转成可搜索快照迁移到对象存储;90 天后删除。
PUT _ilm/policy/logs-lifecycle { "policy": { "phases": { "hot": { "min_age": "0ms", "actions": { "rollover": { "max_size": "30GB", "max_age": "7d" }, "set_priority": { "priority": 100 } } }, "warm": { "min_age": "3d", "actions": { "forcemerge": { "max_num_segments": 1 }, "shrink": { "number_of_shards": 1 }, "set_priority": { "priority": 50 } } }, "cold": { "min_age": "30d", "actions": { "searchable_snapshot": { "snapshot_repository": "my_log_backup" } } }, "delete": { "min_age": "90d", "actions": { "delete": {} } } } } }在这个配置里,forcemerge会把一个分片里的多个 segment 强行合并成 1 个,这样避免了小文件碎片长期占用磁盘;shrink会把主分片数收缩成 1,日志索引数据量不大,单分片足够承担后续检索。searchable_snapshot是冷阶段的重点,它会先在快照仓库(我这里配的是 MinIO)里生成索引的完整快照,然后把索引从本地“换”成可搜索快照,本地只留下元数据和缓存。等时间到 90 天,ILM 直接执行 delete,把这个索引从集群里彻底删除,连快照也一并清理。
为了让 ILM 能顺利把索引迁到冷层,快照仓库也需要提前定义:
PUT _snapshot/my_log_backup { "type": "s3", "settings": { "bucket": "es-logs-backup", "endpoint": "http://minio.local:9000", "base_path": "searchable_snapshot" } }这里不要忽略仓库的base_path,多个集群共用同一个对象存储的时候,这个参数能避免不同集群的快照互相覆盖。
配置完成后,不需要重启节点。新索引只要命中模板,就会自动挂上 ILM 策略。对于已经存在的老索引,如果之前没有关联过策略,可以直接用PUT logs-nginx-*/_settings手动补上index.lifecycle.name,ILM 会按照 min_age 判断是否立即进入对应阶段。
3.4 瘦身账单:6.98GB 省在了哪些环节
整个策略跑完一周后,我重新查看这组日志索引的本地存储总量,从 22.61GB 降到了 15.63GB,一共省下 6.98GB,降幅约 30.9%。这个数字看起来不算惊人,但要看清楚省下的来源结构,因为不同来源的“含金量”是不一样的。
首先,分片从 3 降到 1、副本从 2 降到 1,这一步直接砍掉了大量冗余数据。一个索引从“3 主分片 + 2 副本”变成“1 主分片 + 1 副本”,实际写的数据份数从 9 份降为 2 份。对这个实验样本来说,大量历史索引并没有真的变 9 份,因为早期创建方式不一致,但这步调整对后续日增数据的影响是持续性的。
其次,best_compression配合合成_source,压缩率提升了一截。日志字段高度重复,合成源去掉重复 JSON 原样保存后,存储降低非常明显。最后,30 天前的索引进入 Cold 层之后,原本完整保留在本地磁盘上的 segment 数据被替换成了可搜索快照,本地只剩元数据和少量缓存块。这份样本数据中,有大约 6GB 的索引体积是从本地磁盘“挪”进了对象存储,本地实际占用几乎归零。
所以,如果你看到自己环境里的原始占用量是 200GB,按我这个方案跑完,降到 120GB 到 140GB 是很正常的结果。真正的收益大头在冷数据,这也是为什么我坚持要把冷阶段纳入 ILM,而不是只做压缩和分片调整。
4. 常见问题与排查技巧实录
4.1 迁移完查询变慢,怎么用缓存曲线救国
把索引转成可搜索快照之后,最直观的感受是查询变慢了。老日志搜索从原来的几百毫秒变成 2 秒到 5 秒,这是正常的,因为数据主体已经从本地挪到了对象存储,每次查询都要走网络读盘。要缓解这个问题,我给 Cold 层节点配置了 shared cache 大小,控制在节点可用内存的 20% 左右,同时明确了查询入口:只让监控看板访问最近 7 天的索引,排障查询可以放宽到 30 天,30 天以前的日志只做低频后台检索。
一个更实用的技巧是:如果你的排障场景经常要查“上个月的某一天”的数据,可以提前手动把那个日期的索引 mount 回本地,用稍大一点的磁盘空间换查询体验。你不必把整个月的日志都拉回来,只挂载那几天,效率会高很多。我之前处理过一次线上偶发异常,就是靠临时 mount 回一周前的冷索引,快速定位到问题,而不影响整体存储策略。
4.2 磁盘高水位误报与分片分配抖动
冷数据迁移过程中最容易踩的坑是磁盘高水位告警。因为可搜索快照迁移不是瞬间完成的,ILM 在执行 searchable_snapshot 时,会先创建快照,再把本地索引切换过去,中间会有很短的一段时间本地旧索引和快照数据同时存在,磁盘占用会出现一个“临时峰值”。如果你集群原本磁盘水位已经很高,这个操作很可能触发高水位保护,分片分配开始抖动,整批索引卡在迁移任务里出不去。
我的建议是:不要等磁盘快满了才上冷迁移方案。迁移前先给现有索引做一次 force merge,清理无用的 segments,让磁盘占用先降下来 10% 到 15%,留出缓冲空间。其次,把 ILM 冷阶段的 min_age 设置得比实际需要提前几天,给调度留足余量,避免所有索引在磁盘最紧张的时候同时开始迁移。
还有一个小坑,如果你只有一个节点,并且该节点没有配置data_cold角色,冷阶段迁移会一直停在WAITING状态。解决方案有两种:要么给节点补上冷层角色,要么干脆让索引停留在 Warm 层,不强制进入 Cold 阶段。读者如果在测试环境发现 ILM 一直不执行,优先检查节点角色是不是覆盖了目标阶段。
4.3 一组快速自检清单
我把平时排查混合存储问题的命令整理成了一个小清单,分享出来,你可以直接照着用:
# 查看 ILM 状态 GET logs-*/_ilm/explain # 查看索引在哪个阶段、是否处于错误状态 GET _ilm/status # 查看快照仓库是否正常可访问 GET _snapshot/my_log_backup/_status # 查看节点磁盘水位与分片分配情况 GET _cat/allocation?v # 查看具体索引当前存储量和分片情况 GET _cat/indices/logs-*/?v&s=store.size:desc如果发现 ILM 策略挂了,第一步不是去改策略,而是看GET logs-*/_ilm/explain里的step_info。大多数情况下错误原因会很明确地写出来,比如快照仓库连不上、磁盘水位过高、索引被关闭等。把这套命令做成脚本放到定时任务里,每天跑一次,基本能提前发现 90% 以上的问题。
5. 写在最后:这套混合存储如何落到你自己的环境
如果你现在只有一台服务器,甚至是在 Windows 上跑了一个单节点 ES 做日志平台,这套方案依然能落地。ES 9.5 在 Windows 上安装很简单,解压官方压缩包,配置好 JDK 环境变量,修改config/elasticsearch.yml里的node.roles为["master", "data_hot", "data_warm", "data_cold", "data_content"],然后启动即可。单节点所有角色集于一身,ILM 照样能跑,只是冷数据迁移时本地磁盘空间需要提前规划。
我建议你落地时不要一上来就照抄我的完整策略,而是分三步走。第一步,先只开启best_compression和合成_source,观察两三天索引体积变化;第二步,把分片和副本数调整到合理范围,确认业务没有受影响;第三步,再配置快照仓库和冷阶段迁移,让 30 天前的索引自动进对象存储。步子小一点,出问题时定位也更容易。
关于后续扩展,如果你的日志量继续增长,我建议关注两个方向:一是把查询频繁的旧索引单独挂到 Warm 层,不要直接进 Cold,本地保留一定时期是成本与体验的平衡点;二是给快照仓库加上生命周期清理策略,避免对象存储里积累大量已删除的过期快照,那部分成本同样不可忽视。混合存储不是把数据“变没了”,而是让每一份数据在正确的时间待在正确的介质上。这个思路,比任何具体参数都更重要。