redis日志复制和心跳部分(pipeline机制)

主要依赖Replicator、LogManager、LogStorage这三个实现。
Replicator,leader发送日志和心跳的功能就是在此实现。每个leader>>都会有一个ReplicatorGroup,用来管理所有followers
LogManager用于处理日志,主要就是消费复制或者apply的日志,将其写入磁盘。
LogStorage主要就是日志的底层存储工作。给予RocksDB。

我们先从Replicator开始。首先何时创建:
当节点成为leader后,会启动所有follower和learner的replicator。其实是通过addReplicator方法实现的。

for (final PeerId peer : this.conf.listPeers()) {
    if (peer.equals(this.serverId)) {
        continue;
    }
    LOG.debug("Node {} add a replicator, term={}, peer={}.", getNodeId(), this.currTerm, peer);
    if (!this.replicatorGroup.addReplicator(peer)) {
        LOG.error("Fail to add a replicator, peer={}.", peer);
    }
}
// Start learner's replicators
for (final PeerId peer : this.conf.listLearners()) {
    LOG.debug("Node {} add a learner replicator, term={}, peer={}.", getNodeId(), this.currTerm, peer);
    if (!this.replicatorGroup.addReplicator(peer, ReplicatorType.Learner)) {
        LOG.error("Fail to add a learner replicator, peer={}.", peer);
    }
}

这个learner是什么角色?

addReplicator方法
1.从failureReplicators中移除需要add的peer,这个肯定是要执行的。可能存在failureReplicators不存在当前peer的case。
2.复制一份集群配置,然后调用Replicator.start 方法。
3.成功的话,将返回的ThreadId 加入到replicatorMap,失败加入到failureReplicators。

Replicator#start方法
1.初始化Replicator对象。
2.调用connect方法和对应节点建立连接。
3.创建ThreadId,其实是一个对Replicator对象的不可重入锁。只有获取到锁的情况Replicator才可用。其实就是维护Replicator的竞态条件。全局锁。我理解不可重入的原因就是同一线程不同操作的时候需要保证Replicator的安全。
4.执行监听回调,业务方可以实现对应监听器。
5.打开超时心跳,因为这个心跳是可动态调整的,所以并没有直接使用定时器。每次通过定时任务启动。这个方法会在每次心跳返回的时候再次调用。
final long dueTime = startMs + this.options.getDynamicHeartBeatTimeoutMs();
try {
this.heartbeatTimer = this.timerManager.schedule(() -> onTimeout(this.id), dueTime - Utils.nowMs(),
TimeUnit.MILLISECONDS);
} catch (final Exception e) {
LOG.error(“Fail to schedule heartbeat timer”, e);
onTimeout(this.id);
}

onTimeout触发之后会发送一条心跳信息。如果这个心跳没有返回。那么leader不会一直发送心跳。
6.发送一条探测消息(sendEmptyEntries方法)。
到这里我们基本了解了Replicator的启动流程,其实就是初始化,启动心跳,发送探针消息。其实就是用来询问当前follower的复制偏移。

sendEmptyEntries方法
这个方法如果为false,代表是探测消息,如果是true代表是心跳。
什么时候发送探测消息?

节点刚成为leader(start)
发送日志超时的时候,会发送探测消息。
如果响应超时,如果jraft打开pipeline,会有一个pendingResponses阈值。如果响应队列数据大于这个值会调用该方法,并不会在响应超时的时候,无限loop。
收到无效的日志请求。
发送日志请求不成功

发送心跳的逻辑
这里注意其实jraft会有两种心跳。一种就是在读leader的时候会用到。后面会说。另一种就是保活心跳

if (isHeartbeat) {
    // Sending a heartbeat request
    this.heartbeatCounter++;
    RpcResponseClosure<AppendEntriesResponse> heartbeatDone;
    // Prefer passed-in closure.
    if (heartBeatClosure != null) {
        heartbeatDone = heartBeatClosure;
    } else {
        heartbeatDone = new RpcResponseClosureAdapter<AppendEntriesResponse>() {
            @Override
            public void run(final Status status) {
                onHeartbeatReturned(Replicator.this.id, status, request, getResponse(), monotonicSendTimeMs);
            }
        };
    }
    this.heartbeatInFly = this.rpcService.appendEntries(this.options.getPeerId().getEndpoint(), request,
        this.options.getElectionTimeoutMs() / 2, heartbeatDone);
}

