从源码角度了解Elasticsaerch(分布式协调排序、深分页问题)
引文
Elasticsearch基于Lucene所以很多系统实现都在其中,所以可以先看看Lucene的实现:
https://blog.csdn.net/qq_35040959/article/details/147931034
项目组件
不像Kafka这种顶级项目核心性能组件全自己实现,ELK中有很多引用至第三方开放库;
网络模型-Netty
网络模型多重要不必多说,Elasticsearch基于Netty,一个认可度&社区活跃度都非常高的NIO多路复用模型;
存储模型-Lucene
Elasticsearch基于Lucene上提供分布式服务;具体可以看看这篇文章对存储模型的介绍
https://blog.csdn.net/qq_35040959/article/details/147931034
分布式查询
//GET http://localhost:9200/my-index-000001/_search
{"from": 10, //第二页"size": 10, //查询十条"_source": ["title", "priority"], //指定字段返回"query": {"bool": {"must": [{ "match": { "title": "guide" } }//全匹配]}}
}
构建查询
org.elasticsearch.action.search.TransportSearchAction#executeSearch
分发到每个shard
org.elasticsearch.action.search.AbstractSearchAsyncAction#run
发送shard请求
org.elasticsearch.transport.TransportService#sendLocalRequest
单条查询(查询Lucene)
org.elasticsearch.search.query.QueryPhase#searchWithCollector
[Query]阶段查询结果,只包含指定范围的score与docid
org.elasticsearch.action.search.QueryPhaseResultConsumer#consumeResult
聚合数据(对多个shard的数据聚合排序后决定实际获取的)
org.elasticsearch.action.search.FetchSearchPhase#innerRun -> resultConsumer.reduce();
[Fetch]阶段,读取指定docid文档
org.elasticsearch.action.search.FetchSearchPhase#executeFetch
[Query]&[Fetch]步骤基于Lucene的相关性查询与指定docid查询
深分页-search_after分页查询
**<<分布式查询>>**是基于相关性的查询,这种查询会扫描倒排表构建K-TOP堆,这种方法在深页后(如10000页后)查询成本非常高;
search_after必须指定"sort"排序字段后通过"search_after"定位在排序中的其实位置,这种方式没有构建K-TOP的流程,定位到指定的排序偏移量后直接获取,更加快速;
缺点就是不再是相关性排序;
//GET http://localhost:9200/my-index-000001/_search
{"from": 0, "size": 10, "_source": ["title", "priority"], "query": {"bool": {"must": [{ "match": { "title": "guide" } }]}},"sort": [{ "_id": "asc" } //强制要求:指定排序(不难理解:没有固定的排序规则,那么给定的search_after将没法用于排查此前已经吐出的数据)],"search_after": [ "QHHnZ5cBTxKf9WaM2zpf"] //强制要求:上一页最后一条文档_id
}
基于DocValues实现
深分页-scroll查询
search_after不是基于快照,会受到同步数据插入影响,scroll查询对索引打快照;
资源占用与性能对比:
特性 | Scroll API | Search After API |
---|---|---|
上下文维护 | 需要维护 _scroll_id 上下文 | 无上下文,仅需传递排序值 |
内存占用 | 高(需缓存快照和游标状态) | 低(无持久化状态) |
CPU 开销 | 低(顺序扫描,无需动态排序) | 中等(需动态定位排序值位置) |
网络传输 | 批量高效(适合大数据量导出) | 单页高效(适合高频分页请求) |
实时性与一致性对比
特性 | Scroll API | Search After API |
---|---|---|
数据一致性 | 强一致性(基于快照,数据静态) | 弱一致性(实时数据,可能变化) |
反映更新 | 否(快照期间索引更新不可见) | 是(每次查询读取最新数据) |
适用场景 | 全量导出、离线分析(如日志备份) | 实时分页、深度检索(如用户搜索) |
todo 快照原理
一致性协议
分布式问题
主节点
Elastic有一下节点角色[Master Node(主节点)]、[Data Node(数据节点)]、[Coordinating Node(协调节点)]三个核心角色(还有其他的角色),主要介绍主节点:
主节点(Master Node)
-
职责:管理集群元数据(索引创建/删除、分片分配、节点状态监控)。
-
细分类型:
– 候选主节点(Master-Eligible):参与选举,可成为主节点(配置:node.roles: [master])。
– 仅投票主节点(Voting-Only):仅参与选举投票,不担任主节点(配置:node.roles: [master, voting_only])。 -
最佳实践:
– 生产集群需至少 3个候选主节点,且半数以上非仅投票节点,避免脑裂。
– 与数据节点分离,确保稳定性。
集群中将会只有一个节点成为[Master Node]并为集群中需要强一致性的行为做唯一决策出口;
主节点选举
Elasticsearch 的选举机制是对 Raft (论文)的工程化改良,通过牺牲部分理论严谨性换取分布式搜索场景下的实操效率,核心差异本质是性能与强一致性之间的权衡
1. 多票制
节点在同一任期内可多次投票,支持多个候选人同时当选。
冲突解决:若多个候选人同时当选,ES 采用 “最后当选者有效” 原则(如 Node2 先当选,但收到 Node3 的投票请求后主动退位,最终 Node3 成为主节点)。
目的:避免小规模集群中因节点同时竞选导致选举失败(如 3 节点均自投),提升选举速度。
2. 预投票流程(PreVote)的差异
Raft :
候选人需确认日志足够新(term 更大或 index 更大)且获得多数支持,才发起正式投票。
Elasticsearch 的 PreVote:
检查条件更宽松:仅需候选人 term ≥ 当前节点 term,或 term 相同时候选人版本号 ≥ 当前节点版本号。
风险:宽松条件可能导致网络分区节点频繁发起无效选举,但提高选举速度,同时 ES 通过动态退避机制(back_off_time)降低影响。
3. 状态转换规则的灵活性
Raft 的严格转换:
节点状态需按 Follower → Candidate → Leader 顺序转换,且 Leader 退位后需先回退至 Follower。
Elasticsearch 的灵活转换:
Leader 退位规则:Leader 收到任何更高 term 的投票请求(RequestVote)时,立即退位为 Candidate(而非 Follower)。
目的:加速新主节点产生,避免旧 Leader 阻塞选举进程。
4. 脑裂处理策略
Raft 的严格多数票:
仅获得多数票的分区可选出 Leader,天然避免脑裂。
Elasticsearch 的妥协方案:
允许多个分区同时选出 Leader,但通过 “最后当选者有效” 和 Leader 主动退位 解决冲突(非实时避免脑裂)。
风险:极端情况下可能短暂存在多主,但数据一致性通过分片分配机制(主节点唯一管理元数据)保障。
5. 性能与规模适应性
选举速度:
ES 的并行投票和宽松 PreVote 使小规模集群选举更快(通常 < 500ms)。
Raft 的严格流程在大规模集群中稳定性更优,ES 需依赖参数调优(如增大 cluster.election.duration)减少竞争。
大规模集群挑战:
ES 在超 40 节点时可能出现频繁主节点切换,Raft 可稳定支持 50+ 节点。
cluster.election.initial_timeout: 100ms # 初始等待时间
cluster.election.back_off_time: 100ms # 退避增量
cluster.election.max_timeout: 10s # 最大等待时间
仅投票节点(Voting-only Node):专用于投票但不存储数据,提升大规模集群选举稳定性。
小规模集群(<10 节点):ES 默认配置即可,利用多票制提升选举效率。
大规模集群(>20 节点):
部署专用主节点(无 data 角色)和仅投票节点;
调大 cluster.election.duration(如 1s)减少竞争。
添加新分片&重平衡的影响
一、修改副本分片数(Replicas)
场景:增加或减少副本分片数(如从number_of_replicas=1改为2)。
特点:在线操作,无需重建索引,数据不会丢失。
添加副本可以在线操作:看过raft论文的添加新节点的流程就能理解为什么可以在线操作;
二、修改主分片数(Primary Shards)
场景:调整主分片数量(如从number_of_shards=3改为6)。
特点:必须重建索引!原始索引数据不会自动迁移到新分片布局。
ES数据使用计算Hash分配数据,修改只能重建索引;