zookeeper教程

Posted zhouyanger

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了zookeeper教程相关的知识,希望对你有一定的参考价值。

Zookeeper

一.概述

Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。

  1. 工作机制

 

2.Zookeeper的基本概念

2.1zookeeper数据模型是一种分层的树形结构,和linux的文件系统结构类似,

 

1) zookeeper集群中的角色

Leader: 为客户端提供事务性操作,读写服务;

Follower: 为客户端提供读服务,非事务性操作,客户端到Follower的写请求会转交给Leader角色,Follower会参与Leader的选举和投票;

Observer:为客户端提供读服务,非事务性操作,不参与Leader的选举过程,一般是为了增强zookeeper集群的读请求并发能力;

2)会话(Session)

session是客户端与zookeeper服务端之间建立的长链接;

zookeeper在一个会话中进行心跳检测来感知客户端链接的存活;

zookeeper客户端在一个会话中接收来自服务端的watch事件通知;

zookeeper可以给会话设置超时时间;

3)zookeeper的数据节点(ZNode)

Znode是zookeeper树形结构中的数据节点,用于存储数据;

Znode分为持久节点和临时节点两种类型:

持久节点:一旦创建,除非主动调用删除操作,否则一直存储在zookeeper上;

临时节点:与客户端回话绑定,一旦客户端失效,这个客户端创建的所有临时节点都会被删除;

顺序持久节点:带编码的持久节点

顺序临时节点:带编码的临时节点

4)zookeeper中的watcher

watcher监听在Znode节点上;

当节点的数据更新或子节点的增删状态发生变化都会使客户端的watcher得到通知;

5)zookeeper中的ACL(访问控制)

类似于Linux/Unix下的权限控制,有以下几种访问控制权限:

CREATE:创建子节点的权限;

READ:获取节点数据和子节点列表的权限;

WRITE:更新节点数据的权限;

DELETE: 删除子节点的权限;

ADMIN:设置节点ACL的权限;

4)Zookeeper的特点。

l  集群只要有半数以上节点存活,集群就可用,所以一般集群节点为奇数

l  全局数据一致性:每个节点数据都相同,client连接任意一个节点都可以

l  更新请求顺序进行,来自同一个client的更新按顺序进行

l  数据更新原子性,要么成功,要么失败

二.Zookeeper的安装

 2.1 单机模式安装

1.安装前准备

(1)安装Jdk

(2)拷贝Zookeeper安装包到Linux系统下/usr/local/

(3)解压到指定目录

[root@localhost local]# tar -zxvf zookeeper-3.4.14.tar.gz

2.配置修改

(1)将/usr/local/zookeeper-3.4.14/conf这个路径下的zoo_sample.cfg修改为zoo.cfg;

[root@localhost conf]# mv zoo_sample.cfg zoo.cfg

(2)在/usr/local/zookeeper-3.4.14/这个目录上创建zkData文件夹,打开zoo.cfg文件,修改dataDir路径:

[root@localhost conf]# vim zoo.cfg

dataDir=/usr/local/zookeeper-3.4.14/zkData

3.操作Zookeeper基本命令

(1)启动Zookeeper

[root@localhost zookeeper-3.4.14]# ./bin/zkServer.sh start

(2)查看进程是否启动

[root@localhost zookeeper-3.4.14]# jps

4020 Jps

4001 QuorumPeerMain

(3)查看状态:

[root@localhost zookeeper-3.4.14]# ./bin/zkServer.sh status

(4)启动客户端:

[root@localhost zookeeper-3.4.14]# bin/zkCli.sh

(5)ls path [watch]   列出指定节点下的所有子节点,可选择监听子节点变化

 

(6)create [-s] [-e] path data acl

 创建节点,-s代表创建的节点具有顺序,-e选项代表创建的是临时节点,默认情况下创建的是持久节点,path为节点的全路径,data为创建节点中的数据,acl用来进行权限控制,

(7)get path [watch] 获取path节点的数据内容和属性,watch监听节点变化

(8)set path data  设置节点数据

(9)delete path   删除节点

(9)rmr path   递归删除节点

(11)退出客户端:

[zk: localhost:2181(CONNECTED) 0]  quit

(12)停止Zookeeper

[root@localhost zookeeper-3.4.14]# bin/zkServer.sh stop

2.2 配置参数解读

Zookeeper中的配置文件zoo.cfg中参数含义解读如下:

1.tickTime =2000:通信心跳数,Zookeeper服务器与客户端心跳时间,单位毫秒

Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。

它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2*tickTime)

2.initLimit =10:LF初始通信时限

集群中的Follower跟随者服务器与Leader领导者服务器之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器连接到Leader的时限。

3.syncLimit =5:LF同步通信时限

集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除Follwer。

4.dataDir:数据文件目录+数据持久化路径

主要用于保存Zookeeper中的数据。

5.clientPort =2181:客户端连接端口

