IndexWriter NRT 每 flush 一次就多一个 Segment;若从不合并,查询要在大量小段上重复 相交与打分,词典与文件句柄开销也会膨胀。MergePolicy 决定何时、哪些段合成新段;删除 则在段不可变前提下用 liveDocs 位图标记「逻辑删除」,把物理空间回收推迟到合并之后。
本文是系列第 8 篇:TieredMergePolicy
的分层直觉、软删语义、空间回收延迟,以及 Elasticsearch
forcemerge 为何是运维重操作。
版本锚定:Apache Lucene 9.x / 10.x 默认
TieredMergePolicy;Elasticsearch 8.x 分片内嵌同一套 merge 调度,并额外提供_forcemergeAPI。本篇无自测集群 I/O 数字。
一、为什么需要 merge:段只增不改
Lucene 段文件 flush 后不可原地更新(见第 7 篇)。更新文档在工程上通常是 delete + re-add;删除并不立刻从倒排文件里抹掉 posting,而是:
- 在段的
liveDocs(
Bits)里把 docID 标为「死」; - 查询迭代 postings 时跳过死 doc;
- 等 merge 把多个段(含死 doc)拷贝进新段时,才 物理省略 死 doc,回收磁盘。
因此:删除立即可见于搜索(逻辑不可见),空间不立即回收(物理仍占位)——这是设计权衡,不是实现 bug。
flowchart LR
del["deleteDocuments"]
live["liveDocs bitset<br/>per segment"]
q["Query iterator"]
merge["MergePolicy"]
newseg["New merged segment<br/>dead docs omitted"]
del --> live
live --> q
q -->|"skip dead"| score["Scoring"]
merge --> newseg
live -->|"input to merge"| merge
二、TieredMergePolicy 的分层直觉
Lucene 默认 TieredMergePolicy(替代早期
LogByteSizeMergePolicy 等)把段按
近似大小
划入层级(tier),在同一层内积累足够多段时触发合并,产出更大段并
上浮 到更高 tier。目标是在
写放大、查询段数、合并
I/O 峰值 之间折中。
2.1 参数直觉(非完整源码复述)
| 概念 | 作用 |
|---|---|
maxMergedSegmentBytes |
单段大小上限,防止合并出巨型段拖慢打开 reader |
segmentsPerTier |
每层允许多少个段再触发合并 |
maxMergeAtOnce |
一次合并最多吃几个段,控制单次 I/O burst |
deletesPctAllowed |
段内删除比例过高时优先合并以回收空间 |
Tier 图景(示意,非精确阈值):
flowchart TB
t0["Tier 0: many small segments<br/>fresh flush"]
t1["Tier 1: fewer medium segments"]
t2["Tier 2: large segments"]
t0 -->|"merge N at once"| t1
t1 -->|"merge"| t2
t2 -->|"cap by maxMergedSegmentBytes"| t2
与 LSM compaction 的 leveled / tiered 争论(Dayan & Idreos, SIGMOD 2017 等)类比有帮助,但 Lucene merge 操作的是 已不可变段文件,不是可变 memtable;写放大 表现为合并时重复读写 postings/docvalues,而非 WAL 回放。
2.2 与查询、NRT 的耦合
- 段越多,BooleanScorer 跨段归并成本越高;
- 合并期间 CPU/磁盘繁忙,NRT refresh 与搜索尾延迟可能抖动(第 7、17 篇);
- 合并完成会删除旧段文件,但仅当 没有 reader 引用 旧段——长寿命搜索或慢查询会推迟文件删除。
三、软删除:liveDocs 与可见性
3.1 硬删 API 路径
IndexWriter.deleteDocuments(Term...) /
deleteQuery 在受影响段上更新
liveDocs(或记录删除队列,flush 时写入 .liv
等侧车文件,依版本 codec 而定)。已打开的旧
DirectoryReader 仍看见删除前视图,直到
openIfChanged——与 NRT 追加对称。
3.2 软删除(Soft Deletes)
Lucene 支持用 IndexWriter.softUpdateDocument
等路径配合
SoftDeletesRetentionMergePolicy:被软删的文档在合并时可选择保留(供
IndexSearcher 的
SoftDeletesDirectoryReader
看见),用于「保留历史版本」「可恢复删除」类需求。
Elasticsearch 8.x 在 API 层暴露 soft
delete 相关语义(如 _delete_by_query
与某些保留策略)时,底层仍依赖 Lucene
文档存活位图与合并策略;精确 API 行为以 ES Reference
为准,本篇只钉 位图 + 合并
机制。
| 类型 | 查询默认 | 空间回收 | 典型用途 |
|---|---|---|---|
| 硬删除 | 不可见 | merge 后回收 | 通用删除 |
| 软删除 | 默认不可见,专用 reader 可见 | 可配置保留 | 版本化、审计 |
3.3 常见误解
- 「delete 之后磁盘立刻变小」。liveDocs
只改可见性;
du不变直到 merge 拷贝省略死 doc。 - 「段内 50% 删除就该 force
merge」。应先让 TieredMergePolicy 因
deletesPctAllowed推动合并;force merge 是全量重写,代价完全不同。 - 「副本也会独立 merge」。ES 主分片 merge;副本复制的是段文件结果,不在每个副本重复独立 merge 策略(减少副本 CPU,但复制大合并仍吃网络)。
3.4 更新文档 = 删除 + 追加
updateDocument(Term, doc) 在 Lucene 语义上是
按 Term 删除旧 doc + 写入新 doc(新
docID)。高频主键更新导致:
- 旧 doc 在 liveDocs 中标死,但仍占空间;
- 新 doc 进入新 flush 段或缓冲;
- 段数与删除率双升,加速触发 Tiered 合并。
这与 OLTP「原地 UPDATE 行」直觉不同;写放大 是全文引擎更新密集时的根本代价之一。
3.5 MergeScheduler 与资源隔离
ConcurrentMergeScheduler
默认在独立线程执行合并,可与搜索线程争用磁盘。可调:
maxThreadCount/maxMergeCount:限制并行 merge;MergeRateLimiter:节流字节率,避免打满 IOPS;- ES:
index.merge.scheduler.*与节点层indices.store共同约束。
争论:激进 merge 降低段数、改善查询,但在 云盘突发 IOPS 上限 下可能造成写入 stall——与 RocksDB compaction stall 类似,属存储引擎通用张力,无单一最优配置。
四、空间回收延迟与运维观感
4.1 延迟从哪来
- 删除只改位图,postings 仍在;
- 合并是后台、可节流(
ConcurrentMergeScheduler线程数、MergeRateLimiter); - 高峰写入 时 merge 常让位于 flush,删除堆积;
- 持续更新 使「热段」反复小合并,冷段长期带空洞。
运维上表现为:_cat/segments 里
deleted docs 占比
高、磁盘占用大于「逻辑文档量」,搜索 CPU 偏高。
4.2 监控信号(定性)
| 信号 | 可能含义 |
|---|---|
| 段数量持续上升 | flush 快于 merge,或 merge 被限流 |
deleted 占比高 |
更新/删除密集,等待合并回收 |
| merge 线程长期满 | 磁盘或 index.merge.scheduler 瓶颈 |
| 单段过大 | 接近 maxMergedSegmentBytes,打开 reader
变慢 |
本篇不给出「占比 > X% 必须 force merge」的阈值——依赖硬件与查询 SLA,应结合慢查询与磁盘趋势决策。
五、force merge:代价与边界
IndexWriter.forceMerge(maxNumSegments) / ES
_forcemerge?max_num_segments=1 试图把分片
压到指定段数,本质是
重写几乎全部数据:
- 写放大极高:读旧段 + 写新段,相当于全量压缩;
- 阻塞风险:长事务式 I/O,与写入、搜索争抢资源;
- 不可轻易中断 的生产事故源;ES 文档将其标为昂贵操作;
- Lucene 段不可变 前提下,这是唯一「主动整段」手段,不能当日常 delete 的替代。
适用边界(工程判断,非规则引擎):
- 只读索引、批量导入后 一次 压段优化查询;
- 重大版本迁移后整理段形;
- 不宜 高频调用、不宜在峰值写入期、不宜指望它替代定期 merge。
flowchart TB
normal["Background merge<br/>TieredMergePolicy"]
force["forceMerge API"]
cost["IO and CPU spike"]
gain["Fewer segments<br/>reclaim deletes"]
normal --> gain
force --> cost
force --> gain
Elasticsearch 另提供 only_expunge_deletes
等变体,侧重 清空洞
而非极致压段数——仍属重操作,见 8.x Index segments
文档。
5.1 ILM rollover 与「只读段形整理」
Elasticsearch Index Lifecycle Management 在 rollover 后把旧索引标为只读,再 force merge 到单段 是常见模式:写入路径已切断,合并不与 flush 争抢,代价可控。仍在 热写索引 上照搬此模式,往往触发第 17 篇类故障。
| 阶段 | 写入 | merge 策略 |
|---|---|---|
| 热索引 | 持续 bulk | 仅后台 Tiered |
| 温/冷 | 少写 | 可选 forcemerge 压段 |
| 只读归档 | 无 | 单段 optimize 查询 |
5.2 与写入限流、合并积压的联动
当 ES indexing_pressure 或 Lucene
侧写入背压触发时,bulk 会重试,但 删除与 merge 仍占
I/O。典型恶性循环:高删除率 → 段内空洞多 → merge
更忙 → refresh 变慢 → 客户端加大写入重试。排障应同时看
merge 线程、flush 耗时、deleted
占比,而非只扩容数据节点(第 17
篇)。
六、学术谱系与开放问题
- 段合并 与外部排序、静态索引构建(Zobel & Moffat 倒排综述)同源:批量构建可全局 optimize,增量 场景才需要在线 merge。
- 争论:更大段 → 更少 per-segment 开销,但 更大 merge 突发 与 更长 reader 打开时间;Tiered 策略是工程折中,非最优解证明。
6.1 段信息文件与删除侧车
每个 Segment 附带
SegmentInfo(*.si
等,依版本命名),记录 doc 数、是否含删除、codec
名、诊断属性。删除位图可能落在 live docs
侧车(历史上 .liv
一类文件)。SegmentInfos 在 commit 点列出当前
世代 的段列表——IndexWriter
崩溃后,未 commit 的 flush 段可能 孤立
于目录中,由 IndexWriter
启动时清理或合并,具体行为见官方 Index recovery
说明。
理解这些文件有助于对照 _cat/segments?v
输出:运维看到的 generation、size、deleted
即上述元数据的外显,而非神秘黑盒。
开放问题:
- 云盘限 IOPS 下 merge 调度与搜索 SLA 的自动平衡仍靠运维规则;
- 高比例更新索引是否应 时间分区 + 只读 force merge(ILM rollover)而非单索引硬压;
- 与 向量段 混合时 merge 成本是否进一步推高(第 15 篇)。
七、小结
三句话小结
- 删除在 Lucene 是 liveDocs 逻辑标记,查询跳过死 doc;物理回收 等 merge 把死 doc 拷贝时省略。
- TieredMergePolicy 用小段积成中段、大段,在段数与写放大之间折中;删除比例高会 触发更积极合并。
- force merge 是全量重写式重操作,只适合作只读整理,不能替代日常删除与后台 merge。
下一篇进入查询侧:BooleanScorer、两阶段与 Collector。
参考资料
规范 / 文档
- Apache Lucene
9.x/10.x:
TieredMergePolicy、MergePolicy、MergeScheduler、SoftDeletesRetentionMergePolicy、IndexWriter#forceMerge。 - Elasticsearch 8.x Reference:Index
segments、
_forcemerge、Merge policy(版本对应说明)。
论文 / 综述
- Zobel, J. & Moffat, A., Inverted Files for Text Search Engines, ACM Computing Surveys 2006(段与合并背景)。
站内
返回 系列目录 | 上一篇:IndexWriter NRT | 下一篇:查询执行
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】全文引擎全景:架构叙事与 Lucene 内核之间的一层
定位 Lucene 9.x/10.x 库内核与 Elasticsearch 8.x 服务层相对 architecture/42、storage/29、向量引擎与 PG GIN 的分工;以最小索引故事建立坐标系,交代倒排索引学术谱系与 18 篇路线图。
【全文检索引擎】IndexWriter 与 NRT:从缓冲到可打开的 Reader
拆解 Lucene IndexWriter 的 RAM 缓冲、flush 出段与 DirectoryReader.open(IndexWriter) 近实时语义;说明 Searcher 刷新与「刚写入即可搜」在库层与 ES refresh 层各自保证什么。
【全文检索引擎】Lucene · BM25 · Segment · Elasticsearch NRT
补齐搜索架构叙事与 RAG/向量引擎之间的全文检索内核层:以 Lucene 9.x/10.x 拆解 Analyzer、FST、postings、BM25 与 IndexWriter,并以 Elasticsearch 8.x 拆解分片、refresh/translog 与查询路径;OpenSearch 对照与选型收束。
【全文检索引擎】Lucene 文档模型:Field、docID 与正排/倒排
拆解 Lucene Index/Document/Field 与段内 docID 分配;说明 indexed、stored、docValues 三组正交开关;对照正排与倒排访问路径,为 Analyzer 与 postings 篇奠基。