两万字长文全面理解Zookeeper

目录

  • 一、Zookeeper简介
  • 二、Zookeeper应用场景
      • 2.1 维护配置信息
      • 2.2 分布式锁服务
      • 2.3 集群管理
      • 2.4 生成分布式唯一ID
  • 三、Zookeper的数据模型
      • 3.1 Znode
      • 3.2 节点类型
  • 四、集群安装Zookeeper
      • 4.1下载
      • 4.2配置
      • 4.3 添加myid配置
      • 4.3 启动和停止
      • 4.4 远程登录zookeeper
      • 4.5 启动脚本
  • 五、常用shell操作
      • 5.1 新增节点
      • 5.2 更新节点
      • 5.3 删除节点
      • 5.4 查看节点stat数据结构
      • 5.5 查看节点列表
      • 5.6 监听器
  • 六、Zookeper权限控制ACL
      • 6.1 schemem
      • 6.2 id
      • 6.3 permission
      • 6.4 授权的命令
      • 6.5 具体操作
          • 6.5.1 world 模式
          • 6.5.2 ip模式
          • 6.5.3 Auth模式
          • 6.5.4 digest模式
          • 6.5.6 super模式
  • 六、Zookeeper的Java Api操作
      • 6.1 maven坐标
      • 6.2 连接Zookeeper
      • 6.3 新增节点
      • 6.4 更新节点
      • 6.4 删除节点
      • 6.5 查看节点
      • 6.4 删除节点
      • 6.5 查看节点
  • 七、Zookeeper事件监听机制
      • 7.1 watcher概念
      • 7.2 watcher架构
      • 7.3 watcher特性
      • 7.4 watcher接口
  • 八、一些案例
      • 8.1 配置中心案例
      • 8.2 生成分布式唯一ID
      • 8.3 分布式锁
  • 九、一致性协议zab
  • 十、zookeeper的leader选举
      • 4.1 服务器状态
      • 4.2 服务器启动时期的leader选举
      • 4.3 服务器运行时期的Leader选举

一、Zookeeper简介

是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

二、Zookeeper应用场景

2.1 维护配置信息

java编程经常会遇到配置项, 比如数据库的url、 schema、 user和password等。 通常这些配置项我们会放置在配置文件中, 再将配置文件放置在服务器上当需要更改配置项时, 需要去服务器上修改对应的配置文件。 但是随着分布式系统的兴起, 由于许多服务都需要使用到该配置文件, 因此有必须保证该配置服务的高可用性(highavailability) 和各台服务器上配置数据的一致性。 通常会将配置文件部署在一个集群上,然而一个集群动辄上千台服务器, 此时如果再一台台服务器逐个修改配置文件那将是非常繁琐且危险的的操作, 因此就需要一种服务, 能够高效快速且可靠地完成配置项的更改等操作, 并能够保证各配置项在每台服务器上的数据一致性。
zookeeper就可以提供这样一种服务, 其使用Zab这种一致性协议来保证一致性。 现在有很多开源项目使用zookeeper来维护配置, 比如在hbase中, 客户端就是连接一个zookeeper, 获得必要的hbase集群的配置信息, 然后才可以进一步操作。 还有在开源的消息队列kafka中, 也使用zookeeper来维护broker的信息。 在alibaba开源的soa框架dubbo中也广泛的使用zookeeper管理一些配置来实现服务治理。

2.2 分布式锁服务

一个集群是一个分布式系统, 由多台服务器组成。 为了提高并发度和可靠性,多台服务器上运行着同一种服务。 当多个服务在运行时就需要协调各服务的进度, 有时候需要保证当某个服务在进行某个操作时, 其他的服务都不能进行该操作, 即对该操作进行加锁, 如果当前机器挂掉后, 释放锁并fail over 到其他的机器继续执行该服务。

2.3 集群管理

一个集群有时会因为各种软硬件故障或者网络故障, 出现某些服务器挂掉而被移除集群, 而某些服务器加入到集群中的情况, zookeeper会将这些服务器加入/移出的情况通知给集群中的其他正常工作的服务器, 以及时调整存储和计算等任务的分配和执行等。 此外zookeeper还会对故障的服务器做出诊断并尝试修复。