监听客户端连接的端口。

2.3 集群环境的搭建

 伪集群模式,即一台机器上启动三个zookeeper实例组成集群,真正的集群模式无非就是实例IP地址不同,搭建方法没有区别

1.下载解压zookeeper,方法和上面一样

2.重命名 zoo_sample.cfg文件

[root@localhost conf]# cp zoo.cfg zoo-1.cfg

3.修改配置文件zoo-1.cfg,原配置文件里有的,修改成下面的值,没有的则加上(1)[root@localhost conf]# vim zoo-1.cfg dataDir=/usr/local/zookeeper-3.4.14/zkData/zk01

clientPort=2181

server.1=192.168.0.101:2887:3887

server.2=192.168.0.101:2888:3888

server.3=192.168.0.101:2889:3889

解读

server.A=B:C:D。

A是一个数字,表示这个是第几号服务器;

集群模式下配置一个文件myid,这个文件在dataDir目录下,这个文件里面有一个数据就是A的值,Zookeeper启动时读取此文件,拿到里面的数据与zoo.cfg里面的配置信息比较从而判断到底是哪个server。

B是这个服务器的地址;

C是这个服务器Follower与集群中的Leader服务器交换信息的端口;

D是万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选出一个新的Leader,而这个端口就是用来执行选举时服务器相互通信的端口。

 

4.复制配置,只需修改zoo-2.cfg和zoo-3.cfg的ataDir和clientPort不同即可

 [root@localhost conf]# cp zoo-1.cfg zoo-2.cfg

[root@localhost conf]# cp zoo-1.cfg zoo-3.cfg

 

dataDir=/usr/local/zookeeper-3.4.14/zkData/zk02

clientPort=2182

dataDir=/usr/local/zookeeper-3.4.14/zkData/zk03

clientPort=2183

 

 

5. 标识Server ID

zkData下创建三个文件夹zk01,zk02,zk03,在每个目录中创建文件myid 文件,写入当前实例的server id,即1.2.3

[root@localhost zkData]# mkdir zk01

[root@localhost zkData]# mkdir zk02

[root@localhost zkData]# mkdir zk03

[root@localhost zkData]# touch ./zk01/myid

[root@localhost zkData]# touch ./zk02/myid

[root@localhost zkData]# touch ./zk03/myid

[root@localhost zkData]# vim ./zk01/myid  写入1

[root@localhost zkData]# vim ./zk02/myid  写入2

[root@localhost zkData]# vim ./zk03/myid  写入3

5.启动三个zookeeper实例

[root@localhost zookeeper-3.4.14]# bin/zkServer.sh conf/zoo-1.cfg

[root@localhost zookeeper-3.4.14]# bin/zkServer.sh conf/zoo-2.cfg

[root@localhost zookeeper-3.4.14]# bin/zkServer.sh conf/zoo-3.cfg

(1)分别启动Zookeeper

[atguigu@hadoop102 zookeeper-3.4.10]$ bin/zkServer.sh start

[atguigu@hadoop103 zookeeper-3.4.10]$ bin/zkServer.sh start

[atguigu@hadoop104 zookeeper-3.4.10]$ bin/zkServer.sh start

(2)启动客户端 zkCli.sh  -server ip:port  连接到集群中的那个机器。

 

(3)查看状态

[root@localhost zookeeper-3.4.14]# bin/zkServer.sh status conf/zoo-1.cfg

ZooKeeper JMX enabled by default

Using config: conf/zoo-1.cfg

Mode: follower

[root@localhost zookeeper-3.4.14]# bin/zkServer.sh status conf/zoo-2.cfg

ZooKeeper JMX enabled by default

Using config: conf/zoo-2.cfg

Mode: leader

[root@localhost zookeeper-3.4.14]# bin/zkServer.sh status conf/zoo-3.cfg

ZooKeeper JMX enabled by default

Using config: conf/zoo-3.cfg

Mode: follower

6》节点的ACL控制

6.1 ACL全称是Access Control List,访问控制列表,zookeeper中ACL由三部分组成,即Scheme:id:permission,其中

scheme是验证过程中使用的检验策略

id是权限被赋予的对象,比如ip或某个用户

permission为可以操作的权限,有5个值:crdwa,分别表示create、read、delete、write、admin,admin是修改权限

setAcl path acl命令可以设置节点的访问权限,path是节点路径,acl是要设置的权限(crdwa)。通过getAcl path获取权限

6.2 权限检验策略即scheme有五种类型:worldauthdigestIPsuper

      world检验策略

ACL格式:world:anyone:permission

默认的检测策略为world则id固定位anyone,如果permission为crdwa则ACL为world:anyone:crdwa

/ 创建新节点的ACL默认为:world:anyone:crdwa

[zk: localhost:2181(CONNECTED) 43] create -e /acltest acltestdata

Created /acltest

