Elasticsearch底层存储原理

Elasticsearch 的底层存储原理是一个复杂但设计精妙的系统,它融合了分布式架构、倒排索引、列式存储、缓冲机制和日志结构等多种技术。核心依赖于 Apache Lucene 这个强大的全文检索引擎库。以下是其关键原理的详细解析:

一、 核心基础:Apache Lucene

  1. 索引(Index)是核心单位:

    • Lucene 中的数据存储在 索引 中(注意:此处是 Lucene 的索引概念,非 ES 的 Index)。
    • 一个 ES 索引(Index)由一个或多个 Lucene 索引(分片)组成。
  2. 倒排索引:

    • 核心数据结构: Lucene 的核心是倒排索引
    • 工作原理: 不是记录"文档ID -> 包含的词",而是记录"词 -> 包含该词的文档ID列表"。
    • 组成:
      • Term Dictionary(词项字典): 包含所有文档中出现过的唯一词项(分词后的结果),按字典序排序。便于快速查找词项是否存在。
      • Postings List(倒排列表): 对于每个词项,记录包含它的所有文档ID(DocId)列表,以及在该文档中出现的位置(Position)、偏移量(Offset)和词频(Term Frequency)等信息。
    • 优势: 使得基于关键词的全文检索极其高效。给定一个查询词,能直接定位到包含该词的所有文档。
  3. 段(Segment):

    • 不可变性: Lucene 索引由多个 组成。段一旦写入磁盘就是不可变的(Immutable)。
    • 写入过程: 新添加的文档首先写入内存缓冲区,然后定期"刷新"为一个小的、新的、不可变的段(这个过程叫 refresh),并可以立即被搜索(近实时搜索)。一个段包含完整倒排索引的一部分。
    • 好处:
      • 简化并发控制: 无需锁机制,读取操作只需访问当前有效的已知段集合。
      • 高效缓存: 不变性使得内核可以安全地将段文件缓存到 OS 的 Page Cache 中,极大加速读取。
      • 崩溃恢复简单: 写入新文件比修改旧文件更安全。
    • 缺点: 删除文档不能直接修改旧段,而是通过一个.del文件标记删除;更新文档等价于删除旧文档+添加新文档。
  4. 段合并(Segment Merging):

    • 必要性: 频繁刷新会产生大量小段,导致文件句柄过多、查询性能下降(需要依次查询多个小段)。
    • 过程: Lucene 后台线程(或 ES 的 merge 调度器)会定期选择一些小的、可能包含已删除文档的段,将它们合并成一个更大的新段。
    • 好处:
      • 减少段数量,提升查询效率。
      • 物理删除被标记删除的文档,回收磁盘空间。
      • 优化索引结构(例如,合并词项字典)。
    • 开销: 合并过程消耗大量 I/O 和 CPU 资源,可能影响查询性能。ES 提供了精细的合并策略配置。
  5. 文档值(Doc Values):

    • 解决的问题: 倒排索引擅长"词->文档"查询,但对于聚合(Aggregations)、排序(Sorting)、脚本访问字段值等需要"文档->字段值"的操作效率低下(随机访问每个文档的字段值代价高)。
    • 列式存储: Doc Values 是列式存储结构。在索引时构建,序列化到磁盘。
    • 存储方式: 按文档ID顺序存储每个字段的所有值。例如,所有文档的 price 字段值存储在一起。
    • 优势:
      • 高效聚合/排序: 可以顺序读取一列数据进行聚合计算(如求和、平均值)或排序,充分利用磁盘顺序读写和 CPU 缓存。
      • 高效随机访问: 通过文档ID作为偏移量,也能相对高效地访问单个文档的字段值(相比从_source解析)。
    • 启用: 对于需要聚合、排序或脚本访问的字段,通常建议启用 doc_values: true(默认启用)。对于 text 类型字段通常不启用。
  6. 存储字段(Stored Fields) & _source:

    • Stored Fields: 在索引时明确指定需要存储的字段(store: true)。Lucene 会将这些字段的值单独存储起来。查询时可以直接返回这些字段,无需解析 _source不常用 ,通常用 _source 代替。
    • _source 字段: 一个特殊的存储字段。默认情况下,Elasticsearch 会将你发送的原始 JSON 文档完整地存储在这个字段中。这是搜索结果的 hits._source 的来源。
    • 用途:
      • 重建原始文档(Highlighting, Reindexing, Update by Query)。
      • 默认返回给用户。
    • 禁用: 可以禁用 _source 或在 mapping 中排除某些字段以减少存储空间,但会失去上述能力。

