Zookeeper 部分功能的源码解析

233 阅读9分钟

Zookeeper

阅读目的

  1. 了解zab
  2. 选举过程
  3. I/O协 议(怎么写入数据的)
  4. 写入数据的过程

客户端

客户端怎么初始化的 ZooKeeperBuilder.build()

  1. 实例化 ZooKeeper对象
  2. 解析连接字符串
  3. 配置连接超时
  4. sessionId配置,配置session 的password
  5. 配置默认的watcher
  6. 实例化 ClientCnxnSocket(ClientCnxnSocketNIO/ClientCnxnSocketNetty)
  7. 实例化 ClientCnxn
  8. 实例化sendThread和eventThread
  9. 启动sendThread和eventThread,这两个线程的启动非常重要

怎么连接 sendThread#start

org.apache.zookeeper.ClientCnxn#sendThread#run()

SendThread#run的主要功能概述:

  1. 它会去维护与服务器的链接,没有连接那么他会去尝试重新连接可读写的服务器,如果已经重连过但是重新连接失败了那么需要去找到下一个服务器地址去尝试连接。这个时候可能会去连接到一个只读的节点
  2. 如果已经连接了,还回去尝试支持一个什么安全连接
  3. 然后还回去尝试ping 服务器,维护客户端状态,以及检测当前连接服务器的状态
  4. 如果连接的是一个只读节点那么需要通过这个外层的while循环迭代的去寻找master节点。
  5. 将buffer中的数据推送的服务器,这也是为什么需要SendThread 需要检查连接的服务器是不是master
    代码如下

怎么set数据

  1. Sync 方法
  2. 组装数据
  3. 推送数据到org.apache.zookeeper.ClientCnxn#outgoingQueue 队列上,这个队列的数就是用来发送到服务端的数据,org.apache.zookeeper.ClientCnxn#queuePacket。 这个是就是sendThread 的run 方法中的 clientCnxnSocket.doTransport(to, pendingQueue, ClientCnxn.this); 这个就是将队列中的数据推送到server
  4. 由于是sync的方法,他还会循环等待 这个packet 是否以及发送,由于它使用的是jdk的Object 的wait方法,这个方法maybe it is bad practice!!!
  5. Async 方法
  6. 1,组装数据的时候它会将自己的callback给方法到packet数据结果用于nio的一部返回调用使用
  7. 2
  8. 虽然方法调用已经结束了,但是由于NIO的事件驱动的异步机制,在服务端完成请求后会会产生,请求的回调
    Netty实现版本的这个方法 ClientCnxnSocketNetty.ZKClientHandler#channelRead0,
    原生NIO实现版本的这个方法org.apache.zookeeper.ClientCnxnSocketNIO#doIO,
    怎么get数据
  9. Sync 和前面的方法差不多不过他是在最后将数据取出来了,并返回
  10. Async 这个和前面的 set 的async方法是一致的,都是将命令放到队列中然后通过NIO的异步回调来实现消息读取
    怎么对数据进行watch
  11. 它会将watcher放到 Packet 对象中,并用来在NIo的异步回调中使用

服务端

怎么启动

启动方式

启动方式2: org.apache.zookeeper.server.ZooKeeperServerMain#main
启动方式1:org.apache.zookeeper.server.quorum.QuorumPeerMain#main函数作为命令的启动函数
我们这里只关心QuorumPeerMain的实现和启动流程,shell 里面的启动脚本如下:
从这里来看这里就是

ZOOMAIN="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=$JMXLOCALONLY org.apache.zookeeper.server.quorum.QuorumPeerMain"  
$JAVA -cp "$CLASSPATH" $ZOOMAIN 2> /dev/null  

2.1.1 配置初始化

  1. 配置解析 org.apache.zookeeper.server.quorum.QuorumPeerConfig#parse
    主要就是将我们指定的zookeeper 配置文件给解析成为Properties。 然后将Properties的内容转化成Config对象
  2. 配置内容解析
  3. 基础配置如数据文件存放地址,以及端口超时时间等
  4. 还有一个 QuorumPeerConfig#createQuorumVerifier 这个配置是选举中多server的一个重要配置,以及server group 的配置信息。同时还会尝试备份老的配置(这个可能与配置更新有关系,如服务器节点扩容)
  5. 配置怎么更新,见后面内容

2.1.2 启动purge task

就是一个普通的jdk 提供的定时任务,用于清理snapshot 以及 不需要了的日志内容,我们这里暂时不讨论清理规则等细节

2.1.3 启动server

org.apache.zookeeper.server.quorum.QuorumPeer#start, 注意这个是重写的Thread 的start方法,代码如下:

class QuorumPeer {
    @Override
    public synchronized void start() {
        if (!getView().containsKey(myid)) {
            throw new RuntimeException("My id " + myid + " not in the peer list");
        }
        // 恢复数据
        loadDataBase();
        // 绑定端口
        startServerCnxnFactory();
        try {
            adminServer.start();
        } catch (AdminServerException e) {
            LOG.warn("Problem starting AdminServer", e);
        }
        // 开始选举
        startLeaderElection();
        // 应该是用于监控gc时间过长的问题,因为gc有一个stw问题
        startJvmPauseMonitor();
        // 启动线程,这个是调用父类的Thread#start()方法
        super.start();
    }
}

这个启动完成的内容如下:

  • 恢复数据,这个恢复就是从snapshot的数据文件解析成为datatree,以及epoch相关的信息,将这些信息从磁盘加载到内存里面。
  • 启动nio/netty server ,这里主要就是去绑定端口
  • 启动admin server,这个就是去启动一个jetty server
  • 尝试选举
  • 应该是用于监控gc时间过长的问题,因为gc有一个stw问题
  • 启动当前线程

选举

入口方法:org.apache.zookeeper.server.quorum.FastLeaderElection#start
先介绍几个关键的类

  • org.apache.zookeeper.server.quorum.FastLeaderElection, 选举的主启动类
  • org.apache.zookeeper.server.quorum.FastLeaderElection.Messenger 他的主要功能是负责下面两个类的聚合操作以及封装,封装sender和receiver的多线程操作
  • org.apache.zookeeper.server.quorum.FastLeaderElection.Messenger.WorkerSender: 顾名思义发送消息的一个worker,这个woker还是继承自Thread对象,
  • org.apache.zookeeper.server.quorum.FastLeaderElection.Messenger.WorkerReceiver 顾名思义接受消息的一个worker,这个woker还是继承自Thread对象,他是处理来自QuorumCnxManager接收的消息,以及使用WorkerSender来回复消息
  • org.apache.zookeeper.server.quorum.QuorumCnxManager ,这个有一个神奇的地方就是他会为每一个server创建一个senderServer——一个线程,来发送消息
  • org.apache.zookeeper.server.quorum.QuorumCnxManager.SendWorker 这个就是上文提到的专门用于发送消息的线程
  • org.apache.zookeeper.server.quorum.QuorumCnxManager.RecvWorker 这个会一直去读取数据

这个主要就是不断地发送数据和接受数据的处理,接下来我们看接受到不同消息这个他是怎么处理的
也就是主要看这个类
org.apache.zookeeper.server.quorum.FastLeaderElection.Messenger.WorkerReceiver#run

执行逻辑

  1. 获取从其他server过来的消息

  2. 选举版本适配、兼容处理、解析消息内容,

  3. 如果消息的version > 0x1

  4. 解析消息内容为 QuorumVerifier 对象 :self.configFromString(new String(b, UTF_8))

  5. 如果解析出来的对象的版本大于当前的server的QuorumVerifier 对象的版本,同时当前server处于looking 状态(选举状态)那么更新当前server的额 QuorumVerifier对象,以及相关的配置信息这个回去更新配置文件和内存中的配置

  6. 如果是新的加入者(an observer or a non-voting follower)那么会直接回复当前server 的状态给他,

  7. 如果是有效的选举人员,

  8. 从response 中解析 notification 数据

  9. 同时如果当前server是looking状态,收到的notification,同时将这个notification放recvqueue里面(这个需要看一下为什么),

  10. 同时收到的消息state也是looking and 当前的server逻辑时间(epoch)在收到的消息之前(也就是回复的server epoch已经落后了),回复消息给发消息的server告知当前server的投票和逻辑时钟(epoch)等信息

  11. 如果当前状态不是 looking ,同时回复消息为looking,那么直接将当前server 的leader信息和逻辑时钟(epoch)告知回复的server(这个可能是发起 notification 的server)
    这个QuorumCnxManager.java:1065 这里会启动一个server 来监听所有peer server(对等节点)的连接请求,处理请求的方式采用的是per connection of one thread(每一个连接创建一个线程来处理请求),此外他还在发送消息的时候也是一个peer server 一个线程的方式来发送消息,因为它使用的是类似于mq的方式来发消息

  12. 这里会有一个问题,如果是有效的一次投票请求,那么,他会将消息放到 recvqueue里面去,让下一次while 消费这个消息,同时更新当前配置,也就是更新执行逻辑汇总3的执行动作。(吐槽一下这里的实现真的拉胯,哈哈哈) d83c8e22-5f46-483b-b962-223b051d5741.svg

  13. 监听 服务端接收消息,入口主方法 org.apache.zookeeper.server.NettyServerCnxn#receiveMessage

角色

角色状态

LOOKING,  
FOLLOWING,  
LEADING,  
OBSERVING  

关键类

角色关系图如下,每一个角色就是通过 不同的子类的ZooKeeperServer实现来表达