2.4 生成分布式唯一ID

在过去的单库单表型系统中, 通常可以使用数据库字段自带的auto_increment属性来自动为每条记录生成一个唯一的ID。 但是分库分表后, 就无法在依靠数据库的auto_increment属性来唯一标识一条记录了。 此时我们就可以用zookeeper在分布式环境下生成全局唯一ID。 做法如下: 每次要生成一个新Id时, 创建一个持久顺序节点, 创建操作返回的节点序号, 即为新Id, 然后把比自己节点小的删除即可

三、Zookeper的数据模型

3.1 Znode

zookeeper的数据节点可以视为树状结构(或者目录) , 树中的各节点被称为znode(即zookeeper node) , 一个znode可以有多个子节点。 zookeeper节点在结构上表现为树状; 使用路径path来定位某个znode。

znode, 兼具文件和目录两种特点。 既像文件一样维护着数据、 元信息、 ACL、 时间戳等数据结构, 又像目录一样可以作为路径标识的一部分

两万字长文全面理解Zookeeper_第1张图片

一个Znode大体上有3个部分

  • 节点的数据: 即znode data(节点path, 节点data)的关系就像是java map中(key,value)的关系
  • 节点的子节点children
  • 节点的状态stat: 用来描述当前节点的创建、 修改记录, 包括cZxid、 ctime等

3.2 节点类型

zookeeper中的节点有两种, 分别为临时节点和永久节点。 节点的类型在创建时即被确定, 并且不能改变。

  • 临时节点: 该节点的生命周期依赖于创建它们的会话。 一旦会话(Session)结束, 临
    时节点将被自动删除, 当然可以也可以手动删除。 虽然每个临时的Znode都会绑定到
    一个客户端会话, 但他们对所有的客户端还是可见的。 另外, ZooKeeper的临时节
    点不允许拥有子节点。
  • 持久化节点: 该节点的生命周期不依赖于会话, 并且只有在客户端显示执行删除操作
    的时候, 他们才能被删除

四、集群安装Zookeeper

4.1下载

zookeeper下载地址

4.2配置

服务器IP 主机名 myid的值
192.168.18.101 hadoop101 1
192.168.18.102 hadoop102 2
192.168.18.103 hadoop103 3

因为zookeeper的安装是需要jdk的环境的,这里不说jdk的安装。配置zookeeper非常简单,只需要更改一下存储zookeeper中数据的内存快照、 及事物日志文件

cd /opt/module/zookeeper-3.4.9/conf
cp zoo_sample.cfg zoo.cf
vim zoo.cfg

# 存储zookeeper中数据的内存快照、 及事物日志文件  
dataDir=/opt/module/zookeeper-3.4.9/zkdatas
# 保留多少个快照
autopurge.snapRetainCount=3
# 日志多少小时清理一次
autopurge.purgeInterval=1
# 集群中服务器地址
server.1=hadoop101:2888:3888
server.2=hadoop102:2888:3888
server.3=hadoop103:2888:3888

4.3 添加myid配置

myid相当于每一台机器的标识,后面会详细讲到

在/opt/module/zookeeper-3.4.9/zkdatas/下创建文件名为myid,内容为1

echo 1 > /opt/module/zookeeper-3.4.9/zkdatas/myid

然后分发到各个机器上,注意xsync是自己写的脚本,这个在hadoop那提到过,最后修改每台机器上的myid文件内容

4.3 启动和停止

cd /opt/module/zookeeper-3.4.9/bin
#启动
./zkServer.sh start

#停止
./zkServer.sh stop
#查看状态
./zkServer.sh status

注意:zookeeper不像hadoop一样,zookeeper是不能群起的,所以对每台机器都必须启动一下

4.4 远程登录zookeeper

注意端口号一般为2181

./zkCli.sh -server ip

4.5 启动脚本

因为集群启动需要一个一个机器启动,所以自己写了一个脚本

#!/bin/sh
params=$1
if [ "$params" = "start" ]
then
        for (( i=1 ; i <= 3 ; i = $i + 1 )) ;
         do
                echo ============= hadoop10$i $params =============
                 ssh [email protected]$i "/opt/module/zookeeper-3.4.9/bin/zkServer.sh start"
        done