[zk: localhost:2181(CONNECTED) 44] getAcl /acltest

‘world,‘anyone

: cdrwa

// 设置为ca权限

[zk: localhost:2181(CONNECTED) 46] setAcl /acltest world:anyone:ca

cZxid = 0x6e

ctime = Fri Sep 14 08:13:07 PDT 2018

mZxid = 0x6e

mtime = Fri Sep 14 08:13:07 PDT 2018

pZxid = 0x6e

cversion = 0

dataVersion = 0

aclVersion = 1

ephemeralOwner = 0x100063d34d50005

dataLength = 11

numChildren = 0

[zk: localhost:2181(CONNECTED) 47] getAcl /acltest

‘world,‘anyone

: ca

// 无法再读取数据

[zk: localhost:2181(CONNECTED) 48] get /acltest  

Authentication is not valid : /acltest

 

auth检验策略

ACL格式:auth:id:permission

 

比如auth:username:password:crdwa,auth检验策略表示给认证通过的所有用户设置acl权限。

 

可以通过addauth digest <username>:<password>命令添加用户。

 

如果通过addauth创建多组用户和密码,当使用setAcl修改权限时,所有的用户和密码的权限都会跟着修改,通过addauth新创建的用户和密码组需要重新调用setAcl才会加入到权限组当中去。

 

示例:

 

==================客户端1操作===========================

[zk: localhost:2181(CONNECTED) 66] create -e /acltest acltestdata  // 创建测试的临时节点

Created /acltest

[zk: localhost:2181(CONNECTED) 67] getAcl /acltest

‘world,‘anyone

: cdrwa

[zk: localhost:2181(CONNECTED) 68] addauth digest acluser1:111111  // 添加acluser1用户

[zk: localhost:2181(CONNECTED) 69] addauth digest acluser2:222222  // 添加acluser2用户

// 设置acl权限,此处的用户名和密码并非一定要是上面创建的两个

// 用户名和密码,任何用户名和密码都可以,甚至不存在的用户名和

// 密码都可以,设置完后当前会话中所有用户对/acltest节点都具

// 有crdwa权限

[zk: localhost:2181(CONNECTED) 70] setAcl /acltest auth:acluser1:111111:crdwa 

cZxid = 0x76

ctime = Fri Sep 14 09:13:12 PDT 2018

mZxid = 0x76

mtime = Fri Sep 14 09:13:12 PDT 2018

pZxid = 0x76

cversion = 0

dataVersion = 0

aclVersion = 1

ephemeralOwner = 0x100063d34d50007

dataLength = 11

numChildren = 0

[zk: localhost:2181(CONNECTED) 71] get /acltest

acltestdata

cZxid = 0x76

ctime = Fri Sep 14 09:13:12 PDT 2018

mZxid = 0x76

mtime = Fri Sep 14 09:13:12 PDT 2018

pZxid = 0x76

cversion = 0

dataVersion = 0

aclVersion = 1

ephemeralOwner = 0x100063d34d50007

dataLength = 11

numChildren = 0

 

=====================客户端2的操作====================

// 此时在另一个客户端中get /acltest会发现没有权限

[zk: localhost:2181(CONNECTED) 7] ls /

[acltest, zookeeper]

[zk: localhost:2181(CONNECTED) 8] get /acltest

Authentication is not valid : /acltest

 

=====================回到客户端1操作=====================

// 添加一个新的用户acluser3

[zk: localhost:2181(CONNECTED) 72] addauth digest acluser3:333333

 

=====================回到客户端2的操作====================

// 添加用户acluser3,发现还是没有访问/acltest的权限

[zk: localhost:2181(CONNECTED) 9] addauth digest acluser3:333333

[zk: localhost:2181(CONNECTED) 10] get /acltest

Authentication is not valid : /acltest

 

=====================回到客户端1操作=====================

// 重新设置/acltest权限

[zk: localhost:2181(CONNECTED) 73] setAcl /acltest auth:acluser1:111111:crdwa

cZxid = 0x76

ctime = Fri Sep 14 09:13:12 PDT 2018

mZxid = 0x76

mtime = Fri Sep 14 09:13:12 PDT 2018

pZxid = 0x76

cversion = 0

dataVersion = 0

aclVersion = 2  // aclVersion发生了变化

ephemeralOwner = 0x100063d34d50007

dataLength = 11

numChildren = 0

// 重新设置acl后三个用户均具有了访问/acltest节点的权限

[zk: localhost:2181(CONNECTED) 75] getAcl /acltest 

‘digest,‘acluser1:hHUVzmra9P/TbXlP/4jRhG9jZm8=

: cdrwa

‘digest,‘acluser2:s097oNh4MU8FGvmhrQLPAUjmIs4=

: cdrwa

‘digest,‘acluser3:ML31yxAbaJNcHmBdnsIvVnUy+AI=

: cdrwa

 

=====================回到客户端2的操作====================

