Elasticsearch 索引生命周期管理(ILM):从概念到落地实践与避坑指南

发布时间:2026/9/24 23:30:50
Elasticsearch 索引生命周期管理(ILM):从概念到落地实践与避坑指南 做了这么多年 Elasticsearch 开发和运维我最深的体会是ES 真正考验人的不是在写入那一刻而是数据日积月累之后你怎么还能保证集群不崩、磁盘不爆、查询不慢。索引生命周期管理ILMIndex Lifecycle Management就是 ES 官方提供的一整套自动化治理机制专门用来解决“索引无限膨胀”这个老大难问题。今天这篇就围绕 ILM 的核心机制、落地配置、常见坑点展开顺便把面试里最爱问的那几个点也一次性讲透。1. ILM 到底是什么它解决了我哪块心病1.1 没有 ILM 的日子我都是怎么熬过来的如果你维护过任何规模超过单机的 ES 集群大概率经历过这样的场景业务方每天往 ES 里灌日志索引按天命名比如nginx-access-2026-01-01一开始磁盘很充裕大家相安无事。可三个月之后你打开监控面板会发现磁盘使用率直奔 80%再过两周某天半夜你被告警电话叫醒集群因为触发高水位只读写入全部失败。那时候的常规做法是什么写一堆 shell 脚本和 crontab 定时任务凌晨三点跑一个脚本先把七天前的索引关掉close把三十天前的索引删掉delete还要小心翼翼地把某个大索引的副本数调成 0 来省空间。这方案能用但问题很突出——脚本和业务耦合很深删错索引没人能救你每次磁盘压力变化都要手动调脚本参数集群一旦跨版本升级脚本可能因为 API 变动直接失效。所以当 ES 6.6 正式推出 ILM 时我第一反应是这玩意儿终于来了。它把索引从创建到删除的完整生命周期管理变成了策略声明式配置你只要告诉 ES“这个索引应该经历哪些阶段、每个阶段做什么动作”剩下的全部由 ES 后台的 ILM 服务自动调度执行。1.2 ILM 的核心思想把索引当成会变老的数据对象ILM 的基本模型是把一个索引的一生分成若干阶段phase最常见的划分是Hot热阶段索引还在高频写入数据需要被频繁查询修改通常存放在高性能磁盘上。Warm温阶段索引不再接收写入查询频率下降可以做一些段合并和副本调整来节省资源。Cold冷阶段索引很少被访问可以迁移到廉价存储甚至可以只保留一份数据。Frozen冻结阶段比冷数据更“冰”几乎不查询主要为了归档保留需要时再加载。Delete删除阶段数据超过保留期限自动物理删除索引。每个阶段可以配置一个或者多个动作action比如rollover滚动、shrink收缩、forcemerge段合并、allocate节点迁移、snapshot快照、delete删除等。整个过程对业务方几乎是透明的你只需要保证索引的写入方式是“别名滚动索引”的模式。我对这个设计最满意的地方是它把运维经验从“脚本逻辑”上升成了“数据治理策略”。不同团队可以通过一套规范化策略来管理不同业务的数据新增索引自动套用不需要每个业务方都写一遍轮询脚本。这也是为什么 ILM 从推出到现在几乎是所有中大型 ES 集群的必备配置。提示ILM 是在索引模板index template层面和索引设置层面生效的不是你随便建个索引它就自动管。后面落地部分我会详细演示。2. 深入拆解五个阶段的背后设计逻辑2.1 Hot 阶段别让 rollover 成为一个摆设Hot 阶段是最容易被人误解的部分。很多初学者以为 ILM 就是“索引老了我去压缩一下”其实 ILM 里分量最重的反而是 Hot 阶段的rollover动作。rollover 的核心是解决单索引无限变大的问题。日志类数据如果每天一个索引量大的时候单索引也可能几十 GB查询性能和分片管理都会变得很尴尬。rollover 的做法是当索引满足特定条件比如主分片存储超过 50GB或者文档数超过 5000 万或者时间超过 1 天就自动创建新索引并且把别名指向新索引。这样所有写入都通过别名进入当前活跃索引检索的时候也是通过别名跨多个索引查。我来说一个容易踩的坑。很多人在 Hot 阶段只配了rollover却忘了设置最大索引数量。举个例子你的写索引别名是app_log_write如果业务上不再做任何限制rollover 会一直创建新索引从app_log-000001滚到app_log-000999。这个滚动本身没问题但如果没有后续的 warm/cold/delete 策略兜底它跟手动建索引没有本质区别磁盘照样会爆炸。所以 Hot 阶段的完整写法通常长这样{ policy: { phases: { hot: { min_age: 0ms, actions: { rollover: { max_size: 50gb, max_age: 1d, max_docs: 50000000 }, set_priority: { priority: 100 } } } } } }min_age设为0ms表示索引一创建就进入 Hot 阶段。set_priority是给恢复和分片分配用的优先级Hot 数据优先级要设高这样集群在节点重启或分片重新分配时会优先恢复热数据。2.2 Warm 阶段压缩、合并、降副本一套带走当一个索引被 rollover 之后它就成了旧索引按理说应该尽快进入 Warm 阶段。Warm 阶段最常见的三个动作是allocate、forcemerge、shrink。allocate把索引迁移到指定节点或指定属性节点例如require: {box_type: warm}这样你可以把热数据放在 SSD 节点上温数据迁移到机械硬盘节点。forcemerge把索引的多个分段强制合并成指定数量的段。ES 每个段都有内存开销和存储开销段越多查询越慢。日志类索引进入 Warm 后不再写入合并成一个段最省资源。shrink减少主分片数。比如写入时用了 10 个主分片查询量下降后可以缩到 1 个极大降低分片管理开销。set_priority调低优先级让它别跟热数据抢资源。我实际项目中一个比较典型的 Warm 配置是这样warm: { min_age: 2d, actions: { allocate: { number_of_replicas: 1, require: { box_type: warm } }, forcemerge: { max_num_segments: 1 }, shrink: { number_of_shards: 1 }, set_priority: { priority: 50 } } }这里需要单独提醒一个经验shrink 操作非常耗资源而且需要索引是只读状态。如果索引还在被写入shrink 会一直卡住。ILM 在进入 Warm 阶段时默认会先把索引设为只读这个是自动做的但假如你在外部手动把index.blocks.write关了反而会导致 Shrink 异常。另外forcemerge 和 shrink 哪个先哪个后从我经验看先 forcemerge 再 shrink 更稳。因为 shrink 本质上是通过硬链接方式从源索引创建新索引分段越少shrink 的速度越快、出错概率越低。2.3 Cold 阶段便宜存储和快照归档才是主角Cold 阶段的定位是“几乎不查询但要保留”。在这个阶段索引仍然在集群内可以访问但查询性能已经不是优先考虑的点。常见的动作仍然以 allocate 为主比如把它迁移到box_type: cold的节点也就是大容量机械盘或低频存储节点。这里有个容易被忽略的小知识点ILM 从 Warm 进入 Cold 时并不会自动做快照。很多人以为能恢复到冷存储就是快照其实allocate只是把索引迁移到另一批节点快照是单独的snapshot动作。如果你想把索引长期归档到对象存储或 HDFS需要显式配置 snapshot action这样删除索引之后备份还在随时可以恢复。快照动作配置如下cold: { min_age: 30d, actions: { allocate: { require: { box_type: cold } }, snapshot: { repository: my_s3_repo, ignore_unavailable: false } } }使用了snapshot之后要留意索引的存储形态。在 ES 7.x 里如果你配置了快照并且索引被 ILM 标记为“searchable snapshot”索引数据本身会被卸载查询时需要从快照恢复首次查询会慢一些这个行为会对业务产生一定影响。如果是允许接受秒级或更慢查询的场景问题不大但如果业务要求毫秒级响应冷数据考核要单独设计。2.4 Frozen 阶段和 Delete 阶段能省则省当断则断Frozen 阶段是 7.10 左右引入的作用在于进一步降低冷数据的常驻内存占用。它同样基于 searchable snapshot 机制索引不保留本地副本查询时即时从快照仓库拉取数据。这个阶段最适合那种几个月都不查一次、但出于合规需求必须留着的审计类数据。Frozen 阶段不是必选阶段小规模集群我一般建议直接跳过因为 searchable snapshot 的加载机制有一定开销数据量没到 PB 级别省那点内存意义不大。Delete 阶段最直接也最需要谨慎delete: { min_age: 90d, actions: { delete: {} } }Delete 动作会真正删除索引物理空间直接释放。这里有一个必须执行的生产级建议——在 Delete 之前一定要配置快照到冷备仓库。否则一旦误删数据就是真没了不是开玩笑。我在生产里遇到过一次同事手动删索引结果把备份策略漏掉的事故从那之后我定了一条铁规任何走 ILM 的索引Cold 阶段的 snapshot action 必须配Delete 阶段才能启用。3. 从零开始落地一套 ILM 策略3.1 第一步创建策略用 Kibana 还是 API 都行创建 ILM 策略有两条路。一条是图形化界面在 Kibana 的 Stack Management - Index Lifecycle Policies 里点几下就行。另一条是调用 API适合代码化运维或自动化平台集成。我更喜欢 API 方式因为策略是可以 Git 化的任何变更都能走评审流程。创建策略的完整命令如下PUT _ilm/policy/log_retention_policy { policy: { phases: { hot: { min_age: 0ms, actions: { rollover: { max_size: 50gb, max_age: 1d } } }, warm: { min_age: 3d, actions: { allocate: { require: { box_type: warm } }, forcemerge: { max_num_segments: 1 } } }, cold: { min_age: 30d, actions: { allocate: { require: { box_type: cold } } } }, delete: { min_age: 90d, actions: { delete: {} } } } } }创建策略本身没有任何副作用它只是一个配置对象。策略改名或者删除后已绑定该策略的索引会按旧的策略继续执行这是 ES 的一个常见行为运维上要注意不要以为删了策略就“停住了”。3.2 第二步索引模板绑定策略光有策略不够你得让索引一创建就知道自己归谁管。做法是在索引模板中设置index.lifecycle.name同时设置index.lifecycle.rollover_alias。PUT _index_template/app_log_template { index_patterns: [app-log-*], template: { settings: { number_of_shards: 3, number_of_replicas: 1, index.lifecycle.name: log_retention_policy, index.lifecycle.rollover_alias: app-log-write, routing.allocation.require.box_type: hot } } }这里有个非常容易出错的地方index_patterns要覆盖实际写入的索引名而且rollover_alias指定的别名必须在索引创建时就已经存在。所以初始化流程一般分两步手动创建第一个写索引比如app-log-000001并设置别名。后续 rollover 创建的新索引会自动继承模板配置。PUT app-log-000001 { aliases: { app-log-write: { is_write_index: true } } }注意如果不先创建第一个索引后面 rollover 启动时会因为别名指向的目标不存在而失败这是初学者最常见的“策略不生效”原因之一。3.3 第三步写入、滚动与阶段迁移是怎么串联的现在把整个模型串起来理解一下。业务写入时Java 客户端或者 Canal 同步任务只认别名app-log-write数据实际落到app-log-000001。当这个索引满足 rollover 条件比如达到 50GBILM 会自动创建app-log-000002并且把别名切成指向新索引。这时候app-log-000001不再接收写请求但它索引状态里仍保留 ILM 策略配置。接下来ILM 服务会周期性扫描所有托管索引默认每 10 分钟检查一次判断app-log-000001是否满足进入下一阶段的时间条件。比如 Warm 阶段配了min_age: 3d从第一个索引创建时间算起满足条件后就开始执行 allocate 和 forcemerge。整个迁移过程不需要任何人介入。这里我说一下“周期扫描”这个细节。ILM 的决策不是实时的默认indices.lifecycle.poll_interval是 10 分钟。所以你在 Kibana 里看到某个索引明明满足条件却迟迟不动先别慌等一个轮询周期再看大概率自己就跑了。如果过了两个周期还没动静再往下的游标方向排查。4. 真实场景Java 异步写入与 Canal 同步下的 ILM 实践4.1 Java 异步写入时如何保证滚动索引不掉链子用 Java 操作 ES 写入常见的方案是RestHighLevelClient或新版ElasticsearchClient。大量日志场景下通常会用 Bulk API 异步提交把写入吞吐拉满。这里跟 ILM 配合有一个关键点写入端必须始终通过别名写入不要直接写具体索引名。很多 Java 开发图省事在代码里硬编码索引名app-log-2026-01-01某天 rollover 之后app-log-2026-01-01已经不是待写入索引了新数据全部写到旧索引里。旧索引在 ILM 阶段迁移时会被置为只读然后你的写入就开始报错。这个事故我在生产环境见过不止三次每次排查都要绕一圈才找得到根因。正确的 Java 写入端配置应该是类似这样的思路BulkRequest request new BulkRequest(); request.setRefreshPolicy(WriteRequest.RefreshPolicy.NONE); for (... ) { IndexRequest indexRequest new IndexRequest(app-log-write) .source(jsonMap); request.add(indexRequest); } bulkProcessor.add(request);只要索引名始终是app-log-write这个别名rollover 对业务完全透明新索引创建后别名自动切过去业务无感继续写。还有一个坑是refresh policy不能设得太频繁特别是在 rollover 频繁的时候强制刷新会放大段数量增加后续 forcemerge 的负担。另外Java 写入时的连接池和 bulk 大小需要跟 ILM 的 rollover 阈值统筹考虑。如果你的写作流量极大每秒钟上百万条日志rollover 条件用max_docs比max_size更稳定。因为 bulk 写入大小波动大文档数更可靠。4.2 Canal 同步 MySQL 数据到 ES 时的 ILM 选型Canal 是这些年做 MySQL 到 ES 数据同步时用的比较多的中间件通过模拟 MySQL 从库的方式读取 binlog将变更事件推送到下游。同步类数据的索引生命周期管理跟日志类有很大区别。日志类数据是 append-only按时间滚动索引非常自然。但业务数据比如订单、用户、商品是 update 密集型的同一个 doc 的 id 会反复更新。这种数据如果也按天 rollover一个严重的问题就出现了——同一个逻辑文档可能散落在多个索引里查询时必须跨多个索引去查更新时必须精确找到它实际所在的那个索引否则会出现“数据明明写进去了却查不到”或者“更新了旧分片”的坑。我的建议是同步类数据尽量不要按天 rollover而是按固定大小和固定周期双条件控制rollover 频率要远低于日志类。如果单日数据量不大甚至可以把 rollover 条件中的max_age拉长到 30 天让索引生命周期更长。至于清理仍然是 ILM 来管但 min_age 一般要给到 180 天以上并且要跟业务方确认数据保留期限之后再设 delete 策略。Canal 同步还有一个细节binlog 重放可能产生大量 update 请求如果索引正在做 forcemerge 或 shrink这些阶段要求索引只读那么同步任务会出现短暂写入失败。这个问题的解决方案很粗暴也有效——给同步任务加重试机制并且把阶段切换时间放在业务低峰期。ILM 支持配置不同阶段的min_age你完全可以把 Warm 阶段动作安排在凌晨两点之后的时段。4.3 向量检索数据如何使用 ILM 做降级保留最近很多团队在折腾向量检索ES 的 dense_vector 加 HNSW 算法确实方便但向量索引的存储开销和内存开销都非常惊人。一个 1024 维的向量单条光向量字段就要占用好几 KB加上索引结构开销百万级数据轻松吃满内存。向量数据如果做长期保留我建议走这样一个策略组合Hot 阶段保证实时写入和最近数据的近实时检索Warm 阶段做向量段合并forcemerge 对 HNSW 图结构也有整理作用冷阶段配合 snapshot 把历史向量索引写到对象存储。查询需求如果只是偶尔召回测试可以从快照恢复实时向量检索只保留最近 N 天数据。这里有个经验供参考向量索引的检索性能和索引段数量高度相关段越多召回时需要遍历的图就越多。所以定期 forcemerge 的价值在向量场景下被放大了。但要注意forcemerge 非常耗费 CPU 和内存对向量索引尤其明显一定要在业务低峰期且磁盘 IO 有余量的时候触发。5. 常见问题与排查实录5.1 策略配置了但是索引一直停留在 hot 阶段这个问题一半以上是轮询周期还没到另一半就是前面说的“第一个索引和别名没有按规范创建”。如果你用 Kibana 的 Dev Tools 查询发现某个索引一直处于 hot可以这样排查GET app-log-000001/_ilm/explain执行之后返回结果里能看到当前所在阶段、动作、以及执行失败原因。常见报错包括rollover_alias does not point to a write index说明别名没有正确指向当前索引。index is not the write index for alias别名被别的索引抢占了或者你手动创建新索引时没有把is_write_index设为 true直接通过别名 PUT 了一个新索引。failed to rollover通常是max_size等条件里对应的指标暂时没拿到或者磁盘空间不够创建新索引。_ilm/explain是我每天工作中用得最多的排查工具没有之一。任何 ILM 异常第一反应就是先 explain 一下。它给出的step_info里往往有非常明确的错误原因比你在日志里捞半天快得多。5.2 磁盘高水位和 ILM 打架这个问题在高密度集群里特别容易碰到。例如你设置了 Hot 阶段的索引丢在box_type: hot节点上当高水位触发时ES 默认禁止写入ILM 的 rollover 也创建不了新索引。你会看到集群状态黄色甚至红色索引的 ILM 状态卡在 hot一直重试。对策有两个方向。第一是给 ILM 阶段迁移预留足够的缓冲空间不要让热数据节点超过 70% 容量。第二是设置cluster.routing.allocation.disk.watermark.flood_stage时要理解它和 ILM 恢复机制的协作方式。flood_stage 的默认行为会阻止写操作ILM 的 rollover 也因此失败。在生产里我一般会把热数据节点的实际容量控制在 50% 到 60%一旦超过 70%应该能靠 ILM 自动把老索引迁走到 warm 节点让热节点释放空间。还有一个很隐蔽的坑高水位只读之后即使你把索引迁移走了ES 不会自动解除只读 block。你需要手动执行PUT app-log-000001/_settings { index.blocks.write: false }或者通过POST /_all/_settings批量解除但要小心不能把本来需要只读的索引也放开了。这里我一般会写一个小脚本只解除 ILM 托管的非 warm/cold 索引。5.3 强制合并之后查询变快了还是变慢了以前我刚用 forcemerge 时也困惑过把所有段合并成一个段查询不是应该最快吗实际上要分场景。日志类数据按时间范围查询时一个段反而好因为不需要跨段做结果合并。但如果是高基数、高并发查询段变少并不一定最理想。而且 forcemerge 在合并小段时非常消耗磁盘 IO如果你在业务高峰期跑查询延迟会明显抖动。所以我的实践原则是forcemerge 只针对不再写入的索引且 max_num_segments 设为 1 通常没问题但执行时间要安排在凌晨窗口如果索引数据量极大forcemerge 可能跑几个小时甚至跨天要留足时间余量。另外forcemerge 前把副本数临时调成 0完成后恢复副本数可以显著缩短合并时间但要注意这期间该索引没有副本保护一旦节点故障数据会丢这个窗口越短越好。5.4 用 Dev Tools 快速查看 ILM 各阶段状态在 Kibana 的 Dev Tools 里我常用下面这几个命令来排查 ILM 问题# 查看某个索引的 ILM 详细状态 GET app-log-000001/_ilm/explain # 查看集群当前的 ILM 策略列表 GET _ilm/policy # 手动触发 ILM 状态刷新 POST /_ilm/retryPOST /_ilm/retry是个非常有用的兜底操作。当 ILM 动作失败后索引会被标记为等待重试修复根因后执行 retry 可以让它立刻重新尝试不需要等下一个轮询周期。如果是磁盘空间不足导致的失败你清理空间后执行 retry索引很快就会继续往下一阶段走。6. 最后分享几个我踩过坑才换来的经验第一点ILM 策略变更不是立即生效的。你改了策略之后已存在的索引要等到下一轮 poll 才会重新评估而且如果索引正处于某个 action 的执行中间状态策略变更可能要等这个 action 执行完成才能体现出来。不要一改策略就质疑怎么没效果先等两个 poll 周期再说。第二点不要把所有数据都塞进同一套 ILM 策略。虽然 ILM 很省事但业务类型差异大保留时长、恢复优先级、分片大小要求都不一样。一锅烩的结果就是要么早期删了重要数据要么把垃圾数据白白存了三年。最合理的做法是按业务线分策略比如日志类一套策略订单类一套策略向量类一套策略规则清晰事后可查。第三点如果你用 Canal 同步或者基于 Java 异步批量写入务必把“别名写入”当成硬性约定写入团队规范。我在这个坑上面救过不少团队核心定位思路都一致——先查写入端打的是别名还是具体索引再查 ILM 的 explain 状态基本上十分钟能定位到问题。ILM 说到底是 ES 数据治理的基础设施它不会直接让你的查询变快但能让集群长期保持健康让运维从救火中解放出来把时间花在真正有价值的事情上。如果你刚接触 ES我的建议是先把一个简单的“按天滚动60 天删除”策略跑通逐步加上 Warm 和 Cold 阶段运维边界和成本边界自然会清晰起来。