Zookeeper

入门

概述

Zookeeper从设计模式角度解释:是一个基于观察者模式设计的分布式服务管理框架,负责存储和管理服务器相关数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应。

特点

  1. Zookeeper,一个领导者,剩余都是跟随者组成的集群;
  2. 集群中只要有半数以上的节点存活,Zookeeper集群就能正常服务,所以Zookeeper适合安装基数台服务器;
  3. 全局数据一致,每一个服务端都保存一份相同的数据副本,客户端无论链接哪一台,数据都一致;
  4. 更新请求顺序执行,来自同一个客户端的更新请求按其发送顺序依次执行;
  5. 数据更新原子性,一次数据更新要么成功,要么失败;
  6. 实时性,在一定范围内,客户端多能读到最新数据。

数据结构

ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称做一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据,每个 ZNode 都可以通过其路径唯一标识。

应用场景

提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等。

统一命名服务

在分布式环境下,经常需要对应用/服务进行统一命名,便于识别。

例如:多个Ip统一某个域名,容易使用;

统一配置管理

  1. 分布式环境下,一般要求一个集群中,配置文件是一致的,对配置文件修改后,希望能快速同步到各个节点。
  2. 配置管理科交给zookeeoer实现:可将配置信息写入Zookeeper上的一个Znode,各个客户端服务器监听这个Znode,一旦Znode中数据被修改,Zookeeper将通知其他客户端服务器。(仁云公司使用百度的Disconf依Zookeeper的watch机制实现统一配置管理)

统一集群管理

  1. 分布式环境中,实时掌握每个节点的状态是必要的,可根据节点实时状态做出调整;
  2. Zookeeper可以实现实时监控节点状态变化,可将节点信息写入Znode,监听这个Znode获取它的实时状态变化。

服务器动态上下线

软负载均衡

在Zookeeper中记录每台服务器的访问数,让访问数量最少的服务器去处理最新的客户端请求。

下载使用

https://zookeeper.apache.org/

Zookeeper选举机制

第一次启动

  1. 服务器1启动,发起一次选举。服务器1投自己一票,此时服务器1票数一票,不够半数以上(3),选举无法完成,服务器1状态为LOOKING。
  2. 服务器2启动,发起一次选举。服务器1、2分别投自己一票,此时服务器1发现服务器2的myid比自己大,投给服务器2,服务器2两票,服务器1零票。服务器2的票数还是不够半数以上(3),选举无法完成,服务器1、2状态为LOOKING。
  3. 服务器3启动,发起一次选举,此时服务器1和服务器2都会更改选票为服务器3,此次投票结果为:服务器1零票、服务器2零票、服务器3三票,服务器3的票数已经超过半数,当选为Leader,其余服务器为Following,服务器3的状态更改为LEADING。
  4. 服务器4或5启动,发起一次选举,此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为Following。

非第一次启动

  1. 当zookeeper器群中出现一下情况时会进行重新选举:
    1. 服务器初始化启动;
    2. 服务器运行期间无法与Leader保持链接。
  2. 而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态:
    1. 集群中本来已经存在一个Leader,对于集群已经存在Leader,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,只需要和Leader机器建立连接,并进行状态同步即可。
    2. 集群中不存在Leader,假设ZooKeeper由5台服务器组成,SID分别为1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器是Leader。某一时刻, 3和5服务器出现故障,因此开始进行Leader选举。

(EPOCH,ZXID,SID )(EPOCH,ZXID,SID )(EPOCH,ZXID,SID )

SID为1、2、4的机器投票情况: (1,8,1) (1,8,2) (1,7,4) ;

  1. 选举Leader规则: ①EPOCH大的直接胜出 ②EPOCH相同,事务id大的胜出 ③事务id相同,服务器id大的胜出;
  2. SID:服务器ID。用来唯一标识一台 ZooKeeper集群中的机器,每台机器不能重 复,和myid一致。
  3. ZXID:事务ID。ZXID是一个事务ID,用来标识一次服务器状态的变更。在某一时刻, 集群中的每台机器的ZXID值不一定完全一 致,这和ZooKeeper服务器对于客户端“更 新请求”的处理逻辑有关。
  4. Epoch:每个Leader任期的代号。没有 Leader时同一轮投票过程中的逻辑时钟值是 相同的。每投完一次票这个数据就会增加

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

持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除;

短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除。

说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护;

注意在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序;

  1. 持久化目录节点:客户端与Zookeeper断开链接后,该节点依旧存在;
  2. 持久化顺序编号目录节点:客户端与Zookeeper断开链接后,该节点依旧存在,只是zk给该节点名称进行顺序编号;
  3. 临时目录节点:客户端与Zookeeper断开连接后,该节点被删除;
  4. 临时顺序编号目录节点:客户端与 Zookeeper 断开连接后 ,该 节 点 被 删 除 ,只 是Zookeeper给该节点名称进行顺序编号。

监听器原理

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序。

监听器步骤

  1. 首先需要一个main线程;
  2. 在main线程创建一个zookeeper客户端,会创建两个线程,一个负责网络链接通讯(connet),一个负责监听(listener);
  3. 通过connet线程将注册的监听事件发送给zookeeper;
  4. 在zookeeper的监听器列表中将注册的监听事件添加到列表中;
  5. Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程;
  6. listener线程内部调用了process()方法。

常见监听器

  1. 监听结点数据变化 get patch [watch];
  2. 监听子节点增减的变化 ls path [watch]。

客户端API操作

搭建项目

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.13.1</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>
<dependency>
  <groupId>org.apache.zookeeper</groupId>
  <artifactId>zookeeper</artifactId>
  <version>3.5.7</version>
</dependency>

测试用例

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.junit.Before;
import org.junit.Test;
 
import java.util.List;
 
public class ZkClientDemo {
        private static final String connect = "124.222.109.124:2181";
    private static int sessionTime = 100000;
    private ZooKeeper zooKeeper = null;
 
    @Before
    public void init() throws Exception {
        zooKeeper = new ZooKeeper(connect, sessionTime, watchedEvent -> {
            //收到事件通知后的回调函数(用户的业务逻辑)
            System.out.println(watchedEvent.getType() + "---" + watchedEvent.getPath());
            //再次启动监听
            try {
                List<String> children = zooKeeper.getChildren("/", true);
                for (String child : children) {
                    System.out.println(child);
                }
            } catch (KeeperException | InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
 
    @Test
    public void creatNode() throws Exception {
        /*
            参数1:要创建的节点路径;
            参数2:节点数据;
            参数3:节点权限;
            参数4:节点的类型
         */
        String node = zooKeeper.create("/zk", "zd".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        System.out.println(node);
    }
 
    @Test
    public void getChildren()throws Exception{
        List<String> children = zooKeeper.getChildren("/", true);
        for (String child : children) {
            System.out.println(child);
        }
        Thread.sleep(Long.MAX_VALUE);
    }
}

Zookeeper分布式锁案例

分布式锁

进程1 在使用资源的时候,会先获得锁,进程1 获得锁之后会对该资源保持独占,这样其他进程就无法访问该资源,“进程 1”用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。

Dubbo