// 此时发现acluser3已经能够访问/acltest节点了

[zk: localhost:2181(CONNECTED) 12] get /acltest

acltestdata

cZxid = 0x76

ctime = Fri Sep 14 09:13:12 PDT 2018

mZxid = 0x76

mtime = Fri Sep 14 09:13:12 PDT 2018

pZxid = 0x76

cversion = 0

dataVersion = 0

aclVersion = 2

ephemeralOwner = 0x100063d34d50007

dataLength = 11

numChildren = 0

总结起来就是:auth检验策略下setAcl会设置当前会话所有拥有的所有用户访问节点的权限,当前回话先添加的用户需要重新设置节点的ACL权限才会把新用户对节点的操作权限加上去。

 

digest检验策略

ACL格式:digest:id:permission

 

digest和auth类似,只不过digest格式中的id需要使用sha1进行加密,zookeeper已经为我们提供了相关加密的类,如下所示为对id进行加密的代码:

 

public class DigestTest {

    public static void main(String[] args) {

        try {

            System.out.println(DigestAuthenticationProvider.generateDigest("acluser1:111111"));

        } catch (Exception ex) {

            ex.printStackTrace();

        }

    }

}

示例:

 

=========================客户端1上的操作======================

[zk: localhost:2181(CONNECTED) 86] create /acltest acltestdata  // 添加测试节点

Node already exists: /acltest

// 添加三个用户

[zk: localhost:2181(CONNECTED) 87] addauth digest acluser1:111111

[zk: localhost:2181(CONNECTED) 88] addauth digest acluser2:222222

[zk: localhost:2181(CONNECTED) 89] addauth digest acluser3:333333

// 设置digest类型的acl权限

[zk: localhost:2181(CONNECTED) 90] setAcl /acltest digest:acluser1:hHUVzmra9P/TbXlP/4jRhG9jZm8=:crdwa

cZxid = 0x7c

ctime = Fri Sep 14 18:42:17 PDT 2018

mZxid = 0x7c

mtime = Fri Sep 14 18:42:17 PDT 2018

pZxid = 0x7c

cversion = 0

dataVersion = 0

aclVersion = 1

ephemeralOwner = 0x0

dataLength = 11

numChildren = 0

// 发现只有acluser1用户具有对/acltest节点的crdwa权限

[zk: localhost:2181(CONNECTED) 91] getAcl /acltest

‘digest,‘acluser1:hHUVzmra9P/TbXlP/4jRhG9jZm8=

: cdrwa

 

======================客户端2上的操作=======================

// 只添加acluser2用户,发现没有权限读取/acltest节点

[zk: localhost:2181(CONNECTED) 14] addauth digest acluser2:222222

[zk: localhost:2181(CONNECTED) 15] get /acltest

Authentication is not valid : /acltest

// 添加acluser1后,能够读取/acltest节点

[zk: localhost:2181(CONNECTED) 16] addauth digest acluser1:111111

[zk: localhost:2181(CONNECTED) 17] get /acltest                 

acltestdata

cZxid = 0x7c

ctime = Fri Sep 14 18:42:17 PDT 2018

mZxid = 0x7c

mtime = Fri Sep 14 18:42:17 PDT 2018

pZxid = 0x7c

cversion = 0

dataVersion = 0

aclVersion = 1

ephemeralOwner = 0x0

dataLength = 11

numChildren = 0

小结:调用setAcl时不像auth那样会设置当前会话中所有用户访问节点的权限,只会设置指定的单个用户对节点的访问权限;setAcl设置中id需要使用sha1进行加密。

 

IP检验策略

ACL格式:ip:id:permission

 

id是ip地址,指定某个ip地址可以访问。

 

示例:

 

[zk: localhost:2181(CONNECTED) 96] create -e /acltest acltestdata

Created /acltest

// 设置只有192.168.1.1这台机器可以访问

[zk: localhost:2181(CONNECTED) 97] setAcl /acltest ip:192.168.1.1:crdwa

cZxid = 0x85

ctime = Fri Sep 14 18:59:23 PDT 2018

mZxid = 0x85

mtime = Fri Sep 14 18:59:23 PDT 2018

pZxid = 0x85

cversion = 0

dataVersion = 0

aclVersion = 1

ephemeralOwner = 0x100063d34d5000b

dataLength = 11

numChildren = 0

// 本机地址为127.0.0.1,无法访问

[zk: localhost:2181(CONNECTED) 98] get /acltest

Authentication is not valid : /acltest

super检验策略

super检验策略主要供运维人员维护节点使用,acl中指定的用户具有任何操作任何节点的权限,启动时需要在启动脚本配置如下参数:

 

-Dzookeeper.DigestAuthenticationProvider.superDigest=admin:015uTByzA4zSglcmseJsxTo7n3c=

1

因为我喜欢在本地测试时前台启动zookeeper,因此我在zkServer.sh的如下位置加上该配置:

 

