Elasticsearch 系列 · 延续倒排索引一篇

Elasticsearch 为什么是"近实时"

写一条数据进 Elasticsearch,为什么不是立刻就能被搜到,而是要等大概 1 秒?这篇拆开这背后的完整流程:内存缓冲区、事务日志(translog)、段(segment)、以及 refresh / flush / merge 这三个经常被搞混的动作,配一个可以逐步播放、每一步都验证过内部状态一致性的动画演示。

段(segment) · 一旦生成就不可变的最小索引单元 translog · 不是为了搜索,是为了"进程崩了也不丢数据" 三个不同节奏 · 能搜到 / 落盘安全 / 存储整理,是三件独立的事

01 · 一个违反直觉的现象

刚写完,搜不到,是 bug 吗

很多人第一次用 Elasticsearch 都会踩到这个"坑":调用接口插入一条数据,接口返回成功,紧接着立刻搜索,却发现搜不到——过一小会儿(通常不到 1 秒)再搜,就有了。这不是 bug,是设计如此,官方文档管这个特性叫 near real-time(近实时),"近"这个字就是特意标注给你看的。要理解为什么,得先知道写入的数据经过了哪几层。

02 · 三个核心概念

段 / translog / 三种不同节奏

Segment(段)

Elasticsearch 底层的 Lucene 索引不是一整块,而是很多个小的"段"拼起来的。每个段一旦写出来就不可变——不能往里加文档,也不能真的删掉一个文档,只能整体拿去和别的段合并。这个"不可变"的约束,是接下来一切设计的起点。

Translog(事务日志)

每次写入,除了进内存缓冲区,还会追加写一行到磁盘上的日志文件里。它不是拿来搜索用的,唯一的作用是"万一进程这时候崩了,内存里的数据丢了,重启后能靠重放这份日志把数据找回来"——一份专门为故障恢复准备的安全网。

三种不同节奏

refresh(默认约 1 秒一次)决定"什么时候能被搜到";flush决定"什么时候真正安全落盘、translog 可以清空";merge决定"什么时候把小段拼成大段、腾出被删文档占的空间"。三件事各管各的,互不等待。

03 · 现场直播

一条数据从写入到"可以被搜到"的完整旅程

下面的演示依次做了:写入两条数据、refresh、再写一条、删除最早那条、refresh、再写一条、refresh、flush、最后 merge。每一步都记录了当时"如果现在发起一次搜索,能搜到哪些文档"——这份状态还用一份完全独立的第二种算法重新推导过一遍,两边逐条比对一致,不是随手画的动画。

🔍 此刻发起搜索,能搜到
内存缓冲区(还没 refresh,搜不到)
Translog(自上次 flush 以来的操作记录)
磁盘上的段(segments)
点击"下一步"或"播放"开始。
0 / 0

04 · refresh 到底做了什么

把内存缓冲区"冻"成一个新段

演示第 3 步(第一次 refresh)发生的事:内存缓冲区里的 A、B 两条数据,被写成一个新的、不可变的段,这个段立刻对搜索可见——缓冲区随之清空。这就是"能被搜到"这件事发生的唯一时刻。Elasticsearch 默认每 index.refresh_interval(默认 1 秒)自动做一次这个动作,这也是"近实时"里那个大约 1 秒延迟的来源。

注意:refresh 生成的新段,此刻很可能只存在于操作系统的文件系统缓存里,并没有真正 fsync 到磁盘——"能搜到"不等于"已经安全持久化",这是两件独立的事,分别由 refresh 和接下来要讲的 flush 负责。

05 · 删除是"打个标记",不是真的删

Tombstone(墓碑)

演示第 5 步删除文档 A 时,A 并没有从它所在的段里消失——那个段已经不可变了,做不到。真正发生的是:这个段内部多了一条"A 已删除"的标记(墓碑,tombstone)。搜索的时候,凡是命中了带墓碑标记的文档,会被直接跳过、当作不存在;但它占用的磁盘空间要一直等到这个段被 merge,才会被真正回收。这和我们在 Go map 那篇讲的 tombstone 是同一个思路——不同的系统,同一个"不能就地删除时该怎么办"的解法。

06 · flush:什么时候才真正"安全"

落盘,并且清空 translog

演示第 9 步(flush)做了两件事:把所有还没 fsync 的段真正写到磁盘上;把 translog 清空(准确说是新开一个空的)。第二件事的逻辑是:既然这些操作现在已经安全地体现在磁盘上的段里了,就不再需要靠重放 translog 来恢复它们,继续留着只会让 translog 文件越滚越大。

观察这一步的 可搜索文档集合完全没有变化——flush 不影响"能搜到什么",它只影响"崩溃后能不能恢复、还需不需要重放多少数据"。这正是本文最想讲清楚的一点:refresh 管可见性,flush 管持久性,是两条互不干扰的时间线,现实里 flush 的频率通常比 refresh 低得多。

07 · merge:把碎片拼起来,顺便扫墓

为什么段不能无限增多

Elasticsearch 默认每秒就可能产生一个新段(如果这段时间内有写入的话)。段一多,一次搜索就要挨个查完所有段再合并结果,还会占用大量文件句柄和内存——merge 就是后台安静地把几个小段拼成一个大段。演示最后一步:三个段(其中一个带着 A 的墓碑)被合并成一个新段,合并过程中,带墓碑的文档被彻底清除,磁盘空间才真正被回收。

三者的关系,一句话总结:refresh 决定你多久能看到新数据;flush 决定数据多久变得"扛得住宕机";merge 决定存储多久被真正清理干净。三件事默认节奏都不一样,互相独立触发,不要把它们当成同一件事的三个名字。

08 · 一个实际的取舍

能不能让它更"实时"

refresh_interval 调小(甚至设成写入时立即触发)确实能让数据更快可搜,但代价是:每次 refresh 都会生成一个新段,频率越高,段积累得越快,后台 merge 的压力越大,反而可能拖慢整体的写入和查询性能。所以真实项目里,如果业务能接受"几秒延迟",通常会保持默认或者调大这个间隔(比如日志类场景常见调到 30 秒甚至更久),用短暂的延迟换取更好的吞吐量——这是一个典型的"实时性 vs 系统开销"的权衡,没有免费的午餐。

09 · 参考与说明

这篇文章做了哪些简化

☕ 如果这篇文章帮到你,可以请作者喝杯咖啡 · 爱发电