当前位置: 首页 > news >正文

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(易混淆点)

维度refreshflush
做了什么内存 → 新 SegmentSegment 落盘 + 清 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}}# 写入完成后:恢复 refreshPUT /my_index/_settings{"index":{"refresh_interval":"1s"}}
关闭 refresh 的效果: ├── 数据持续写入 Buffer → 不产生新 Segment ├── 没有 Segment → 不触发 merge └── IO 降低 → 写入吞吐大幅提升

13.3 写入优化参数速查表

参数默认值写入优化建议说明
refresh_interval1s-1(关闭)或 30s减少 Segment 生成
number_of_replicas10(写入阶段)减少副本同步开销
translog.durabilityrequestasync减少 fsync 次数
bulk_size-5~15MB/批平衡内存和网络
index.buffer_size10% JVM20%~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
http://www.jsqmd.com/news/697940/

相关文章:

  • 终极开源游戏启动器:Starward的完整使用指南与高效技巧
  • 解读2026年中古风咖啡厅预算,宜昌靠谱装修服务有哪些 - 工业品牌热点
  • 揭秘Home Assistant本地控制架构:突破云端依赖的美的智能家电技术实现
  • 从限购到畅通:GLM-5.1 Coding Plan接入攻略
  • 把 BigQuery 接进 SAP HANA Cloud,Google BigQuery Remote Source 的实战思路与落地细节
  • 从0到1掌握TMDB:API Key、Session_ID、Account_ID获取指南(含一键获取脚本,调用源码和SDK)
  • 5分钟掌握网站离线下载:Python网站下载器实用指南
  • 总结2026年宜昌意式风格建筑排名,意式风格地毯选购攻略 - mypinpai
  • B站视频下载终极指南:用BilibiliDown三步搞定离线观看
  • 5个技巧快速掌握AKShare:Python金融数据获取终极指南
  • 保姆级教程:用CS5266+MA8621芯片组,从零设计一个Type-C七合一拓展坞(附PCB/原理图)
  • 别再扔了!手把手教你用美工刀和砂纸复活严重氧化的烙铁头(附日常保养技巧)
  • 终极图表数据提取指南:如何用WebPlotDigitizer提升科研效率700%
  • 从机器人到AR:旋转向量与矩阵的Python实现,在OpenCV和三维视觉里怎么用?
  • 华为Pura X Max正式开售:阔折叠的破局者,华为生态棋局落下重要一子
  • 从SBC到LDAC:高通QCC30xx/51xx系列蓝牙音频平台解码能力全解析
  • 讲讲南昌市东堃职业培训学校,口碑如何值得推荐吗? - 工业推荐榜
  • 出飞鸟源码运营版本可开房
  • EPLAN新手必看:从栅格设置到PLC绘图的20个高频快捷键与实用技巧
  • OpenClaw安全实践指南:构建Web3与智能合约的纵深防御体系
  • 如何在数百个Excel文件中快速查找特定数据?QueryExcel多文件检索工具详解
  • 5分钟快速入门:OBS StreamFX终极指南,让普通直播秒变专业级
  • 非涉密区域外来人员实名登记与安全管控系统:从0到1的技术方案与实践解析
  • 如何为群晖NAS高效部署Realtek USB网卡驱动:企业级实战指南
  • 用Python算算你的助学贷款:一个真实大学生财务规划小工具(附完整代码)
  • 把 Amazon Athena 接进 SAP HANA Cloud,远程源创建这件事,真正要盯住的不是语法,而是查询结果落点、加密方式和 workgroup
  • Dialogflow Web V2:前端直连AI对话,构建无后端智能客服
  • 杭州离婚谈判律师张玉:深耕家事领域的专业法律服务者 - 律界观察
  • ctf show web入门17
  • BLE Mesh vs ZigBee:谁才是智能家居的终极方案?