其实follower处理心跳的逻辑比较简单。
1.根据请求的任期,进行对应的操作(更新leader或通知leader跟高的任期)
2.如果日志对不上,会返回false。并本地最后一条日志索引号。
3.如果都ok返回成功。
心跳消息的回调。RpcResponseClosureAdapter的run逻辑
1.如果不ok,也就是请求失败,启动下一轮心跳定时器。调用监听回调方法通知失败。更新Replicator的状态为probe
2.如果当前节点term失效,执行stepDown,也就是心跳响应,返回了更高的term
3.如果日志不匹配。发送探测请求。启动下一轮心跳定时器。
4.更新lastRpcSendTimestamp。

这里可以看出来,如果当前leader失效,就不会再启动心跳定时器。

AppendEntriesRequestProcessor#processRequest0
if (node.getRaftOptions().isReplicatorPipeline()) {
    final String groupId = request.getGroupId();
    final String peerId = request.getPeerId();
    final int reqSequence = getAndIncrementSequence(groupId, peerId, done.getRpcCtx().getConnection());
    final Message response = service.handleAppendEntriesRequest(request, new SequenceRpcRequestClosure(done,
        reqSequence, groupId, peerId));
    if (response != null) {
        sendSequenceResponse(groupId, peerId, reqSequence, done.getRpcCtx(), response);
    }
    return null;
} else {
    return service.handleAppendEntriesRequest(request, done);
}

如果开启Pipeline,则会走前面if逻辑,对于Pipeline优化后面说。这里说一下,处理日志的是一个单线程的过程。因为没有io。通过队列异步解藕。
handleAppendEntriesRequest方法
这个方法,用于处理日志请求,心跳请求和探测请求。
因为不同的请求,都有一部分逻辑是公用的,也就是检测当前leader的合法性。
1.如果不可用,直接返回无效
2.如果请求中term小于当前term,说明leader失效。
3.检查更新当前节点的term。更新lastLeaderTimestamp
4.如果正在安装快照,那么拒绝接受该请求,返回busy。
5.如果last日志索引不一致。返回false,并返回当前lastLogIndex。
6.如果是心跳请求,或者探测请求,更新LastCommitIndex。返回相应信息。
探测请求其实就是所有follower的日志都必须跟随当前leader,如果超前,那么会多次探测,直到和leader一致。

7.如果是日志消息,根据请求解析日志,这里每条日志都有一个EntryMeta,用于记录对应日志的元数据信息。解析完成后,如需校验则进行校验。
8.调用logManager的appendEntries 方法添加日志。并且注册了回调FollowerStableClosure 。
其实logManager成功append之后,回调会响应leader。后面会分析logManager的append。
我们再来看一下是如何处理响应消息的。

onRpcReturned方法
这个方法主要就是发送探针日志、发送正常日志、发送安装快照之后的回调。
其实首先就是对消息的预处理,比如pipeline的实现。利用了优先队列存储已响应的请求。保证消息的有序。只会按照发送顺序处理响应。如果顺序错乱则会无视所有已发送的请求。
1.如果消息版本不正确,则忽略。
2.构造响应,加入到优先队列,如果阻塞的响应过多。
3.迭代队列,如果seq不匹配,直接返回。因为raft是需要顺序处理所有响应的。
如果消息错乱那么会重置当前发送状态
void resetInflights() {
this.version++;
this.inflights.clear();
this.pendingResponses.clear();
final int rs = Math.max(this.reqSeq, this.requiredNextSeq);
this.reqSeq = this.requiredNextSeq = rs;
releaseReader();
}

并在一定时间后重新发送探测消息。
4.如果有正常的消息处理,根据类型执行对应的操作。我们这里主要看处理日志消息的方法