start-foreground)

    ZOO_CMD=(exec "$JAVA")

    if [ "${ZOO_NOEXEC}" != "" ]; then

      ZOO_CMD=("$JAVA")

    fi

    "${ZOO_CMD[@]}" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}"

    "-Dzookeeper.DigestAuthenticationProvider.superDigest=admin:015uTByzA4zSglcmseJsxTo7n3c="

    -cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG"

1

2

3

4

5

6

7

8

测试示例,重新启动zookeeper服务:

 

=========================客户端1上操作=======================

[zk: localhost:2181(CONNECTED) 104] create -e /acltest acltestdata

Created /acltest

[zk: localhost:2181(CONNECTED) 105] addauth digest acluser1:111111

[zk: localhost:2181(CONNECTED) 107] setAcl /acltest digest:acluser1:hHUVzmra9P/TbXlP/4jRhG9jZm8=:crdwa    

cZxid = 0x8a

ctime = Fri Sep 14 19:17:37 PDT 2018

mZxid = 0x8a

mtime = Fri Sep 14 19:17:37 PDT 2018

pZxid = 0x8a

cversion = 0

dataVersion = 0

aclVersion = 1

ephemeralOwner = 0x10006e2720e0000

dataLength = 11

numChildren = 0

[zk: localhost:2181(CONNECTED) 108] getAcl /acltest

‘digest,‘acluser1:hHUVzmra9P/TbXlP/4jRhG9jZm8=

: cdrwa

 

========================客户端2上操作=========================

[zk: localhost:2181(CONNECTED) 20] ls /

[acltest, zookeeper]

[zk: localhost:2181(CONNECTED) 21] get /acltest

Authentication is not valid : /acltest

// 添加启动脚本配置的super用户

[zk: localhost:2181(CONNECTED) 22] addauth digest admin:111111

// 可以访问到节点

[zk: localhost:2181(CONNECTED) 23] get /acltest              

acltestdata

cZxid = 0x8a

ctime = Fri Sep 14 19:17:37 PDT 2018

mZxid = 0x8a

mtime = Fri Sep 14 19:17:37 PDT 2018

pZxid = 0x8a

cversion = 0

dataVersion = 0

aclVersion = 1

ephemeralOwner = 0x10006e2720e0000

dataLength = 11

numChildren = 0

三 zookeeper集群的选举

Zookeeper集群的选举是通过ZAB(Zookeeper Atomic Broadcast)协议,即Zookeeper原子消息广播协议算法实现的,zab是paxos算法实现的一种。

Paxos算法概要

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

分布式系统中的节点通信存在两种模型:共享内存(Shared memory)和消息传递(Messages passing)。基于消息传递通信模型的分布式系统,不可避免的会发生以下错误:进程可能会慢、被杀死或者重启,消息可能会延迟、丢失、重复,在基础 Paxos 场景中,先不考虑可能出现消息篡改即拜占庭错误的情况。Paxos 算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。

 

Paxos算法类似于两阶段提提交,其算法执行过程分为两个阶段。具体如下:

  1.  阶段一(prepare阶段):
    (a) Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求。Pareper(N)
    (b) 如果一个Acceptor收到一个编号为N的Prepare请求,如果小于它已经响应过的请求,则拒绝,不回应或回复error。若N大于该Acceptor已经响应过的所有Prepare请求的编号(maxN),那么它就会将它已经接受过(已经经过第二阶段accept的提案)的编号最大的提案(如果有的话,如果还没有的accept提案的话返回{pok,null,null})作为响应反馈给Proposer,同时该Acceptor承诺不再接受任何编号小于等于N的Pareper提案(阶段一)。承诺三:不再接受Proposal ID小于(注意:这里是< )当前请求的Propose请求(阶段二的accept)。
  •  
  •  阶段二(accept阶段):
    (a) 如果一个Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它就会发送一个针对[N,V]提案的Accept请求给半数以上的Acceptor。注意:V就是收到的响应中编号最大的提案的value(某个acceptor响应的它已经通过的{acceptN,acceptV}),如果响应中不包含任何提案,那么V就由Proposer自己决定。
    (b) 如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Acceptor没有对编号大于N的Prepare请求做出过响应,它就接受该提案。如果N小于Acceptor以及响应的prepare请求,则拒绝,不回应或回复error(当proposer没有收到过半的回应,那么他会重新进入第一阶段,递增提案号,重新提出prepare请求)。
    在上面的运行过程中,每一个Proposer都有可能会产生多个提案。但只要每个Proposer都遵循如上述算法运行,就一定能保证算法执行的正确性。
  • 具体实例理解:
    问题背景:假设我们有下图的系统,想要在server1,server2,server3选一个master。

    prepare阶段
     1. 每个server向proposer发送消息,表示自己要当leader,假设proposer收到消息的时间不一样,顺序是: proposer2 -> proposer1 -> proposer3,消息编号依次为1、2、3。
      紧接着,proposer将消息发给acceptor中超过半数的子成员(这里选择两个),如图所示,proposer2向acceptor2和acceptor3发送编号为1的消息,proposer1向acceptor1和accepto2发送编号为2的消息,proposer3向acceptor2和acceptor3发送编号为3的消息。
  1. 假设这时proposer1发送的消息先到达acceptor1和acceptor2,它们都没有接收过请求,所以接收该请求并返回【pok,null,null】给proposer1,同时acceptor1和acceptor2承诺不再接受编号小于2的请求;
      紧接着,proposer2的消息到达acceptor2和acceptor3,acceptor3没有接受过请求,所以返回proposer2 【pok,null,null】,acceptor3并承诺不再接受编号小于1的消息。而acceptor2已经接受proposer1的请求并承诺不再接收编号小于2的请求,所以acceptor2拒绝proposer2的请求;
      最后,proposer3的消息到达acceptor2和acceptor3,它们都接受过提议,但编号3的消息大于acceptor2已接受的2和acceptor3已接受的1,所以他们都接受该提议,并返回proposer3 【pok,null,null】;
      此时,proposer2没有收到过半的回复,所以重新取得编号4,并发送给acceptor2和acceptor3,此时编号4大于它们已接受的提案编号3,所以接受该提案,并返回proposer2 【pok,null,null】。