二、 Elasticsearch 的分布式封装与增强

  1. 索引(Index)与分片(Shard):

    • ES 的 索引(Index) 是逻辑命名空间,包含具有相似结构的文档集合。
    • 分片是物理单元: 一个索引被水平分割成多个分片 。每个分片本质上是一个独立的、完整的 Lucene 索引
    • 主分片 vs 副本分片:
      • 主分片: 负责处理索引和删除请求。是数据的"权威"拷贝。数量在创建索引时定义,之后无法修改(除非 Reindex)。
      • 副本分片: 主分片的拷贝。提供数据冗余(高可用),分担查询请求(提高查询吞吐量)。数量可以动态调整。
    • 分布式协调: ES 集群自动管理分片的分配(在哪个节点上)、负载均衡和故障转移。
  2. 写入流程:

    1. 客户端请求: 客户端向协调节点发送写入(Index/Delete/Update)请求。
    2. 路由: 协调节点根据文档ID(或路由键)和索引设置计算出文档应属于哪个主分片(shard = hash(document_id) % number_of_primary_shards)。
    3. 转发请求: 协调节点将请求转发给该主分片所在的节点。
    4. 主分片处理:
      • 写入内存缓冲区: 文档先被写入主分片的 Lucene 索引的内存缓冲区。
      • 写入事务日志: 同时 ,操作被追加到主分片的 Translog(事务日志) 中。Translog 保证操作的持久性,防止内存数据丢失(默认 request 方式,每次写请求成功后同步刷盘;async 方式异步刷盘,风险更高)。这是数据安全的关键!
    5. Refresh(可选但常见): 默认情况下,每秒或达到一定大小后,内存缓冲区的内容会被写入一个新的 Lucene 段 (此时段还是缓存在 OS Page Cache,尚未 fsync)。新段可以被搜索(近实时,NRT)。这不是数据安全的保证点(OS 崩溃可能丢失这部分数据)。
    6. Flush: 当满足条件(Translog 大小、时间间隔、显式请求)时:
      • 执行 Lucene commit:将所有在内存中的段(Page Cache 中)强制 fsync 到磁盘,确保物理写入。
      • 清空内存缓冲区。
      • 创建一个新的 Translog(旧的 Translog 不再需要)。
    7. 复制到副本: 主分片处理成功后,将相同的操作并行转发给所有对应的副本分片所在的节点。副本分片执行相同的操作(写入内存 Buffer + Translog)。所有副本分片都报告成功后,主分片才向协调节点报告成功,协调节点再返回给客户端。副本保证了高可用。
    8. Segment Merging & Translog Trimming: 后台持续进行段合并。Flush 后,旧的、已提交的 Translog 文件会被删除。
  3. 读取流程(Search):

    1. 客户端请求: 客户端向协调节点发送搜索请求。
    2. Query Phase:
      • 广播查询: 协调节点将查询广播到索引的所有相关分片(主分片或副本分片,通常选择负载最低的副本)。
      • 分片本地执行: 每个分片在自己的 Lucene 索引上独立执行查询。
      • 返回文档ID与排序信息: 每个分片将匹配文档的 _id 和足够用于排序/聚合/评分的信息(通常是 _id + _score + sort values + agg buckets)返回给协调节点。此时不获取实际文档内容(_source)。
    3. Fetch Phase:
      • 聚合结果: 协调节点聚合所有分片返回的结果(排序、分页、聚合计算)。
      • 获取文档内容: 协调节点确定需要返回给客户端的文档列表后,向这些文档所在的分片发送 multi-get 请求,获取这些文档的完整内容(_source 字段)。
    4. 返回结果: 协调节点组装完整的搜索结果(包括 _source)返回给客户端。
  4. 存储机制优化:

    • 文件系统缓存(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),内存占用低,查询速度快。

总结关键点

  1. Lucene 是核心引擎: 提供强大的倒排索引(搜索)、Doc Values(聚合/排序)、段管理(不可变段+合并)等基础功能。
  2. 分片是物理单元: ES 索引被水平拆分为分片(Lucene 索引),实现分布式存储和处理。
  3. 副本保证高可用与读取扩展: 每个分片有零个或多个副本。
  4. 近实时搜索(NRT): 得益于 refresh 操作(内存 Buffer -> 可搜索的 Segment),默认 1 秒可见。
  5. 写入持久化保障: Translog + Flush 机制确保写入操作即使在节点故障时也能恢复。写入成功意味着数据已安全存储在 Translog 中(request 级别)。
  6. 列式存储(Doc Values): 赋能高效的聚合、排序和脚本访问。
  7. _source 存储原始文档: 用于重建文档和返回结果。
  8. 依赖文件系统缓存: 将尽可能多的可用内存留给 OS Page Cache 是优化性能的关键。
  9. 不可变性与段合并: 段不可变简化了并发和缓存,合并优化了索引结构并回收空间。

理解这些底层原理对于高效地设计 Elasticsearch 索引结构、配置集群、调优性能和排查问题至关重要。