fi
if [ "$params" = "stop" ]
then
        for (( i=1 ; i <= 3 ; i = $i + 1 )) ;
         do
                echo ============= hadoop10$i $params =============
                 ssh [email protected]$i "/opt/module/zookeeper-3.4.9/bin/zkServer.sh stop"
        done
fi
if [ "$params" = "status" ]
then
        for (( i=1 ; i <= 3 ; i = $i + 1 )) ;
         do
                echo ============= hadoop10$i $params =============
                 ssh [email protected]$i "/opt/module/zookeeper-3.4.9/bin/zkServer.sh status"
        done
fi

将该脚本放到/usr/local/bin/zk.sh就可以使用,一键启动脚本

五、常用shell操作

5.1 新增节点

create [-s] [-e] path data #其中-s 为有序节点, -e 临时节点

5.2 更新节点

set /hadoop "345"

5.3 删除节点

delete path

想删除某个节点及其所有后代节点, 可以使用递归删除, 命令为 rmr path

5.4 查看节点stat数据结构

get path

两万字长文全面理解Zookeeper_第2张图片

节点各个属性如下表。 其中一个重要的概念是 Zxid(ZooKeeper Transaction Id), ZooKeeper 节点的每一次更改都具有唯一的 Zxid, 如果 Zxid1 小于 Zxid2, 则Zxid1 的更改发生在 Zxid2 更改之前。

状态属性 说明
cZxid 数据节点创建时的事务 ID
ctime 数据节点创建时的时间
mZxid 数据节点最后一次更新时的事务 ID
mtime 数据节点最后一次更新时的时间
pZxid 数据节点的子节点最后一次被修改时的事务 ID
cversion 子节点的更改次数
dataVersion 节点数据的更改次数
aclVersion 节点的 ACL 的更改次数
ephemeralOwner 如果节点是临时节点, 则表示创建该节点的会话的 SessionID; 如果节点是持久节点, 则该属性值为 0
dataLength 数据内容的长度
numChildren 数据节点当前的子节点个数

5.5 查看节点列表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpHOlTDs-1608376830488)(Zookeeper(1)].assets/image-20201216203412038.png)

ls /

5.6 监听器

使用 get path [watch] 注册的监听器能够在节点内容发生改变的时候, 向客户端发出通知。 需要注意的是 zookeeper 的触发器是一次性的 (One-time trigger), 即触发一次后就会立即失效 。

get /hadoop watch

当使用另外一个客户端修改/hadoop的值得时候,watch将发出通知

在这里插入图片描述

六、Zookeper权限控制ACL

zookeeper的文件系统类似于linux,client 可以创建节点、 更新节点、 删除节点, 那么如何做到节点的权限的控制呢? zookeeper的access control list 访问控制列表可以做到这一点。

zookeeper中权限控制是通过访问控制列表来实现的,访问控制列表格式为:

权限模式(schemem):授权对象(id):权限(permission)
  • zooKeeper的权限控制是基于每个znode节点的, 需要对每个节点设置权限
  • 每个znode支持设置多种权限控制方案和多个权限
  • 子节点不会继承父节点的权限, 客户端无权访问某节点, 但可能可以访问它的子节点

6.1 schemem

权限模式:采用何种方式授权

方案 描述
world 只有一个用户: anyone, 代表登录zokeeper所有人(默认)
ip 对客户端使用IP地址认证
auth 使用已添加认证的用户认证
digest 使用“用户名:密码”方式认证

6.2 id

授权对象ID是指, 权限赋予的实体, 例如: IP 地址或用户

6.3 permission

授予什么权限

权限 ACL简写 描述
create c 可以创建子节点
delete d 可以删除子节点(仅下一级节点)
read r 可以读取节点数据及显示子节点列表
write w 可以设置节点数据
admin a 可以设置节点访问控制列表权限

6.4 授权的命令

命令 使用方式 描述
getAcl getAcl 读取ACL权限
setAcl setAcl 设置ACL权限
addauth addauth 添加认证用户

6.5 具体操作