accept阶段
1
  Proposer3收到半数以上(两个)的回复,并且返回的value为null,所以,proposer3提交了【3,server3】的提案。
  Proposer1也收到过半回复,返回的value为null,所以proposer1提交了【2,server1】的提案。
  Proposer2也收到过半回复,返回的value为null,所以proposer2提交了【4,server2】的提案。
(这里要注意,并不是所有的proposer都达到过半了才进行第二阶段,这里只是一种特殊情况)
2
  Acceptor1和acceptor2接收到proposer1的提案【2,server1】,acceptor1通过该请求,acceptor2承诺不再接受编号小于4的提案,所以拒绝;
  Acceptor2和acceptor3接收到proposer2的提案【4,server2】,都通过该提案;
  Acceptor2和acceptor3接收到proposer3的提案【3,server3】,它们都承诺不再接受编号小于4的提案,所以都拒绝。
所以proposer1和proposer3会再次进入第一阶段,但这时候 Acceptor2和acceptor3已经通过了提案(AcceptN = 4,AcceptV=server2),并达成了多数,所以proposer会递增提案编号,并最终改变其值为server2。最后所有的proposer都肯定会达成一致,这就迅速的达成了一致。
  此时,过半的acceptor(acceptor2和acceptor3)都接受了提案【4,server2】,learner感知到提案的通过,learner开始学习提案,所以server2成为最终的leader。

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

 

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

3.2 zookeeper的选举过程

Zookeeper集群中节点有如下三种角色:

 

1)Leader:事务请求的唯一调度和处理者,保证集群事务处理的顺序性,同时也是集群内部个服务器的调度者;

Follower:处理客户端的非事务请求,转发事务请求给Leader服务器,参与事务请求Proposal的投票,参与Leader选举投票;

Observer:处理客户端非事务请求,转发事务请求给Leader服务器,不参与任何形式的投票,包括选举和事务投票(超过半数确认),此角色存在通常是为了提高读性能;

 

2)Zookeeper集群中节点存在如下几种状态:

LOOKING:寻找Leader的状态,当服务器处于此状态时,表示当前没有Leader,需要进入选举流程;

FOLLOWING:跟随者状态,表明当前服务器角色是Follower;

OBSERVING:观察者状态,表明当前服务器是Observer;

LEADING:领导者状态,表明当前服务器角色为Leader;

 

3)影响节点称为Leader的因素,超过一半的选举就会成为leader

Zookeeper通过以下因素来判断一个节点能否称为Leader:

数据的新旧程度:只有最新的数据节点才有机会成为Leader,在Zookeeper中通过事物id(zxid)的大小来表示数据的新旧,越大代表数据越新;

myid:集群在启动的时候,会在数据目录下配置myid文件,里面的数字代表当前zk节点的编号,当zk节点数据一样新时,myid中数字越大的就会被选举成为Leader,当集群中已经有Leader时,新加入的节点不会影响原来的集群;

3)以一个简单的例子来说明整个选举的过程。

假设有五台服务器组成的Zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么,如图5-8所示。

 

图5-8 Zookeeper的选举机制

(1)服务器1启动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING;

(2)服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的ID比自己目前投票推举的(服务器1)大,更改选票为推举服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING

(3)服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;

(4)服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;

(5)服务器5启动,同4一样当小弟。

运次那个过程中Leader宕机后新Leader的选举

假设server2为主节点,并且server2宕机,剩下server1和server3进行Leader的选举。选举流程如下:

 

