深入剖析 Elasticsearch (ES) 的近实时搜索原理
在日常使用 Elasticsearch (ES) 时,我们常常会惊叹于它的速度:刚刚写入的数据,几乎瞬间就能被搜索到。这背后的功臣就是其近实时 (Near Real-Time, NRT) 搜索能力。
那么,ES 是如何在保证数据可靠性的前提下,实现如此高效的搜索的呢?这一切都围绕着 Refresh
、Translog
和 Segment
这几个核心概念展开。
一、为什么不能是“完全”实时?
首先我们要明白,如果每一次数据写入都立刻触发磁盘 I/O 操作来更新倒排索引,那么频繁的磁盘读写将会成为巨大的性能瓶颈,系统吞吐量会急剧下降。这就像每写一个字就要保存一次整个文档,效率极低。
因此,ES 采用了一种经典的权衡策略:牺牲一点点“实时性”,换来巨大的性能提升。这个策略的核心就是 Refresh
操作。
二、核心基石:倒排索引与 Lucene Segment
在深入之前,我们需要理解两个基础概念:
倒排索引:ES 搜索快的根本。它通过内容(词条)反向找到包含它的文档 ID。
Lucene Segment:Lucene (ES 的底层库) 中的索引一旦生成就不可变。索引文件由多个
Segment
组成,每个Segment
自身就是一个完整的、不可变的倒排索引。新的文档写入会生成新的Segment
,删除操作则是通过一个特殊的.del
文件标记来实现。
不可变的好处:缓存友好、无需锁机制、可以常驻内存,极大提升了查询性能。
三、全部过程
1、客户端发送请求
你发送一个命令,要求将一条新数据(例如一个商品信息)添加到名为 products
的索引中。
POST /products/_doc
{
"name": "iPhone 15",
"price": 5999,
"description": "一款强大的智能手机"
}
2、数据到达协调节点
Elasticsearch 集群中的某个节点收到这个请求,它被称为“协调节点”。它根据文档的 ID(或自动生成的 ID)计算出这个文档应该被存储在哪个主分片上,并将请求转发给该主分片所在的“数据节点”。
3、数据节点处理写入(关键两步)
数据节点收到文档后,会立即按顺序完成两个操作,这两个操作保证了数据不会丢失:
(1)写入 Translog(预写日志)
节点将操作命令原原本本地记录到 Translog 文件中。
目的: 这是数据的“安全绳”。即使系统现在崩溃,重启后也能通过重放 Translog 来恢复数据,保证数据不丢。
特点: 这是直接追加到磁盘的操作,非常可靠。
(2)存入内存缓冲区
节点将文档的 JSON 数据放入内存的一个临时区域(内存缓冲区)。
此时的状态: 数据在内存里,非常脆弱(断电即失),并且完全不能被搜索到。
至此,写入请求就可以返回成功响应给客户端了! 客户端知道数据已经被ES接受并且不会丢失了。
4、Refresh(刷新)- “近实时”的魔法时刻
这是一个后台定时任务,默认每隔 1 秒执行一次。它做了以下事情:
创建新分段:将内存缓冲区中所有新增或修改的文档,创建一个新的 Lucene 分段(Segment)。
存入文件缓存:这个新分段不会立即硬生生地写入磁盘(那很慢),而是先存放在操作系统的文件缓存(Page Cache) 中。这是一个位于内存和磁盘之间的高速区域。
使数据可读:重新打开索引器,让这个新的分段对搜索可见。
!!!里程碑事件!!!
经过这个 Refresh 操作后,之前写入的 "iPhone 15"
文档,虽然还没有真正持久化到物理磁盘上,但已经可以被搜索到了!
这就是“近实时”的由来: 从数据被接受到可被搜索,延迟约 1 秒(即 Refresh 的间隔)。
5、Flush(刷盘)- 真正的持久化
这是另一个后台任务,触发条件通常是:Translog 文件大小达到一定阈值 或 每隔30分钟。
执行一次 Refresh:首先确保内存缓冲区中所有数据都已经生成分段并可搜索。
持久化分段:将文件缓存中所有新生成的分段(不仅限于刚刚的那个)物理写入磁盘,真正实现数据持久化。
清空 Translog:因为数据已经安全落在磁盘上,就不再需要 Translog 中的这部分记录了,所以会截断(Trim)清空当前的 Translog 文件,并创建一个新的。
至此,数据走完了从接收到完全持久化的全过程,既安全又可查。
6、客户端执行搜索
在 Refresh 操作完成之后,你执行搜索:
请求被发到协调节点。
协调节点询问
products
索引的所有分片(包括主分片和副本分片)。每个分片在本地搜索自己包含的所有分段(包括刚刚在文件缓存里的那个新分段)。
每个分片将搜索结果返回给协调节点。
协调节点合并、排序所有结果,最终将包含
"iPhone 15"
的搜索结果返回给你
四、总结顺序链
写入请求 -> 写入Translog(保底)-> 存入内存缓冲区(不可查)-> [每隔1秒] Refresh -> 生成新分段在文件缓存(可查了!)-> [定时/定量] Flush -> 分段持久化到磁盘 -> 清空Translog
这个设计精髓在于:将“让数据可被搜索”(Refresh)和“让数据持久化”(Flush)这两个高开销的操作解耦。通过频繁、轻量的 Refresh 实现“近实时”搜索,再通过低频、批量的 Flush 来保证最终持久化。
五、相关疑问
1、什么是分段?
分段是 Lucene 索引的不可变、独立且自包含的一部分。 它是 Lucene(Elasticsearch 的底层搜索引擎)底层最基本的数据存储单元。
简单来说:
一个 Lucene 索引(在 ES 里对应一个分片)是由一个或多个分段组成的。
每个分段内部包含了文档数据的所有信息:倒排索引、正排数据(用于排序和聚合)、文档值等。
分段一旦创建,就不可再被修改(Immutable)
2、分段是如何创建的?
还记得 Refresh 操作吗?它就是创建新分段的时刻!
新写入的数据首先放在内存缓冲区。
当 Refresh 发生时(默认每秒1次),ES 会将内存缓冲区中的所有数据一次性创建成一个新的、独立的分段。
这个新分段被打开后,里面的数据就立刻可以被搜索到了。
这就是 “近实时搜索” 的底层实现:不是修改原有巨大的索引,而是快速生成一个包含最新数据的小型新分段。
3、分段带来的问题与合并
如果每秒都创建一个新分段,很快就会出现成百上千个小分段。这会导致:
资源耗尽:每个分段会消耗文件句柄、内存。
搜索性能下降:一次搜索需要依次检查大量的小分段,然后再合并结果,效率会变低。
解决方案:分段合并 (Merging)
Lucene 会在后台自动执行一个过程,叫做分段合并。
它会选择一些大小相近的小分段,将它们合并成一个更大的新分段。
合并过程非常智能,它会:
真正删除那些已被标记为“删除”的文档。
优化数据的物理排列,提升压缩率和查询效率。
合并完成后,旧的小分段会被删除,新的、更大的分段会取代它们