6.5.1 world 模式
[zk: localhost:2181(CONNECTED) 5] create /node1 "node"
#赋予/node1节点不能创建子节点的权限
[zk: localhost:2181(CONNECTED) 8] setAcl /node1 world:anyone:drwa
#试图创建子节点
[zk: localhost:2181(CONNECTED) 11] create /node1/node11 "node11"
Authentication is not valid : /node1/node11

[zk: localhost:2181(CONNECTED) 12] getAcl /node1
'world,'anyone
: drwa
6.5.2 ip模式
[zk: localhost:2181(CONNECTED) 13] create /hive "hive"
#设置只有ip为192.168.18.102的主机才能访问/hive节点
[zk: localhost:2181(CONNECTED) 14] setAcl /hive ip:192.168.18.102:cdrwa
#当前节点已经不能访问
[zk: localhost:2181(CONNECTED) 15] get /hive
Authentication is not valid : /hive
[zk: localhost:2181(CONNECTED) 16] getAcl /hive
'ip,'192.168.18.102
: cdrwa
6.5.3 Auth模式
[zk: localhost:2181(CONNECTED) 0] addauth digest  zookeeper1:12345
[zk: localhost:2181(CONNECTED) 1] create /node3 "node1"
Created /node3
[zk: localhost:2181(CONNECTED) 2] setAcl /node3 auth:zookeeper1:cdrwa
6.5.4 digest模式
setAcl <path> digest:<user>:<password>:<acl>

这里的密码是通过了SHA1及BASE64处理的密文

#先计算出密文
[root@hadoop101 ~]# echo -n user1:12345 | openssl dgst -binary -sha1 | openssl base64
+owfoSBn/am19roBPzR1/MfCblE=

digest授权

#创建节点
[zk: localhost:2181(CONNECTED) 16] create /node5 "node5"
Created /node5
#设置权限使用digest
[zk: localhost:2181(CONNECTED) 17] setAcl /node5 digest:user1:+owfoSBn/am19roBPzR1/MfCblE=:crdwa
#尝试获取节点
[zk: localhost:2181(CONNECTED) 0] get /node5
Authentication is not valid : /node5
#添加认证用户
[zk: localhost:2181(CONNECTED) 1] addauth digest user1:12345
#获取节点成功
[zk: localhost:2181(CONNECTED) 2] get /node5
node5
cZxid = 0x40000001e
ctime = Thu Dec 17 20:26:31 CST 2020
mZxid = 0x40000001e
mtime = Thu Dec 17 20:26:31 CST 2020
pZxid = 0x40000001e
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
6.5.6 super模式

zookeeper的权限管理模式有一种叫做super, 该模式提供一个超管可以方便的访问
任何权限的节点。

假设这个超管是: user2:admin, 需要先为超管生成密码的密文

六、Zookeeper的Java Api操作

  • 连接到zookeeper服务器。 zookeeper服务器为客户端分配会话ID。
  • 定期向服务器发送心跳。 否则, zookeeper服务器将过期会话ID, 客户端需要重新连
    接。
  • 只要会话ID处于活动状态, 就可以获取/设置znode。
  • 所有任务完成后, 断开与zookeeper服务器的连接。 如果客户端长时间不活动, 则
    zookeeper服务器将自动断开客户端。

6.1 maven坐标

<dependency>
    <groupId>junitgroupId>
    <artifactId>junitartifactId>
    <version>3.8.1version>
    <scope>testscope>
dependency>


<dependency>
    <groupId>org.apache.zookeepergroupId>
    <artifactId>zookeeperartifactId>
    <version>4.13.1version>
dependency>


<dependency>
    <groupId>log4jgroupId>
    <artifactId>log4jartifactId>
    <version>1.2.16version>
dependency>


<dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-apiartifactId>
    <version>1.6.1version>
dependency>

6.2 连接Zookeeper