onAppendEntriesReturned方法
这个方法才是真正处理消息的回调方法。上面的方法只是实现了消息的预处理。预处理失败就不会执行这个方法
1.如果请求不OK,则会阻塞一段时间后再测探测,而不是一直执行失败的请求。这个是物理链路问题
2.如果success为false,说明失败,清空发送请求的队列。更新nextIndex ,发送探测消息。这个是raft状态问题导致的失败。
3.如果成功,如果是探测消息,那么会更新r.state。
如果是日志消息成功,那么会调用commitAt方法更新提交偏移。
更新r.nextIndex。
if (entriesSize > 0) {
if (r.options.getReplicatorType().isFollower()) {
// Only commit index when the response is from follower.
r.options.getBallotBox().commitAt(r.nextIndex, r.nextIndex + entriesSize - 1, r.options.getPeerId());
}
} else {
// The request is probe request, change the state into Replicate.
r.state = State.Replicate;
}
r.nextIndex += entriesSize;
r.hasSucceeded = true;
r.notifyOnCaughtUp(RaftError.SUCCESS.getNumber(), false);
// dummy_id is unlock in _send_entries
if (r.timeoutNowIndex > 0 && r.timeoutNowIndex < r.nextIndex) {
r.sendTimeoutNow(false, false);
}

commitAt方法。
final long startAt = Math.max(this.pendingIndex, firstLogIndex);
Ballot.PosHint hint = new Ballot.PosHint();
for (long logIndex = startAt; logIndex <= lastLogIndex; logIndex++) {
final Ballot bl = this.pendingMetaQueue.get((int) (logIndex - this.pendingIndex));
hint = bl.grant(peer, hint);
if (bl.isGranted()) {
lastCommittedIndex = logIndex;
}
}
if (lastCommittedIndex == 0) {
return true;
}
this.pendingMetaQueue.removeFromFirst((int) (lastCommittedIndex - this.pendingIndex) + 1);
this.pendingIndex = lastCommittedIndex + 1;
this.lastCommittedIndex = lastCommittedIndex;
} finally {
this.stampedLock.unlockWrite(stamp);
}
this.waiter.onCommitted(lastCommittedIndex);

核心代码如上,其中pendingIndex为上一次阻塞的偏移。他为lastCommittedIndex + 1。没有真正commit的ballot都会在pendingMetaQueue中存在,每次响应成功都会调用bl.grant方法。最后根据bl.isGranted结果断定是否更新
lastCommittedIndex。
最后调用this.waiter.onCommitted执行状态机提交操作。

https://juejin.cn/post/6844904016057483277
这是另一篇分析

向该Follower发送一个AppendEntriesRequest请求,onRpcReturned负责响应请求。
发送完请求后调用addInflight初始化一个Inflight实例,加入到inflights集合中,如下:

private void addInflight(final RequestType reqType, final long startIndex, final int count, final int size,
                         final int seq, final Future<Message> rpcInfly) {
    this.rpcInFly = new Inflight(reqType, startIndex, count, size, seq, rpcInfly);
    this.inflights.add(this.rpcInFly);
    this.nodeMetrics.recordSize("replicate-inflights-count", this.inflights.size());
}

Inflight 是对批量发送出去的 logEntry 的一种抽象,他表示哪些 logEntry 已经被封装成日志复制 request 发送出去了,这里是将logEntry封装到Inflight中。

Leader批量的发送日志给Follower

https://www.sofastack.tech/blog/sofa-jraft-pipeline-principle/
特点1: 被复制的日志是有序且连续的
如果棋谱传递的顺序不一样,最后下出的棋局可能也是完全不同的。而 SOFAJRaft 在日志复制时,其日志传输的顺序也要保证严格的顺序,所有日志既不能乱序也不能有空洞 (也就是说不能被漏掉)。

特点2: 复制日志是并发的
SOFAJRaft 中 Leader 节点会同时向多个 Follower 节点复制日志,在 Leader 中为每一个 Follower 分配一个 Replicator,专用来处理复制日志任务。在棋局中我们也针对每个直播平台安排一个记录员,用来将对弈棋谱同步给对应的直播平台。

特点3: 复制日志是批量的
SOFAJRaft 中 Leader 节点会将日志成批的复制给 Follower,就像旗童会每次携带多步棋信息到场外。

特点4: 日志复制中的快照
在改进 3 中,我们让新加入的直播平台直接复制当前的棋局,而不再回放过去的每一步棋谱,这就是 SOFAJRaft 中的快照 (Snapshot) 机制。用 Snapshot 能够让 Follower 快速跟上 Leader 的日志进度,不再回放很早以前的日志信息,即缓解了网络的吞吐量,又提升了日志同步的效率。

特点5: 复制日志的 pipeline 机制
在改进 4 中,我们让多个旗童参与信息传递,这样记录员和直播平台间就可以以“流式”的方式传递信息,这样既能保证信息传递有序也能保证信息传递持续。

