跳过正文
Elasticsearch 索引策略:ILM 生命周期管理与写入性能优化

Elasticsearch 索引策略:ILM 生命周期管理与写入性能优化

·876 字·5 分钟·
目录
ELK Stack 完全手册 - 这篇文章属于一个选集。
§ : 本文

ES 集群搭起来之后,接下来最重要的事情是索引策略——分片怎么设、数据怎么流转、写入性能怎么调。这块如果不做好,用不了多久集群就会开始变慢,甚至出现磁盘告警。我们的日志平台在早期就踩过分片数设太多的坑,整个集群响应时间翻了好几倍,排查了好几天才定位到根因。这篇把实际经验都整理出来。

索引设计三要素
#

在配置任何东西之前,先把这三个要素想清楚。

分片数规划
#

ES 的一个分片对应 Lucene 的一个索引实例。分片数直接影响:

  • 写入并行度:分片越多,可以并发写入的节点越多,但单个 bulk 请求的路由开销也越大
  • 查询并行度:查询会下发到所有相关分片并发执行,分片多理论上更快,但超过节点数之后收益递减,协调开销反而更大
  • 集群元数据压力:每个分片在 Master 节点上都有状态记录,几万个分片时 Master 会明显变慢

实际规划原则:

单个分片大小建议控制在 10-50GB(日志场景),超过 50GB 的分片查询会变慢,Merge 操作也更耗时。按这个标准反推分片数:

分片数 = ceil(索引每日数据量 / 目标分片大小)

我们日志平台每天 15GB 数据,每个 rollover 周期保留 1 天的热数据,目标分片大小 20GB:

分片数 = ceil(15GB / 20GB) = 1 个主分片

加 1 个副本,每个索引 2 个分片。不要一上来就设 5 个主分片,除非你的数据量真的需要。

副本数设置
#

副本的作用:读取高可用 + 查询负载分担。日志场景建议:

  • 热数据:1 副本(保证高可用)
  • 温数据:1 副本(可以降成 0 节省空间,但失去容错能力)
  • 冷数据:0 副本(归档数据,只需要能查到即可)

ILM 可以自动在数据迁移时调整副本数,不需要手动操作。

Mapping 设计
#

Mapping 定义了字段的数据类型和索引行为,提前设计好可以避免后期 mapping 爆炸问题。

对于日志类数据,一个比较实用的 mapping 模板:

{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "@timestamp": { "type": "date" },
      "level": { "type": "keyword" },
      "service": { "type": "keyword" },
      "trace_id": { "type": "keyword", "index": false },
      "message": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "http": {
        "properties": {
          "method": { "type": "keyword" },
          "status_code": { "type": "short" },
          "path": { "type": "keyword" },
          "duration_ms": { "type": "float" }
        }
      },
      "Kubernetes": {
        "properties": {
          "namespace": { "type": "keyword" },
          "pod_name": { "type": "keyword" },
          "container_name": { "type": "keyword" }
        }
      }
    }
  }
}

关键点:"dynamic": "strict" 禁止动态字段——任何 mapping 里没有定义的字段写入时会报错,而不是自动创建新字段。这是防止 mapping 爆炸最重要的设置,后面踩坑部分会详细讲。

ILM 四阶段配置
#

ILM(Index Lifecycle Management)是 ES 管理索引数据流转的机制,从 Hot 到 Warm 到 Cold 最后到 Delete,自动完成分片分配、副本调整、索引冻结和删除。

完整 ILM 策略示例
#

PUT _ilm/policy/logs-lifecycle
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_primary_shard_size": "20gb",
            "max_age": "1d"
          },
          "set_priority": {
            "priority": 100
          }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "set_priority": {
            "priority": 50
          },
          "allocate": {
            "number_of_replicas": 1,
            "require": {
              "data": "warm"
            }
          },
          "forcemerge": {
            "max_num_segments": 1
          },
          "shrink": {
            "number_of_shards": 1
          }
        }
      },
      "cold": {
        "min_age": "30d",
        "actions": {
          "set_priority": {
            "priority": 0
          },
          "allocate": {
            "number_of_replicas": 0,
            "require": {
              "data": "cold"
            }
          },
          "freeze": {}
        }
      },
      "delete": {
        "min_age": "60d",
        "actions": {
          "delete": {
            "delete_searchable_snapshot": true
          }
        }
      }
    }
  }
}

几个关键配置解释:

rollover 触发条件

rollover 支持三个触发条件(任意一个满足就触发):

  • max_primary_shard_size:主分片大小,推荐 20-50GB
  • max_age:索引年龄,日志场景通常设 1 天
  • max_docs:文档数量,一般不用这个

注意:rollover 只有在同时满足以下条件时才会执行:

  1. ILM 策略里配置了 rollover
  2. 索引通过 alias 关联了数据流或索引别名
  3. 当前索引是别名的 write index

warm 阶段的 forcemerge

