Zookeeper详解(附源码)

173 阅读16分钟

Zookeeper

zookeeper分布式锁案例 算法Paxos解决一致性算法的问题 ZAB协议进一步解决一致性算法等

基础

zookeeper主要是文件系统和通知机制

  • 文件系统主要是用来存储数据
  • 通知机制主要是服务器或者客户端进行通知,并且监督

基于观察者模式设计的分布式服务管理框架,开源的分布式框架

特点

特点.png

  • 一个leader,多个follower的集群
  • 集群只要有半数以上包括半数就可正常服务,一般安装奇数台服务器
  • 全局数据一致,每个服务器都保存同样的数据,实时更新
  • 更新的请求顺序保持顺序(来自同一个服务器)
  • 数据更新的原子性,数据要么成功要么失败
  • 数据实时更新性很快
主要的集群步骤为
  1. 服务端启动时去注册信息(创建都是临时节点)
  2. 获取到当前在线服务器列表,并且注册监听
  3. 服务器节点下线
  4. 服务器节点上下线事件通知
  5. process(){重新再去获取服务器列表,并注册监听}
数据结构

与 Unix 文件系统很类似,可看成树形结构,每个节点称做一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据。也就是只能存储小数据,每个 ZNode 都可以通过其路径唯一标识。

应用场景
  • 统一命名服务(域名服务)

统一命名服务.png

  • 统一配置管理(一个集群中的所有配置都一致,且也要实时更新同步)

统一配置管理.png

  • 统一集群管理(掌握实时状态)将节点信息写入ZooKeeper上的一个ZNode。监听ZNode获取实时状态变化

统一集群管理.png

  • 服务器节点动态上下线

  • 软负载均衡(根据每个节点的访问数,让访问数最少的服务器处理最新的数据需求)

软负载均衡.png

zookeeper集群操作

比如三个节点部署zookeeper,那么需要3台服务器,在每台服务器中解压zookeeper压缩包并修改其配置文件等.

区别在于要在创建保存zookeeper的数据文件夹中新建一个myid文件(和源码的myid对应上)相当于服务器的唯一编号,具体编号是多少,对应该服务器是哪一台服务器编号

之后将其终端执行xsync /opt/apache-zookeeper-3.5.7-bin,主要功能是同步发脚本,在conf的配置文件zoo.cfg末尾添加如下配置标识服务器的编号

server.2=hadoop102:2888:3888
server.3=hadoop103:2888:3888
server.4=hadoop104:2888:3888

当前主要配置编号的参数是server.A=B:C:D

  • A标识第几台服务器
  • B标识服务器地址
  • C标识服务器 Follower 与集群中的 Leader 服务器交换信息的端口
  • D主要是选举,如果Leader 服务器挂了。这个端口就是用来执行选举时服务器相互通信的端口,通过这个端口进行重新选举leader

选举机制

第一次启动

选举.png 非第一次启动,在没有leader的时候,其判断依据是:epoch任职期>事务id>服务器sid

非第一次选举.png

集群启动停止脚本
#!/bin/bash
case $1 in
"start"){
for i in hadoop102 hadoop103 hadoop104
do
echo ---------- zookeeper $i 启动 ------------
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh 
start"
done
};;
"stop"){
for i in hadoop102 hadoop103 hadoop104
do
echo ---------- zookeeper $i 停止 ------------ 
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh 
stop"
done
};;
"status"){
for i in hadoop102 hadoop103 hadoop104
do
echo ---------- zookeeper $i 状态 ------------ 
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh 
status"
done
};;
esac

客户端命令行操作

命令行语法

znode 节点数据信息

(1)czxid:创建节点的事务 zxid:每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之前发生。

(2)ctime:znode 被创建的毫秒数(从 1970 年开始)

(3)mzxid bznode 最后更新的事务 zxid

(4)mtime:znode 最后修改的毫秒数(从 1970 年开始)

(5)pZxid:znode 最后更新的子节点 zxid

(6)cversion:znode 子节点变化号,znode 子节点修改次数

(7)dataversion:znode 数据变化号

(8)aclVersion:znode 访问控制列表的变化号

(9)ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是 0。

(10)dataLength:znode 的数据长度

(11)numChildren:znode 子节点数量

节点类型
  • 持久/短暂
  • 有序号/无序号

节点.png

监听器

