“Elasticsearch分布式一致性原理剖析”系列将会对Elasticsearch的分布式一致性原理进行详细的剖析,介绍其实现方式、原理以及其存在的问题等(基于6.2版本)。前两篇文章介绍了ES中集群如何组成,master选举算法,master更新meta的流程等,并分析了选举、Meta更新中的一致性问题。本文会分析ES中的数据流,包括其写入流程、算法模型PacificA、SequenceNumber与Checkpoint等,并比较ES的实现与标准PacificA算法的异同。目录如下:
问题背景
数据写入流程
PacificA算法
SequenceNumber、Checkpoint与故障恢复
ES与PacificA的比较
小结
问题背景
用过ES的同学都知道,ES中每个Index会划分为多个Shard,Shard分布在不同的Node上,以此来实现分布式的存储和查询,支撑大规模的数据集。对于每个Shard,又会有多个Shard的副本,其中一个为Primary,其余的一个或多个为Replica。数据在写入时,会先写入Primary,由Primary将数据再同步给Replica。在读取时,为了提高读取能力,Primary和Replica都会接受读请求。
![](https://img-blog.csdnimg.cn/img_convert/7df932682286c19f265d177229972f25.png)
在这种模型下,我们能够感受到ES具有这样的一些特性,比如:
数据高可靠:数据具有多个副本。
服务高可用:Primary挂掉之后,可以从Replica中选出新的Primary提供服务。
读能力扩展:Primary和Replica都可以承担读请求。
故障恢复能力:Primary或Replica挂掉都会导致副本数不足,此时可以由新的Primary通过复制数据产生新的副本。
另外,我们也可以想到一些问题,比如:
数据怎么从Primary复制到Replica?
一次写入要求所有副本都成功吗?
Primary挂掉会丢数据吗?
数据从Replica读,总是能读到最新数据吗?
故障恢复时,需要拷贝Shard下的全部数据吗?
可以看到,对于ES中的数据一致性,虽然我们可以很容易的了解到其大概原理,但是对其细节我们还有很多的困惑。那么本文就从ES的写入流程,采用的一致性算法,SequenceId和Checkpoint的设计等方面来介绍ES如何工作,进而回答上述这些问题。需要注意的是,本文基于ES6.2版本进行分析,可能很多内容并不适用于ES之前的版本,比如2.X的版本等。
数据写入流程
首先我们来看一下数据的写入流程,读者也可以阅读这篇文章来详细了解:
https://zhuanlan.zhihu.com/p/34669354。
Replication角度: Primary -> Replica
我们从大的角度来看,ES写入流程为先写入Primary,再并发写入Replica,最后应答客户端,流程如下:
检查Active的Shard数。
finalString activeShardCountFailure=checkActiveShardCount();
写入Primary。
primaryResult=primary.perform(request);
并发的向所有Replicate发起写入请求
performOnReplicas(replicaRequest,globalCheckpoint,replicationGroup.getRoutingTable());
等所有Replicate返回或者失败后,返回给Client。
privatevoiddecPendingAndFinishIfNeeded(){
assertpendingActions.get()>0:"pending action count goes below 0 for request ["+request+"]";
if(pendingActions.decrementAndGet()==0){
finish();
}
}
上述过程在ReplicationOperation类的execute函数中,完整代码如下:
publicvoidexecute()throwsException{
finalString activeShardCountFailure=checkActiveShardCount();
finalShardRouting primaryRouting=primary.routingEntry();
finalShardId primaryId=primaryRouting.shardId();
if(activeShardCountFailure!=null){
finishAsFailed(newUnavailableShardsException(primaryId,
"{} Timeout: [{}], request: [{}]",activeShardCountFailure,request.timeout(),request));
return;
}
totalShards.incrementAndGet();
pendingActions.incrementAndGet();// increase by 1 until we finish all primary coordination
primaryResult=primary.perform(request);
primary.updateLocalCheckpointForShard(primaryRouting.allocationId().getId(),primary.localCheckpoint());
finalReplicaRequest replicaRequest=primaryResult.replicaRequest();
if(replicaRequest!=null){
if(logger.isTraceEnabled()){
logger.trace("[{}] op [{}] completed on primary for request [{}]",primaryId,opType,request);
}
// we have to get the replication group after successfully indexing into the primary in order to honour recovery semantics.
// we have to make sure that every operation indexed into the primary after recovery start will also be replicated
// to the recovery target. If we used an old replication grou