温数据不再写入,可以把多个 Lucene segment 合并成 1 个(max_num_segments: 1),减少查询时的 segment 扫描开销,节省磁盘空间(删除标记被真正清除)。

forcemerge 是 IO 密集型操作,会触发大量磁盘读写,建议在低峰期执行。ECK 环境下可以通过 ILM 的 min_age 控制执行时间窗口。

cold 阶段的 freeze

冻结(freeze)会把索引的内存状态释放掉,减少内存占用,但每次查询时需要重新加载,有延迟。如果冷数据完全不查,可以直接删副本;如果偶尔需要查,freeze 是好选择。

索引模板配置
#

ILM 策略需要和索引模板关联,这样新创建的索引才会自动应用策略:

PUT _index_template/logs-template
{
  "index_patterns": ["logs-*"],
  "data_stream": {},
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 1,
      "index.lifecycle.name": "logs-lifecycle",
      "index.routing.allocation.require.data": "hot",
      "index.refresh_interval": "10s",
      "index.translog.durability": "async",
      "index.translog.sync_interval": "30s"
    },
    "mappings": {
      "dynamic": "strict",
      "properties": {
        "@timestamp": { "type": "date" },
        "level": { "type": "keyword" },
        "service": { "type": "keyword" },
        "message": { "type": "text" }
      }
    }
  },
  "priority": 200
}

注意 "data_stream": {} 这一行——使用 Data Streams 而不是传统的 alias + index 组合,是 ES 8.x 推荐的日志数据管理方式。Data Streams 自动管理 rollover,不需要手动维护 alias。

创建数据流
#

# 使用索引模板创建数据流
PUT _data_stream/logs-myapp

# 验证
GET _data_stream/logs-myapp

写入数据时,直接写入数据流名称即可:

POST logs-myapp/_doc
{
  "@timestamp": "2026-04-11T10:00:00Z",
  "level": "INFO",
  "service": "payment-service",
  "message": "Payment processed successfully"
}

写入性能优化
#

写入性能对日志平台很关键,几个核心优化点:

Bulk API
#

单条写入和批量写入性能差异巨大。ES 的 HTTP 请求每次都有 TCP 握手、序列化、路由计算等开销,单条写入在高并发下很快就会成为瓶颈。

建议的 bulk 大小:

  • 每批 5-15MB 数据(压缩前)
  • 每批 500-5000 条文档
  • 具体数字需要根据文档大小测试调整
POST /_bulk
{ "index": { "_index": "logs-myapp" } }
{ "@timestamp": "2026-04-11T10:00:00Z", "level": "INFO", "message": "event 1" }
{ "index": { "_index": "logs-myapp" } }
{ "@timestamp": "2026-04-11T10:00:01Z", "level": "ERROR", "message": "event 2" }

客户端并发度: 并发 bulk 请求数建议等于数据节点数,超过之后收益很小,反而增加协调节点的聚合压力。

refresh_interval 调整
#

ES 默认每 1 秒刷新一次(refresh_interval: 1s),刷新会把 in-memory buffer 的数据写入新的 Lucene segment,刷新后数据才可被搜索到(近实时搜索)。

每次刷新都会创建新 segment,segment 越多查询越慢,Merge 开销越大。对于日志场景,1 分钟内的延迟通常可以接受,可以放大 refresh_interval:

PUT logs-myapp/_settings
{
  "index.refresh_interval": "30s"
}

批量导入历史数据时,可以临时关闭刷新:

PUT logs-myapp/_settings
{
  "index.refresh_interval": "-1"
}

导入完成后记得恢复。

translog 异步刷盘
#

translog(事务日志)是 ES 的 WAL,每次写操作都会写入 translog,默认每次写入都同步刷盘(request 模式),这对写入性能影响很大。

对于日志场景,数据丢失几十秒的写入通常可以接受,可以改为异步模式:

PUT logs-myapp/_settings
{
  "index.translog.durability": "async",
  "index.translog.sync_interval": "30s"
}

这样 translog 每 30 秒刷盘一次,而不是每次写入都刷。如果节点在 30 秒内崩溃,最多丢失 30 秒的数据。

注意: 这个设置在索引模板里配置,不要用 API 临时修改生产索引的 translog 设置,容易忘记恢复。

写入线程池监控
#

如果 bulk 请求出现 429 错误(Too Many Requests),说明写入线程池满了:

GET /_cat/thread_pool/write?v&h=node_name,name,active,rejected,completed

如果 rejected 持续增长,说明写入量超过集群处理能力,需要:

  1. 增加数据节点
  2. 降低写入速率(客户端限速)
  3. 增大写入队列大小(会增加内存压力,治标不治本)

踩坑记录
#

坑1:分片数设置过多导致集群变慢

这是我们踩得最深的坑。早期规划的时候,参考了网上的"经验":每天数据按 5 个主分片来,加上 7 天热数据,总分片数 = 5 * 7 * 2(含副本)= 70 个,看起来不多。

