最近在看Zookeeper,想把学习Zookeeper的过程记录下来,这篇博客主要是为了对Zookeeper做一个宏观的记录。

一、什么是Zookeeper:

ZooKeeper是一个集中的服务,用于维护配置信息、命名、提供分布式同步和提供组服务。它可以在分布式系统中协作多个任务,在分布式系统中,开发面临的困难主要有:消息延迟、处理器性能和时钟偏移,后面两个会间接引起第一个问题,当我们面临一个网络错误时,很难确定是网络超时还是系统崩溃,Zookeeper提供了一套功能强大的API去解决这些问题,这些功能主要包括:

  • 保障强一致性、有序性和持久性。
  • 实现通用的同步原语的能力。
  • 简单的并发处理机制。

在Zookeeper Server侧主要包括四个个部分:

  • ZKDatabase:类似文件系统的数据结构,Zookeeper用该对象模型存储数据
  • ServerCnxn:代表客户端连接的服务器,用于接收客户端的请求,并转发到具体的服务器上
  • ZookeeperServer:ZK角色服务器,主要有三种角色,Leader、Follower和Observer。Zookeeper处于不同的角色时会把请求交给对应角色服务器处理
  • RequestProcessor:请求处理器,用于处理每一个请求。

二、Zookeeper服务启动过程:

zookeeper启动日志配置 zookeeper启动状态_java

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的角色服务器主要分为三种:

  1. LeaderZookeeperServer
  2. FollowerZookeeperServer
  3. 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();
    }

zookeeper启动日志配置 zookeeper启动状态_java_02

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();
    }

zookeeper启动日志配置 zookeeper启动状态_java_03

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的启动过程和处理请求的过程大致结束了,本片文章比较长,越是到后面越有点力不从心,接下来会不断的修正和完善这篇文章,欢迎前来指正。