TL;DR
- 场景:写入后立刻搜不到、节点重启怕丢数据,只知道”近实时”却搞不清 ES 底层是怎么保证性能和可靠性的。
- 结论:Segment 不可变、Refresh 负责”可搜”、Flush+Translog 负责”可恢复”,一套链路共同定义了 Elasticsearch 的性能上限和数据安全边界。
- 产出:梳理索引写入到 NRT 搜索的全链路流程,给出 refresh_interval、flush、translog.durability 等关键参数的调优思路和常见故障模式速查表。
版本矩阵
| 版本范围 | 已验证 | 说明 |
|---|---|---|
| Elasticsearch 7.x | 部分 | 写入/Refresh/Flush/Translog 原理与文中描述一致,API 略有差异 |
| Elasticsearch 8.x(2025) | 部分 | 核心机制相同,安全与默认配置需以 8.x 官方文档为准 |
| Elasticsearch 6.x 及以下 | 否 | 仅在原理层面大致适用,具体参数名与默认值可能不同 |
索引文档写入和近实时搜索原理
基本概念
Segments in Lucene
众所周知,Elasticsearch 存储的基本单元是 Shard,ES 中的一个 Index 可能分为多个 Shard,事实上每个 Shard 都是一个 Lucence 的 Index,并且每个 Lucence Index 由多个 Segment 组成,每个 Segment 事实上是一些倒排索引的集合,每次创建一个新的 Document,都会归属于一个新的 Segment,而不会去修改原来的 Segment。且每次的文档删除操作,会仅仅标记 Segment 中该文档为删除状态,而不会真正的立马物理删除,所以说 ES 的 index 可以理解为一个抽象的概念。
Translog-Hbase WAL(Write Ahead Log)
Write Ahead Log 预写入日志:新文档被索引意味着文档会被首先写入内存 buffer 和 translog 文件,每个 shard 都对应一个 translog 文件。
Refresh In Elasticsearch
在 Elasticsearch 中,_refresh 操作默认每秒执行一次,意味着将内存 buffer 的数据写入到一个新的 Segment 中,这个时候索引变成了可检索的,写入新 Segment 后会清空内存 buffer。
Flush In Elasticsearch
Flush 操作意味着将内存 buffer 的数据全部写入到新的 Segment 中,并将内存中所有 Segments 全部刷盘,并且清空 translog 日志的过程。
近实时搜索
基本流程
Elasticsearch 写入流程,当一个写请求到达 Elasticsearch 后,ES 将数据写入 MemoryBuffer 中,并添加事务日志(translog)。如果每次一条数据写入内存后立即写到硬盘上,由于写入的数据肯定是离散的,因此写入磁盘的操作也就是随机写入了。硬盘随机写入的效率相当低,会严重降低 ES 的性能。
因此 ES 在设计时在 MemoryBuffer 和硬盘之间加入了高速缓存(FileSystemCache)来提高 ES 的写效率。
当写请求发送到 ES 后,ES 将数据写入 MemoryBuffer 中,此时写入的数据还不能查询到。默认设置下,ES 每1秒钟将 MemoryBuffer 中的数据 Refresh 到 Linux 的 FileSystemCache,并清空 MemoryBuffer,此时写入的数据就可以被查询到了。
Refresh API
在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 Refresh,默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch 是”近”实时搜索:文档的变化并不是立即对搜索可见,但会在一秒之内变成可见。
这些行为可能会对新用户操作困惑,他们索引了一个文档然后尝试搜索它,但却没有搜索到。这个问题的解决方法是用 Refresh API 执行一次手动刷新:
POST /_refresh
POST /my_blogs/_refresh
POST /my_blogs/_doc/1?refresh
{"xxx": "xxx"}
PUT /test/_doc/2?refresh=true
{"xxx": "xxx"}
- 刷新(Refresh)所有的索引
- 只刷新(Refresh)blogs 索引
- 只刷新文档
并不是所有的情况都需要每秒刷新,可能你正在使用 Elasticsearch 索引大量的文件,你可能想优化索引速度而不是近实时搜索,可以通过设置 refresh_interval,降低每个索引的刷新频率。
PUT /my_logs
{
"settings": {
"refresh_interval": "30s"
}
}
refresh_interval 可以在既存索引上动态更新,在生产环境中,当你正在建立一个大的索引时,可以先关闭自动刷新,待开始使用该索引时,再把他们调回来。
PUT /my_logs/_settings
{
"refresh_interval": -1
}
PUT /my_logs/_settings
{
"refresh_interval": "1s"
}
持久化变更
基本流程
持久化变更 flush:即使通过每秒刷新(Refresh)实现了近实时搜索,仍然要经常进行完整提交来确保从失败中恢复。但在两次提交之间发生变化的文档怎么办?我们也不希望丢掉这些数据。
Elasticsearch 增加了一个 Translog,叫做事务日志,在每一次对 Elasticsearch 操作时都会进行日志记录,通过 translog,整个流程是下面这个样子:
第一步:一个文档被索引之后,就会被添加到内存缓冲区中,并且追加到了 translog,如下图描述一样:新的文档被添加到内存缓冲区并且追加到了事务日志。
第二步:刷新(refresh)使分片处于下图描述的状态,分片每秒刷新(refresh)一次:
- 这些内存缓冲区的文档被写入到一个一个新的段中,且没有进行 fsync 操作
- 这个段被打开,使其可被搜索
- 内存缓存区被清空
刷新(refresh)完成后,缓存被清空但是事务日志不会。
第三步:这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志,事务日志不断积累文档。
每隔一段时间:例如 translog 变得越来越大,索引被刷新(flush),一个新的 translog 被创建,并且一个全量提交被执行。
- 所有在内存缓冲区的文档都被写入一个新的段(Segment)
- 缓冲区被清空
- 一个提交点被写入硬盘
- 文件系统缓存通过 fsync 被刷新(flush)
- 老的 translog 被删除
translog 提供所有还没有被刷到磁盘的操作的一个持久化记录,当 Elasticsearch 启动的时候,它会从磁盘中使用最后一个提交点去恢复已经的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。
translog 也被用来提供实时 CRUD,当你试着通过 ID 查询、解析、删除一个文档,它会在尝试从相应的段中检索之前,首先检查 translog 任何最近的变更。这意味着它总是能够实时的获取到文档的最新版本。在刷新(flush)之后,段被全量提交,并且事务日志被清空。
flush API
这个执行一个提交并且截断 translog 的行为在 Elasticsearch 被称作一次 flush,分片每30分钟被自动刷新(flush),或者在 translog 太大(512M)的时候也会刷新。
flush API 可以被用来执行一个手工的刷新(flush):
POST /blogs/_flush
POST /_flush?wait_for_ongoing
- 刷新(flush)blogs 索引
- 刷新(flush)所有的索引并且等待所有刷新在返回前完成
我们很少需要自己手动执行一个 flush 操作,通常情况下,自动刷新就够了。
这就是说,在重启节点或者关闭之前执行 flush 有益于你的索引,当 Elasticsearch 尝试恢复或重新打开一个索引的时候,它需要重放 translog 中所有的操作,所以如果日志越短,恢复的会越快。
Translog 安全问题
Translog 有多安全?
Translog 的目的是保证操作不会丢失,但是却引出了对应的问题:
在文件被 fsync 到磁盘前,被写入的文件在重启之后就会丢失。这个过程在主分片和复制分片都会发生。最终,基本上,这意味着在整个请求被 fsync 到主分片和复制分片的 translog 之前,你的客户端不会得到一个 200 的 OK 响应,在每次写请求后执行一个 fsync 会带来性能上的损失,尽管实践表明这个损失并不大(特别是 bulk 导入,在一次请求时平摊了大量的文档开销)
但是对于一些大容量的偶尔丢失几秒数据问题并不严重的集群,使用异步的 fsync还是比较有益的。比如,写入的数据被缓存到内存中,再每5秒执行一次 fsync。
这个行为可以通过设置 durability 参数为 async 来启动。
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
这个选项可以针对索引单独设置,并且可以动态修改,如果你决定使用异步 translog 的话,你需要保证在发生 crash 时,丢失掉 sync_interval 时间段的数据也无所谓。请在决定前知晓这个特性。
如果你不确定这个行为的后果,最好使用默认参数:“index.translog.durability”: “request” 来避免数据丢失。
错误速查
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| 写入返回 201/200,但立即搜索不到文档 | 近实时语义:refresh_interval 为 1s,尚未触发 Refresh | 查看索引 settings 中 refresh_interval,监控 refresh 频率 | 关键链路可临时调用 _refresh,或缩短 refresh_interval;批量写入阶段相反应调大或关闭 |
| 节点重启后丢失几秒钟内的写入 | translog 使用 async 模式,sync_interval 内的数据未 fsync | 查看 index.translog.durability 和 index.translog.sync_interval | 对关键业务使用 durability=request,缩短 sync_interval;重要变更前手动 _flush |
| 集群恢复或分片重启时间异常长 | 长期未 flush,translog 体积过大,恢复时需要重放大量操作 | 查看索引的 translog 大小、RECOVERING 状态耗时 | 调整 flush 策略;在流量低谷执行 _flush;必要时使用滚动索引减小单索引体量 |
| 写入吞吐低,磁盘 IO 持续偏高 | refresh_interval 过小或频繁手工 _refresh / _flush | 监控每秒 refresh/flush 次数与 segment 生成频率 | 导入阶段设置 refresh_interval=-1,导入完成后再恢复;减少不必要的手工 _refresh/_flush |
| 磁盘空间增长快,segment 数量爆炸 | 删除/更新频繁、merge 压力大,flush/滚动策略不当 | 通过 _cat/segments、store size 观察 segment 数量与大小 | 采用滚动索引治理冷数据,定期对只读索引用 force merge,优化删除/更新模式 |