尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Elasticsearch混合存储实战:ILM策略让日志存储瘦身30%

Elasticsearch混合存储实战:ILM策略让日志存储瘦身30% 前两天帮朋友整理一套日志集群被存储占用吓了一跳光一个月的日志索引堆在本地磁盘上就占了 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_coldILM 把历史索引迁移到 Cold 层时就会选择这类节点落地。实际部署时节点可以兼职多种角色但我建议你做日志平台时尽量把角色拆开尤其是 Hot 层节点不要兼 Cold 层角色否则数据迁移会变成在原地“倒腾”省不了空间。2.2 可搜索快照把索引“一半放本地点一半放对象存储”混合存储能够实现存储瘦身核心功臣是可搜索快照searchable snapshot。传统快照是给你一个“备份文件”要恢复数据必须先把索引完整还原回本地才能搜索。可搜索快照不一样它把索引的核心数据封装成快照存到对象存储上本地只挂载一份“文件的目录和访问入口”查询时可以直接去对象存储里读数据也可以把常用数据缓存到本地磁盘。打个比方传统方案是你要看一个档案必须把整箱档案从仓库搬回办公室看完再送回去可搜索快照是你不用搬箱子直接在仓库里翻档案办公室桌上只放一张索引卡记着每份材料在哪一层哪个位置。如果经常查某几份你再把它们从箱子里拿出来放在最近的抽屉里这就是本地缓存。正是因为可搜索快照的存在Cold 和 Frozen 层的数据不再需要占用完整的本地副本空间。本地磁盘上只需要保留索引元数据和查询缓存绝大多数 segment 数据都躺在对象存储里。日志这种老数据查询频率极低的场景用这个方案能把本地磁盘占用“压缩”掉一大半。S3、对象存储、MinIO 都支持社区里也有不少团队直接用 MinIO 内网搭建低成本冷存储池。2.3 ILM 就是这套体系的“自动转运中枢”分层节点和可搜索快照单独使用能解决空间问题但无法解决“管数据”的问题。你不可能每天晚上手动去检查哪些索引该迁移、哪些该删除这时候将由 ILMIndex 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.63GB3.1 先摸清现状哪些索引占大头动手之前我习惯先跑一条命令把索引体积排序摸清底数GET _cat/indices?vsstore.size:deschindex,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 merge30 天后的数据转成可搜索快照迁移到对象存储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.nameILM 会按照 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-*/?vsstore.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本地保留一定时期是成本与体验的平衡点二是给快照仓库加上生命周期清理策略避免对象存储里积累大量已删除的过期快照那部分成本同样不可忽视。混合存储不是把数据“变没了”而是让每一份数据在正确的时间待在正确的介质上。这个思路比任何具体参数都更重要。
返回列表