【Elasticsearch从入门到精通】第12篇:Elasticsearch读写原理——主备复制模型与数据一致性
上一篇【第11篇】Elasticsearch索引API详解
下一篇【第13篇】Elasticsearch索引API深度解析
摘要
Elasticsearch作为分布式搜索引擎,其读写原理是理解整个系统行为的基石。本文从Primary-Backup主备复制模型切入,详细拆解一次写入请求从协调节点路由到primary分片本地执行再到replica分片同步确认的完整链路,同时覆盖读请求的分片选择策略与结果合并排序机制。文章重点解析consistency参数中one、quorum、all三种级别的一致性保障差异,以及primary故障与replica故障场景下的容错处理逻辑。通过一次线上写入失败的完整排查过程,将理论落到实践,帮助读者真正掌握ES读写行为的调试与优化方法。
一、数据复制模型:Primary-Backup设计
1.1 为什么需要复制
Elasticsearch运行在分布式集群中,单台机器随时可能宕机。如果一份数据只存一个副本,节点故障就意味着数据丢失。正因为此,ES为每个分片引入了主备复制模型(Primary-Backup Replication):每个索引被切分为多个主分片(Primary Shard),每个主分片可配置0个或多个副本分片(Replica Shard)。
索引 "products":3个primary分片,1个replica副本 ┌─────────────────────────────────────────────────┐ │ 分片 P0 (primary) 分片 R0 (replica) │ │ 分片 P1 (primary) 分片 R1 (replica) │ │ 分片 P2 (primary) 分片 R2 (replica) │ └─────────────────────────────────────────────────┘关键规则:
- 主分片数量在索引创建时确定,后续不可修改
- 副本分片数量可随时动态调整
- Primary及其对应的Replica绝不能在同一节点上
- 写操作只能由Primary分片处理,副本只负责复制
- 读操作可以由Primary或任意Replica处理
1.2 Primary和Replica的角色分工
| 角色 | 写入 | 读取 | 故障转移 |
|---|---|---|---|
| Primary分片 | 唯一写入入口,索引文档 | 可参与读取 | 故障时Replica晋升为Primary |
| Replica分片 | 被动接收Primary的复制数据 | 可参与读取(与Primary等价) | 故障时仅减少可用副本数 |
这个模型的设计哲学很简单:写走Primary保证数据一致性,读走所有分片实现负载均衡。
二、基本写模型流程
2.1 写请求的完整链路
一次文档写入的完整流程涉及三个角色:协调节点(Coordinating Node)、Primary分片所在节点、Replica分片所在节点。
客户端 → 协调节点 → Primary分片 → Replica分片 → 协调节点 → 客户端详细步骤:
- 客户端发送写请求到集群中的任意节点(该节点自动成为协调节点)
- 协调节点计算路由:根据文档ID通过哈希算法确定目标分片
shard = hash(_id) % number_of_primary_shards - 协调节点转发请求到Primary分片所在的节点
- Primary分片执行写入:验证文档、解析字段、写入Lucene索引
- Primary将操作转发给所有In-Sync Replica分片
- Replica分片执行相同操作并确认成功
- Primary收集确认:当满足consistency要求数量的Replica确认后,返回成功给协调节点
- 协调节点返回结果给客户端
2.2 路由计算机制
文档写入哪个分片,由路由公式决定:
shard_num = hash(_routing) % num_primary_shards默认情况下,_routing等于文档的_id。你也可以通过routing参数指定自定义路由值,确保相关联的文档落在同一个分片上。
POST/products/_doc?routing=user_123{"name":"无线蓝牙耳机","price":299,"category":"电子产品"}2.3 刷新与刷盘机制
写入操作并不会立即可见。ES在内存中维护一个索引缓冲区(Indexing Buffer),文档先写入缓冲区,然后经历两个关键操作:
| 操作 | 触发条件 | 作用 | 性能影响 |
|---|---|---|---|
| Refresh(刷新) | 默认每1秒自动执行 | 将缓冲区数据生成新的Lucene段(Segment),使文档可搜索 | 轻量,频繁执行影响不大 |
| Flush(刷盘) | translog达到阈值或每30分钟 | 将内存中的段通过fsync持久化到磁盘,清空translog | 较重,不应频繁执行 |
刷新频率可通过index.refresh_interval设置:
PUT/products/_settings{"index":{"refresh_interval":"5s"}}关键认知:文档写入后,默认最多1秒后即可搜索到(NRT,Near Real-Time,近实时搜索)。如果需要写入后立即可见,可以在写入请求上加refresh=wait_for参数:
POST/products/_doc?refresh=wait_for{"name":"机械键盘","price":599}2.4 Translog事务日志
为防止节点宕机丢失内存中的写入数据,ES引入了Translog(事务日志):
- 每次写入操作先追加到Translog(顺序写,性能极高)
- 写入成功后,数据才写入内存缓冲区
- 节点重启时,通过重放Translog恢复未持久化的数据
这与数据库的WAL(Write-Ahead Log)机制一致——先记日志,再改数据。
三、写流程错误处理
3.1 Primary分片故障
当Primary分片所在的节点宕机时,ES的故障转移流程如下:
- Master节点检测到Primary节点失联(默认30秒内无心跳)
- Master从In-Sync Replica中选举新的Primary
- 集群元数据更新,新Primary接管写入职责
- 协调节点重新路由请求到新Primary
整个过程中可能会有短暂的写入失败,客户端需要做好重试处理。重建索引时指定wait_for_active_shards可以控制最小存活分片数:
PUT/products{"settings":{"index":{"number_of_shards":3,"number_of_replicas":2}}}3.2 Replica分片故障
Replica故障的处理相对简单:
- Primary继续处理写入,将操作记录在Translog中
- Master将故障Replica标记为Out-of-Sync
- 当Replica节点恢复后,Master通知Primary将差异数据同步过去
- 如果故障时间过长,需要从Primary做全量拷贝
3.3 写入失败的常见原因与排查
| 故障类型 | 现象 | 排查手段 |
|---|---|---|
| 分片分配失败 | 索引状态为Yellow或Red | GET _cat/shards?v查看未分配原因 |
| 磁盘空间不足 | 写入返回403 Forbidden | GET _cat/allocation?v查看磁盘使用率 |
| 版本冲突 | 返回409 VersionConflict | 检查并发写入场景,考虑使用version_type=external |
| 映射冲突 | 字段类型不匹配 | GET /index/_mapping检查字段定义 |
| 协调节点超时 | 写入hang住后超时 | 检查GC日志和线程池队列深度 |
四、基本读模型
4.1 读请求的完整链路
读请求的流程与写请求类似,但多了一步结果合并:
- 客户端发送查询请求到任意节点(协调节点)
- 协调节点确定目标分片:根据查询范围选择需要参与的分片
- 协调节点分发查询到各分片(优先本地分片,减少网络开销)
- 每个分片独立执行查询,返回Top N结果给协调节点
- 协调节点合并结果:对来自各分片的结果进行全局排序、去重
- 协调节点返回最终结果给客户端
4.2 分片选择策略
对于读请求,ES支持多种分片选择策略,由preference参数控制:
GET/products/_search?preference=_local{"query":{"match":{"name":"耳机"}}}| preference值 | 行为 | 适用场景 |
|---|---|---|
_local | 优先使用本地分片,减少网络开销 | 对实时性要求不高的查询 |
_primary | 只查询Primary分片 | 需要最新写入数据的场景 |
_replica | 只查询Replica分片 | 降低Primary负载 |
_primary_first | 先Primary,不可用时用Replica | 偏向一致性的选择 |
_replica_first | 先Replica,不可用时用Primary | 偏向性能的选择 |
_only_local | 只查询本地分片 | 极限降低延迟 |
| 自定义字符串 | 相同字符串落到相同分片 | 翻页一致性和缓存利用 |
默认行为:ES使用自适应副本选择(Adaptive Replica Selection,ARS),根据各分片的响应时间、队列长度、服务时间等指标动态选择最快的副本,而非简单的round-robin轮询。
4.3 结果整合与排序
协调节点需要合并来自多个分片的结果,这一过程称为两阶段查询:
- Query阶段:协调节点向各分片发送查询,每个分片返回
from + size个文档的排序值和ID - Fetch阶段:协调节点根据排序结果,向包含这些文档的分片请求完整文档内容
这就是search_type默认为query_then_fetch的原因——先查后取。
五、一致性保证机制
5.1 Consistency参数详解
ES通过consistency参数(7.x版本后迁移为wait_for_active_shards)控制写入时所需的最小确认分片数。
| 级别 | 含义 | 最小确认数 | 适用场景 |
|---|---|---|---|
one | 只要Primary写入成功即返回 | 1个分片 | 可容忍数据丢失的日志场景 |
quorum(默认) | 需要多数分片确认 | (replicas + 1) / 2 + 1个分片 | 通用业务场景 |
all | 所有分片都需确认 | replicas + 1个分片 | 金融、交易等强一致性场景 |
5.2 Quorum的计算
假设有1个Primary + 2个Replica,共3个分片,则quorum为:
quorum = int((primary + replicas) / 2) + 1 = int((1 + 2) / 2) + 1 = int(1.5) + 1 = 2即至少需要2个分片确认(可以是Primary + 1个Replica)。如果只有2个分片存活(比如1个Replica故障),quorum仍然可以满足,写入不受影响。
在新版ES中,使用wait_for_active_shards参数:
PUT/orders/_doc/1001?wait_for_active_shards=2{"product":"笔记本电脑","price":6999,"status":"paid"}5.3 版本控制与乐观锁
ES使用_version字段实现乐观锁,防止并发写入冲突:
PUT/products/_doc/abc123?version=5{"name":"蓝牙耳机升级版","price":359}如果当前文档版本不是5,请求会返回409冲突错误。这是分布式系统中实现数据一致性的经典手段。
| 版本控制方式 | 说明 | 适用场景 |
|---|---|---|
| Internal Versioning | ES自动维护的_version字段,从1递增 | 默认版本控制 |
| External Versioning | 业务系统传入外部版本号 | 与外部系统保持版本同步 |
| If-Seq-No / If-Primary-Trem | 基于序列号和任期号的条件更新 | 替代_version的现代方式 |
使用if_seq_no和if_primary_term进行条件更新:
PUT/products/_doc/abc123?if_seq_no=10&if_primary_term=1{"name":"蓝牙耳机终极版","price":399}六、实战案例:写入失败完整排查过程
6.1 故障场景
某电商系统反馈:商品信息批量导入时,部分文档写入失败,返回503 Service Unavailable或超时错误。环境配置:
- 集群:3个节点
- 索引
products:3 Primary + 1 Replica - 并发写入:5个线程,每批1000条
6.2 排查步骤
第一步:检查集群健康状态
GET_cluster/health响应:
{"cluster_name":"ecommerce","status":"yellow","unassigned_shards":2,"number_of_nodes":3,"number_of_data_nodes":3,"active_primary_shards":3,"active_shards":4,"relocating_shards":0,"initializing_shards":0}发现集群状态为Yellow,有2个分片未分配。根据CAP理论,此时可能某些副本不可用。
第二步:查看未分配分片详情
GET_cat/shards/products?v&h=index,shard,prirep,state,unassigned.reason,node输出显示有2个replica分片处于UNASSIGNED状态,原因是NODE_LEFT(节点离开集群)。
第三步:检查节点和磁盘状态
GET_cat/allocation?v发现其中一个数据节点磁盘使用率达到95%,触发了ES的磁盘水位线保护机制——默认当磁盘使用超过95%时,ES会将对应节点的分片迁移出去,拒绝新分片的分配。这导致replica分片无法分配。
第四步:检查写入失败的详细日志
GET/_cluster/allocation/explain{"index":"products","shard":0,"primary":false}响应中explanation字段提示:the node is above the high watermark cluster setting,确认是磁盘水位线问题。
6.3 解决方案
清理磁盘空间或调整水位线设置(临时方案):
PUT_cluster/settings{"transient":{"cluster.routing.allocation.disk.watermark.low":"90%","cluster.routing.allocation.disk.watermark.high":"95%","cluster.routing.allocation.disk.watermark.flood_stage":"98%"}}清理后触发分片重新分配:
POST_cluster/reroute?retry_failed=true分片分配完成后,集群恢复Green状态,写入恢复正常。
6.4 根因总结
| 层级 | 问题 | 原因 | 解决 |
|---|---|---|---|
| 表现层 | 写入返回503/超时 | wait_for_active_shards默认all,找不到足够分片 | 先恢复集群健康 |
| 中间层 | 分片未分配 | 磁盘水位线触发保护 | 清理磁盘或扩容 |
| 根因层 | 磁盘使用率过高 | 日志未及时清理、索引未设置生命周期 | 配置ILM策略自动清理 |
七、最佳实践总结
7.1 写入优化
- 批量写入优先使用
_bulkAPI,减少网络往返次数 - 合理设置
refresh_interval:高写入场景设为30s甚至-1(关闭自动刷新),写入完成后再恢复 - 关闭不需要的实时性:批处理导入时设置
refresh_interval=-1和number_of_replicas=0,完成后恢复 - 使用自动生成的ID:ES自行生成ID比客户端指定ID性能更好(避免额外的查找开销)
- 监控线程池队列:关注
write和search线程池的拒绝情况
7.2 一致性配置
- 默认使用quorum:平衡性能和一致性
- 日志分析场景用one:允许少量数据丢失,换取更高写入吞吐
- 金融交易场景用all:虽然影响性能,但数据完整性优先
- 生产环境
number_of_replicas >= 1:至少保留一个副本保证容灾
7.3 集群运维
- 设置磁盘水位线告警:磁盘使用率80%就该关注
- 配置ILM生命周期策略:自动清理过期索引
- 定期检查集群健康:
GET _cluster/health纳入监控 - 写入失败时先看集群状态:Yellow/Red状态是多数写入问题的根源
7.4 读请求优化
- 根据场景选择preference:需要最新数据用
_primary,追求性能用_local - 合理设置size:不要无意义地请求超大结果集
- 使用
filter_path减少传输量:只返回需要的字段
GET/products/_search?filter_path=hits.hits._source.name,hits.hits._source.price{"query":{"match":{"name":"耳机"}}}八、小结
Elasticsearch的读写原理并不神秘。写请求遵循协调节点→Primary分片→Replica分片的单向链路,由Primary统一管理写入以确保一致性;读请求则通过协调节点的两阶段查询(Query Then Fetch),从多个分片并行获取结果并合并排序。理解这套机制,你就能在遇到性能瓶颈或写入故障时快速定位问题——是Primary分片负载过高、是副本同步延迟、还是磁盘水位线触发了保护机制——都能在分片级别的视角下找到答案。
下一篇文章我们将深入Elasticsearch的索引API,探讨索引的生命周期管理和映射策略。
上一篇【第11篇】Elasticsearch索引API详解
下一篇【第13篇】Elasticsearch索引API深度解析