监听器.png

客户端向服务端写数据流程

写流程之写入请求直接发送给Leader节点

客户端给服务器的leader发送写请求,写完数据后给手下发送写请求,手下写完发送给leader,超过半票以上都写了则发回给客户端。之后leader在给其他手下让他们写,写完在发数据给leader

发送leader.png

写流程之写入请求发送给follower节点

客户端给手下发送写的请求,手下给leader发送写的请求,写完后,给手下发送写的请求,手下写完后给leader发送确认,超过半票,leader确认后,发给刻划断,之后leader在发送写请求给其他手下

服务器动态上下线监听

  1. 服务器上线的时候其实就是服务器启动时去注册信息(创建的都是临时节点)
  2. 客户端获取到当前在线的服务器列表
  3. 服务器节点下线后给集群管理
  4. 集群管理服务器节点的下线时间通知给客户端
  5. 客户端通过获取服务器列表重选选择服务器

分布式锁

原生zookeeper分布式锁

创建节点,判断是否是最小的节点,如果不是最小的节点,需要监听前一个的节点

健壮性可以通过CountDownLatch

分布式锁.png

监听函数
  • 如果集群状态是连接,则释放connectlatch
  • 如果集群类型是删除,且前一个节点的位置等于该节点的文职,则释放该节点
  • 判断节点是否存在不用一直监听,获取节点信息要一直监听getData