在 SOFAJRaft 中我们也有类似的机制来保证日志复制流式的进行,这种机制就是 pipeline。Pipeline 使得 Leader 和 Follower 双方不再需要严格遵从 “Request - Response - Request” 的交互模式,Leader 可以在没有收到 Response 的情况下,持续的将复制日志的 AppendEntriesRequest 发送给 Follower。

在具体实现时,Leader 只需要针对每个 Follower 维护一个队列,记录下已经复制的日志,如果有日志复制失败的情况,就将其后的日志重发给 Follower。这样就能保证日志复制的可靠性,具体细节我们在源码解析中再谈。

在这里插入图片描述
源码解析
上面就是日志复制在原理层面的介绍,而在代码实现中主要是由 Replicator 和 NodeImpl 来分别实现 Leader 和 Follower 的各自逻辑,主要的方法列于下方。在处理源码中有三点值得我们关注。
在这里插入图片描述
在这里插入图片描述
Leader 节点在通过 Replicator 和 Follower 建立连接之后,要发送一个 Probe 类型的探针请求,目的是知道 Follower 已经拥有的的日志位置,以便于向 Follower 发送后续的日志。
在这里插入图片描述
关注2: 用 Inflight 来辅助实现 pipeline
Inflight 是对批量发送出去的 logEntry 的一种抽象,他表示哪些 logEntry 已经被封装成日志复制 request 发送出去了。
Leader 维护一个 queue,每发出一批 logEntry 就向 queue 中 添加一个代表这一批 logEntry 的 Inflight,这样当它知道某一批 logEntry 复制失败之后,就可以依赖 queue 中的 Inflight 把该批次 logEntry 以及后续的所有日志重新复制给 follower。既保证日志复制能够完成,又保证了复制日志的顺序不变。

前几天小明接到一个需求,要开发一个“简单”的支付处理流程,用来处理用户下单后的一系列处理流程。这个处理流程有很多环节,包括:订单计算(包括折扣计算),金额校验,库存校验,优惠券校验,执行支付,扣减优惠券,扣减库存,通知第三方物流,通知用户付款成功,通知商家发货等等。
小明接到这个需求后,心想这个需求不难,就是简单的计算、校验、调接口、发消息之类的。写if-else谁还不会?于是开始刷刷刷写下了三百行代码,就跟下面条一样,一气呵成。

代码的可读性很重要,这样以后维护起来才方便。所以抽了一些方法出来,主入口只保留最核心的流程。顺便还加了单元测试来保证内部逻辑的正确性。
在这里插入图片描述
老大语重心长地说道“小明啊,代码可读性很重要,可维护性也很重要。你看,有些环节在其它场景说不定也能用到,比如库存校验、执行支付、发通知等等。”

“我懂的,这样,我把这些流程抽成单独的类,这样以后就可以复用了”。

在这里插入图片描述
老大看后点点头:“不错,现在看起来好多了。不过你也知道,我们现在xx商城业务发展很快,这个支付的场景其实变化很大的。比如说我们打算最近上架一批虚拟产品,就像会员或者游戏皮肤什么的,它是没有库存的,针对这种产品我们就不需要库存校验和扣减的环节。还有我们打算发展外卖行业,那在最后的通知环节就有些不同了,可能要通知外卖小哥。另外我们还打算最近搞个运营活动,有些产品是可以有推荐奖励的,用户付款成功后我们要返利给推荐人,而且这个运营活动一过,这个环节就得去掉。你看看能不能想办法支持一下,把这个流程搞得灵活一点,以后支持新业务尽量成本低一点。”

Pipeline设计模式有三个概念:Pipeline、Valve、Context。它们的关系大概是这样:
在这里插入图片描述
一条Pipeline有一个Context,多个Valve。这些Valve是很小的、单元化的,一个Valve只做一件简单的事。前后Valve之间的通信由Context来承载。Context是一个简单的POJO类,存放这条Pipeline里面的数据。Pipeline设计模式的精髓在于它的可配置化。使用Pipeline,如果你想调换Valve的顺序,或者某些业务是不是用某个Valve,都是可以在外部配置的。这样就可以很灵活地适配多样化的业务,针对不同的业务配置不同的处理流程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值