转自:https://zhuanlan.zhihu.com/p/27375422
ZooKeeper 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
说明
ZAB 协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的一致性协议。基于该协议,ZooKeeper 实现了一种主从模式的系统架构来保持集群中各个副本之间的数据一致性。
之前看了下ZAB协议的论文,也搞清楚了一些关键问题的解决思路,但是没看代码总有一种雾里看花水中望月之感,本着不作死就不会死的精神,大概翻阅了Zookeeper的ZAB模块实现,特总结与大家分享。
我会从“通信协议”、“核心数据结构以及API”两方面主要描述Zookeeper中ZAB协议的具体实现,重点关注ZAB协议实现中抽象的对象以及对象之间的关联。弱化请求处理流程,因为这些在描述ZAB协议中重点描述。
通信协议
ZAB协议中节点之间通信主要发生在Leader和Follower之间。他们之间主要的命令分为控制类和数据传输类。
其中控制类命令主要包含以下命令:
- PING: Leader发送给Follower,确定双方依然存活且网络连通正常;
- COMMIT:Leader发送给Follower,通知其提交某个特定的zxid的日志。
而数据传输类则主要包含:
- PROPOSAL: Leader通过该命令向Follower发送自己的日志数据
有一个不太明白的是SYNC命令,好像在Leader和Follower之间正常的数据交互中不会有该命令的身影。
数据结构
LearnerHandler
Leader抽象出来的与Follower节点进行信息交互的对象。同时,该对象维护了Leader与Follower之间通信状态(如是否出现网络不通情况)。
Follower节点启动后,会主动连接Leader,而Leader会监听Follower的建立连接的请求。并为每个Follower的tcp连接创建一个LearnerHandler对象,该对象会:
- 接收Follower发来的请求,可能包括以下请求:Follower转发的客户端的更新请求(命令类型:Leader.REQUEST),Follower对Leader的Proposal命令的回复消息ACK,Follower给Leader发送的PING(Follower会给Leader发送PING消息?);
- 给Follower发送心跳消息。
public class LearnerHandler extends ZooKeeperThread {
final Leader leader;
protected long sid = 0;
final LinkedBlockingQueue<QuorumPacket> queuedPackets =
new LinkedBlockingQueue<QuorumPacket>();
private SyncLimitCheck syncLimitCheck = new SyncLimitCheck();
private BinaryInputArchive ia;
private BinaryOutputArchive oa;
private BufferedOutputStream bufferedOutput;
private volatile boolean sendingThreadStarted = false;
private boolean needOpPacket = true;
private long leaderLastZxid;
......
}
Leader
Leader抽象了集群当前的主节点,此类节点负责:
- 处理所有客户端的更新请求,并将这些请求使用ZAB协议以日志同步方式广播至所有的Follower;
- 通过心跳信息维护集群状态,在必要时会结束自己的Leader状态,触发新的选主
public class Leader {
......
final LeaderZooKeeperServer zk;
final QuorumPeer self;
......
// 所有Follower信息
private final HashSet<LearnerHandler> learners =
new HashSet<LearnerHandler>();
......
// Leader主函数
void lead() throws IOException, InterruptedException {
......
}
}
Leader对象对外提供一些重要API:
- propose(): 将客户端的请求Request包装成QuorumPacket并发往Followers,sendPacket()会将该QuorumPacket发往每个Follower的消息队列;
- processAck():Leader收到Follower对Proposal消息的确认后调用该方法,该方法里面会判断某个消息是否被多数Follower确认,如果是,那么会提交该消息。在LearnerHandler内收到Follower的Ack消息时会触发函数processAck。
Follower
Follower抽象了集群的从节点,从节点负责:
- 接受Leader命令,同步Leader节点日志,并将其应用到自身的状态机
- 维护与Leader的心跳,并在Leader节点异常时会发起一次选主
public class Follower extends Learner{
private long lastQueued;
final FollowerZooKeeperServer fzk;
// 从节点主函数
void followLeader() throws InterruptedException {
......
}
}
Follower中最主要的功能是接受并处理Leader发过来的命令,Leader和Follower之间的命令类型见“通信协议”,每种命令的处理方法如下:
- PROPOSAL: Follower将其记录日志即可,调用方法FollowerZooKeeperServer::logRequest(),然后给Leader返回ACK;
- COMMIT:Follower将请求的zxid进行提交,所谓的提交其实就是将该命令应用到状态机中。
QuorumPeer
QuorumPeer负责维护节点的选主状态信息,无论是Leader还是Follower,都需要记录这些信息。比如,当前节点的状态,当前节点选择了谁作为主,等等等等。
其实,每个节点启动时都是运行在QuorumPeer的主循环之内,在循环内进行选主过程,完成选主后,根据选主结果决定本节点角色(Leader/Follower)。接下来就进入Leader/Follower的处理逻辑,直到该由于种种异常节点需要重新发起选主,便再一次进入QuorumPeer的主循环了。
public class QuorumPeer extends ZooKeeperThread implements QuorumStats.Provider {
// 定义节点三种状态
public enum ServerState {
LOOKING, FOLLOWING, LEADING, OBSERVING;
}
// 当前节点id
private long myid;
// 记录当前投票信息
volatile private Vote currentVote;
// 记录当前节点状态,默认是LOOKING
private ServerState state = ServerState.LOOKING;
// 节点启动入口
public synchronized void start() {
......
}
// 节点运行主循环
@Override
public void run() {
......
while (running) {
switch (getPeerState()) {
// 该状态下开始选主
case LOOKING:
try {
reconfigFlagClear();
if (shuttingDownLE) {
shuttingDownLE = false;
startLeaderElection();
}
// 开始选主咯
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
setPeerState(ServerState.LOOKING);
}
break;
case OBSERVING:
// 观察者角色,忽略
break;
case FOLLOWING:
// 变成Follower,进入followLeader()
try {
setFollower(makeFollower(logFactory));
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
follower.shutdown();
setFollower(null);
updateServerState();
}
break;
case LEADING:
// 变成Leader,进入lead()
try {
setLeader(makeLeader(logFactory));
leader.lead();
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
updateServerState();
}
break;
}
start_fle = Time.currentElapsedTime();
......
}
}
}