Snipaste-角色.png

  • ZooKeeperServer
  • QuorumZooKeeperServer Abstract base class for all ZooKeeperServers that participate in a quorum.`, 实现选举逻辑的角色分配相关的抽象类。
  • LearnerZooKeeperServer Parent class for all ZooKeeperServers for Learners,从服务器的父类抽象
  • FollowerZooKeeperServer Just like the standard ZooKeeperServer. We just replace the request processors: FollowerRequestProcessor -> CommitProcessor -> FinalRequestProcessor 负责串联这三个processor
  • ObserverZooKeeperServer
  • LeaderZooKeeperServer Just like the standard ZooKeeperServer. We just replace the request processors: LeaderRequestProcessor -> PrepRequestProcessor -> ProposalRequestProcessor -> CommitProcessor -> Leader.ToBeAppliedRequestProcessor -> FinalRequestProcessor负责串联这5个processor
  • ReadOnlyZooKeeperServer ReadOnlyRequestProcessor -> PrepRequestProcessor -> FinalRequestProcessor
    暂时无法在文档外展示此内容
    RequestProcessor
  • AckRequestProcessor
  • SendAckRequestProcessor
  • UnimplementedRequestProcessor
  • SyncRequestProcessor
  • ObserverRequestProcessor
  • CommitProcessor (mq模型)!!
  • FinalRequestProcessor
  • PrepRequestProcessor (mq模型)
    一个的processor
  • ProposalRequestProcessor
  • 如果是 follower 发过来的请求那么会加入到sync 队列中
  • 如果是client的请求,会将sync动作放到队列中(也就是会被SyncRequestProcessor处理)
  • ToBeAppliedRequestProcessor
  • ReadOnlyRequestProcessor
  • LeaderRequestProcessor
  • 升级 请求、推送到下一个请求

一次请求的全过程

在这里我们不考虑连接的问题

  • 代码开始位置
    CnxnChannelHandler#channelRead
  • Server 入口
    org.apache.zookeeper.server.ZooKeeperServer#processPacket
  • 期间会去处理限流问题
    org.apache.zookeeper.server.ZooKeeperServer#submitRequestNow
  • RequestProcessor关系图展示如下\

00c429b7-8dba-4d47-bf67-2e08436a499f.svg

zookeeper实现的二阶段提交,注意zookeeper实现的二阶段提交不会在commit之后再要求follower进行确认,而是propose成功后直接对所有的follower 进行commit操作就完了;

two_phase_paper.png

论文中的图片展示

50a0626d-4519-47c0-bb2c-9b6addd94c5a.svg

实际实现的情况

配置更新

机器的增加和减少怎么处理,也会使用相同的二阶段提交实现新配置的写入,

  • 在配置的时候进行写入更新数据,怎么避免提交的多数人问题
  • zookeeper会在,执行二阶段提交之前去获取最新的committed 的配置,将其作为合法人数认定基础配置。比如说最新的已提交配置是11个机器,新的机器来了12个,那么新写入数据的(在12个机器的配置更新提交之前)这个时候只需要6个机器得到回应就可以在leader进行commit动作了

运行zookeeper源码

注意这里运行需要修改一些maven以来,见provided的依赖改成compile

源码启动server已经启动客户端

--- zoo1.cfg  
clientPort=2182  
dataDir=./data2  
server.1=localhost:2181:3181  
server.2=localhost:2182:3182  
server.3=localhost:2183:3183  
  
initLimit=100000  
syncLimit=100000  
initLimit=100000  
  
serverId=1 (他在 data文件夹的myid文件中)  
  
  
  
--- zoo2.cfg  
clientPort=2182  
dataDir=./data2  
server.1=localhost:2181:3181  
server.2=localhost:2182:3182  
server.3=localhost:2183:3183  
  
initLimit=100000  
syncLimit=100000  
initLimit=100000  
  
serverId=2 (他在 data文件夹的myid文件中)  
  
  
--- zoo3.cfg  
  
dataDir=./data3  
server.1=localhost:2181:3181  
server.2=localhost:2182:3182  
server.3=localhost:2183:3183  
  
initLimit=100000  
syncLimit=100000  
initLimit=100000  
  
serverId=3 (他在 data文件夹的myid文件中)  
  • 同时配置一个arg(这个arg就是配置文件的路径)
  • 启动java文件 org.apache.zookeeper.server.quorum.QuorumPeerMain#main

客户端发起请求

class ClientTest{
    @Test
    public void test_write() throws Exception {

        ZooKeeper zk = new ZooKeeperBuilder("127.0.0.1:2181", 1000)
                .build();
        try{

            String path ="/demo5";
            zk.create(path,"xxxx".getBytes(StandardCharsets.UTF_8),OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
            zk.setData(path,"xxxx".getBytes(StandardCharsets.UTF_8),0);
            System.out.println(new String(zk.getData(path,false, new Stat())));
        }finally {
            zk.close();
        }
    }
}