Elasticsearch 底层原理系列(四):Segment 合并机制
2020-11-11·4 分钟阅读
前言
在上一章我们了解到,每次 Refresh 都会生成一个新的 Segment。随着写入的进行,Segment 数量会不断增加。过多的 Segment 会影响查询性能,因此 Lucene 会定期执行 Segment 合并(Merge)操作。
本章将深入解析 Segment 合并机制,帮助理解 ES 如何在后台自动优化索引结构。
技术亮点
| 技术点 | 难度 | 面试价值 | 本文覆盖 |
|---|---|---|---|
| Segment 合并原理 | ⭐⭐⭐ | 高频考点 | ✅ |
| Merge Policy | ⭐⭐⭐⭐ | 进阶考点 | ✅ |
| Force Merge | ⭐⭐⭐ | 常见问题 | ✅ |
| 合并性能优化 | ⭐⭐⭐⭐ | 实战价值 | ✅ |
面试考点
- 为什么需要 Segment 合并?
- Lucene 的 Merge Policy 是怎样的?
- Force Merge 什么时候使用?有什么风险?
- 如何优化 Segment 合并性能?
为什么需要 Segment 合并
Segment 过多的问题
┌─────────────────────────────────────────────────────────────────────────┐
│ Segment 过多的问题 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 每个 Refresh 生成一个 Segment: │
│ │
│ 1秒/次 × 3600秒 = 3600 个 Segment/小时 │
│ │
│ 问题: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 查询时: │ │
│ │ • 需要遍历所有 Segment │ │
│ │ • 每个 Segment 需要打开文件句柄 │ │
│ │ • 每个 Segment 需要加载 FST 到内存 │ │
│ │ • 结果合并开销增大 │ │
│ │ │ │
│ │ 文件系统: │ │
│ │ • 文件句柄耗尽 │ │
│ │ • 文件系统性能下降 │ │
│ │ │ │
│ │ 内存占用: │ │
│ │ • 每个 Segment 的元数据占用内存 │ │
│ │ • FST 索引占用内存 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
合并的好处
┌─────────────────────────────────────────────────────────────────────────┐
│ Segment 合并的好处 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Before Merge (100 个小 Segment): │
│ ┌───┐┌───┐┌───┐┌───┐┌───┐ ┌───┐ │
│ │ S1││ S2││ S3││ S4││ S5│ ... │S100│ │
│ └───┘└───┘└───┘└───┘└───┘ └───┘ │
│ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ ▼ │
│ 每个查询需要访问 100 个 Segment │
│ │
│ │ │
│ ▼ Merge │
│ │
│ After Merge (10 个大 Segment): │
│ ┌─────────┐┌─────────┐┌─────────┐ ┌─────────┐ │
│ │ Big S1 ││ Big S2 ││ Big S3 │ ... │ Big S10 │ │
│ └─────────┘└─────────┘└─────────┘ └─────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ 每个查询只需访问 10 个 Segment │
│ │
│ 合并的好处: │
│ ✅ 减少文件句柄数量 │
│ ✅ 减少内存占用 │
│ ✅ 提高查询速度 │
│ ✅ 清理已删除文档 │
│ ✅ 提高压缩率 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Segment 合并流程
合并过程详解
┌─────────────────────────────────────────────────────────────────────────┐
│ Segment 合并流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 选择待合并的 Segment │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 根据 Merge Policy 选择: │ │
│ │ • 相似大小的 Segment │ │
│ │ • 小 Segment 优先合并 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 2: 创建新的大 Segment │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 合并 Segment 1 + Segment 2 + Segment 3 │ │
│ │ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │ S1 │ + │ S2 │ + │ S3 │ │ │
│ │ │10MB │ │12MB │ │8MB │ │ │
│ │ └─────┘ └─────┘ └─────┘ │ │
│ │ │ │ │ │ │
│ │ └─────────┼─────────┘ │ │
│ │ ▼ │ │
│ │ ┌───────────┐ │ │
│ │ │ New S │ (约 30MB,实际会更小因为压缩) │ │
│ │ └───────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 3: 更新 Commit Point │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 新 Commit Point: │ │
│ │ • 包含新的大 Segment │ │
│ │ • 排除旧的小 Segment │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 4: 删除旧 Segment │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 旧的 S1, S2, S3 被删除 │ │
│ │ (此时没有 reader 在使用它们) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
删除文档的处理
┌─────────────────────────────────────────────────────────────────────────┐
│ 合并时清理删除文档 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Segment 是不可变的,删除只是标记: │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Segment 1: │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ Doc 1 │ Doc 2 │ Doc 3 │ Doc 4 │ Doc 5 │ │ │
│ │ │ 活跃 │ 删除 │ 活跃 │ 删除 │ 活跃 │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 合并时: │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ Doc 1 │ Doc 3 │ Doc 5 │ │ │
│ │ │ 活跃 │ 活跃 │ 活跃 │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 被标记删除的文档在合并时被真正删除 │ │
│ │ 磁盘空间被回收 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 重要:这就是为什么删除文档不会立即释放空间! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Merge Policy(合并策略)
TieredMergePolicy
ES 默认使用 TieredMergePolicy,其核心思想是:
┌─────────────────────────────────────────────────────────────────────────┐
│ TieredMergePolicy 策略 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 核心原则: │
│ 1. 合并相似大小的 Segment │
│ 2. 小 Segment 优先合并 │
│ 3. 限制合并层级数量 │
│ │
│ Segment 层级示意: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Level 0 (最小): S1 S2 S3 S4 S5 S6 S7 S8 │ │
│ │ │ │ │ │ │ │
│ │ └───┼───┘ │ │
│ │ ▼ │ │
│ │ Level 1: M1 M2 │ │
│ │ │ │ │ │
│ │ └───┘ │ │
│ │ ▼ │ │
│ │ Level 2 (最大): L1 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 关键配置: │
│ • index.merge.policy.expunge_deletes_allowed: 10 │
│ (删除文档占比超过 10% 时触发合并) │
│ • index.merge.policy.floor_segment: 2MB │
│ (小于此大小的 Segment 被视为同一层级) │
│ • index.merge.policy.max_merge_at_once: 10 │
│ (一次最多合并 10 个 Segment) │
│ • index.merge.policy.segments_per_tier: 10 │
│ (每层允许的最大 Segment 数量) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
合并触发条件
┌─────────────────────────────────────────────────────────────────────────┐
│ 合并触发条件 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 自动触发(后台线程) │
│ • 新 Segment 产生时检查是否需要合并 │
│ • 根据策略自动选择待合并的 Segment │
│ │
│ 2. 删除文档占比触发 │
│ • 已删除文档占比超过 expunge_deletes_allowed │
│ • 优先合并包含大量删除的 Segment │
│ │
│ 3. 手动触发 │
│ • Force Merge API │
│ • 强制合并到指定数量的 Segment │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Force Merge
使用场景
┌─────────────────────────────────────────────────────────────────────────┐
│ Force Merge 使用场景 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 适用场景: │
│ ✅ 静态索引(不再更新的数据) │
│ ✅ 历史日志数据 │
│ ✅ 归档数据 │
│ ✅ 批量导入完成后的索引 │
│ │
│ 不适用场景: │
│ ❌ 活跃写入的索引 │
│ ❌ 频繁更新的索引 │
│ ❌ 实时数据流 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Force Merge API
// 合并到 1 个 Segment
POST /my_index/_forcemerge?max_num_segments=1
// 合并到 5 个 Segment
POST /my_index/_forcemerge?max_num_segments=5
// 只合并包含删除文档的 Segment
POST /my_index/_forcemerge?only_expunge_deletes=true
Force Merge 的风险
┌─────────────────────────────────────────────────────────────────────────┐
│ Force Merge 风险警告 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ⚠️ 重要警告: │
│ │
│ 1. 重量级操作 │
│ • 消耗大量 CPU 和 I/O │
│ • 可能影响其他操作 │
│ │
│ 2. 不应频繁执行 │
│ • ES 已经有自动合并机制 │
│ • 频繁 force merge 会造成浪费 │
│ │
│ 3. 合并后 Segment 很大 │
│ • 自动合并可能跳过大 Segment │
│ • 后续更新效率降低 │
│ │
│ 4. 可能导致段合并风暴 │
│ • 多个分片同时合并 │
│ • 系统资源耗尽 │
│ │
│ 最佳实践: │
│ • 只对静态索引执行 force merge │
│ • 在业务低峰期执行 │
│ • 使用 max_num_segments=1 要谨慎 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Merge 性能调优
配置参数
PUT /my_index/_settings
{
"index": {
"merge": {
"scheduler": {
"max_thread_count": 1 // 合并线程数
},
"policy": {
"max_merged_segment": "5gb", // 合并后最大 Segment 大小
"segments_per_tier": 10, // 每层 Segment 数量
"max_merge_at_once": 10 // 一次合并数量
}
}
}
}
调优建议
┌─────────────────────────────────────────────────────────────────────────┐
│ Merge 调优建议 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 控制合并线程数 │
│ • 默认:max_thread_count = Math.max(1, cores/2) │
│ • SSD:可适当增加 │
│ • HDD:建议设为 1 │
│ │
│ 2. 调整合并策略参数 │
│ • 大索引:增大 max_merged_segment │
│ • 高写入:减小 segments_per_tier │
│ │
│ 3. 监控合并速度 │
│ • GET /_cat/segments?v │
│ • GET /_nodes/stats/indices/merges │
│ │
│ 4. 控制 refresh 频率 │
│ • refresh 越频繁,Segment 越多 │
│ • 批量写入时增大 refresh_interval │
│ │
└─────────────────────────────────────────────────────────────────────────┘
监控命令
# 查看 Segment 信息
GET /_cat/segments/my_index?v&h=index,shard,segment,size,size.memory
# 查看合并统计
GET /_nodes/stats/indices/merges
# 查看索引统计
GET /my_index/_stats?filter_path=indices.my_index.total.merges
Segment 状态监控
关键指标
┌─────────────────────────────────────────────────────────────────────────┐
│ Segment 监控指标 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 通过 _cat/segments API 查看: │
│ │
│ index shard segment size docs del del_count │
│ my_idx 0 _0 5mb 1000 0 0 │
│ my_idx 0 _1 3mb 500 0 0 │
│ my_idx 0 _2 1mb 200 1 50 ← 有删除文档 │
│ │
│ 关键字段: │
│ • size: Segment 大小 │
│ • docs: 活跃文档数 │
│ • del: 是否有删除文档 │
│ • del_count: 删除文档数 │
│ │
│ 健康指标: │
│ • Segment 数量 < 每分片 100 个 │
│ • 单个 Segment 大小 10MB - 5GB │
│ • 删除文档占比 < 10% │
│ │
└─────────────────────────────────────────────────────────────────────────┘
总结
本章深入解析了 Segment 合并机制:
| 要点 | 说明 |
|---|---|
| 合并目的 | 减少文件句柄、提高查询效率、清理删除文档 |
| 触发时机 | 自动触发(策略决定)、删除占比触发、手动触发 |
| 合并策略 | TieredMergePolicy,合并相似大小的 Segment |
| Force Merge | 仅用于静态索引,避免频繁使用 |
| 调优关键 | 控制线程数、调整策略参数、监控合并状态 |
参考资料
下一章预告
下一章将探讨 查询执行原理,包括:
- Query DSL 解析与执行
- 评分机制(BM25)
- 查询优化策略
相关文章
Elasticsearch 底层原理系列(九):生产实践与性能调优
2020-12-31·6 分钟阅读
深入解析 Elasticsearch 生产环境最佳实践,包括硬件选型、JVM 调优、索引设计、性能监控与调优、常见问题排查。
Elasticsearch 底层原理系列(八):分布式一致性
2020-12-20·5 分钟阅读
深入解析 Elasticsearch 分布式一致性机制,包括 Master 选举、脑裂问题、故障检测与恢复、以及数据一致性保证。
Elasticsearch 底层原理系列(七):分片与路由机制
2020-12-13·5 分钟阅读
深入解析 Elasticsearch 分片路由机制,包括路由算法、自定义路由、分片数量规划、以及分片重平衡策略。