前面讲到了一个checkpoint(检查点的概念),在每次写入数据过程都需要更新LocalCheckpoint(本地检查点)和GlobalCheckpoint(全局检查点)。
3.3 更新checkpoint
了解checkpoint之前,先来看下Primary Terms和Sequence Numbers:
Primary Terms: 由主节点分配给每个主分片,每次主分片发生变化时递增。主要作用是能够区别新旧两种主分片,只对最新的Terms进行操作。
Sequence Numbers: 标记发生在某个分片上的写操作。由主分片分配,只对写操作分配。假设索引test有两个主分片一个副本分片,当0号分片的序列号增加到5时,它的主分片离线,副本提升为新的主,对于后续的写操作,序列号从6开启递增。1号分片有自己独立的Sequence Numbers。
主分片在每次向副本转发写请求时,都会带上这两个值。
有了Primary Terms和Sequence Numbers,理论上好像就可以检测出分片之间的差异(从旧的主分片删除新的主分片操作历史中不存在的操作,并且将缺少的操作索引到旧主分片),但是当同时为每秒成百上千的事件做索引时,比较数百万个操作的历史是不切实际的,且耗费大量的存储成本,所以ES维护了一个GlobalCheckpoint的安全标记。
先来看下checkpoint的概念和作用:
GlobalCheckpoint: 全局检查点是所有活跃分片历史都已经对齐的序列号,即所有低于全局检查点的操作都保证已被所有活跃的分片处理完毕。这意味着,当主分片失效时,我们只需要比较新主分片和其他副本分片之间的最后一个全局检查点之后的操作即可。当就主分片恢复时,使用它知道的全局检查点,与新的主分片进行比较。这样,我们只需要进行小部分操作比较,而不是全部。
主分片负责推进全局检查点,它通过跟踪副本上完成的操作来实现。一旦检测到有副本分片已经超出给定序列号,它将相应的更新全局检查点。副本分片不会跟踪所有操作,而是维护一个本地检查点。
LocalCheckpoint: 本地检查点也是一个序列号,所有序列号低于它的操作都已在该分片上(写lucene和translog成功)处理完毕。
全局检查点和本地检查点在内存中维护,但也会保存在每个lucene提交的元数据中。
我们通过源码来看,写入过程中是如何更新本地检查点和全局检查点的:
主分片写入成功之后,会进行LocalCheckpoint的更新操作,代码入口:ReplicationOperation#execute()->PrimaryShardReference#updateLocalCheckpointForShard(…)->IndexShard#updateLocalCheckpointForShard(…)->ReplicationTracker#updateLocalCheckpoint(…),代码如下:
primaryResult = primary.perform(request); //写主,写lucene和translog
primary.updateLocalCheckpointForShard(primaryRouting.allocationId().getId(), primary.localCheckpoint()); //更新LocalCheckpoint
public synchronized void updateLocalCheckpoint(final String allocationId, final long localCheckpoint) {
.....
//获取主分片本地的checkpoints,包括LocalCheckpoint和GlobalCheckpoint
CheckpointState cps = checkpoints.get(allocationId);
.....
// 检查是否需要更新LocalCheckpoint,即需要更新的值是否大于当前已有值
boolean increasedLocalCheckpoint = updateLocalCheckpoint(allocationId, cps, localCheckpoint);
// pendingInSync是一个保存等待更新LocalCheckpoint的Set,存放allocation IDs
boolean pending = pendingInSync.contains(allocationId);
// 如果是待更新的,且当前的localCheckpoint大于等于GlobalCheckpoint(每次都是先更新Local再Global,正常情况下,Local应该大于等于Global)
if (pending && cps.localCheckpoint >= getGlobalCheckpoint()) {
//从待更新集合中移除
pendingInSync.remove(allocationId);
pending = false;
//此分片是否同步,用于更新GlobalCheckpoint时使用
cps.inSync = true;
replicationGroup = calculateReplicationGroup();
logger.trace("marked [{}] as in-sync", allocationId);
notifyAllWaiters();
}
//更新GlobalCheckpoint
if (increasedLocalCheckpoint && pending == false) {
updateGlobalCheckpointOnPrimary();
}
assert invariant();
}
继续看是如何更新GlobalCheckpoint的:
private synchronized void updateGlobalCheckpointOnPrimary() {
assert primaryMode;
final CheckpointState cps = checkpoints.get(shardAllocationId);
final long globalCheckpoint = cps.globalCheckpoint;
// 计算GlobalCheckpoint,即检验无误后,取Math.min(cps.localCheckpoint, Long.MAX_VALUE)
final long computedGlobalCheckpoint = computeGlobalCheckpoint(pendingInSync, checkpoints.values(), getGlobalCheckpoint());
// 需要更新到的GlobalCheckpoint值比当前的global值大,则需要更新
if (globalCheckpoint != computedGlobalCheckpoint) {
cps.globalCheckpoint = computedGlobalCheckpoint;
logger.trace("updated global checkpoint to [{}]", computedGlobalCheckpoint);
onGlobalCheckpointUpdated.accept(computedGlobalCheckpoint);
}
}
主分片的检查点更新完成之后,会向副本分片发送对应的写请求,发送请求时同时传入了globalCheckpoint和SequenceNumbers。
final long globalCheckpoint = primary.globalCheckpoint();
final long maxSeqNoOfUpdatesOrDeletes = primary.maxSeqNoOfUpdatesOrDeletes();
final ReplicationGroup replicationGroup = primary.getReplicationGroup();
markUnavailableShardsAsStale(replicaRequest, replicationGroup);
performOnReplicas(replicaRequest, globalCheckpoint, maxSeqNoOfUpdatesOrDeletes, replicationGroup);
写副本分片时,进入performOnReplica方法,当监听到分片写入成功之后,则开始更新本地检查点,然后更新全局检查点,更新分方法和之前一样,通过比较当前的检查点是否大于历史检查点,如果是则更新。
replicasProxy.performOn(shard, replicaRequest, globalCheckpoint, maxSeqNoOfUpdatesOrDeletes, new ActionListener<ReplicaResponse>() {
@Override
public void onResponse(ReplicaResponse response) {
successfulShards.incrementAndGet();
try {
//更新LocalCheckpoint
primary.updateLocalCheckpointForShard(shard.allocationId().getId(), response.localCheckpoint());
//更新globalCheckpoint
primary.updateGlobalCheckpointForShard(shard.allocationId().getId(), response.globalCheckpoint());
} catch (final AlreadyClosedException e) {
....
} catch (final Exception e) {
....
}
decPendingAndFinishIfNeeded();
}
@Override
public void onFailure(Exception replicaException) {
.....
}
});
总结思考
总体的写入流程源码已经分析完成:
- 数据可靠性:ES通过副本和Translog保障数据的安全;
- 服务可用性:在可用性和一致性取舍上,ES更倾向于可用性,只要主分片可用即可执行写入操作;
- 数据一致性:只要主写入成功,数据就可以被读取,所以查询时操作在主分片和副本分片上可能会得到不同的结果;
- 原子性:索引的读写、别名操作都是原子操作,不会出现中间状态。但是bulk不是原子操作,不能用来实现事物;