public class DistributedLock {
​
    // zookeeper server 列表
    private String connectString =
            "hadoop102:2181,hadoop103:2181,hadoop104:2181";
    // 超时时间
    private int sessionTimeout = 2000;
    private ZooKeeper zk;
    private String rootNode = "locks";
    private String subNode = "seq-";
    // 当前 client 等待的子节点
    private String waitPath;
    //ZooKeeper 连接
    private CountDownLatch connectLatch = new CountDownLatch(1);
    //ZooKeeper 节点等待
    private CountDownLatch waitLatch = new CountDownLatch(1);
​
    // 当前 client 创建的子节点
    private String currentNode;
​
    // 和 zk 服务建立连接,并创建根节点
    public DistributedLock() throws IOException,
            InterruptedException, KeeperException {
        zk = new ZooKeeper(connectString, sessionTimeout, new
                Watcher() {
                    @Override
                    public void process(WatchedEvent event) {
                        // 连接建立时, 打开 latch, 唤醒 wait 在该 latch 上的线程
                        if (event.getState() ==
                                Event.KeeperState.SyncConnected) {
                            connectLatch.countDown();
                        }
                        // 发生了 waitPath 的删除事件
                        if (event.getType() ==
                                Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
                            waitLatch.countDown();
                        }
                    }
                });
        // 等待连接建立
        connectLatch.await();
        //获取根节点状态
        Stat stat = zk.exists("/" + rootNode, false);
        //如果根节点不存在,则创建根节点,根节点类型为永久节点
        if (stat == null) {
            System.out.println("根节点不存在");
            zk.create("/" + rootNode, new byte[0],
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }
​
    // 加锁方法
    public void zkLock() {
        try {
            //在根节点下创建临时顺序节点,返回值为创建的节点路径
            currentNode = zk.create("/" + rootNode + "/" + subNode,
                    null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);
            // wait 一小会, 让结果更清晰一些
            Thread.sleep(10);
            // 注意, 没有必要监听"/locks"的子节点的变化情况
            List<String> childrenNodes = zk.getChildren("/" +
                    rootNode, false);
            // 列表中只有一个子节点, 那肯定就是 currentNode , 说明 client 获得锁
            if (childrenNodes.size() == 1) {
                return;
            } else {
                //对根节点下的所有临时顺序节点进行从小到大排序
                Collections.sort(childrenNodes);
                //当前节点名称
                String thisNode = currentNode.substring(("/" +
                        rootNode + "/").length());
                //获取当前节点的位置
                int index = childrenNodes.indexOf(thisNode);
                if (index == -1) {
                    System.out.println("数据异常");
                } else if (index == 0) {
                    // index == 0, 说明 thisNode 在列表中最小, 当前 client 获得锁
                    return;
                } else {
                    // 获得排名比 currentNode 前 1 位的节点
                    this.waitPath = "/" + rootNode + "/" +
                            childrenNodes.get(index - 1);
                    // 在 waitPath 上注册监听器, 当 waitPath 被删除时, zookeeper 会回调监听器的 process 方法
                    zk.getData(waitPath, true, new Stat());
                    //进入等待锁状态
                    waitLatch.await();
                    return;
                }
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
​
    // 解锁方法
    public void zkUnlock() {
        try {
            zk.delete(this.currentNode, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }
}

curator分布式锁

原生的 Java API 开发存在的问题

  • 会话连接是异步的,需要自己去处理。比如使用CountDownLatch
  • Watch 需要重复注册,不然就不能生效
  • 开发的复杂性还是比较高的
  • 不支持多节点删除和创建。需要自己去递归

Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。

public class CuratorLockTest {
​
    private String rootNode = "/locks";
​
    // zookeeper server 列表
    private String connectString =
            "hadoop102:2181,hadoop103:2181,hadoop104:2181";
    // connection 超时时间
    private int connectionTimeout = 2000;
    // session 超时时间
    private int sessionTimeout = 2000;
​
    public static void main(String[] args) {
        new CuratorLockTest().test();
    }
​
    // 测试
    private void test() {
        // 创建分布式锁 1
        final InterProcessLock lock1 = new
                InterProcessMutex(getCuratorFramework(), rootNode);
        // 创建分布式锁 2
        final InterProcessLock lock2 = new
                InterProcessMutex(getCuratorFramework(), rootNode);
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                try {
                    lock1.acquire();
                    System.out.println("线程 1 获取锁");
                    // 测试锁重入
                    lock1.acquire();
                    System.out.println("线程 1 再次获取锁");
                    Thread.sleep(5 * 1000);
                    lock1.release();
                    System.out.println("线程 1 释放锁");
                    lock1.release();
                    System.out.println("线程 1 再次释放锁");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                try {
                    lock2.acquire();
                    System.out.println("线程 2 获取锁");
                    // 测试锁重入
                    lock2.acquire();
                    System.out.println("线程 2 再次获取锁");
                    Thread.sleep(5 * 1000);
                    lock2.release();
                    System.out.println("线程 2 释放锁");
                    lock2.release();
                    System.out.println("线程 2 再次释放锁");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
​
    // 分布式锁初始化
    public CuratorFramework getCuratorFramework() {
        //重试策略,初试时间 3 秒,重试 3 次
        RetryPolicy policy = new ExponentialBackoffRetry(3000, 3);
        //通过工厂创建 Curator
        CuratorFramework client =
                CuratorFrameworkFactory.builder()
                        .connectString(connectString)
                        .connectionTimeoutMs(connectionTimeout)
                        .sessionTimeoutMs(sessionTimeout)
                        .retryPolicy(policy).build();
        //开启连接
        client.start();
        System.out.println("zookeeper 初始化完成...");
        return client;
    }
}

企业面试真题

选举机制

半数机制,超过半数的投票通过,即通过。

(1)第一次启动选举规则:

投票过半数时,服务器 id 大的胜出

(2)第二次启动选举规则:

  • EPOCH 大的直接胜出
  • EPOCH 相同,事务 id 大的胜出
  • 事务 id 相同,服务器 id 大的胜出
集群安装

安装奇数台:

生产经验

  • 10台服务器:3台zk
  • 20台服务器:5台zk
  • 100台服务器:11台zk
  • 200台服务器:11台zk

服务器台数多:好处,提高可靠性;坏处:提高通信延时

算法基础

Paxos 算法

一种基于消息传递且具有高度容错特性的一致性算法

Paxos算法解决的问题:就是如何快速正确的在一个分布式系统中对某个数据值达成一致,并且保证不论发生任何异常,都不会破坏整个系统的一致性

paxos解决.png

Paxos算法描述

Paxos算法描述.png

Paxos算法流程

流程.png

针对上述描述做三种情况的推演举例:为了简化流程,我们这里不设置 Learner

情况1.png

情况2.png

Paxos 算法缺陷:在网络复杂的情况下,一个应用 Paxos 算法的分布式系统,可能很久无法收敛,甚至陷入活锁的情况。

情况3.png

造成这种情况的原因是系统中有一个以上的 Proposer,多个 Proposers 相互争夺 Acceptor,造成迟迟无法达成一致的情况。针对这种情况,一种改进的 Paxos 算法被提出:从系统中选出一个节点作为 Leader,只有 Leader 能够发起提案。这样,一次 Paxos 流程中只有一个Proposer,不会出现活锁的情况,此时只会出现例子中第一种情况。

ZAB协议

什么是 ZAB 算法

Zab 借鉴了 Paxos 算法,是特别为 Zookeeper 设计的支持崩溃恢复的原子广播协议。基于该协议,Zookeeper 设计为只有一台客户端(Leader)负责处理外部的写事务请求,然后 Leader 客户端将数据同步到其他 Follower 节点。即 Zookeeper 只有一个 Leader 可以发起提案

Zab 协议内容

Zab 协议包括两种基本的模式:消息广播、崩溃恢复。

消息广播

消息广播.png

崩溃恢复

崩溃恢复——异常假设

崩溃恢复.png

崩溃恢复——Leader选举

崩溃恢复——Leader选举.png

崩溃恢复——数据恢复

崩溃恢复——数据恢复.png

CAP理论

cap.png

Zookeeper源码

持久化源码

LeaderFollower 中的数据会在内存和磁盘中各保存一份。所以需要将内存中的数据持久化到磁盘中。

org.apache.zookeeper.server.persistence 包下的相关类都是序列化相关的代码。

持久化.png

1)快照
public interface SnapShot {
​
    // 反序列化方法
    long deserialize(DataTree dt, Map<Long, Integer> sessions) throws IOException;
​
    // 序列化方法
    void serialize(DataTree dt, Map<Long, Integer> sessions, File name) throws IOException;
​
    /**
     * find the most recent snapshot file
     * 查找最近的快照文件
     */
    File findMostRecentSnapshot() throws IOException;
​
    // 释放资源
    void close() throws IOException;
}
2)操作日志
public interface TxnLog {
    // 设置服务状态
    void setServerStats(ServerStats serverStats);
​
    // 滚动日志
    void rollLog() throws IOException;
    // 追加
    boolean append(TxnHeader hdr, Record r) throws IOException;
    // 读取数据
    TxnIterator read(long zxid) throws IOException;
​
    // 获取最后一个 zxid
    long getLastLoggedZxid() throws IOException;
​
    // 删除日志
    boolean truncate(long zxid) throws IOException;
​
    // 获取 DbId
    long getDbId() throws IOException;
​
    // 提交
    void commit() throws IOException;
    // 日志同步时间
    long getTxnLogSyncElapsedTime();
​
    // 关闭日志
    void close() throws IOException;
    // 读取日志的接口
    public interface TxnIterator {// 获取头信息
        TxnHeader getHeader();
​
        // 获取传输的内容
        Record getTxn();
​
        // 下一条记录
        boolean next() throws IOException;
​
        // 关闭资源
        void close() throws IOException;
​
        // 获取存储的大小
        long getStorageSize() throws IOException;
    }
}
3)处理持久化的核心类

持久化核心.png zookeeper-jute 代码是关于 Zookeeper 序列化相关源码

1)序列化和反序列化方法

record.png

@Public
public interface Record {
    void serialize(OutputArchive var1, String var2) throws IOException;
​
    void deserialize(InputArchive var1, String var2) throws IOException;
}
2)迭代
public interface Index {
    // 结束
    public boolean done();
    // 下一个
    public void incr();
}
3)序列化支持的数据类型
4)反序列化支持的数据类型

ZK服务端初始化源码解析

源码1.png

ZK 服务端启动入口

QuorumPeerMain.java

public static void main(String[] args) {
       // 创建了一个 zk 节点
       QuorumPeerMain main = new QuorumPeerMain();
       try {
           // 初始化节点并运行,args 相当于提交参数中的 zoo.cfg
           main.initializeAndRun(args);
       } catch (IllegalArgumentException e) {
... ...
       }
       LOG.info("Exiting normally");
       System.exit(0);
   }

initializeAndRun

 protected void initializeAndRun(String[] args)
            throws ConfigException, IOException, AdminServerException
    {
// 管理 zk 的配置信息
        QuorumPeerConfig config = new QuorumPeerConfig();
        if (args.length == 1) {
// 1 解析参数,zoo.cfg 和 myid
            config.parse(args[0]);
        }
        // 2 启动定时任务,对过期的快照,执行删除(默认该功能关闭)
        // Start and schedule the the purge task
        DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
                .getDataDir(), config.getDataLogDir(), config
                .getSnapRetainCount(), config.getPurgeInterval());
        purgeMgr.start();
        if (args.length == 1 && config.isDistributed()) {
            // 3 启动集群
            runFromConfig(config);
        } else {
            LOG.warn("Either no config or no quorum defined in config, running "
                    + " in standalone mode");
            // there is only server in the quorum -- run as standalone
            ZooKeeperServerMain.main(args);
        }
    }
解析参数 zoo.cfg myid

QuorumPeerConfig.java

 public void parse(String path) throws ConfigException {
        LOG.info("Reading configuration from: " + path);
​
        try {
// 校验文件路径及是否存在
            File configFile = (new VerifyingFileFactory.Builder(LOG)
                    .warnForRelativePath()
                    .failForNonExistingPath()
                    .build()).create(path);
​
            Properties cfg = new Properties();
            FileInputStream in = new FileInputStream(configFile);
            try {
                // 加载配置文件
                cfg.load(in);
                configFileStr = path;
            } finally {
                in.close();
            }
            // 解析配置文件
            parseProperties(cfg);
        } catch (IOException e) {
            throw new ConfigException("Error processing " + path, e);
        } catch (IllegalArgumentException e) {
            throw new ConfigException("Error processing " + path, e);
        } 
 
 ... ...
    }

QuorumPeerConfig.java

  public void parseProperties(Properties zkProp)
            throws IOException, ConfigException {
        int clientPort = 0;
        int secureClientPort = 0;
        String clientPortAddress = null;
        String secureClientPortAddress = null;
        VerifyingFileFactory vff = new
                VerifyingFileFactory.Builder(LOG).warnForRelativePath().build();
// 读取 zoo.cfg 文件中的属性值,并赋值给 QuorumPeerConfig 的类对象
        for (Entry<Object, Object> entry : zkProp.entrySet()) {
            String key = entry.getKey().toString().trim();
            String value = entry.getValue().toString().trim();
            if (key.equals("dataDir")) {
                dataDir = vff.create(value);
            } else if (key.equals("dataLogDir")) {
                dataLogDir = vff.create(value);
            } else if (key.equals("clientPort")) {
                clientPort = Integer.parseInt(value);
            } else if (key.equals("localSessionsEnabled")) {
                localSessionsEnabled = Boolean.parseBoolean(value);
            } else if (key.equals("localSessionsUpgradingEnabled")) {
                localSessionsUpgradingEnabled = Boolean.parseBoolean(value);
            } else if (key.equals("clientPortAddress")) {
                clientPortAddress = value.trim();
            } else if (key.equals("secureClientPort")) {
                secureClientPort = Integer.parseInt(value);
            } else if (key.equals("secureClientPortAddress")){
                secureClientPortAddress = value.trim();
            } else if (key.equals("tickTime")) {
                tickTime = Integer.parseInt(value);
            } else if (key.equals("maxClientCnxns")) {
                maxClientCnxns = Integer.parseInt(value);
            } else if (key.equals("minSessionTimeout")) {
                minSessionTimeout = Integer.parseInt(value);
            }
            ... ...
        }
        ... ...
        
        if (dynamicConfigFileStr == null) {
            setupQuorumPeerConfig(zkProp, true);
            if (isDistributed() && isReconfigEnabled()) {
                // we don't backup static config for standalone mode.
                // we also don't backup if reconfig feature is disabled.
                backupOldConfig();
            }
        }
    }

QuorumPeerConfig.java

void setupQuorumPeerConfig(Properties prop, boolean configBackwardCompatibilityMode)
        throws IOException, ConfigException {
    quorumVerifier = parseDynamicConfig(prop, electionAlg, true,
            configBackwardCompatibilityMode);
    setupMyId();
    setupClientPort();
    setupPeerType();
    checkValidity();
}

QuorumPeerConfig.java

private void setupMyId() throws IOException {
    File myIdFile = new File(dataDir, "myid");
    // standalone server doesn't need myid file.
    if (!myIdFile.isFile()) {
        return;
    }
    BufferedReader br = new BufferedReader(new FileReader(myIdFile));
    String myIdString;
    try {
        myIdString = br.readLine();
    } finally {
        br.close();
    }
    try {
        // 将解析 myid 文件中的 id 赋值给 serverId
        serverId = Long.parseLong(myIdString);
        MDC.put("myid", myIdString);
    } catch (NumberFormatException e) {
        throw new IllegalArgumentException("serverid " + myIdString
                + " is not a number");
    }
}

过期快照删除

可以启动定时任务,对过期的快照,执行删除。默认该功能时关闭的

  protected void initializeAndRun(String[] args)
            throws ConfigException, IOException, AdminServerException {
// 管理 zk 的配置信息
        QuorumPeerConfig config = new QuorumPeerConfig();
        if (args.length == 1) {
// 1 解析参数,zoo.cfg 和 myid
            config.parse(args[0]);
        }
        // 2 启动定时任务,对过期的快照,执行删除(默认是关闭)
        // config.getSnapRetainCount() = 3 最少保留的快照个数
        // config.getPurgeInterval() = 0 默认 0 表示关闭
        // Start and schedule the the purge task
        DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
                .getDataDir(), config.getDataLogDir(), config
                .getSnapRetainCount(), config.getPurgeInterval());
        purgeMgr.start();
        if (args.length == 1 && config.isDistributed()) {
            // 3 启动集群
            runFromConfig(config);
        } else {
            LOG.warn("Either no config or no quorum defined in config, running "
                    + " in standalone mode");
            // there is only server in the quorum -- run as standalone
            ZooKeeperServerMain.main(args);
        }
    }
​
    protected int snapRetainCount = 3;
    protected int purgeInterval = 0;
​
    public void start() {
        if (PurgeTaskStatus.STARTED == purgeTaskStatus) {
            LOG.warn("Purge task is already running.");
            return;
        }
// 默认情况 purgeInterval=0,该任务关闭,直接返回
        // Don't schedule the purge task with zero or negative purge interval.
        if (purgeInterval <= 0) {
            LOG.info("Purge task is not scheduled.");
            return;
        }
// 创建一个定时器
        timer = new Timer("PurgeTask", true);
// 创建一个清理快照任务
        TimerTask task = new PurgeTask(dataLogDir, snapDir, snapRetainCount);
// 如果 purgeInterval 设置的值是 1,表示 1 小时检查一次,判断是否有过期快照,
        有则删除
        timer.scheduleAtFixedRate(task, 0, TimeUnit.HOURS.toMillis(purgeInterval));
        purgeTaskStatus = PurgeTaskStatus.STARTED;
    }
​
static class PurgeTask extends TimerTask {
    private File logsDir;
    private File snapsDir;
    private int snapRetainCount;
​
    public PurgeTask(File dataDir, File snapDir, int count) {
        logsDir = dataDir;
        snapsDir = snapDir;
        snapRetainCount = count;
    }
​
    @Override
    public void run() {
        LOG.info("Purge task started.");
        try {
// 清理过期的数据
            PurgeTxnLog.purge(logsDir, snapsDir, snapRetainCount);
        } catch (Exception e) {
            LOG.error("Error occurred while purging.", e);
        }
        LOG.info("Purge task completed.");
    }
}
​
    public static void purge(File dataDir, File snapDir, int num) throws IOException {
        if (num < 3) {
            throw new IllegalArgumentException(COUNT_ERR_MSG);
        }
        FileTxnSnapLog txnLog = new FileTxnSnapLog(dataDir, snapDir);
        List<File> snaps = txnLog.findNRecentSnapshots(num);
        int numSnaps = snaps.size();
        if (numSnaps > 0) {
            purgeOlderSnapshots(txnLog, snaps.get(numSnaps - 1));
        }
    }

初始化通信组件