变更状态:Leader宕机后,其他节点的状态变为LOOKING;

生成投票信息:每个server发出一个投自己的票的投票,假定生成的投票信息为(myid, zxid)的形式,server1的投票信息为(1, 123),并将该投票信息发给server3,server3的投票信息为(3, 122),并将该投票信息发给server1;

投票处理:server3收到server1的投票信息(1, 123),发现该投票的zxid 123比server1自己投票信息中的zxid 122大,则server3修改自己的投票信息为(1, 123),然后发给server1

投票处理:server1收到server3的投票信息(3, 122),发现zxid 122比自己的投票信息中的zxid 123要小,则不改变自己的投票;

统计投票信息:server3统计收到的投票(包括自己投的),(1, 123)是两票,server1统计收到的投票(包括自己投的),(1, 123)是两票;

修改服务器状态:server3中选出的Leader是1,而自己是3,因此自己进入FOLLOWING状态,即follower角色,server1中选出的Leader是1,自己就是1,因此进入LEADING状态,即Leader角色;

当Leader选举完成后,Follower需要与新的Leader同步数据。进入数据同步阶段。

 

5.zookeeper的数据同步

Zookeeper数据同步

当Leader完成选举后,Follower需要与新的Leader同步数据。在Leader端需要做如下工作:

 

Leader告诉其他Follower当前最新数据是什么即zxid,Leader会构建一个NEWLEADER包,包括当前最大的zxid,发送给所有的Follower或者Observer;

Leader给每个Follower创建一个线程LearnerHandler来负责处理每个Follower的数据同步请求,同时主线程开始阻塞,只有超过一半的Follower同步完成,同步过程才完成,Leader才能成为真正的Leader;

Leader端根据同步算法进行同步操作;

而在Follower端会做如下工作:

 

选举完成后,尝试与Leader建立同步连接,如果一段时间没有连接上就报错超时,重新回到选举状态;

向Leader发送FOLLOWERINFO封包,带上自己最大的zxid;

根据同步算法进行同步操作;

具体使用哪种同步算法取决于Follower当前最大的zxid,在Leader端会维护最小事务idminCommittedLog和最大事务idmaxCommittedLog两个zxid,minComittedLog是没有被快照存储的日志文件的第一条(每次快照储存完,会重新生成一个事务日志文件),maxCommittedLog是事务日志中最大的事务。Zookeeper中实现了以下数据同步算法:

 

直接差异化同步(DIFF同步)

仅回滚同步(TRUNC),即删除多余的事务日志,比如原来的Leader节点宕机后又重新加入,可能存在它自己写入并提交但是别的节点还没来得及提交的数据;

先回滚(TRUNC)再差异化(DIFF)同步;

全量同步(SNAP);

我们用peerLastZxid代表Follower端最大的事务id,结合几个具体的场景学习如何选择同步算法。

 

场景一:同一选举轮次下Follower从Leader同步数据

假设Leader端未被快照存储的zxid为0x500000001、0x500000002、0x500000003、0x500000004、0x500000005,此时Follower端最大已提交的zxid(即peerLastZxid)为0x500000003,因此需要把0x500000004、0x500000005同步给Follower,直接使用差异化同步(DIFF)即可,Leader端差异化同步消息的发送顺序如下:

 

发送顺序       数据包类型   对应的zxid

1     PROPOSAL    0x500000004

2     COMMIT 0x500000004

3     PROPOSAL    0x500000005

4     COMMIT 0x500000005

Follower端同步过程如下:

 

Follower端首先收到DIFF指令,进入DIFF同步阶段;

Follower收到同步的数据和提交命令,并应用到内存数据库当中;

同步完成后,Leader会发送一个NEWLEADER指令,通知Follower已经将最新的数据同步给Follower了,Follower收到NEWLEADER指令后反馈一个ack消息,表明自己已经同步完成;

单个Follower同步完成后,Leader会进入集群的"过半策略"等待状态,当有超过一半的Follower都同步完成以后,Leader会向已经完成同步的Follower发送UPTODATE指令,用于通知Follower已经完成数据同步,可以对外提供服务了,最后Follower收到Leader的UPTODATE指令后,会终止数据同步流程,向Leader再次反馈一个ack消息。

 

场景二:Leader宕机了,但不久之后又重新加入集群

Leader在本地提交事务完成,还没来得及把事务提交提议发送给其他节点前宕机了。

 

