本文描述问题及解决方法同样适用于 腾讯云 Elasticsearch Service(ES)。
在生产环境中,我们遇到了一个严重的磁盘占用问题:2025年11月25日17:10,集群磁盘异常达到洪水位,全量索引发生只读,直到重启后才恢复。
最初怀疑是快照备份导致的,但经过深入排查发现:
真正的根因是:2025-11-25 16:53:16 开始对500TB存量数据执行大规模forcemerge操作,与快照备份任务并行执行,导致forcemerge产生的大量临时segment文件无法释放,最终磁盘占用暴增50TB!
本文将通过深入分析Elasticsearch 8.16源码,揭示forcemerge与快照备份交互时的磁盘占用陷阱。
完整时间线
时间 | 事件 | 磁盘占用 |
11-24 下午 | 快照备份任务开始 | 500TB(正常) |
11-25 16:53:16 | ? 开始对存量数据执行forcemerge | 500TB |
11-25 16:53:16 之后 | forcemerge持续进行,磁盘开始异常增长 | 500TB → 不断增长 |
11-26 17:10 | 磁盘达到洪水位,索引只读 | 550TB |
重启后 | 自动释放临时segment文件 | 500TB(恢复正常) |
关键发现:
存量数据规模:500TB(TB级别索引)
正常写入量:相对于500TB存量很少(可忽略)
forcemerge开始时间:2025-11-25 16:53:16
快照任务:11-24下午开始,11-25仍在进行中
磁盘异常增长:50TB(全部是forcemerge产生的临时segment)
重启释放:50TB空间被释放
相关截图:



ForceMerge + 快照 = 磁盘炸弹
ForceMerge是Elasticsearch提供的强制合并segment的操作,用于:
减少segment数量,提高查询性能
删除已标记删除的文档,释放磁盘空间
提高压缩率
会读取多个小segment,合并成大segment
合并过程中,旧segment和新segment同时存在
合并完成后,旧segment才能被删除
ForceMerge的磁盘占用模型
合并前:
├─ segment_1.si (10GB)
├─ segment_2.si (10GB)
├─ segment_3.si (10GB)
└─ segment_4.si (10GB)
总计:40GB
合并中:
├─ segment_1.si (10GB) ← 旧segment,等待删除
├─ segment_2.si (10GB) ← 旧segment,等待删除
├─ segment_3.si (10GB) ← 旧segment,等待删除
├─ segment_4.si (10GB) ← 旧segment,等待删除
└─ segment_5.si (40GB) ← 新segment,合并结果
总计:80GB(翻倍)
合并后(正常情况):
└─ segment_5.si (40GB) ← 旧segment被删除
总计:40GB(恢复正常)当快照备份与forcemerge并行执行时:
快照开始时,通过acquireIndexCommit()获取当前IndexCommit引用
该IndexCommit包含forcemerge开始前的所有segment文件
forcemerge读取旧segment(segment_1, segment_2, ...)
生成新的merged segment(segment_merged)
创建新的IndexCommit
虽然forcemerge完成,但旧segment仍被快照引用
CombinedDeletionPolicy.onCommit()检查到引用计数>0
跳过删除,旧segment继续占用磁盘
新segment:40GB(forcemerge结果)
旧segment:40GB(被快照引用,无法删除)
总计:80GB
对于500TB存量数据,如果全部执行forcemerge,磁盘占用可能达到1000TB

