Segment 合并

22 May 2026 – wusfe · 5 min read

Segment 合并:什么时候该 force merge

Lucene 的不可变 Segment 设计带来了一系列并发读写的好处,但也留下了一个坑——随着写入,Segment 数量不断增长,查询需要遍历越来越多的 Segment,性能也随之下降。Segment Merge 就是解决这个问题的后台机制。但 Merge 本身是一把双刃剑,本文讲清楚什么时候该主动干预、什么时候该放任自流。

Lucene Tiered Merge Policy 工作原理

ES 默认使用 Tiered Merge Policy(分层合并策略),核心思想是将大小相近的 Segment 合并在一起,逐步形成层级结构。

写入流程:
1. 新文档先写入 buffer → refresh 成小 Segment(约几 MB)
2. Tiered Merge Policy 按大小和数量选择一组 Segment 合并 → 生成更大的 Segment
3. 合并后删除旧 Segment(实际上只是标记删除,Lucene 的删除文件)

合并决策参数:
# index 级设置
index.merge.policy.max_merged_segment: 5gb      # 单个 Segment 最大 5GB
index.merge.policy.segments_per_tier: 10         # 每层最多 10 个 Segment
index.merge.policy.max_merge_at_once: 10         # 一次合并最多选 10 个 Segment
index.merge.policy.floor_segment: 2mb            # 小于此值的 Segment 优先合并
index.merge.scheduler.max_thread_count: 4        # 同时最多 4 个 merge 线程

Tiered Merge Policy 的合并决策逻辑

  1. 按 Segment 大小排序,去重(跳过正在合并的)。
  2. 计算"合并预算":允许的最大 Segment 数 = segments_per_tier 的倍数。
  3. 从最小的 Segment 开始,选择大小相近的一组(hit ratio 判定),直到总大小超过 max_merged_segment 或数量超过 max_merge_at_once。
  4. 如果遍历完还有剩余 Segment 超出预算,强制选一组合并(即使大小不匹配)。

Merge 对搜索性能的影响

正面影响

每多一个 Segment,一次查询就要多遍历一个"微型倒排索引"。假设一个 50GB 的分片有 30 个 Segment,查询需要:

  • 打开 30 个 SegmentReader
  • 在每个 Segment 的倒排表中查找 term
  • 合并 30 路结果
  • 对合并后的结果评分排序

Merge 后只剩 5 个 Segment,开销降低约 80%。

GET /my_index/_segments
{
  "verbose": true
}

返回结果中 num_search_segments 越少越好。一个健康分片的搜索 Segment 应该在 10 个左右。

负面影响

Merge 本质上是重写索引文件:读取 N 个旧 Segment → 按 merge policy 重新排序归并 → 写入新 Segment。这是一个纯 IO 操作:

merge_io_volume = sum(selected_segments_size)  # 读
                 + result_segment_size         # 写

如果合并的 Segment 总大小是 10GB,就要读 10GB、写约 10GB,磁盘 IO 冲击可观。

force merge 的正确使用场景

force merge API 强制将分片内所有 Segment 合并到指定数量:

POST /my_index/_forcemerge?max_num_segments=1

适用场景

场景 1:只读索引优化

// ILM 策略:索引进入 cold 阶段后 force merge
PUT /_ilm/policy/logs_policy
{
  "policy": {
    "phases": {
      "hot": { "actions": { "rollover": { "max_size": "50gb", "max_age": "1d" } } },
      "warm": { "min_age": "2d", "actions": { "shrink": { "number_of_shards": 1 } } },
      "cold": {
        "min_age": "7d",
        "actions": {
          "forcemerge": { "max_num_segments": 1 },
          "freeze": {}
        }
      },
      "delete": { "min_age": "30d", "actions": { "delete": {} } }
    }
  }
}

7 天前的日志几乎不再写入,force merge 到 1 个 Segment,搜索性能达到最优且无副作用。

场景 2:归档索引

不再接收写入的历史数据索引,在归档前一次性 force merge:

POST /archive_2023/_forcemerge?max_num_segments=1&only_expunge_deletes=true
  • only_expunge_deletes=true:只合并有删除标记的 Segment(用于清理大量软删除)。
  • only_expunge_deletes=false(默认):合并全部 Segment。

场景 3:大量删除后释放磁盘

POST /my_index/_forcemerge?only_expunge_deletes=true

ES 的删除是标记删除(.del 文件),不会立即释放空间。如果通过 delete_by_query 删除了 50% 的数据:

  • _forcemerge?only_expunge_deletes=true 会将剩下的 50% 合并成新 Segment,释放被删除文档占用的磁盘空间。
  • 注意:这也会带来大量 IO,要在低峰期执行。

绝对不要在生产写入索引上 force merge

将 force merge 用在持续写入的索引上是生产事故的最常见操作失误之一

原因:

  1. IO 风暴:force merge 到 1 个 Segment 意味着把所有 Segment 全部重写。如果分片是 100GB,就是 100GB 读 + 100GB 写。此时索引持续写入增量的 translog,IO 争抢下写入延迟可能从 5ms 飙升到 500ms。

  2. Merge 循环陷阱:force merge 生成一个超大 Segment 后,后续写入会再次生成小 Segment。Tiered Merge Policy 又会尝试将这些小 Segment 与超大 Segment 合并——但超大 Segment 可能超过 max_merged_segment,导致某些小 Segment 长时间无法合并,搜索退化。

  3. GC 开销:超大 Segment 合并期间,JVM 需要持有大量临时 buffer,可能触发 GC。

踩坑案例

某运维在生产订单索引上定期执行 _forcemerge?max_num_segments=1,认为能"提升搜索性能"。结果每次执行时,磁盘 IO 使用率接近 100%,写入请求超时率从 0 飙升到 20%。恰逢双十一,导致大量订单创建失败。最终方案是移除定期 force merge,让 Tiered Merge Policy 自然地做增量合并。

限制 Merge 的 IO 影响

如果担心正常 Merge 影响写入性能,可以限制 Merge 的 IO 吞吐:

PUT /_cluster/settings
{
  "persistent": {
    "indices.store.throttle.max_bytes_per_sec": "20mb"
  }
}

默认值是 20mb,即每个节点所有 Merge 操作的 IO 速率不超过 20MB/s。对于 SSD 集群可以适当调高,对于 HDD 或混合负载集群可以调低。

也可以按类型分开限制:

PUT /_cluster/settings
{
  "persistent": {
    "indices.store.throttle.max_bytes_per_sec": "40mb",
    "indices.store.throttle.type": "merge"  
  }
}

force merge 的最佳实践 Checklist

# 实践 说明
1 只在只读索引上做 force merge 前确保索引停止写入
2 低峰期执行 凌晨 3 点比白天 3 点好得多
3 控制并发 _forcemerge 是异步的,一次不要对多个索引同时执行
4 限速 配合 max_bytes_per_sec 限制 IO 带宽
5 一个 Segment 不是银弹 max_num_segments=1 对超大索引不现实,5-10 个也可接受
6 监控磁盘空间 force merge 需要额外的临时空间(双写)
7 配合 ILM 自动化 让 ILM 在 warm/cold 阶段自动做 force merge

小结

Segment Merge 是 ES 自动运维的一部分,绝大多数时候不需要人工介入。记住两条铁律:force merge 只用于只读/归档索引限制 Merge IO 避免影响业务写入。如果你的索引还在持续写入而查询变慢了,应该优先从 Query Cache、filter 优化、分片规划等方向排查,而不是寄希望于 force merge。