上一篇解决了集群协调——master 选举和 cluster state 同步。这一篇进入 segment 随时间增长后怎样管理。

第 02 篇介绍了 segment 是不可变的,写入产生新 segment。随着写入持续,segment 数量会不断增长。segment 太多搜索变慢,删除的文档占据的空间不会自动回收。Segment merge 和 ILM 分别在 Lucene 层和 index 层解决这个存储生命周期问题。

本文抓两个问题:为什么需要 segment merge,以及 ILM 怎样管理索引的生命周期。

Segment Merge

为什么需要 merge

每次 refresh 产生一个新 segment。持续写入的 index 会积累大量小 segment。问题有二:

  • 搜索时需要遍历所有 segment。segment 越多,搜索越慢。
  • 删除文档只是在 .liv 文件中打标记,物理空间不回收。只有 merge 时才真正删除标记文档、回收空间。
1
2
3
4
5
6
7
8
9
10
11
12
写入阶段:
refresh → seg0(10 docs)
refresh → seg1(10 docs)
refresh → seg2(10 docs)
...
refresh → seg99(10 docs)
→ 100 个小 segment,搜索遍历 100 次

merge 后:
seg0..seg9 → merged_seg_A(100 docs)
seg10..seg19 → merged_seg_B(100 docs)
→ 10 个大 segment,搜索遍历 10 次

TieredMergePolicy

ES/Lucene 默认使用 TieredMergePolicy,自动在后台合并 segment。核心逻辑:

  • 把 segment 按大小分层(tier)。
  • 在同一层中找到可以合并的候选集(通常 10 个左右小 segment)。
  • 合并为一个更大的 segment。
  • 合并后的 segment 可能进入更高层。

TieredMergePolicy 的分层合并逻辑:小 segment 在底层积累到一定数量后被合并为更大的 segment,合并产物可能继续参与更高层的合并。

flowchart TB
    subgraph 写入产生的小 Segment
        s0[seg0<br/>2MB]
        s1[seg1<br/>2MB]
        s2[seg2<br/>2MB]
        s3[seg3<br/>3MB]
        s4[seg4<br/>2MB]
        s5[seg5<br/>3MB]
        s6[seg6<br/>2MB]
        s7[seg7<br/>2MB]
        s8[seg8<br/>3MB]
        s9[seg9<br/>2MB]
    end

    subgraph 第一轮 Merge
        m1[merged_A<br/>23MB]
    end

    subgraph 多轮 Merge 后
        m2[merged_X<br/>200MB]
    end

    s0 & s1 & s2 & s3 & s4 & s5 & s6 & s7 & s8 & s9 --> m1
    m1 -.->|与同层其他 segment<br/>继续合并| m2

关键参数:

参数 默认值 含义
max_merge_at_once 10 一次最多合并几个 segment
max_merged_segment 5GB 合并后的 segment 最大不超过此值
segments_per_tier 10 每层允许的 segment 数
floor_segment 2MB 小于此值的 segment 优先被合并

merge 在后台线程执行,受 index.merge.scheduler.max_thread_count 控制并发线程数(默认 = max(1, min(4, CPU/2)))。merge 消耗 I/O 和 CPU,过于激进的 merge 会影响写入和搜索。

Force Merge

手动触发合并:

1
POST /my-index/_forcemerge?max_num_segments=1

max_num_segments=1 把所有 segment 合并为一个。这会消耗大量 I/O,只应在不再写入的索引上使用(如历史数据索引、日志归档索引)。

在持续写入的 index 上调 force merge 会导致:新写入产生新 segment → 又需要 merge → 无限循环。

删除文档与空间回收

删除一个文档时,ES 在 segment 的 .liv 文件中标记该文档为已删除。已删除的文档在搜索时被跳过,但物理空间仍被占用。

只有 merge 时,已删除文档才真正被排除——merge 生成的新 segment 不包含已删除的文档,旧 segment 的空间被回收。

1
2
seg0: [doc1, doc2(deleted), doc3] → 占 3 份空间
merge → new_seg: [doc1, doc3] → 只占 2 份空间,doc2 的空间被回收

Index Lifecycle Management (ILM)

Segment merge 是 Lucene 层面的优化。ILM 在 index 层面管理索引从创建到删除的完整生命周期,典型用于日志和时序数据。

五个阶段

