最近在看Zookeeper,想把学习Zookeeper的过程记录下来,这篇博客主要是为了对Zookeeper做一个宏观的记录。
一、什么是Zookeeper:
ZooKeeper是一个集中的服务,用于维护配置信息、命名、提供分布式同步和提供组服务。它可以在分布式系统中协作多个任务,在分布式系统中,开发面临的困难主要有:消息延迟、处理器性能和时钟偏移,后面两个会间接引起第一个问题,当我们面临一个网络错误时,很难确定是网络超时还是系统崩溃,Zookeeper提供了一套功能强大的API去解决这些问题,这些功能主要包括:
- 保障强一致性、有序性和持久性。
- 实现通用的同步原语的能力。
- 简单的并发处理机制。
在Zookeeper Server侧主要包括四个个部分:
- ZKDatabase:类似文件系统的数据结构,Zookeeper用该对象模型存储数据
- ServerCnxn:代表客户端连接的服务器,用于接收客户端的请求,并转发到具体的服务器上
- ZookeeperServer:ZK角色服务器,主要有三种角色,Leader、Follower和Observer。Zookeeper处于不同的角色时会把请求交给对应角色服务器处理
- RequestProcessor:请求处理器,用于处理每一个请求。
二、Zookeeper服务启动过程:
1、初始化Config
- 根据入参中提供的zoo.cfg文件,解析该文件并生成配置信息对象
public void runFromConfig(QuorumPeerConfig config) throws IOException {
try {
ManagedUtil.registerLog4jMBeans();
} catch (JMException e) {
LOG.warn("Unable to register log4j JMX control", e);
}
LOG.info("Starting quorum peer");
try {
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
//创建仲裁成员
quorumPeer = new QuorumPeer();
//设置客户端连接地址
quorumPeer.setClientPortAddress(config.getClientPortAddress());
//设置快照文件路径和事务日志文件路径
quorumPeer.setTxnFactory(new FileTxnSnapLog(
new File(config.getDataLogDir()),
new File(config.getDataDir())));
//设置集群服务器信息
quorumPeer.setQuorumPeers(config.getServers());
//设置选举算法
quorumPeer.setElectionType(config.getElectionAlg());
//设置服务器Id
quorumPeer.setMyid(config.getServerId());
//设置时间单位
quorumPeer.setTickTime(config.getTickTime());
//设置最小回话超时时间
quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
//设置最大回话超时时间
quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
//设置初始化时间 单位TickTime
quorumPeer.setInitLimit(config.getInitLimit());
//设置发出请求和接收响应的同步时间 单位TickTime
quorumPeer.setSyncLimit(config.getSyncLimit());
quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
quorumPeer.setCnxnFactory(cnxnFactory);
quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
quorumPeer.setLearnerType(config.getPeerType());
quorumPeer.start();
quorumPeer.join();
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Quorum Peer interrupted", e);
}
}
2、加载ZKDatabase数据
- ZK服务器首先从快照文件中加载数据。
- 再根据事务日志修正快照文件中的数据(Zookeeper会获取该快照开始的前一个提交,并利用事务日志文件重放该提交之后的事务)
public long restore(DataTree dt, Map<Long, Integer> sessions,
PlayBackListener listener) throws IOException {
//将快照中的内容反序列化的ZKDatabase中
snapLog.deserialize(dt, sessions);
FileTxnLog txnLog = new FileTxnLog(dataDir);
//从事务日志中根据快照文件最早的事务ID读取所有的事务
TxnIterator itr = txnLog.read(dt.lastProcessedZxid+1);
long highestZxid = dt.lastProcessedZxid;
TxnHeader hdr;
while (true) {
// iterator points to
// the first valid txn when initialized
hdr = itr.getHeader();
if (hdr == null) {
//empty logs
return dt.lastProcessedZxid;
}
if (hdr.getZxid() < highestZxid && highestZxid != 0) {
LOG.error(highestZxid + "(higestZxid) > "
+ hdr.getZxid() + "(next log) for type "
+ hdr.getType());
} else {
highestZxid = hdr.getZxid();
}
try {
//重放所有的事务
processTransaction(hdr,dt,sessions, itr.getTxn());
} catch(KeeperException.NoNodeException e) {
throw new IOException("Failed to process transaction type: " +
hdr.getType() + " error: " + e.getMessage(), e);
}
listener.onTxnLoaded(hdr, itr.getTxn());
if (!itr.next())
break;
}
return highestZxid;
}
3、启动ServerCnxn服务
ServerCnxn代表这一个面上客户端的socket连接,Zookeeper中默认的ServerCnxn设置是NIOServerCnxnFactory,采用Reactor模式使用JavaNio api编写的网络连接服务。ServerCnxn主要负责接收客户端的请求并将请求转发给具体的ZookeeperServer执行。
4、领导选举
当一个服务器进入LOOKING状态时,会向集群中每一个服务器发送投票信息Vote,投票中包含服务器标识符(sid)和最新事务ID(zxid),当一个服务器收到一个投票信息时,该服务器会根据以下规则修改自己的投票信息
- 将接收的VoteId和VoteZxid作为一个标识符,并获取接收方当前的zxid,用myzxid和mysid表示接收方自己的值。
- if(VoteZxid>myzxid) || if(VoteZxid==myzxid &&VoteId>mysid )则修改自己的投票信息。
- 否则,保留自己的投票信息。
当服务器接收到的仲裁成员数量(大于仲裁成员的一半)的投票都一样时,表明Leader已经产生了。
//判断收到的票数是否大于投票成员数目的二分之一,则认为产生了Leader
if (termPredicate(recvset, proposedLeader,
proposedZxid)) {
// Otherwise, wait for a fixed amount of time
LOG.info("Passed predicate");
Thread.sleep(finalizeWait);
// Notification probe = recvqueue.peek();
// Verify if there is any change in the proposed leader
//检查票是否都一样,若一样则删除
while ((!recvqueue.isEmpty())
&& !totalOrderPredicate(
recvqueue.peek().leader, recvqueue
.peek().zxid)) {
recvqueue.poll();
}
//集合为空则表明投票都一样 产生了Leader,并进行设置
if (recvqueue.isEmpty()) {
// LOG.warn("Proposed leader: " +
// proposedLeader);
self.setPeerState(
(proposedLeader == self.getId()) ?
ServerState.LEADING :
ServerState.FOLLOWING);
leaveInstance();
return new Vote(proposedLeader, proposedZxid);
}
}
5、启动角色服务器
Zookeeper会根绝当前的角色来执行响应角色的服务器处理请求,Zookeeper的角色服务器主要分为三种:
- LeaderZookeeperServer
- FollowerZookeeperServer
- ObserverZookeeperServer
Zookeeper中,每种类型的服务器都会注册不同的RequestProcessor来执行真正的处理过程。
LeaderZookeeperServer:
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(
finalProcessor, getLeader().toBeApplied);
commitProcessor = new CommitProcessor(toBeAppliedProcessor,
Long.toString(getServerId()), false);
commitProcessor.start();
ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this,
commitProcessor);
proposalProcessor.initialize();
firstProcessor = new PrepRequestProcessor(this, proposalProcessor);
((PrepRequestProcessor)firstProcessor).start();
}
PreRequestProcessor:预处理请求,将请求转换为具体类型的请求。
ProposalRequestProcessor:该处理器会准备一个提案,并将该提案发送给追从者,ProposalRequestProcessor将会把所有请求转发给CommitRequestProcessor,而且对于写操作,还会将请求转发给SyncRequestProcessor。
SyncRequestProcessor:该处理器将操作持久话到事务日志中,并生成快照数据。
AckRequestProcessor:生成一条确认消息返回给自己。
CommitRequestProcessor:该处理器会将收到足够多确认消息的事务进行提交。
ToBeAppliedRequestProcessor:将请求从从提议列表中删除,并提交到待应用列表中。
FinalRequestProcessor:如果请求对象中包含事务数据,该处理器将会接收对Zookeeper树的修改否则,将数据返给客户端。
FollowerZookeeperServer:
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
commitProcessor = new CommitProcessor(finalProcessor,
Long.toString(getServerId()), true);
commitProcessor.start();
firstProcessor = new FollowerRequestProcessor(this, commitProcessor);
((FollowerRequestProcessor) firstProcessor).start();
syncProcessor = new SyncRequestProcessor(this,
new SendAckRequestProcessor((Learner)getFollower()));
syncProcessor.start();
}
FollowerRequestProcessor:处理客户端请求,将请求转发给CommitProcessor,同时也会转发写到Leader。
CommitRequestProcessor:对于读请求,会直接转发到FinalRequestProcessor,对于写请求,在提交到FinalRequestProcessor之前会等待事务提交。当群首接收一个新的写请求操作时,会生成一个提议,并将该提议发送给追随者。当追随者收到一个提议后,会发送给SyncRequestProcessor处理器处理,并通过SendRequestProcessor发送确认消息。当群首接收到足够多的消息确认提交这个提议,并发送提交事务的消息给追随者。当追随者接收到提交事务的消息时,通过CommitProcessor处理器处理。
ObserverZooKeeperServer:
ObserverZooKeeperServer类似与FollowerRequestProcessor,但是不参与响应提议的过程。
protected void setupRequestProcessors() {
// We might consider changing the processor behaviour of
// Observers to, for example, remove the disk sync requirements.
// Currently, they behave almost exactly the same as followers.
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
commitProcessor = new CommitProcessor(finalProcessor,
Long.toString(getServerId()), true);
commitProcessor.start();
firstProcessor = new ObserverRequestProcessor(this, commitProcessor);
((ObserverRequestProcessor) firstProcessor).start();
syncProcessor = new SyncRequestProcessor(this,
new SendAckRequestProcessor(getObserver()));
syncProcessor.start();
}
到此为止,Zookeeper的启动过程和处理请求的过程大致结束了,本片文章比较长,越是到后面越有点力不从心,接下来会不断的修正和完善这篇文章,欢迎前来指正。