 protected void initializeAndRun(String[] args)
            throws ConfigException, IOException, AdminServerException
    {
// 管理 zk 的配置信息
        QuorumPeerConfig config = new QuorumPeerConfig();
        if (args.length == 1) {
// 1 解析参数,zoo.cfg 和 myid
            config.parse(args[0]);
        }
        // 2 启动定时任务,对过期的快照,执行删除(默认是关闭)
        // config.getSnapRetainCount() = 3 最少保留的快照个数
        // config.getPurgeInterval() = 0 默认 0 表示关闭
        // Start and schedule the the purge task
        DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
                .getDataDir(), config.getDataLogDir(), config
                .getSnapRetainCount(), config.getPurgeInterval());
        purgeMgr.start();
        if (args.length == 1 && config.isDistributed()) {
            // 3 启动集群(集群模式)
            runFromConfig(config);
        } else {
            LOG.warn("Either no config or no quorum defined in config, running "
                    + " in standalone mode");
            // there is only server in the quorum -- run as standalone
            // 本地模式
            ZooKeeperServerMain.main(args);
        }
    }
1)通信协议默认NIO (可以支持 Netty
public void runFromConfig(QuorumPeerConfig config)
        throws IOException, AdminServerException {
        … …
    LOG.info("Starting quorum peer");
    try {
        ServerCnxnFactory cnxnFactory = null;
        ServerCnxnFactory secureCnxnFactory = null;
        // 通信组件初始化,默认是 NIO 通信
        if (config.getClientPortAddress() != null) {
            cnxnFactory = ServerCnxnFactory.createFactory();
            cnxnFactory.configure(config.getClientPortAddress(),
                    config.getMaxClientCnxns(), false);
        }
        if (config.getSecureClientPortAddress() != null) {
            secureCnxnFactory = ServerCnxnFactory.createFactory();
            secureCnxnFactory.configure(config.getSecureClientPortAddress(),
                    config.getMaxClientCnxns(), true);
        }
        // 把解析的参数赋值给该 zookeeper 节点
        quorumPeer = getQuorumPeer();
        quorumPeer.setTxnFactory(new FileTxnSnapLog(
                config.getDataLogDir(),
                config.getDataDir()));
        quorumPeer.enableLocalSessions(config.areLocalSessionsEnabled());
        quorumPeer.enableLocalSessionsUpgrading(
                config.isLocalSessionsUpgradingEnabled());
        //quorumPeer.setQuorumPeers(config.getAllMembers());
        quorumPeer.setElectionType(config.getElectionAlg());
        quorumPeer.setMyid(config.getServerId());
        quorumPeer.setTickTime(config.getTickTime());
        quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
        quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
        quorumPeer.setInitLimit(config.getInitLimit());
        quorumPeer.setSyncLimit(config.getSyncLimit());
        quorumPeer.setConfigFileName(config.getConfigFilename());
        // 管理 zk 数据的存储
        quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
        quorumPeer.setQuorumVerifier(config.getQuorumVerifier(), false);
        if (config.getLastSeenQuorumVerifier() != null) {
            quorumPeer.setLastSeenQuorumVerifier(config.getLastSeenQuorumVerifier(),
                    false);
        }
        quorumPeer.initConfigInZKDatabase();
        // 管理 zk 的通信
        quorumPeer.setCnxnFactory(cnxnFactory);
        quorumPeer.setSecureCnxnFactory(secureCnxnFactory);
        quorumPeer.setSslQuorum(config.isSslQuorum());
        quorumPeer.setUsePortUnification(config.shouldUsePortUnification());
        quorumPeer.setLearnerType(config.getPeerType());
        quorumPeer.setSyncEnabled(config.getSyncEnabled());
        quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
        if (config.sslQuorumReloadCertFiles) {
            quorumPeer.getX509Util().enableCertFileReloading();
        }
            … …
        quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize);
        quorumPeer.initialize();
​
        // 启动 zk
        quorumPeer.start();
        quorumPeer.join();
    } catch (InterruptedException e) {
        // warn, but generally this is ok
        LOG.warn("Quorum Peer interrupted", e);
    }
}
​
static public ServerCnxnFactory createFactory() throws IOException {
    String serverCnxnFactoryName =
            System.getProperty(ZOOKEEPER_SERVER_CNXN_FACTORY);
    if (serverCnxnFactoryName == null) {
        serverCnxnFactoryName = NIOServerCnxnFactory.class.getName();
    }
    try {
        ServerCnxnFactory serverCnxnFactory = (ServerCnxnFactory)
                Class.forName(serverCnxnFactoryName)
                        .getDeclaredConstructor().newInstance();
        LOG.info("Using {} as server connection factory", serverCnxnFactoryName);
        return serverCnxnFactory;
    } catch (Exception e) {
        IOException ioe = new IOException("Couldn't instantiate "
                + serverCnxnFactoryName);
        ioe.initCause(e);
        throw ioe;
    }
}
​
public static final String ZOOKEEPER_SERVER_CNXN_FACTORY =
        "zookeeper.serverCnxnFactory";