但是!我们有十几个业务服务,每个服务单独一条数据流,加上系统内置的 .monitoring-*.kibana_* 等索引,总分片数很快超过了 5000。

现象:集群查询 p99 从 200ms 涨到了 5s,Master 节点 CPU 经常 100%。

诊断:

GET /_cluster/health?pretty
# 看 active_shards 总数

GET /_cat/indices?v&s=pri.store.size:desc
# 看每个索引的分片大小,识别过小的分片

发现大量分片只有几百 MB,远没有到 20GB 的目标大小。

根因:rollover 的 max_age: 1d 条件触发了,不管分片有多小都每天 rollover,导致小分片堆积。

解决方案:

"rollover": {
  "max_primary_shard_size": "20gb",
  "max_age": "1d",
  "min_primary_shard_size": "5gb"  // ES 8.2+ 支持最小分片大小
}

同时把流量小的业务服务合并到同一个数据流,通过 service 字段区分,而不是每个服务单独一条数据流。

坑2:Mapping 爆炸(Mapping Explosion)

现象:某次上线了一个新版本,应用把 HTTP 请求的全部 headers 都打到了日志里,包括动态生成的 X-Request-* 自定义 header。默认 dynamic: true 的情况下,ES 为每个 header key 创建了一个 keyword 字段,几天内字段数从几十个暴涨到几万个。

影响:

  • Master 节点内存暴涨(维护所有字段的 cluster state)
  • 新文档写入越来越慢(每次写入都要更新 cluster state)
  • Kibana 字段列表加载超时

解决过程很痛苦——ES 不支持删除字段,只能 reindex:

# 1. 创建新索引,设置正确的 mapping
PUT logs-app-v2
{
  "mappings": {
    "dynamic": "strict",
    "properties": { ... }
  }
}

# 2. reindex 数据(会很慢)
POST _reindex
{
  "source": { "index": "logs-app-*" },
  "dest": { "index": "logs-app-v2" }
}

更好的预防方案:

{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "http": {
        "properties": {
          "headers": {
            "type": "object",
            "enabled": false  // 不索引,只存储原始 JSON
          }
        }
      }
    }
  }
}

对于结构不固定的嵌套对象,设置 "enabled": false 可以存储但不索引,避免动态字段爆炸。

坑3:ILM 策略不生效

现象:配置了 ILM 策略,但数据没有按时从 hot 迁移到 warm。

排查:

GET logs-myapp/_ilm/explain

看到 "step": "ERROR",错误信息是:"The index 'logs-myapp-000001' is not the write index for alias 'logs-myapp'"

原因:手动创建了索引但忘记设置 write index,导致 rollover 失败,ILM 状态机卡死了。

修复:

POST _aliases
{
  "actions": [
    {
      "add": {
        "index": "logs-myapp-000001",
        "alias": "logs-myapp",
        "is_write_index": true
      }
    }
  ]
}

# 然后重试 ILM
POST logs-myapp-000001/_ilm/retry

教训:使用 Data Streams 而不是手动管理 alias,可以避免这类问题。

ILM 运维常用命令
#

# 查看数据流的 ILM 状态
GET logs-myapp/_ilm/explain

# 查看某个索引当前处于哪个阶段
GET .ds-logs-myapp-2026.04.11-000001/_ilm/explain

# 强制推进到下一个阶段(调试用)
POST .ds-logs-myapp-2026.04.11-000001/_ilm/move/phase
{
  "current_step": {
    "phase": "hot",
    "action": "rollover",
    "name": "check-rollover-ready"
  },
  "next_step": {
    "phase": "warm",
    "action": "allocate",
    "name": "allocate"
  }
}

# 查看所有 ILM 策略
GET /_ilm/policy

# 查看 ILM 执行统计
GET /_ilm/stats

索引策略是 ES 运维的基础,做好了集群可以长期稳定运行。下一篇讲备份和恢复,这是保障数据安全的最后一道防线。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。
ELK Stack 完全手册 - 这篇文章属于一个选集。
§ : 本文

相关文章

Elastic Agent + Fleet:下一代统一日志采集管理实践

·1209 字·6 分钟
Filebeat + Metricbeat + Auditbeat 三个 Agent 各管一摊,配置分散难以维护。Elastic Agent 将它们统一为一个 All-in-One Agent,配合 Fleet 实现中央化管理。本文记录从部署到踩坑的完整实践过程。

PostgreSQL 运维实战:配置调优、连接池、慢查询与高可用

·1918 字·10 分钟
系统梳理 PostgreSQL 运维核心技能:从 shared_buffers、WAL 参数调优,到 PgBouncer 事务模式配置;从 pg_stat_statements 慢查询分析到 PITR 时间点恢复;以及主从流复制、膨胀表清理和 Prometheus 监控指标的完整实践。