1
Hot → Warm → Cold → Frozen → Delete
阶段 特征 典型操作
Hot 活跃写入和搜索,需要高性能存储(SSD) rollover(滚动到新 index)
Warm 不再写入,仍有搜索需求 force merge, shrink, 迁移到 warm 节点
Cold 低频搜索,可用较慢存储(HDD) 迁移到 cold 节点
Frozen 极低频搜索,数据可部分卸载 部分存储在共享存储(searchable snapshots)
Delete 数据不再需要 删除 index

ILM 的五个阶段构成一条单向状态机,index 按 min_age 条件自动向下游迁移:

stateDiagram-v2
    [*] --> Hot
    Hot --> Warm : min_age 30d
    Warm --> Cold : min_age 90d
    Cold --> Frozen : min_age 180d
    Frozen --> Delete : min_age 365d
    Delete --> [*]

    Hot : 活跃写入 + 搜索<br/>SSD 存储<br/>rollover 触发新 index
    Warm : 只读<br/>force merge + shrink<br/>迁移到 warm 节点
    Cold : 低频搜索<br/>HDD 存储
    Frozen : 极低频搜索<br/>searchable snapshots
    Delete : 删除 index<br/>不可逆

ILM Policy 定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
PUT _ilm/policy/my-lifecycle
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "7d"
}
}
},
"warm": {
"min_age": "30d",
"actions": {
"forcemerge": { "max_num_segments": 1 },
"shrink": { "number_of_shards": 1 },
"allocate": { "require": { "data": "warm" } }
}
},
"cold": {
"min_age": "90d",
"actions": {
"allocate": { "require": { "data": "cold" } }
}
},
"delete": {
"min_age": "365d",
"actions": { "delete": {} }
}
}
}
}

这个 policy 的语义:index 在 hot 阶段持续写入,当 shard 大小超过 50GB 或 7 天后 rollover 到新 index;30 天后进入 warm(force merge + shrink + 迁移到 warm 节点);90 天后进入 cold;365 天后删除。

绑定到 Index Template

1
2
3
4
5
6
7
8
9
10
PUT _index_template/my-template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"index.lifecycle.name": "my-lifecycle",
"index.lifecycle.rollover_alias": "logs"
}
}
}

观察 ILM 状态

1
GET /logs-000001/_ilm/explain

返回当前 index 所在的 ILM 阶段、进入该阶段的时间、下一步操作等信息。

模式提炼:不可变结构 + 后台压缩 + 分层存储

1
write (append-only) → accumulate → background compaction → tiered storage → eventual deletion
系统 不可变单元 压缩/合并 分层存储
ES / Lucene Segment TieredMergePolicy + force merge ILM (hot/warm/cold/frozen)
Kafka Log segment Log compaction + retention Tiered storage (KIP-405)
HBase HFile Minor/Major compaction 无内置分层
Cassandra SSTable Size/Leveled compaction 无内置分层
S3 / 对象存储 Object Lifecycle rules (Standard/IA/Glacier)

工程迁移表

概念 Elasticsearch ILM Kafka HBase S3 Lifecycle
保留策略 min_age per phase retention.ms / retention.bytes TTL per column family Transition rules
滚动 Rollover (size/age) Topic partition rotation Region split
压缩 Force merge Log compaction Major compaction
缩容 Shrink (减少 shard) Region merge
删除 Delete action Log deletion TTL deletion Expiration

常见误解

误解一:merge 让搜索变快所以应该频繁触发。 Merge 确实减少 segment 数量让搜索更快,但 merge 本身消耗大量 I/O。在持续写入的 index 上频繁 force merge 会和写入竞争资源。应该让后台 merge policy 自动工作,只在不再写入的 index 上做 force merge。

误解二:ILM 的 delete 阶段会删除数据。 是的,delete 阶段会真正删除 index。这是不可逆的。确保 min_age 配置正确,或在删除前做 snapshot 备份。

误解三:删除文档后磁盘空间立即释放。 不会。删除只打标记,空间在 merge 时回收。如果一个 index 不再写入且删除了大量文档,需要 force merge 来回收空间。

练习

  1. 创建一个 index,分 10 批写入数据(每批之间 _refresh),用 _segments 观察 segment 数量增长。然后 _forcemerge?max_num_segments=1,再次观察。

  2. 删除几条文档,用 _segments 查看 deleted_docs 字段。force merge 后验证 deleted_docs 变为 0。

  3. 创建一个 ILM policy,绑定到 index template,用 _ilm/explain 观察 index 的生命周期状态。

系列导航

上一篇 下一篇
集群协调:Master 选举与集群状态同步 性能模型:搜索延迟、写入吞吐与调优思路

参考资料