2)初始化 NIO 服务端 Socket(并未启动)

NIOServerCnxnFactory.java

 public void configure(InetSocketAddress addr, int maxcc, boolean secure) throws IOException
    {
        if (secure) {
            throw new UnsupportedOperationException("SSL isn't supported in
                    NIOServerCnxn");
        }
        configureSaslLogin();
        maxClientCnxns = maxcc;
        sessionlessCnxnTimeout = Integer.getInteger(
                ZOOKEEPER_NIO_SESSIONLESS_CNXN_TIMEOUT, 10000);
        // We also use the sessionlessCnxnTimeout as expiring interval for
        // cnxnExpiryQueue. These don't need to be the same, but the expiring
        // interval passed into the ExpiryQueue() constructor below should be
        // less than or equal to the timeout.
        cnxnExpiryQueue =
                new ExpiryQueue<NIOServerCnxn>(sessionlessCnxnTimeout);
        expirerThread = new ConnectionExpirerThread();
        int numCores = Runtime.getRuntime().availableProcessors();
        // 32 cores sweet spot seems to be 4 selector threads
        numSelectorThreads = Integer.getInteger(
                ZOOKEEPER_NIO_NUM_SELECTOR_THREADS,
                Math.max((int) Math.sqrt((float) numCores/2), 1));
        if (numSelectorThreads < 1) {
            throw new IOException("numSelectorThreads must be at least 1");
        }
        numWorkerThreads = Integer.getInteger(
                ZOOKEEPER_NIO_NUM_WORKER_THREADS, 2 * numCores);
        workerShutdownTimeoutMS = Long.getLong(
                ZOOKEEPER_NIO_SHUTDOWN_TIMEOUT, 5000);
 ... ...
        for(int i=0; i<numSelectorThreads; ++i) {
            selectorThreads.add(new SelectorThread(i));
        }
        // 初始化 NIO 服务端 socket,绑定 2181 端口,可以接收客户端请求
        this.ss = ServerSocketChannel.open();
        ss.socket().setReuseAddress(true);
        LOG.info("binding to port " + addr);
// 绑定 2181 端口
        ss.socket().bind(addr);
        ss.configureBlocking(false);
        acceptThread = new AcceptThread(ss, addr, selectorThreads);
    }

ZK 服务端加载数据源码解析

持久化.png

  1. zk 中的数据模型,是一棵树,DataTree,每个节点,叫做 DataNode
  2. zk 集群中的 DataTree 时刻保持状态同步
  3. Zookeeper 集群中每个 zk 节点中,数据在内存和磁盘中都有一份完整的数据。
  • 内存数据:DataTree
  • 磁盘数据:快照文件 + 编辑日志

ZK服务端初始化源码解析

源码2.png

冷启动数据恢复快照数据

冷启动数据恢复编辑日志

选举源码

选举源码.png

选举准备

选举准备.png

选举执行

执行选举.png

Follower 和 Leader 状态同步

选举结束后,每个节点都需要根据自己的角色更新自己的状态。选举出的 Leader 更新自己状态为 Leader,其他节点更新自己状态为 Follower

Leader 更新状态入口:leader.load()

Follower更新状态入口:follower.followerLeader()

(1)follower 必须要让 leader 知道自己的状态:epoch、zxid、sid

  • 必须要找出谁是 leader
  • 发起请求连接 leader
  • 发送自己的信息给 leader
  • leader 接收到信息,必须要返回对应的信息给 follower

(2)当leader得知follower的状态了,就确定需要做何种方式的数据同步DIFF、TRUNC、SNAP

(3)执行数据同步

(4)当 leader 接收到超过半数 followerack 之后,进入正常工作状态,集群启动完成了

最终总结同步的方式:

(1)DIFF 咱两一样,不需要做什么

(2)TRUNC followerzxidleaderzxid 大,所以 Follower 要回滚

(3)COMMIT leaderzxidfollowerzxid 大,发送 Proposalfoloower 提交执行

(4)如果 follower 并没有任何数据,直接使用 SNAP 的方式来执行数据同步(直接把数据全部序列到 follower)

状态同步.png

状态同步源码.png

Leader 启动源码

leader启动.png

服务端 Follower 启动

follower启动.png

客户端启动

客户端启动.png