本文是 ES 存储引擎系列的第一篇,梳理段(Segment)、提交点(Commit Point)、Translog 三个核心概念,它们是理解 ES 近实时搜索与实时 CRUD 机制的基础。
写在前面
ES 基于 Lucene 构建,并在 Lucene 之上引入了提交点(Commit Point)这一概念——一个列出了所有已知段的文件。理解 ES 的存储机制,本质上就是理解”段 + 提交点 + Translog”这三者如何协同工作,在性能与数据安全之间做平衡。
graph LR
A["内存缓冲区<br>In-memory Buffer"] -->|Refresh| B["段 Segment<br>不可变倒排索引"]
A -->|实时追加| C["Translog<br>事务日志"]
B -->|Flush / fsync| D["磁盘"]
C -->|Flush 后清空| D
D --> E["提交点 Commit Point<br>segments_N"]
E -->|引用| B一、段(Segment)
1.1 什么是段
段是 Lucene 索引的基础构建模块,是一个不可变的倒排索引子集。
- 一个完整的 Lucene 索引(对应 ES 里面的一个分片)是由多个段组成的。
- 每个段本身是一个完整的、独立的倒排索引,包含了索引中一部分文档的数据(词项字典、倒排列表等)。
- 段一旦创建,就是不可变的(无法修改一个已存在的段)。
类比理解:可以把”分片”理解为一本书,把每个”段”理解为书里的一个独立章节。每次写入新内容不是去修改已有章节,而是新增一个章节;要找一段内容时,需要把所有章节都翻一遍(搜索时合并多个段的结果)。章节积累多了就需要”合订”(段合并),把多个小章节整理成一个大章节,提升翻阅效率。
1.2 查看段信息
1 | # 查看所有索引的段信息 |
字段说明:
| 字段 | 说明 |
|---|---|
index | 索引名称 |
shard | 分片编号 |
prirep | 表示是主分片(p)还是副本分片(r) |
ip | 节点 IP 地址 |
segment | 段名称 |
generation | 段代编号,数字越大表示段越新 |
docs.count | 该段中包含的文档数(未删除的) |
docs.deleted | 该段中标记为删除的文档数 |
size | 段磁盘大小 |
size.memory | 段在内存中的大小(单位字节) |
committed | 是否已提交(true 表示已提交,即已持久化) |
searchable | 是否可搜索(true 表示可搜索) |
version | Lucene 版本 |
compound | 是否为复合文件(true 表示段文件被合并为一个文件) |
实用排查技巧:当索引出现性能下降时,GET /_cat/segments/my_index?v&s=docs.deleted:desc 可以快速定位”删除文档比例过高”的段——docs.deleted 远大于 docs.count 的段,往往是触发段合并、释放磁盘空间的优先目标。
1.3 不可变性的价值
段的不可变性看似”浪费”(每次写入都要新建段、定期合并),但这个设计选择带来了四个关键收益:
① 不需要锁
读写无需锁,因为不用担心数据在读取时被修改,极大提升了搜索的并发性能。写入不阻塞读取——当新的段被创建时,现有的段不会被修改,搜索可以继续在旧的段上进行,直到新的段准备好并被原子性地切换进去。
原子性的切换:当一个新的段通过 Refresh 操作创建完成后,它被打开并加入到当前索引的可搜索段集合里面,这个加入操作是原子的——对于搜索请求来讲,要么完全看不到这个新段,要么完全看到这个新段,基于提交点机制实现。
② 内核文件系统缓存友好
这是段设计带来的最大性能优势之一。一旦一个段文件被操作系统加载到文件系统缓存(page cache)中,由于它永远不会改变,可以一直留在那里。后续的搜索请求就可以直接在内存的缓存中进行,避免了昂贵的磁盘 I/O 操作。
③ 过滤器缓存等长期有效
对于过滤器缓存(Filter Cache)这样的缓存,因为段永远不会改变,这个缓存项永远有效,直到这个段被合并或删除——这避免了在每次数据变更时都使大量缓存失效,极大地提升了过滤查询的重复性能。
④ 压缩优势
因为数据是批量写入并保持不变的,压缩算法可以处理更大的、连续的数据块,从而获得更高的压缩率。
不可变性的代价:正因为段不可变,删除文档不会立即释放磁盘空间,而是先做”逻辑删除”标记;段数量也会随着频繁写入不断膨胀。这些代价由后续的”段合并”机制来偿还——本质上是用后台异步整理的成本,换取了前台读写的高性能,是一种典型的空间换时间、异步换同步的工程权衡。
二、提交点(Commit Point)
2.1 什么是提交点
提交点是一个文件,它记录了当前索引中所有已知的、有效的段,以及它们的信息。
- 分片级别,通常被命名为
segments_N(N 递增,如segments_1、segments_2),类似一个清单,列了”在当前时间点,构成这个完整索引的所有段文件有哪些”。
graph TD
A["Commit Point"] --> B["段 1"]
A --> C["段 2"]
A --> D["段 3"]
B --> E["Searchable 可搜索"]
C --> E
D --> E2.2 为什么删除文档不会立刻释放空间?
段是不可变的,每个提交点会包含一个 .del 文件,文件中会列出被删除文档的段信息。被标记删除的文档依然可以被查询到,但是会在最终结果被返回前从结果集中移除。
**这是”逻辑删除”而非”物理删除”**:和很多数据库系统(如 MySQL InnoDB 的 MVCC、Kafka 的墓碑消息)的设计思路类似——先打标记、延迟回收,把”真正释放空间”这个相对昂贵的操作,挪到后台异步进行(即段合并),避免阻塞前台的写入/删除请求。
2.3 核心作用
① 定义一致性视图
提交点为索引提供了一个在某个时间点上的、完整且一致的视图,确保了:
- 搜索一致性:搜索基于一个固定的段集合进行,不会出现读到一半数据被修改的情况。
- 原子性更新:当新的段被加入或旧的段被合并删除时,是通过创建一个新的提交点来原子性地切换整个索引视图的。
② 实现故障恢复
当 Elasticsearch 节点重启时,它会找到最新的提交点文件(segments_N),根据这个提交点,就知道需要将哪些段文件加载到内存中来重建索引。
如何找到最新的提交点?
Lucene 使用一个名为 segments.gen 的辅助文件,这个文件很小,只包含两个 long 整数,指向当前最新提交点的世代号。启动时,系统先读取 segments.gen 找到大概方向,然后会验证 segments_N 文件是否存在和完整,最终确认最新的提交点。
提交点还提供恢复基线:这个由提交点定义的索引状态,是 Translog 重放的起点。Translog 中所有发生在这个提交点之后的操作,都会被重新执行。
③ 管理段的生命周期
提交点是段的”生死簿”。一个段只有在被某个提交点引用时,才是”活”的。一旦一个段不再被任何当前或未来提交点引用,它就变成了磁盘上的”僵尸文件”,可以被安全地删除(主要发生在段合并里面)。
额外延伸:Lucene 的 IndexDeletionPolicy
Lucene 内部通过 IndexDeletionPolicy 来决定哪些旧的提交点(以及它们引用但不再被新提交点引用的段)可以被物理删除。默认策略 KeepOnlyLastCommitDeletionPolicy 只保留最新一次提交,一旦新提交点生成,旧提交点引用的”僵尸段”就会被清理。ES 在快照备份场景下,会临时通过快照机制”钉住”某些历史提交点,防止被过早清理,这也是为什么长期未完成的快照任务可能导致磁盘空间异常增长的原因之一。
三、Translog(事务日志)
3.1 什么是 Translog
Translog 全称 Transaction Log,即事务日志。它是一个仅追加(append-only)的日志文件,用于记录所有对分片进行的未持久化的操作。
- 本质是写操作日志 / 重做日志(Redo Log),为了防止在数据被持久化到 Lucene 段之前,因硬件故障或节点重启导致数据丢失。
- 每个分片都有自己的 Translog 文件,按时间顺序记录所有创建、更新、删除文档的操作。
类比理解:Translog 在设计思想上与 MySQL InnoDB 的 Redo Log、Kafka 的日志分段(Log Segment)非常相似——都是”先写日志再异步落地数据结构”的 WAL(Write-Ahead Log)模式,本质上都是用顺序写盘的低成本,去交换随机更新数据结构(B+树/倒排索引)的高成本,同时保障故障恢复时数据不丢失。
3.2 主要作用
① 数据安全和故障恢复
- 持久化保证:当一个写操作被确认成功,数据可能不会被立即搜索到(需要 Refresh),但该操作已经被记录在了 Translog 中。
- 恢复重放:在节点故障重启后,Elasticsearch 首先从提交点恢复最后一个持久化的索引状态,然后重放 Translog 中所有之后的操作,从而将分片恢复到故障前的最新状态。
② 实现实时 GET 操作
这是 Translog 的一个关键副产品。它使得通过文档 ID 进行 GET、UPDATE、DELETE 操作是实时的,即使文档还没有被 Refresh 成可搜索的段(详见本系列第二篇关于”实时 CRUD”的介绍)。
3.3 配置参数
| 参数名称 | 描述 | 默认值 | 建议与说明 |
|---|---|---|---|
index.translog.durability | 持久化策略 | request | request:每个写请求都执行 fsync,最安全但性能影响大。async:异步持久化,性能更好,但故障时可能丢失数据 |
index.translog.sync_interval | async 模式下,Translog 刷盘的时间间隔 | 5s | 增大此值(如 120s)可减少磁盘 I/O,提升性能,但会增加数据丢失的风险 |
index.translog.flush_threshold_size | Translog 触发 flush 的最大大小 | 512mb | 增大此值(如 1gb)可减少 flush 和段合并的频率,提升写入性能,但会增加故障恢复时间 |
index.translog.flush_threshold_period | 触发 flush 的最大时间间隔 | 30m | 即使 Translog 未达大小阈值,超过此时间也会触发 flush |
index.translog.retention.size | 为支持操作恢复,保留的 Translog 文件总大小 | 512mb | 即使 Translog 中的操作已经被包含在 Lucene 的提交点(已 flush 到磁盘),ES 仍会保留这些 Translog 文件以便恢复使用,超过此大小旧文件将被删除 |
index.translog.retention.age | Translog 文件的最长保留时间 | 12h | 超过这个时间的 Translog 文件将被删除 |
3.4 调优建议
追求极致写入速度:如果业务能容忍在发生故障时丢失少量数据(例如有手段进行补录),可以采用异步(async)持久化策略,并适当调大 sync_interval(如 120 秒)和 flush_threshold_size(如 1GB)。这可以显著降低磁盘 I/O,是影响写入速度的最大因素。
保证数据可靠性:如果系统对数据安全要求很高,则应坚持使用默认的请求(request)持久化策略,确保每次写请求都持久化到磁盘。
进一步延伸:retention.size / retention.age 存在的意义
这两个参数容易被忽略,但在副本分片恢复场景中很关键:当一个副本分片短暂离线后重新加入集群,ES 优先尝试用”基于序列号的增量恢复”(Sequence-Number-based Recovery)——即只重放该副本掉线期间主分片 Translog 中新增的操作,而不是全量复制整个分片数据。这种增量恢复能否成功,取决于主分片的 Translog 是否还保留着副本掉线期间的操作记录,这正是 retention.size/retention.age 控制的范围。如果保留的 Translog 不够(比如副本掉线时间过长,超过了 retention.age),就只能退化为代价高得多的全量恢复(Full Recovery)。因此在网络不稳定、节点经常短暂掉线重连的集群里,适当调大这两个参数能显著提升恢复效率,但代价是占用更多磁盘空间。
总结
段、提交点、Translog 三者共同构成了 ES 存储引擎”高性能 + 高可靠”的基石:
- 段通过不可变性换取了无锁并发、缓存友好、压缩率高等多重收益;
- 提交点为索引提供了原子性切换的一致性视图,同时充当段的生命周期”生死簿”;
- Translog 作为 WAL 日志,弥补了”数据写入内存 → 真正落盘”之间的时间差,是故障恢复和实时 GET 的基础。
理解了这三者,再看下一篇”近实时搜索与实时 CRUD 操作机制”,就能更清楚地理解 Refresh、Flush、段合并这些操作分别在解决什么问题。