public class ZookeeperConnection {
    public static void main(String[] args) throws IOException {
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        String ip = "192.168.18.101:2181";
        ZooKeeper zooKeeper = new ZooKeeper(ip, 5000, new Watcher() {
            public void process(WatchedEvent watchedEvent) {
                if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
                    System.out.println("链接创建成功!");
                    countDownLatch.countDown();
                }
            }
        });
        try {
            countDownLatch.await();
            System.out.println(zooKeeper.getSessionId());
            zooKeeper.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

两万字长文全面理解Zookeeper_第3张图片

ZooKeeper构造函数的参数:

  • connectionString - zookeeper主机(注意端口2181)
  • sessionTimeout - 会话超时(以毫秒为单位)
  • watcher - 实现“监视器”对象。 zookeeper集合通过监视器对象返回连接状态。

6.3 新增节点

// 同步方式
create(String path, byte[] data, List<ACL> acl, CreateMode createMode)
// 异步方式
create(String path, byte[] data, List<ACL> acl, CreateMode createMode,
AsyncCallback.StringCallback callBack,Object ctx)  
  • path:znode路径。 例如, /node1 /node1/node11
  • data:要存储在指定znode路径中的数据
  • acl:要创建的节点的访问控制列表。 zookeeper API提供了一个静态接口:ZooDefs.Ids 来获取一些基本的acl列表。 例如, ZooDefs.Ids.OPEN_ACL_UNSAFE返回打开znode的acl列表。
  • createMode:节点的类型,这是一个枚举。
  • callBack:异步回调接口
  • ctx:传递上下文参数

6.4 更新节点

// 同步方式
setData(String path, byte[] data, int version)
// 异步方式
setData(String path, byte[] data, int version, AsyncCallback.StatCallback
callBack, Object ctx)
  • path:znode路径
  • data:要存储在指定znode路径中的数据。
  • version:znode的当前版本。 每当数据更改时, ZooKeeper会更新znode的版本号。-1代表版本号不作为修改条件
  • callBack:异步回调接口
  • ctx:传递上下文参数

6.4 删除节点

// 同步方式
delete(String path, int version)
// 异步方式
delete(String path, int version, AsyncCallback.VoidCallback callBack,
Object ctx)
  • path :znode路径。
  • version:znode的当前版本
  • callBack:异步回调接口
  • ctx:传递上下文参数

6.5 查看节点

// 同步方式
getData(String path, boolean b, Stat stat)
// 异步方式
getData(String path, boolean b, AsyncCallback.DataCallback callBack,
Object ctx)
  • path:znode路径。
  • b:是否使用连接对象中注册的监视器。
  • stat :返回znode的元数据。
  • callBack:异步回调接口
    传递上下文参数

6.4 删除节点

// 同步方式
delete(String path, int version)
// 异步方式
delete(String path, int version, AsyncCallback.VoidCallback callBack,
Object ctx)
  • path :znode路径。
  • version:znode的当前版本
  • callBack:异步回调接口
  • ctx:传递上下文参数

6.5 查看节点

// 同步方式
getData(String path, boolean b, Stat stat)
// 异步方式
getData(String path, boolean b, AsyncCallback.DataCallback callBack,
Object ctx)
  • path:znode路径。
  • b:是否使用连接对象中注册的监视器。
  • stat :返回znode的元数据。
  • callBack:异步回调接口
  • ctx:传递上下文参数

七、Zookeeper事件监听机制

7.1 watcher概念

zookeeper提供了数据的发布/订阅功能,多个订阅者可同时监听某一特定主题对象,当该主题对象的自身状态发生变化时(例如节点内容改变、节点下的子节点列表改变等),会实时、主动通知所有订阅者

zookeeper采用了Watcher机制实现数据的发布/订阅功能。该机制在被订阅对象发生变化时会异步通知客户端,因此客户端不必在Watcher注册后轮询阻塞,从而减轻了客户端压力。

7.2 watcher架构

两万字长文全面理解Zookeeper_第4张图片

  1. 客户端首先将Watcher注册到服务端,同时将Watcher对象保存到客户端的Watch管理器中
  2. 当ZooKeeper服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑,从而完成整体的数据发布/订阅流程。

Watcher实现由三个部分组成:

  • Zookeeper服务端
  • Zookeeper客户端
  • 客户端的ZKWatchManager对象

7.3 watcher特性

特性 说明
一次性 watcher是一次性的,一旦被触发就会移除,再次使用时需要重新注册
客户端顺 序回 调 watcher回调是顺序串行化执行的,只有回调后客户端才能看到最新的数据状态。一个watcher回调逻辑不应该太多,以免影响别的watcher执行
轻量级 WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径,并不会告诉数据节点变化前后的具体内容;
时效性 watcher只有在当前session彻底失效时才会无效,若在session有效期内快速重连成功,则watcher依然存在,仍可接收到通知;

7.4 watcher接口

  • Watcher通知状态(KeeperState)
枚举属性 说明
SyncConnected 客户端与服务器正常连接时
Disconnected 客户端与服务器断开连接时
Expired 会话session失效时
AuthFailed 身份认证失败时
  • Watcher事件类型(EventType)

EventType是数据节点(znode)发生变化时对应的通知类型。EventType变化时KeeperState永远处于SyncConnected通知状态下;当KeeperState发生变化时,EventType永远为None。其路径为org.apache.zookeeper.Watcher.Event.EventType,是一个枚举类,枚举属性如下:

枚举属性 说明
None
NodeCreated Watcher监听的数据节点被创建时
NodeDeleted Watcher监听的数据节点被删除时
NodeDataChanged Watcher监听的数据节点内容发生变更时(无论内容数据 是否变化)
NodeChildrenChanged Watcher监听的数据节点的子节点列表发生变更时

等会儿通过案例来使用这些接口

八、一些案例

8.1 配置中心案例

工作中有这样的一个场景: 数据库用户名和密码信息放在一个配置文件中,应用读取该配置文件,配置文件信息放入缓存。若数据库的用户名和密码改变时候,还需要重新加载缓存,比较麻烦,通过ZooKeeper可以轻松完成,当数据库发生变化时自动完成缓存同步。

public class MyConfigCenter {
    private static String IP = "192.168.18.102:2181";
    private CountDownLatch count = new CountDownLatch(1);
    private ZooKeeper zooKeeper;
    private static MyConfig config = new MyConfig();

    public MyConfigCenter(){
        this.getValue();
    }

    class MyWatch implements Watcher {

        public void process(WatchedEvent watchedEvent) {
            if(watchedEvent.getType() == Event.EventType.None){
                if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
                    System.out.println("连接成功");
                    count.countDown();
                }else if (watchedEvent.getState() == Event.KeeperState.Disconnected){
                    System.out.println("连接断开!");
                } else if (watchedEvent.getState() == Event.KeeperState.Expired){
                    System.out.println("连接超时!自动重新连接。。。");
                    try {
                        zooKeeper = new ZooKeeper(IP,5000,this);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else if (watchedEvent.getState() == Event.KeeperState.AuthFailed){
                    System.out.println("验证失败!");
                }
                //监听节点发生变化
            }else if (watchedEvent.getType() == Event.EventType.NodeDataChanged) {
                getValue();
            }
        }
    }
    public void getValue(){
        try {
            //判断zookeeper是否为空值,防止重复创建
            if(zooKeeper == null) {
                zooKeeper = new ZooKeeper(IP, 5000, new MyWatch());
				count.await();
            }
            config.setUsername(new String(zooKeeper.getData("/config/username", new MyWatch(), null)));
            config.setPassword(new String(zooKeeper.getData("/config/password", new MyWatch(), null)));
            config.setUrl(new String(zooKeeper.getData("/config/url", new MyWatch(), null)));
           
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyConfigCenter center = new MyConfigCenter();
        while (true){
            //休眠3秒
            try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
            System.out.println(config);
        }
    }
}
public class MyConfig {
    private String url;
    private String username;
    private String password;
}

测试:

两万字长文全面理解Zookeeper_第5张图片

当节点的配置信息发生变化时

两万字长文全面理解Zookeeper_第6张图片

8.2 生成分布式唯一ID

在过去的单库单表型系统中,通常可以使用数据库字段自带的auto_increment属性来自动为每条记录生成一个唯一的ID。但是分库分表后,就无法在依靠数据库的auto_increment属性来唯一标识一条记录了。此时我们就可以用zookeeper在分布式环境下生成全局唯一ID。

public class GloballyUniqueId {
    private String IP = "192.168.18.102:2181";
    private CountDownLatch count = new CountDownLatch(1);
    private ZooKeeper zooKeeper;
    private String defaultPath = "/uniqueId";
    public GloballyUniqueId(){
        if (zooKeeper == null) {
            try {
                zooKeeper  = new ZooKeeper(IP, 5000, new MyWatch());
                count.await();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
    class MyWatch implements Watcher {

        public void process(WatchedEvent watchedEvent) {
            if (watchedEvent.getType() == Event.EventType.None) {
                if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
                    System.out.println("连接成功");
                    count.countDown();
                } else if (watchedEvent.getState() == Event.KeeperState.Disconnected) {
                    System.out.println("连接断开!");
                } else if (watchedEvent.getState() == Event.KeeperState.Expired) {
                    System.out.println("连接超时!自动重新连接。。。");
                    try {
                        zooKeeper = new ZooKeeper(IP, 5000, this);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else if (watchedEvent.getState() == Event.KeeperState.AuthFailed) {
                    System.out.println("验证失败!");
                }
            }
        }


    }

    public String getId() {
        String path = "";
        try {

            //创建临时节点
            path = zooKeeper.create(defaultPath, new byte[0],
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

        } catch (Exception e) {
            e.printStackTrace();
        }
        //System.out.println(path);
       //   /uniqueId0000000015
        return path.substring(9);
    }

    public static void main(String[] args) {
        GloballyUniqueId Id = new GloballyUniqueId();
        while (true){
            try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
            System.out.println(Id.getId());

        }

    }
}

两万字长文全面理解Zookeeper_第7张图片

8.3 分布式锁

设计思路:

  1. 每个客户端往/Locks下创建临时有序节点/Locks/Lock000000001
  2. 客户端取得/Locks下子节点,并进行排序,判断排在最前面的是否为自己,如果自己的锁节点在第一位,代表获取锁成功
  3. 如果自己的锁节点不在第一位,则监听自己前一位的锁节点。例如,自己锁节点Lock 000000001
  4. 当前一位锁节点(Lock000000002)的逻辑
  5. 监听客户端重新执行第2步逻辑,判断自己是否获得了锁

多线程中的锁可以有sync锁,这里我采用的是自旋的方式

public class MyLock {

    private String IP = "192.168.18.102:2181";
    private CountDownLatch count = new CountDownLatch(1);
    private ZooKeeper zooKeeper;
    private String lockPath = "/Locks4";
    private String lockName = "Lock_";
    private String path = "";
    private boolean flag = false;
    class MyWatch implements Watcher {
        public void process(WatchedEvent watchedEvent) {
            if (watchedEvent.getType() == Event.EventType.None) {
                if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
                    System.out.println("连接成功");
                    count.countDown();
                }else if (watchedEvent.getState() == Event.KeeperState.Disconnected) {
                    System.out.println("连接断开!");
                } else if (watchedEvent.getState() == Event.KeeperState.Expired) {
                    System.out.println("连接超时!自动重新连接。。。");
                    try {
                        zooKeeper = new ZooKeeper(IP, 5000, this);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else if (watchedEvent.getState() == Event.KeeperState.AuthFailed) {
                    System.out.println("验证失败!");
                }
            }
        }
    }
    public MyLock(){
        if (zooKeeper == null) {
            try {
                zooKeeper  = new ZooKeeper(IP, 5000,new MyWatch());
                count.await();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
    public void lock(){
        createNode();
        attempLock();
    }

    public void createNode(){
        try {
            Stat stat = zooKeeper.exists(lockPath, null);
            if(stat == null){
                zooKeeper.create(lockPath, new byte[0],
                        ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
            //创建临时有序子节点
            path = zooKeeper.create(lockPath + "/" + lockName, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            System.out.println("节点创建成功" + path);
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    Watcher watcher = new Watcher() {
        public void process(WatchedEvent watchedEvent) {
            //监听到删除节点
            if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
//                synchronized (this) {
//                    notifyAll();
//                }
                flag = true;
            }
        }
    };

    public void attempLock(){

        try {
            List<String> list = zooKeeper.getChildren(lockPath, false);
            Collections.sort(list);
            // /Locks/Lock_000000001
            //判断自己创建的这个节点是否是子节点中的第一个节点
            // 获取 Lock_000000001
            int i = list.indexOf(path.substring(lockPath.length() + 1));
            //说明是第一个进来的
            if(i == 0){
                System.out.println("获取锁成功!");
                return;
            }else{
                String prePath = list.get(i - 1);
                Stat stat = zooKeeper.exists(lockPath + "/" + prePath, watcher);
                if(stat == null){
                    attempLock();
                }else{
//                    synchronized (watcher){
//                        watcher.wait();
//                    }
                    while(flag){}
                    attempLock();
                }

            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void unlock(){
        try {
            zooKeeper.delete(path, -1);
            flag = false;
            System.out.println("锁已经释放" + path);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }
}

九、一致性协议zab

ZAB 协议是 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。通过该协议,Zookeepe 基于
主从模式的系统架构来保持集群中各个副本之间数据的一致性。具体如下:

Zookeeper 使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用原子广播协议将数据状
态的变更以事务 Proposal 的形式广播到所有的副本进程上去。如下图

两万字长文全面理解Zookeeper_第8张图片

ZAB 协议包括两种基本的模式,分别是崩溃恢复和消息广播:

  1. 崩溃恢复
    当整个服务框架在启动过程中,或者当 Leader 服务器出现异常时,ZAB 协议就会进入恢复模式,通过过半选举机制产生新的 Leader,之后其他机器将从新的 Leader 上同步状态,当有过半机器完成状态同步后,就退出恢复模式,进入消息广播模式。
  2. 消息广播
    ZAB 协议的消息广播过程使用的是原子广播协议。在整个消息的广播过程中,Leader 服务器会每个事物请求生成对应的 Proposal,并为其分配一个全局唯一的递增的事务 ID(ZXID),之后再对其进行广播。具体过程如下:

Leader 服务会为每一个 Follower 服务器分配一个单独的队列,然后将事务 Proposal 依次放入队列中,并根据 FIFO(先进先出) 的策略进行消息发送。Follower 服务在接收到 Proposal 后,会将其以事务日志的形式写入本地磁盘中,并在写入成功后反馈给 Leader 一个 Ack 响应。当 Leader 接收到超过半数 Follower 的 Ack 响应后,就会广播一个 Commit 消息给所有的 Follower 以通知其进行事务提交,之后 Leader 自身也会完成对事务的提交。而每一个 Follower 则在接收到 Commit 消息后,完成事务的提交。

十、zookeeper的leader选举

4.1 服务器状态

  • looking:寻找leader状态。当服务器处于该状态时,它会认为当前集群中没有

  • leader,因此需要进入leader选举状态。

  • leading: 领导者状态。表明当前服务器角色是leader。

  • following: 跟随者状态。表明当前服务器角色是follower。

  • observing:观察者状态。表明当前服务器角色是observer。

    成为leader。

4.2 服务器启动时期的leader选举

集群初始化阶段,当有一台服务器server1启动时,其单独无法进行和完成leader选举,当第二台服务器server2启动时,此时两台机器可以相互通信,每台机器都试图找到leader,于是进入leader选举过程。选举过程如下:

  1. 每个server发出一个投票。由于是初始情况,server1和server2都会将自己作为leader服务器来进行投票,每次投票会包含所推举的服务器的myid和zxid,使用(myid, zxid)来表示,此时server1的投票为(1, 0),server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。

  2. 集群中的每台服务器接收来自集群中各个服务器的投票。

  3. 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行pk,pk
    规则如下:

    1. 优先检查zxid。zxid比较大的服务器优先作为leader。
    2. 如果zxid相同,那么就比较myid。myid较大的服务器作为leader服务器。
    3. 对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的zxid,均为0,再比较myid,此时server2的myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
  4. 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于server1、server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了leader

  5. 改变服务器状态。一旦确定了leader,每个服务器就会更新自己的状态,如果是follower,那么就变更为following,如果是leader,就变更为leading。

4.3 服务器运行时期的Leader选举

在zookeeper运行期间,leader与非leader服务器各司其职,即便当有非leader服务器宕机或新加入,此时也不会影响leader,但是一旦leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮leader选举,其过程和启动时期的Leader选举过程基本一致。

你可能感兴趣的:(大数据,zookeeper)