InternalEngine.forceMerge()
// InternalEngine.java:1950
@Override
public void forceMerge(boolean flush, int maxNumSegments, boolean onlyExpungeDeletes,
boolean upgrade, boolean upgradeOnlyAncientSegments) throws EngineException {
ensureOpen();
// 关键:flush确保所有数据持久化
if (flush) {
flush(false, true);
}
try (ReleasableLock lock = readLock.acquire()) {
ensureOpen();
// 调用Lucene的forceMerge
// 这会生成新的merged segment文件
indexWriter.forceMerge(maxNumSegments, true);
// 关键:forcemerge完成后,创建新的IndexCommit
indexWriter.commit();
// 这里会触发CombinedDeletionPolicy.onCommit()
// 但如果有快照引用旧commit,旧segment不会被删除
} catch (Exception e) {
throw new ForceMergeFailedEngineException(shardId, e);
}
}indexWriter.forceMerge()会读取所有旧segment,生成新的merged segment
在合并过程中,旧segment和新segment同时存在,磁盘占用翻倍
indexWriter.commit()创建新commit后,会触发onCommit()检查是否可以删除旧commit
如果快照正在进行,旧commit的引用计数>0,删除被跳过
CombinedDeletionPolicy.onCommit()
// CombinedDeletionPolicy.java:107
@Override
public synchronized void onCommit(List<? extends IndexCommit> commits) throws IOException {
// 更新safeCommit和lastCommit引用
updateCommits(commits);
// 关键:遍历所有commit,决定哪些可以删除
for (IndexCommit commit : commits) {
// ForceMerge场景:如果commit被快照引用,跳过删除
if (acquiredIndexCommits.containsKey(commit)) {
// 这里就是问题所在!
// forcemerge前的旧commit被快照引用
// 即使forcemerge完成,旧segment也无法删除
continue; // 跳过删除
}
// 如果是safeCommit或lastCommit,保留
if (commit.equals(safeCommit) || commit.equals(lastCommit)) {
continue;
}
// 其他commit可以删除
deleteCommit(commit);
}
}
forcemerge完成后,会有两个commit:
commit_old:forcemerge前的commit,包含旧segment
commit_new:forcemerge后的commit,包含新merged segment
如果快照引用了commit_old,acquiredIndexCommits.containsKey(commit_old) 返回 true
删除被跳过,旧segment继续占用磁盘
这就是为什么forcemerge后磁盘占用不降反升的原因
CombinedDeletionPolicy.onInit()
// CombinedDeletionPolicy.java:89
@Override
public synchronized void onInit(List<? extends IndexCommit> commits) throws IOException {
// 更新safeCommit和lastCommit
updateCommits(commits);
// 关键:删除所有旧commit
for (IndexCommit commit : commits) {
// 重启后acquiredIndexCommits为空
// 所以这个检查永远不会命中
if (acquiredIndexCommits.containsKey(commit)) {
continue; // 不会执行
}
// 只保留safeCommit和lastCommit
if (commit.equals(safeCommit) || commit.equals(lastCommit)) {
continue;
}
// 删除所有旧commit
// 包括forcemerge前的commit_old
deleteCommit(commit);
}
// 触发Lucene删除未使用的segment文件
indexWriter.deleteUnusedFiles();
}
重启后,acquiredIndexCommits是空的(新创建的Map)
onInit()会删除所有旧commit,只保留最新的commit
对于forcemerge场景:
commit_old(forcemerge前)被删除
commit_new(forcemerge后)被保留
indexWriter.deleteUnusedFiles()删除commit_old的所有segment文件
这就是为什么重启能释放50TB空间的原因
不要在快照期间执行ForceMerge(重要原则)
在ForceMerge期间避免大规模全量备份
建立ForceMerge执行检查机制(规避场景有限)
#!/bin/bash
# forcemerge_safe_check.sh
ES_HOST="localhost:9200"
echo "=== 检查是否有进行中的快照 ==="
RUNNING_SNAPSHOTS=$(curl -s "$ES_HOST/_snapshot/_status" | jq '.snapshots | length')
if [ "$RUNNING_SNAPSHOTS" -gt 0 ]; then
echo "❌ 发现 $RUNNING_SNAPSHOTS 个进行中的快照,禁止执行forcemerge!"
echo "进行中的快照:"
curl -s "$ES_HOST/_snapshot/_status" | jq '.snapshots[] | {repository, snapshot, state}'
exit 1
fi
echo "✅ 没有进行中的快照,可以安全执行forcemerge"
# 执行forcemerge
echo "=== 开始执行forcemerge ==="
curl -X POST "$ES_HOST/_forcemerge?max_num_segments=1"
4.ForceMerge前预留足够磁盘空间
所需磁盘空间 = 当前索引大小 × 2 + 安全余量
示例:
- 索引大小:1TB
- 所需空间:1TB × 2 + 10TB = 12TB
- 当前可用:10TB
- 结论:空间不足,不能执行forcemerge不是快照备份本身导致的磁盘占用,而是大规模ForceMerge与快照并行执行导致的
读取旧segment,生成新merged segment
合并过程中,旧segment和新segment同时存在
正常情况下,合并完成后旧segment会被删除
快照通过acquireIndexCommit()获取IndexCommit引用
被引用的commit包含的segment文件无法删除
引用计数>0时,CombinedDeletionPolicy跳过删除
快照引用了forcemerge前的commit(包含旧segment)
forcemerge生成新segment后,旧segment因被引用无法删除
磁盘占用 = 旧segment + 新segment ≈ 2倍索引大小
500TB存量数据执行forcemerge
即使只有10%索引执行forcemerge,也会产生50TB临时占用
多个索引并行forcemerge,问题进一步放大