Elasticsearch 的底层存储原理是一个复杂但设计精妙的系统,它融合了分布式架构、倒排索引、列式存储、缓冲机制和日志结构等多种技术。核心依赖于 Apache Lucene 这个强大的全文检索引擎库。以下是其关键原理的详细解析:
一、 核心基础:Apache Lucene
-
索引(Index)是核心单位:
- Lucene 中的数据存储在 索引 中(注意:此处是 Lucene 的索引概念,非 ES 的 Index)。
- 一个 ES 索引(Index)由一个或多个 Lucene 索引(分片)组成。
-
倒排索引:
- 核心数据结构: Lucene 的核心是倒排索引。
- 工作原理: 不是记录"文档ID -> 包含的词",而是记录"词 -> 包含该词的文档ID列表"。
- 组成:
- Term Dictionary(词项字典): 包含所有文档中出现过的唯一词项(分词后的结果),按字典序排序。便于快速查找词项是否存在。
- Postings List(倒排列表): 对于每个词项,记录包含它的所有文档ID(
DocId
)列表,以及在该文档中出现的位置(Position
)、偏移量(Offset
)和词频(Term Frequency
)等信息。
- 优势: 使得基于关键词的全文检索极其高效。给定一个查询词,能直接定位到包含该词的所有文档。
-
段(Segment):
- 不可变性: Lucene 索引由多个段 组成。段一旦写入磁盘就是不可变的(Immutable)。
- 写入过程: 新添加的文档首先写入内存缓冲区,然后定期"刷新"为一个小的、新的、不可变的段(这个过程叫
refresh
),并可以立即被搜索(近实时搜索)。一个段包含完整倒排索引的一部分。 - 好处:
- 简化并发控制: 无需锁机制,读取操作只需访问当前有效的已知段集合。
- 高效缓存: 不变性使得内核可以安全地将段文件缓存到 OS 的 Page Cache 中,极大加速读取。
- 崩溃恢复简单: 写入新文件比修改旧文件更安全。
- 缺点: 删除文档不能直接修改旧段,而是通过一个
.del
文件标记删除;更新文档等价于删除旧文档+添加新文档。
-
段合并(Segment Merging):
- 必要性: 频繁刷新会产生大量小段,导致文件句柄过多、查询性能下降(需要依次查询多个小段)。
- 过程: Lucene 后台线程(或 ES 的
merge
调度器)会定期选择一些小的、可能包含已删除文档的段,将它们合并成一个更大的新段。 - 好处:
- 减少段数量,提升查询效率。
- 物理删除被标记删除的文档,回收磁盘空间。
- 优化索引结构(例如,合并词项字典)。
- 开销: 合并过程消耗大量 I/O 和 CPU 资源,可能影响查询性能。ES 提供了精细的合并策略配置。
-
文档值(Doc Values):
- 解决的问题: 倒排索引擅长"词->文档"查询,但对于聚合(Aggregations)、排序(Sorting)、脚本访问字段值等需要"文档->字段值"的操作效率低下(随机访问每个文档的字段值代价高)。
- 列式存储: Doc Values 是列式存储结构。在索引时构建,序列化到磁盘。
- 存储方式: 按文档ID顺序存储每个字段的所有值。例如,所有文档的
price
字段值存储在一起。 - 优势:
- 高效聚合/排序: 可以顺序读取一列数据进行聚合计算(如求和、平均值)或排序,充分利用磁盘顺序读写和 CPU 缓存。
- 高效随机访问: 通过文档ID作为偏移量,也能相对高效地访问单个文档的字段值(相比从_source解析)。
- 启用: 对于需要聚合、排序或脚本访问的字段,通常建议启用
doc_values: true
(默认启用)。对于text
类型字段通常不启用。
-
存储字段(Stored Fields) & _source:
- Stored Fields: 在索引时明确指定需要存储的字段(
store: true
)。Lucene 会将这些字段的值单独存储起来。查询时可以直接返回这些字段,无需解析_source
。不常用 ,通常用_source
代替。 _source
字段: 一个特殊的存储字段。默认情况下,Elasticsearch 会将你发送的原始 JSON 文档完整地存储在这个字段中。这是搜索结果的hits._source
的来源。- 用途:
- 重建原始文档(Highlighting, Reindexing, Update by Query)。
- 默认返回给用户。
- 禁用: 可以禁用
_source
或在mapping
中排除某些字段以减少存储空间,但会失去上述能力。
- Stored Fields: 在索引时明确指定需要存储的字段(
二、 Elasticsearch 的分布式封装与增强
-
索引(Index)与分片(Shard):
- ES 的 索引(Index) 是逻辑命名空间,包含具有相似结构的文档集合。
- 分片是物理单元: 一个索引被水平分割成多个分片 。每个分片本质上是一个独立的、完整的 Lucene 索引。
- 主分片 vs 副本分片:
- 主分片: 负责处理索引和删除请求。是数据的"权威"拷贝。数量在创建索引时定义,之后无法修改(除非 Reindex)。
- 副本分片: 主分片的拷贝。提供数据冗余(高可用),分担查询请求(提高查询吞吐量)。数量可以动态调整。
- 分布式协调: ES 集群自动管理分片的分配(在哪个节点上)、负载均衡和故障转移。
-
写入流程:
- 客户端请求: 客户端向协调节点发送写入(Index/Delete/Update)请求。
- 路由: 协调节点根据文档ID(或路由键)和索引设置计算出文档应属于哪个主分片(
shard = hash(document_id) % number_of_primary_shards
)。 - 转发请求: 协调节点将请求转发给该主分片所在的节点。
- 主分片处理:
- 写入内存缓冲区: 文档先被写入主分片的 Lucene 索引的内存缓冲区。
- 写入事务日志: 同时 ,操作被追加到主分片的 Translog(事务日志) 中。Translog 保证操作的持久性,防止内存数据丢失(默认
request
方式,每次写请求成功后同步刷盘;async
方式异步刷盘,风险更高)。这是数据安全的关键!
- Refresh(可选但常见): 默认情况下,每秒或达到一定大小后,内存缓冲区的内容会被写入一个新的 Lucene 段 (此时段还是缓存在 OS Page Cache,尚未
fsync
)。新段可以被搜索(近实时,NRT)。这不是数据安全的保证点(OS 崩溃可能丢失这部分数据)。 - Flush: 当满足条件(Translog 大小、时间间隔、显式请求)时:
- 执行 Lucene
commit
:将所有在内存中的段(Page Cache 中)强制fsync
到磁盘,确保物理写入。 - 清空内存缓冲区。
- 创建一个新的 Translog(旧的 Translog 不再需要)。
- 执行 Lucene
- 复制到副本: 主分片处理成功后,将相同的操作并行转发给所有对应的副本分片所在的节点。副本分片执行相同的操作(写入内存 Buffer + Translog)。所有副本分片都报告成功后,主分片才向协调节点报告成功,协调节点再返回给客户端。副本保证了高可用。
- Segment Merging & Translog Trimming: 后台持续进行段合并。Flush 后,旧的、已提交的 Translog 文件会被删除。
-
读取流程(Search):
- 客户端请求: 客户端向协调节点发送搜索请求。
- Query Phase:
- 广播查询: 协调节点将查询广播到索引的所有相关分片(主分片或副本分片,通常选择负载最低的副本)。
- 分片本地执行: 每个分片在自己的 Lucene 索引上独立执行查询。
- 返回文档ID与排序信息: 每个分片将匹配文档的
_id
和足够用于排序/聚合/评分的信息(通常是_id
+_score
+sort values
+agg buckets
)返回给协调节点。此时不获取实际文档内容(_source
)。
- Fetch Phase:
- 聚合结果: 协调节点聚合所有分片返回的结果(排序、分页、聚合计算)。
- 获取文档内容: 协调节点确定需要返回给客户端的文档列表后,向这些文档所在的分片发送
multi-get
请求,获取这些文档的完整内容(_source
字段)。
- 返回结果: 协调节点组装完整的搜索结果(包括
_source
)返回给客户端。
-
存储机制优化:
- 文件系统缓存(OS Page Cache): ES 重度依赖 OS 内核的文件系统缓存来加速对 Lucene 索引文件的访问。通常建议将一半或更多的物理内存留给 OS Cache。
- 文件格式优化: Lucene 使用高度优化的二进制文件格式(如倒排列表使用 Frame of Reference, FOR 编码 + Packed Ints,Postings 使用 PForDelta 压缩),并对数字、日期、枚举等进行特定压缩(如 GCD, LZM)。
- 稀疏性处理: 对于稀疏字段(大多数文档不存在的字段)有特殊的存储优化。
- FST(Finite State Transducer): 用于高效存储和查询词项字典(Term Dictionary),内存占用低,查询速度快。
总结关键点
- Lucene 是核心引擎: 提供强大的倒排索引(搜索)、Doc Values(聚合/排序)、段管理(不可变段+合并)等基础功能。
- 分片是物理单元: ES 索引被水平拆分为分片(Lucene 索引),实现分布式存储和处理。
- 副本保证高可用与读取扩展: 每个分片有零个或多个副本。
- 近实时搜索(NRT): 得益于
refresh
操作(内存 Buffer -> 可搜索的 Segment),默认 1 秒可见。 - 写入持久化保障: Translog + Flush 机制确保写入操作即使在节点故障时也能恢复。写入成功意味着数据已安全存储在 Translog 中(
request
级别)。 - 列式存储(Doc Values): 赋能高效的聚合、排序和脚本访问。
_source
存储原始文档: 用于重建文档和返回结果。- 依赖文件系统缓存: 将尽可能多的可用内存留给 OS Page Cache 是优化性能的关键。
- 不可变性与段合并: 段不可变简化了并发和缓存,合并优化了索引结构并回收空间。
理解这些底层原理对于高效地设计 Elasticsearch 索引结构、配置集群、调优性能和排查问题至关重要。