Elasticsearch 索引过程全解析:从写入到持久化的完整流程
Elasticsearch 的索引过程(文档写入、存储与检索)是其核心能力,涉及分片路由、内存缓冲、持久化机制、分段管理等多个环节。理解这一过程有助于优化写入性能、保障数据安全,并解释 “准实时搜索” 等特性的底层原理。
数据写入:分片路由机制
文档写入 Elasticsearch 时,首先需要确定存储到哪个主分片(副本分片仅用于同步和容错,不直接接收写入)。
分片路由公式
分片的分配通过以下公式计算:
1 | shard = hash(routing) % number_of_primary_shards |
routing:路由键,默认是文档的_id,也可在写入时通过routing参数自定义(如按用户 ID 路由,确保同一用户的文档在同一分片)。number_of_primary_shards:索引的主分片数量(创建后不可修改)。
示例:若索引有 3 个主分片,某文档 _id=123,则 hash(123) % 3 计算结果为 0、1 或 2,对应写入的主分片。
写入流程(协调节点与主 / 副本分片)
- 请求接收:客户端向任意节点(协调节点)发送写入请求。
- 路由计算:协调节点通过上述公式确定目标主分片,并转发请求到该主分片所在的节点。
- 主分片写入:主分片节点写入文档,成功后同步请求到所有副本分片。
- 响应确认:所有副本分片写入成功后,主分片节点向协调节点返回 “成功” 响应,最终反馈给客户端。
索引文档:从内存到磁盘的流转
文档写入主分片后,并非直接持久化到磁盘,而是经历 “内存缓冲→文件系统缓存→磁盘” 的多阶段流转,平衡性能与可靠性。
初始写入:Memory Buffer 与 Translog
Memory Buffer:文档先写入内存缓冲区(JVM 堆内存),此时数据不可搜索。
Translog(事务日志):同时,写入操作被记录到 Translog(磁盘文件),用于故障恢复(若节点宕机,内存数据丢失,可通过 Translog 恢复)。
此时状态:数据在内存,未进入倒排索引,不可搜索,但 Translog 保障了数据不会因崩溃丢失。
准实时可见:Refresh 操作
Elasticsearch 是 “准实时” 搜索引擎,数据写入后需等待 Refresh 操作才能被搜索到。
- 触发时机:默认每隔
index.refresh_interval(1 秒)自动触发,也可通过POST /index/_refresh手动触发。 - 操作内容:
- 将 Memory Buffer 中的数据写入 File System Cache(文件系统缓存,操作系统内存),生成一个新的 Segment(分段)。
- 清空 Memory Buffer(数据已转移到 File System Cache)。
- 关键特性:
- Segment 是 Lucene 的倒排索引文件,一旦进入 File System Cache,数据即可被搜索(无需写入磁盘)。
- 这就是 “准实时” 的原因:数据可见延迟由
refresh_interval控制(可通过调整该参数平衡实时性与性能)。
持久化到磁盘:Flush 操作
File System Cache 仍是内存,为避免断电丢失,需通过 Flush 操作将数据写入磁盘。
- 触发时机:
- 定时触发(默认 30 分钟)。
- Translog 大小达到阈值(默认 512MB 或 80% 堆内存,可通过
index.translog.flush_threshold_size配置)。 - 手动触发:
POST /index/_flush。
- 操作内容:
- 将 File System Cache 中的所有 Segment 强制刷写到磁盘(通过
fsync系统调用)。 - 清空并滚动 Translog(生成新的 Translog 文件,旧文件可删除)。
- 将 File System Cache 中的所有 Segment 强制刷写到磁盘(通过
Translog 的配置与作用
Translog 是数据安全的核心保障,关键配置:
index.translog.sync_interval:Translog 从内存刷到磁盘的间隔(默认 5 秒,确保崩溃时最多丢失 5 秒数据)。index.translog.durability:async(默认):异步刷写 Translog 到磁盘,性能优先。request:每次写入请求后立即刷写 Translog 到磁盘,安全性优先(性能下降)。
分段(Segment)管理:不可变与动态更新
Lucene 的 Segment 是倒排索引的物理存储单位,具有不可修改的特性(一旦写入磁盘,无法修改)。这种设计提升了读写并发,但需要特殊机制处理文档的新增、删除和修改。
Segment 的特性
- 不可修改:磁盘上的 Segment 只读,避免写入时的锁竞争,提升查询性能。
- 独立索引:每个 Segment 都是一个完整的倒排索引,可单独被搜索,最终结果是所有 Segment 的合并。
文档操作的实现方式
- 新增:直接创建新的 Segment 存储新文档(无需修改旧 Segment)。
- 删除:
- 不直接删除 Segment 中的文档,而是在
.del文件中标记文档为 “已删除”。 - 查询时,匹配到的文档若在
.del中,则从结果中过滤(物理删除在 Segment 合并时进行)。
- 不直接删除 Segment 中的文档,而是在
- 修改:
- 本质是 “删除旧版本 + 新增新版本”:旧版本被标记删除,新版本写入新 Segment。
- 依赖文档的
_version字段(每次修改版本号递增,确保查询时返回最新版本)。
Segment 合并:优化查询性能
随着写入增多,会产生大量小 Segment(每次 Refresh 生成一个),过多 Segment 会导致:
- 查询时需遍历所有 Segment,耗时增加。
- 内存占用上升(每个 Segment 需加载元数据)。
Elasticsearch 会自动触发 Segment 合并,将小 Segment 合并为大 Segment。
合并过程
- 触发时机:后台线程(
MergePolicy)定期检查,当小 Segment 数量达到阈值时自动合并。 - 操作内容:
- 选取多个小 Segment,合并为一个大 Segment(过程中忽略
.del标记的文档)。 - 新 Segment 写入磁盘后,替换原小 Segment,并删除旧文件(包括
.del文件)。
- 选取多个小 Segment,合并为一个大 Segment(过程中忽略
合并配置与优化
index.merge.policy.max_merged_segment:合并后最大 Segment 大小(默认 5GB),超过此值的 Segment 不再合并。index.merge.scheduler.max_thread_count:合并线程数(默认 CPU 核心数的一半),避免合并占用过多资源。- 手动合并:
POST /index/_forcemerge?max_num_segments=1(强制合并为 1 个 Segment,适合只读索引或归档场景,生产环境谨慎使用)