为描述方便,假设集群有三个节点,分别是A、B、C,没有宕机前Leader是B,已经发送过0x500000001和0x500000002的数据修改提议和事务提交提议,并且发送了0x500000003的数据修改提议,但在B节点发送事务提交提议之前,B宕机了,由于是本机发送,所以B的本地事务已经提交,即B最新的数据是0x500000003,但发送给A和C的事务提议失败了,A和C的最新数据依然是0x500000002,B宕机后,A和C会进行Leader选举,假设C成为新的Leader,并且进行过两次数据修改,对应的zxid为0x600000001、0x600000002,然而此时B机器恢复后加入新集群(AC),重新进行数据同步,对B来说,peerLastZxid为0x500000003,对于当前的Leader C来说,minCommitedLog=0x500000001, maxCommittedLog=0x600000002(总共是0x500000001、0x500000002、0x600000001、0x600000002几个未被快照的事务)。这种情况下使用(TRUNC + DIFF)的同步方式,同步过程如下:

 

B恢复并且向已有的集群(AC)注册后,向C发起同步连接的请求;

B向Leader C发送FOLLOWERINFO包,带上Follower自己最大的zxid(0x500000003);

C发现自己没有0x500000003这个事务提交记录,就向B发送TRUNC指令,让B回滚到0x500000002;

B回滚完成后,向C发送信息包,确认完成,并说明当前的zxid为0x500000002;

C向B发送DIFF同步指令;

B收到DIFF指令后进入同步状态,并向C发送ACK确认包;

C陆续把对应的差异数据修改提议和Commit提议发给B,当数据发送完成后,再发送通知包给B;

B将数据修改提议应用于内存数据结构并Commit,(当集群中过半机器同步完成)当收到C通知已经同步完成后,B给回应ACK,并且结束同步;

这种情况仍然是minCommitedLog < peerLastZxid < maxCommittedLog的情况。

 

场景三:节点宕机并且很久之后才重新加入集群

当集群中某个节点宕机时间过长,在恢复并且加入集群时,集群中数据的事务日志文件已经生成多个,此时minCommittedLog比该节点宕机时的最大zxid还要大。例如假设ABC集群中B宕机,几天后才恢复,此时minCommittedLog为0x6000008731,而peerLastZxid为0x500000003,这种情况下采用全量同步(SNAP)的方式,同步过程如下:

 

当Leader C发现B的zxid小于minCommittedLog时,向B发送SNAP指令;

B收到同步指令,进入同步阶段;

Leader C会从内存数据库中获取全量的数据发送给B;

B获取数据处理完成后,C还会把全量同步期间产生的最新的数据修改提议和Commit提议以增量(DIFF)的方式发送给B;

6. Zookeeper广播流程

当zk集群选举完成,并且数据同步结束后即可开始对外提供服务,接收读写请求,当Leader

接收到客户端新的事物请求后,会向集群的Follower广播该事物请求,广播流程如下:

 

Leader首先会根据客户端的事务请求生成对应的事务修改提议,并根据zxid的顺序(收到多个客户端事务请求)向所有的Follower发送数据修改提议;

当Follower收到Leader的数据修改提议后,会根据接收的先后顺序处理这些提议,即如果收到了1、2、3三条数据修改提议,如果处理完成了第三条,则代表1、2条一定已经处理成功;

Leader收到Follower针对某个数据修改提议过半的正确反馈(ack)后,发起对该事务修改提议的提交,即重新发起一个事务提交的提议;

Follower收到事务提交的提议后,记录事务提交,并把数据更新到内存数据库;

8.zookeeper的应用场景

提供的服务包括:统一命名服务、数据订阅与发布(统一配置管理)、统一集群管理、服务器节点动态上下线、软负载均衡,分布式锁,队列等。

统一命名服务

 

统一配置管理

 

统一集群管理

 

服务器节点动态上下线

 

软负载均衡

 

分布式锁

方式一:事务获取锁的时候再zookeeper上创建一个临时节点,完成事务删除节点,其他事物watch该节点

缺点:会产生惊群,一个事务完成其他所有事物全发生监听。也是不公平的锁

方式二:·  我们将锁抽象成目录,多个线程在此目录下创建瞬时的顺序节点,因为Zk会为我们保证节点的顺序性,所以可以利用节点的顺序进行锁的判断。

  • 首先创建顺序节点,然后获取当前目录下最小的节点,判断最小节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
  • 获取锁失败的节点获取当前节点上一个顺序节点,对此节点注册监听,当节点删除的时候通知当前节点。
  • 当unlock的时候删除节点之后会通知下一个节点。

实现了公平锁

 

队列

使用 ZooKeeper 实现 FIFO 队列,入队操作就是在 queue_fifo 下创建自增序的子节点,并把数据(队列大小)放入节点内。出队操作就是先找到 queue_fifo 下序号最下的那个节点,取出数据,然后删除此节点。

以上是关于zookeeper教程的主要内容,如果未能解决你的问题,请参考以下文章

zookeeper启动报错(保姆级解决教程)快来看看是不是和你的错一样~

片段 getActivity 不起作用

在ansible模板中使用动态组名称

《黑马ZooKeeper教程(ZooKeeper框架入门到精通)》

Linux下安装zookeeper教程

Zookeeper基础教程:Zookeeper连接使用—zkCli