Elasticsearch 原理深度剖析 🔍
理解 ES 内部机制,掌握搜索引擎技术精髓
ES 架构概览 #
初识 Elasticsearch
Elasticsearch 是一个基于 Lucene 的分布式搜索和分析引擎,被设计用于处理大规模数据的全文搜索、结构化搜索和分析。了解其核心架构是掌握 ES 原理的第一步。
核心概念 #
概念 | 说明 |
---|---|
文档 (Document) | ES 中的基本数据单元,类似于数据库中的一行记录,以 JSON 格式存储 |
类型 (Type) | ES 7.0 之前的概念,类似数据库表,现已废弃 |
索引 (Index) | 文档的集合,类似于数据库中的数据库概念 |
分片 (Shard) | 索引被分割成的片段,每个分片是一个完整的 Lucene 实例 |
副本 (Replica) | 分片的备份,提供高可用性和搜索性能 |
节点 (Node) | 单个 ES 实例,存储数据并参与集群的索引和搜索功能 |
集群 (Cluster) | 一个或多个节点的集合,共同持有数据并提供联合索引和搜索能力 |
分布式架构 #
ES 的分布式特性主要体现在以下几个方面:
- 水平扩展:通过添加节点可以线性扩展系统容量
- 分片机制:将索引分成多个分片,分布在不同节点上
- 复制机制:每个分片可以有多个副本,提供高可用性
- 自动均衡:系统会自动均衡分片在节点间的分布
集群组件 #
节点类型
- 主节点(Master Node):负责轻量级集群管理操作
- 数据节点(Data Node):存储数据并执行数据相关操作
- 客户端节点(Client Node):转发集群请求,不存储数据
- 协调节点(Coordinating Node):分发请求并合并结果
- 摄取节点(Ingest Node):预处理文档,执行转换
集群状态
- 绿色:所有主分片和副本分片都正常运行
- 黄色:所有主分片正常运行,但至少有一个副本分片不正常
- 红色:至少有一个主分片(及其对应的所有副本)不正常
集群状态反映了数据的完整性和可用性,是监控 ES 健康状况的重要指标。
ES 内部结构 #
理解底层原理
深入了解 ES 的内部结构和底层 Lucene 架构,对于优化查询性能和解决复杂问题至关重要。
索引结构 #
Elasticsearch 索引由以下关键部分组成:
分片 (Shards)
索引被分散到多个分片上,每个分片是一个独立的 Lucene 索引,分片分为主分片和副本分片
段 (Segments)
分片由多个不可变的段组成,新文档被索引到新段中,段定期合并以优化性能
提交点 (Commit Point)
提交点是索引状态的快照,记录所有已知段的文件
Lucene 架构 #
Elasticsearch 底层依赖 Lucene 作为核心搜索库。理解 Lucene 的基本架构有助于深入了解 ES 的工作原理:
Lucene 核心组件
- IndexWriter:负责创建和维护索引
- IndexReader:提供对索引的只读访问
- IndexSearcher:利用 IndexReader 执行搜索
- Query:表示用户的搜索条件
- Analyzer:处理文本,执行分词、过滤等操作
段的结构 #
段是 Lucene 索引的基本构建块,每个段包含多个文件,存储不同类型的数据:
文件后缀 | 用途 |
---|---|
.nvd, .nvm | 标准化因子,用于词项权重计算 |
.tim, .tip | 词条字典,存储词元到倒排表的映射 |
.doc | 存储文档、词频信息 |
.pos | 位置信息,用于短语查询 |
.pay | 有效载荷数据,存储偏移量等 |
.fdx, .fdt | 存储原始文档字段 |
.dvd, .dvm | DocValues,用于聚合和排序 |
索引的倒排结构
Lucene 使用倒排索引结构,这是信息检索系统的核心:
倒排索引中的每个词元对应一个倒排表,包含:
- 包含该词元的文档 ID 列表
- 词频(TF)信息
- 位置信息(用于短语查询)
- 偏移量信息(用于高亮显示)
索引文档流程详解 #
写入过程的理解
ES 的文档索引过程是一个复杂的分布式流程,涉及多个节点协作和多层内存/磁盘操作,了解这个过程有助于优化写入性能。
写入流程概述 #
协调节点处理 #
索引请求首先到达协调节点,协调节点执行以下操作:
- 验证请求:检查请求的有效性,包括索引名称、映射兼容性等
- 生成文档 ID:如果请求中没有指定 ID,则生成一个唯一的文档 ID
- 确定路由目标:根据路由算法确定文档应该存储在哪个分片
- 默认使用文档 ID 的哈希值:
shard_num = hash(_routing) % num_primary_shards
- 也可以通过指定
_routing
参数来自定义路由
- 默认使用文档 ID 的哈希值:
- 转发请求:将请求转发到包含目标分片的节点
主分片索引过程 #
主分片节点接收到索引请求后,执行以下步骤:
- 验证操作:检查版本冲突、映射兼容性等
-
执行前置操作:
- 应用动态映射更新(如果需要)
- 执行用户定义的 Ingest Pipeline
-
文档解析与分析:
- 解析文档字段
- 对文本字段执行分析过程(分词、标准化等)
-
写入内存缓冲区:
- 文档首先写入 Lucene 的内存缓冲区
- 同时写入事务日志(Translog)
- 标记操作完成:操作在主分片上完成,准备同步到副本
事务日志(Translog)
Translog 是 ES 的预写日志(WAL),提供持久性保证:
- 每个分片维护自己的 Translog
- 记录尚未刷盘的所有操作
- 在节点重启后用于恢复尚未持久化的操作
- 默认每个请求都会提交(fsync)到磁盘
版本控制机制
ES 使用版本号处理并发写入:
- 内部版本控制:每次更新自动递增版本号
- 外部版本控制:用户指定版本号,ES 确保递增
- 使用
_version
字段存储当前版本 - 支持乐观并发控制(OCC)模式
副本同步 #
主分片完成索引操作后,需要将变更同步到副本分片:
- 并行复制:主分片将操作并行转发给所有副本分片
- 副本执行:副本分片执行与主分片相同的索引操作
- 确认机制:所有副本分片确认操作完成后,主分片向协调节点报告成功
- 响应客户端:协调节点收到主分片的成功响应后,向客户端返回成功
写一致性
ES 支持不同级别的写一致性要求,通过 wait_for_active_shards
参数控制:
- one(默认):只需主分片确认
- all:所有主分片和副本分片都必须确认
- 指定数字:要求特定数量的分片确认
刷新、冲刷与合并 #
刷新(Refresh)
将内存缓冲区的内容转换为段,使其可被搜索,但不刷盘
- 默认每 1 秒自动刷新
- 产生新的 Lucene 段
- 通过
refresh_interval
设置
冲刷(Flush)
完整持久化操作,将段刷盘并清除 translog
- 定期自动执行(30 分钟或 translog 过大)
- 触发 Lucene commit
- 创建提交点(checkpoint)
合并(Merge)
将多个小段合并为较大的段,优化搜索性能
- 后台自动执行
- 减少段数量,提高搜索效率
- 删除已标记删除的文档
读取文档流程详解 #
理解 ES 的读取机制
Elasticsearch 提供了两种主要的读取操作:基于 ID 的文档检索(GET)和基于查询的搜索(SEARCH)。两者的流程和性能特征有明显差异。
读取流程概述 #
GET 操作
根据 ID 检索单个文档:
- 协调节点根据文档 ID 计算目标分片
- 请求被转发到拥有该分片的节点
- 节点从本地分片检索文档
- 返回文档给协调节点
- 协调节点返回文档给客户端
SEARCH 操作
基于查询条件搜索文档:
- 协调节点广播查询请求到相关分片
- 每个分片执行本地搜索
- 分片返回文档 ID 和排序值(query 阶段)
- 协调节点合并结果并确定最终集合
- 协调节点请求完整文档(fetch 阶段)
- 返回结果集给客户端
Get 操作 #
2. 检查事务日志
3. 检查段缓存
4. 检查Lucene索引 Shard-->>CN: 返回文档 CN-->>Client: 返回响应
Get 操作的详细流程:
- 路由确定:协调节点根据文档 ID 和路由值确定文档所在的分片
- 分片选择:协调节点从可用的主分片或副本分片中选择一个进行请求,支持负载均衡
- 文档查找:目标节点按以下顺序查找文档:
- 版本映射(Version Map):内存中的最近更新缓存
- 事务日志(Translog):包含尚未刷新到 Lucene 的最新操作
- 段缓存(Segment Memory):最近刷新的段
- Lucene 索引:持久化的段文件
- 实时性:默认 Get 操作是实时的,会考虑尚未刷新的更新
- 结果返回:找到文档后,返回给协调节点,再返回给客户端
Search 操作 #
Search 操作是 ES 最复杂也是最强大的功能,它分为查询阶段和获取阶段两个主要步骤:
查询阶段详解 #
查询阶段 (Query Phase)
- 查询分发:协调节点将查询请求发送到每个相关分片(主分片或其副本)
- 本地执行:每个分片在本地执行查询,步骤包括:
- 解析查询条件(Query 和 Filter)
- 创建查询执行计划
- 在段级别执行查询
- 收集匹配文档并计算相关性分数
- 对结果排序并建立本地优先队列(默认大小为 10)
- 结果返回:各分片仅返回文档 ID、排序值和相关性分数,不返回完整文档
- 结果合并:协调节点合并所有分片的结果,进行全局排序
获取阶段 (Fetch Phase)
- 多 ID 获取:协调节点向相关分片发送多文档获取请求,请求最终结果集中的文档
- 文档加载:分片从存储中加载完整的文档内容
- 后处理:执行文档转换和过滤,包括:
- 源过滤(_source filtering)
- 字段包含/排除处理
- 高亮处理(如果请求)
- 脚本字段计算
- 最终组装:协调节点将所有文档组装为最终的搜索结果
- 响应返回:完整的搜索结果返回给客户端
检索优化 #
ES 提供了多种机制优化检索性能:
查询优化
- 提前终止(Early Termination):当满足特定条件时提前停止搜索
- 跳过(Skipping):跳过不可能匹配的段
- 布尔查询优化:先执行成本低的过滤条件
- 缓存利用:利用查询缓存、过滤器缓存等提高性能
分页优化
- 浅分页:对于 from + size < 10000 的请求,使用标准分页
- 深分页问题:避免 deep paging 导致的性能问题
- search_after:基于游标的分页,适用于大结果集的遍历
- scroll API:适用于一次性检索大量文档,创建时间点快照
性能提示
优化搜索性能的关键策略:
- 尽可能使用 Filter 上下文,可以缓存结果
- 对于高频搜索模式,使用索引排序优化
- 适当使用 preference 参数控制路由,提高缓存利用率
- 使用 _source 过滤减少网络传输
- 对于复杂聚合,考虑使用预计算或近似聚合
性能要点 #
缓存机制 #
ES 使用多种缓存提高性能:
缓存类型 | 用途 | 特点 |
---|---|---|
Node Query Cache | 缓存查询结果(仅限于 Filter 上下文) | LRU 策略,基于段级别,段变更时自动失效 |
Shard Request Cache | 缓存聚合、建议器等结果 | 默认启用,分片刷新时失效 |
Field Data Cache | 缓存字段数据,用于聚合和排序 | 基于堆内存,占用空间较大 |
Index Buffer | 用于索引操作的缓冲区 | 提高写入性能,默认占 JVM 堆的 10% |
相关性评分 #
ES 中的相关性评分是搜索引擎的核心功能之一:
评分计算
ES 7.0 开始默认使用 BM25 算法计算相关性,主要考虑以下因素:
- 词频(TF):词在文档中出现的频率,出现越多分数越高(非线性增长)
- 逆文档频率(IDF):衡量词的稀有程度,越稀有权重越大
- 字段长度归一化:较短字段中的匹配通常比长字段中的匹配更相关
- 协调因子:匹配查询中更多词条的文档分数更高
相关性评分可能出现的问题及解决方案:
分片评分问题
在多分片环境下,由于每个分片只知道自己的文档统计信息,可能导致评分不统一。
解决方案:
- 使用
search_type=dfs_query_then_fetch
进行全局评分 - 确保索引有合理数量的分片
- 在单个分片上测试评分逻辑
评分调优
自定义和调整相关性评分的常用方法:
- 使用
boost
参数提升特定字段或词的权重 - 使用函数评分查询(function_score)添加自定义因子
- 使用字段加权(field boosting)调整多字段查询权重
- 通过
script_score
实现完全自定义的评分逻辑
总结
通过深入理解 Elasticsearch 的内部原理,我们能够:
- 构建更高效的索引和查询策略
- 更好地诊断和解决性能问题
- 合理规划资源分配和集群扩展
- 在架构设计时做出更明智的决策
ES 的设计结合了搜索引擎的复杂性和分布式系统的挑战,理解其工作原理是充分利用这一强大工具的关键。