土法炼钢兴趣小组的算法知识备份

【全文检索引擎】MergePolicy 与删除:段合并与 liveDocs

文章导航

分类入口
databasestorage
标签入口
#lucene#mergepolicy#tieredmergepolicy#livedocs#soft-delete#force-merge#elasticsearch#segment

目录

IndexWriter NRT 每 flush 一次就多一个 Segment;若从不合并,查询要在大量小段上重复 相交与打分,词典与文件句柄开销也会膨胀。MergePolicy 决定何时、哪些段合成新段;删除 则在段不可变前提下用 liveDocs 位图标记「逻辑删除」,把物理空间回收推迟到合并之后。

本文是系列第 8 篇:TieredMergePolicy 的分层直觉、软删语义、空间回收延迟,以及 Elasticsearch forcemerge 为何是运维重操作。

版本锚定:Apache Lucene 9.x / 10.x 默认 TieredMergePolicy;Elasticsearch 8.x 分片内嵌同一套 merge 调度,并额外提供 _forcemerge API。本篇无自测集群 I/O 数字。


一、为什么需要 merge:段只增不改

Lucene 段文件 flush 后不可原地更新(见第 7 篇)。更新文档在工程上通常是 delete + re-add;删除并不立刻从倒排文件里抹掉 posting,而是:

  1. 在段的 liveDocsBits)里把 docID 标为「死」;
  2. 查询迭代 postings 时跳过死 doc;
  3. 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 的耦合


三、软删除:liveDocs 与可见性

3.1 硬删 API 路径

IndexWriter.deleteDocuments(Term...) / deleteQuery 在受影响段上更新 liveDocs(或记录删除队列,flush 时写入 .liv 等侧车文件,依版本 codec 而定)。已打开的旧 DirectoryReader 仍看见删除前视图,直到 openIfChanged——与 NRT 追加对称。

3.2 软删除(Soft Deletes)

Lucene 支持用 IndexWriter.softUpdateDocument 等路径配合 SoftDeletesRetentionMergePolicy被软删的文档在合并时可选择保留(供 IndexSearcherSoftDeletesDirectoryReader 看见),用于「保留历史版本」「可恢复删除」类需求。

Elasticsearch 8.x 在 API 层暴露 soft delete 相关语义(如 _delete_by_query 与某些保留策略)时,底层仍依赖 Lucene 文档存活位图与合并策略;精确 API 行为以 ES Reference 为准,本篇只钉 位图 + 合并 机制。

类型 查询默认 空间回收 典型用途
硬删除 不可见 merge 后回收 通用删除
软删除 默认不可见,专用 reader 可见 可配置保留 版本化、审计

3.3 常见误解

  1. 「delete 之后磁盘立刻变小」。liveDocs 只改可见性;du 不变直到 merge 拷贝省略死 doc。
  2. 「段内 50% 删除就该 force merge」。应先让 TieredMergePolicy 因 deletesPctAllowed 推动合并;force merge 是全量重写,代价完全不同。
  3. 「副本也会独立 merge」。ES 主分片 merge;副本复制的是段文件结果,不在每个副本重复独立 merge 策略(减少副本 CPU,但复制大合并仍吃网络)。

3.4 更新文档 = 删除 + 追加

updateDocument(Term, doc) 在 Lucene 语义上是 按 Term 删除旧 doc + 写入新 doc(新 docID)。高频主键更新导致:

这与 OLTP「原地 UPDATE 行」直觉不同;写放大 是全文引擎更新密集时的根本代价之一。

3.5 MergeScheduler 与资源隔离

ConcurrentMergeScheduler 默认在独立线程执行合并,可与搜索线程争用磁盘。可调:

争论:激进 merge 降低段数、改善查询,但在 云盘突发 IOPS 上限 下可能造成写入 stall——与 RocksDB compaction stall 类似,属存储引擎通用张力,无单一最优配置。


四、空间回收延迟与运维观感

4.1 延迟从哪来

  1. 删除只改位图,postings 仍在;
  2. 合并是后台、可节流ConcurrentMergeScheduler 线程数、MergeRateLimiter);
  3. 高峰写入 时 merge 常让位于 flush,删除堆积;
  4. 持续更新 使「热段」反复小合并,冷段长期带空洞。

运维上表现为:_cat/segmentsdeleted 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 试图把分片 压到指定段数,本质是 重写几乎全部数据

适用边界(工程判断,非规则引擎):

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 篇)。


六、学术谱系与开放问题

6.1 段信息文件与删除侧车

每个 Segment 附带 SegmentInfo*.si 等,依版本命名),记录 doc 数、是否含删除、codec 名、诊断属性。删除位图可能落在 live docs 侧车(历史上 .liv 一类文件)。SegmentInfos 在 commit 点列出当前 世代 的段列表——IndexWriter 崩溃后,未 commit 的 flush 段可能 孤立 于目录中,由 IndexWriter 启动时清理或合并,具体行为见官方 Index recovery 说明。

理解这些文件有助于对照 _cat/segments?v 输出:运维看到的 generation、size、deleted 即上述元数据的外显,而非神秘黑盒。

开放问题

  1. 云盘限 IOPS 下 merge 调度与搜索 SLA 的自动平衡仍靠运维规则;
  2. 高比例更新索引是否应 时间分区 + 只读 force merge(ILM rollover)而非单索引硬压;
  3. 向量段 混合时 merge 成本是否进一步推高(第 15 篇)。

七、小结

三句话小结

  1. 删除在 Lucene 是 liveDocs 逻辑标记,查询跳过死 doc;物理回收 等 merge 把死 doc 拷贝时省略。
  2. TieredMergePolicy 用小段积成中段、大段,在段数与写放大之间折中;删除比例高会 触发更积极合并
  3. force merge 是全量重写式重操作,只适合作只读整理,不能替代日常删除与后台 merge。

下一篇进入查询侧:BooleanScorer、两阶段与 Collector


参考资料

规范 / 文档

  1. Apache Lucene 9.x/10.x:TieredMergePolicyMergePolicyMergeSchedulerSoftDeletesRetentionMergePolicyIndexWriter#forceMerge
  2. Elasticsearch 8.x Reference:Index segments_forcemergeMerge policy(版本对应说明)。

论文 / 综述

  1. Zobel, J. & Moffat, A., Inverted Files for Text Search Engines, ACM Computing Surveys 2006(段与合并背景)。

站内

  1. IndexWriter NRT查询执行排障系列目录

返回 系列目录 | 上一篇:IndexWriter NRT | 下一篇:查询执行

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-15 · database / storage

【全文检索引擎】Lucene · BM25 · Segment · Elasticsearch NRT

补齐搜索架构叙事与 RAG/向量引擎之间的全文检索内核层:以 Lucene 9.x/10.x 拆解 Analyzer、FST、postings、BM25 与 IndexWriter,并以 Elasticsearch 8.x 拆解分片、refresh/translog 与查询路径;OpenSearch 对照与选型收束。


By .