Elasticsearch 底层存储与写入链路:从 Segment 到 Merge,一篇搞懂
Elasticsearch 底层存储与写入链路:从 Segment 到 Merge,一篇搞懂
作者:皮蛋0solo粥 | 发布日期:2026-04-22
标签:Elasticsearch、Lucene、Segment、写入链路、搜索引擎、底层原理
目录
- 引言:先建立正确的心智模型
- 一、核心概念:Segment(不可变的小索引文件)
- 二、写入一条数据,到底发生了什么?
- 三、更新和删除的真相
- 四、为什么需要 Merge(段合并)
- 五、Merge 到底干了什么?
- 六、写入链路全景图
- 七、逐层拆解写入流程
- 八、refresh:近实时搜索的关键
- 九、flush:真正的持久化
- 十、translog:数据安全的第一道保障
- 十一、为什么 ES 查询快?倒排索引
- 十二、实战现象解释:文档数↑但 size↓
- 十三、结合实际场景理解:bulk + embedding + 大量写入
- 十四、三道理解检查题(面试自测)
- 十五、总结:最基础的三板斧 + 写入链路一句话
引言:先建立正确的心智模型
很多人一开始就错在这里:
❌ 误解:Elasticsearch 像 MySQL,一条数据写进去就"更新文件"
真实情况是:
✅ Elasticsearch 基于 Apache Lucene,底层是"追加写 + 不可变文件"模型
你可以理解为:
ES ≈ 一个不断写新文件、很少修改旧文件的系统
如果你带着"ES 像 MySQL"的假设去调优,你会觉得很多现象不可解释。但如果从"追加写 + 不可变文件"出发,一切都会变成"必然"。
一、核心概念:Segment(不可变的小索引文件)
1.1 什么是 Segment?
Segment 是 Lucene 中最核心的存储单元:
👉 一个 Segment 就是一个已经写好、不可再修改的小索引文件
一个 Index(索引)由多个 Shard(分片)组成,每个 Shard 内部维护了一组 Segment 文件。
Index
└── Shard 0
│ ├── Segment 1(不可变)
│ ├── Segment 2(不可变)
│ └── Segment 3(不可变)
└── Shard 1
├── Segment 1(不可变)
└── Segment 2(不可变)
1.2 Segment 的关键特性
| 特性 | 说明 |
|---|---|
| 不可变 | Segment 一旦生成就不会修改 |
| 自包含 | 每个 Segment 包含完整的倒排索引和存储数据 |
| 独立查询 | 搜索时可以并行扫描多个 Segment |
| 逐步增长 | 写入时不断产生新的 Segment |
二、写入一条数据,到底发生了什么?
不是修改原数据,而是:
写入数据 → 生成一个新的 Segment
时间线:
T1: 写入 doc1 → 生成 segment_1
T2: 写入 doc2 → 生成 segment_2
T3: 写入 doc3 → 生成 segment_3
Shard 内部:[segment_1, segment_2, segment_3]
到这里要形成一个核心认知:
ES 的数据是"越写越多文件",而不是"修改已有文件"
三、更新和删除的真相
3.1 更新一条数据?
ES 不会"就地修改",而是执行两步操作:
旧数据 → 标记删除(打 delete 标记)
新数据 → 写入一个新 Segment
示例:更新 doc_id=5
旧 Segment 中:doc_id=5 → 标记为 deleted
新 Segment 中:doc_id=5(新版本)→ 正常写入
3.2 删除一条数据?
只是打标记(deleted = true),并不会立刻释放空间。
删除 doc_id=10
Segment 中:doc_id=10 → 标记为 deleted
磁盘空间:暂不释放(等 merge 清理)
3.3 核心公式
update = delete(标记旧版本)+ insert(写入新版本)
delete = 标记删除(不释放空间)
四、为什么需要 Merge(段合并)
如果一直只写不合并,会出现什么问题?
写入 10000 条数据 → 产生几千个小 Segment
问题:
├── 查询要扫描几千个 Segment → 慢
├── 有大量"已删除但没清理"的数据 → 浪费空间
├── 文件句柄占用多 → 系统资源紧张
└── 倒排索引分散 → 无法利用压缩优化
所以引入一个机制:
👉 Merge(段合并):后台定期将多个小 Segment 合并成少量大 Segment
五、Merge 到底干了什么?
Merge 执行三个核心操作:
5.1 合并 Segment
seg_1 + seg_2 + seg_3 + seg_4 → seg_big
5.2 清理已删除数据
合并前:
seg_1: [doc1✅, doc2✅, doc3❌deleted]
seg_2: [doc4❌deleted, doc5✅]
合并后:
seg_big: [doc1✅, doc2✅, doc5✅] ← doc3 和 doc4 被物理删除
5.3 重新压缩数据
- 倒排索引重新组织
- 字典编码(FST)优化
- 存储格式更紧凑
合并前总大小:seg_1(5KB) + seg_2(3KB) + seg_3(4KB) = 12KB(含 3KB 废数据)
合并后总大小:seg_big(7KB) ← 压缩 + 清理,省了 5KB
六、写入链路全景图
一次写入不是"直接落盘",而是分阶段完成:
Client
│
▼
Coordinating Node(协调节点)── 不存数据,只做路由调度
│
▼
Primary Shard(主分片)── 真正写数据的地方
│
├──→ Replica Shard(副本分片)── 数据冗余 + 读负载均衡
│
▼
Indexing Buffer(内存缓冲区)── 数据先写这里,不可搜索
│
▼
Refresh(每秒一次,默认)── Buffer → 新 Segment(可搜索,但未持久化)
│
▼
Flush(条件触发)── Segment 落盘 + 清空 Translog(持久化完成)
│
▼
Merge(后台持续)── Segment 合并 + 清理删除 + 压缩优化
一句话版本:
写入 = 先写内存 → 再变成 Segment → 最后后台优化
七、逐层拆解写入流程
7.1 Coordinating Node(协调节点)
职责:
├── 接收客户端请求
├── 根据 routing 规则找到目标 Shard
├── 转发请求到 Primary Shard
└── 汇总各 Shard 结果返回客户端
关键:不存数据,只做调度
7.2 Primary Shard(主分片)
职责:
├── 接收写入请求
├── 执行实际写入操作
├── 保证数据一致性
└── 写完后同步给 Replica Shard
写入是先写 Primary,再同步 Replica:
Client → Primary(写成功)→ Replica(同步)→ 返回成功
7.3 Replica Shard(副本分片)
职责:
├── 数据冗余(容灾,Primary 挂了可以切换)
├── 提供读能力(负载均衡)
└── 提升查询吞吐
7.4 Indexing Buffer(内存缓冲区)
写入的数据先放这里
├── ❌ 不可搜索(还没变成 Segment)
├── ❌ 不在磁盘(纯内存)
└── ✅ 写入延迟极低(内存操作)
八、refresh:近实时搜索的关键
8.1 refresh 做了什么?
Indexing Buffer(内存)──refresh──→ 新 Segment(可搜索)
refresh 将内存中的数据转成一个新的 Segment,并打开 Searcher 使其对查询可见。
8.2 refresh 之后的状态
| 状态 | 说明 |
|---|---|
| ✅ 可搜索 | Searcher 已打开新 Segment |
| ❌ 未持久化 | 数据仍在内存中,宕机可能丢失 |
8.3 默认行为
# 默认每 1 秒 refresh 一次
index.refresh_interval: 1s
这就是 ES 被称为 Near Real-Time(近实时) 搜索引擎的原因——写入后最多 1 秒即可被搜索到。
8.4 refresh 的代价(很重要)
每次 refresh:
├── 生成一个新 Segment
├── Segment 越多 → 查询要扫越多
└── 触发 Merge 的概率越大
代价链:
频繁 refresh → 大量小 Segment → merge 压力激增 → IO/CPU 上升 → 写入吞吐下降
九、flush:真正的持久化
9.1 flush 做了什么?
flush 操作:
1. 将内存中的 Segment 写入磁盘(fsync)
2. 清空 Translog
3. 更新 checkpoint
9.2 flush 触发时机
| 触发条件 | 说明 |
|---|---|
translog 太大 | 默认 512MB,防止恢复时间过长 |
| 定时触发 | 每 5 秒检查一次 |
| 手动触发 | POST /index/_flush |
9.3 refresh vs flush(易混淆点)
| 维度 | refresh | flush |
|---|---|---|
| 做了什么 | 内存 → 新 Segment | Segment 落盘 + 清 Translog |
| 数据可搜索? | ✅ 是 | ✅ 是 |
| 数据持久化? | ❌ 否(内存 Segment) | ✅ 是(磁盘 Segment) |
| 触发频率 | 每秒(默认) | 条件触发(远低于 refresh) |
| 代价 | 中等(产生新 Segment) | 较高(磁盘 fsync) |
十、translog:数据安全的第一道保障
10.1 什么是 translog?
translog(transaction log)是 ES 的预写日志(WAL),类似于 MySQL 的 redo log。
写入流程实际上是:
写数据 → 写 translog(磁盘,顺序写)→ 写 buffer(内存)
10.2 translog 的作用
如果 ES 宕机:
├── 内存中的 Buffer 数据丢失
├── 磁盘上的 Segment 数据保留
└── Translog 记录了"Segment 之外"的写入
恢复时:
├── 读取已有 Segment
└── 重放 Translog → 补回未 flush 的数据
10.3 translog 的配置
# translog 刷盘策略
index.translog.durability: request # 每次写入都 fsync(默认,最安全)
index.translog.durability: async # 定时 fsync(性能更好,可能丢数据)
# translog 大小阈值
index.translog.flush_threshold_size: 512mb
10.4 数据安全的三个阶段(面试级表述)
阶段 1:写入 translog 成功 → 具备"可恢复性"(宕机不丢数据)
阶段 2:refresh 完成 → 具备"可搜索性"(查询可见)
阶段 3:flush 完成 → 具备"持久性"(Translog 清空,数据在 Segment 中)
一句话总结:数据在 refresh 后对搜索可见,在 translog 写入后具备容灾能力,在 flush 后完成最终持久化。
十一、为什么 ES 查询快?倒排索引
11.1 核心数据结构
传统数据库(正排索引):
文档 → 内容
doc1 → "感冒药 阿莫西林 头孢"
doc2 → "止咳糖浆 胃药"
doc5 → "感冒药 布洛芬"
doc20 → "感冒药 对乙酰氨基酚"
ES(倒排索引):
词 → 出现在哪些文档
"感冒药" → [doc1, doc5, doc20]
"阿莫西林" → [doc1]
"止咳糖浆" → [doc2]
"布洛芬" → [doc5]
11.2 查询过程
查询"感冒药":
├── 不用扫描全量文档
├── 直接查倒排索引:"感冒药" → [doc1, doc5, doc20]
└── 只需读取这 3 个文档 → 极快
查询"感冒药 AND 布洛芬":
├── "感冒药" → [doc1, doc5, doc20]
├── "布洛芬" → [doc5]
└── 交集 = [doc5] → 只返回 doc1
11.3 倒排索引的组成
倒排索引
├── Term Dictionary(词典):所有去重词的有序列表
│ └── 用 FST(Finite State Transducer)压缩存储
├── Postings List(倒排链):每个词对应的文档 ID 列表
│ └── 用 Frame of Reference(FOR)压缩
└── Doc Values(正排数据):用于排序、聚合
└── 列式存储,按字段独立存储
十二、实战现象解释:文档数↑但 size↓
这是一个非常经典的"反直觉"现象。理解了前面的 Segment 和 Merge 机制,答案就水到渠成了:
写入阶段:
├── 产生大量小 Segment
├── 包含很多 delete 标记(update = delete + insert 产生的)
└── 数据比较"松散",压缩率低
Merge 后:
├── 删除了所有标记为 deleted 的无效数据
├── 重新压缩 Segment(倒排索引优化)
└── Segment 数量减少,文件系统开销降低
结果:
文档数 ↑(有效文档确实多了)
磁盘 size ↓(浪费空间被清理了)
十三、结合实际场景理解:bulk + embedding + 大量写入
在医药商品搜索系统的实际场景中,我们经常需要做大量数据写入(含 embedding 向量),这时候理解写入链路就非常关键。
13.1 为什么 bulk 写入比逐条写入快?
逐条写入(1000 条):
├── 1000 次 refresh → 1000 个 Segment → merge 压力巨大
└── 1000 次网络往返 → 延迟累积
bulk 写入(1000 条/批):
├── 1 次 refresh → 1 个 Segment → merge 压力小
└── 1 次网络往返 → 延迟极低
13.2 为什么大数据量写入时要调大 refresh_interval?
# 写入阶段:关闭 refresh,攒够数据再开
PUT /my_index/_settings
{
"index": {
"refresh_interval": "-1" # 关闭自动 refresh
}
}
# 写入完成后:恢复 refresh
PUT /my_index/_settings
{
"index": {
"refresh_interval": "1s"
}
}
关闭 refresh 的效果:
├── 数据持续写入 Buffer → 不产生新 Segment
├── 没有 Segment → 不触发 merge
└── IO 降低 → 写入吞吐大幅提升
13.3 写入优化参数速查表
| 参数 | 默认值 | 写入优化建议 | 说明 |
|---|---|---|---|
refresh_interval | 1s | -1(关闭)或 30s | 减少 Segment 生成 |
number_of_replicas | 1 | 0(写入阶段) | 减少副本同步开销 |
translog.durability | request | async | 减少 fsync 次数 |
bulk_size | - | 5~15MB/批 | 平衡内存和网络 |
index.buffer_size | 10% JVM | 20%~30% JVM | 增大 Buffer |
13.4 写入完成后别忘了恢复
# 恢复副本
PUT /my_index/_settings
{
"index": {
"number_of_replicas": 1,
"refresh_interval": "1s"
}
}
# 强制 merge(可选,让 Segment 尽快合并)
POST /my_index/_forcemerge?max_num_segments=1
十四、三道理解检查题(面试自测)
Q1:删除数据后,磁盘空间为什么不会立刻减少?
✅ ES 的删除只是打标记(
deleted = true),物理空间在 Merge 时才真正释放。Merge 是后台异步执行的,不是实时触发。
Q2:bulk 写入为什么比逐条写入性能高?
✅ bulk 将多条写入合并为一次请求,减少了 refresh 次数和网络往返。每次 refresh 会生成新 Segment,而 Segment 越多,merge 压力越大,写入吞吐越低。bulk 从根源上减少了 Segment 的生成。
Q3:为什么 refresh 太频繁会拖慢写入?
✅ refresh 会生成新的 Segment。频繁 refresh 导致 Segment 数量激增,触发更多 Merge 操作。Merge 是重量级操作(读多 Segment + 写大 Segment + 清理删除 + 重压缩),大量消耗 IO 和 CPU,最终拖慢写入吞吐。
十五、总结:最基础的三板斧 + 写入链路一句话
如果你只记三件事:
| # | 核心认知 | 说明 |
|---|---|---|
| 1️⃣ | ES 不修改数据,只追加 | update = delete + insert |
| 2️⃣ | 数据存储在 Segment(不可变) | Segment 一旦生成就不会改 |
| 3️⃣ | Merge 负责"清理 + 压缩 + 合并" | 后台持续优化存储结构 |
写入链路一句话(面试级表述):
Elasticsearch 的写入流程是先写入 Primary Shard,同时记录 Translog 并写入内存 Buffer。数据在 Refresh 时会被转为 Segment,从而对搜索可见。随后在 Flush 时持久化到磁盘并清理 Translog。后台通过 Merge 合并 Segment、清理删除数据并优化存储结构。
写入链路全景(一图总结):
写入请求
│
▼
Coordinating Node ── 路由到目标 Shard
│
▼
Primary Shard
├── 写 Translog(磁盘,容灾保障)
├── 写 Indexing Buffer(内存)
└── 同步到 Replica Shard
│
▼
Refresh(1s 默认)── Buffer → 新 Segment → 可搜索
│
▼
Flush(条件触发)── Segment 落盘 + 清 Translog → 持久化
│
▼
Merge(后台持续)── 合并 Segment + 清理删除 + 压缩优化
│
▼
优化后的存储(文档数↑,size 可能↓)
参考资料:
- Elasticsearch 官方文档 - Index Lifecycle
- Apache Lucene - Segment File Format
- 《Elasticsearch 权威指南》- 索引管理
- Lucene 源码 - IndexWriter / SegmentMerger
更多推荐
所有评论(0)