前言:
- 本文有些概念可能有错误,因为ZooKeeper本人还不太熟悉,等学习熟悉之后再回来补充
一、ZooKeeper集群结构与原理
- 最典型集群模式: Master/Slave模式(主备模式)。在这种模式中,通常Master服务器作为主服务器提供写服务,其他的 Slave 服务器作为从服务器通过异步复制的方式获取Master服务器最新的数据提供读服务
- 但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了 Leader、Follower 和 Observer 三种角色。如下图所示:
- 角色介绍:
- Leader:ZooKeeper集群中的所有机器通过一个Leader选举过程选出一个Leader机器作为领导者,LEADER既可以为客户端提供写服务器又能提供读服务
- Follower:只能提供读服务。Follower有选举权
- Observer:只能提供读服务。Observer无选举权;并且Observer也不参与写操作的“过半写成功”策略,因此Observer机器可以在不影响写性能的情况下提升集群的读性能
- 上面角色的总结为:
二、ZooKeeper集群的搭建
- 集群搭建前的提示与建议:
- 为了获得可靠的 ZooKeeper 服务,用户应该在一个集群上部署 ZooKeeper
- 只要集群上大多数的ZooKeeper服务启动了,那么总的 ZooKeeper 服务将是可用的
- 另外,最好使用奇数台机器, 因为zookeeper集群是以宕机个数过半才会让整个集群宕机的。 如果 zookeeper拥有 5 台机器,那么它就能处理 2 台机器的故障了
- 搭建zookeeper集群时,一定要先停止已经启动的zookeeper节点
- 下面我们搭建的集群中有5台机器,其中1台Leader、2台Follower、2台Observer
三、本机集群搭建
第一步(创建环境)
- 在根目录下建立一个zookeeper目录用来搭建集群,然后进入目录创建5个目录,用来存放5个节点
mkdir zookeeper cd zookeeper mkdir node1 node2 node3 node4 node5
- 分别在5个目录下再分别创建conf/目录、log/目录、data/目录,分别用来存储配置文件、日志文件、数据文件
mkdir node1/conf/ node1/log/ node1/data/ mkdir node2/conf/ node2/log/ node2/data/ mkdir node3/conf/ node3/log/ node3/data/ mkdir node4/conf/ node4/log/ node4/data/ mkdir node5/conf/ node5/log/ node5/data/
- 再分别在每个节点的log/目录下创建zoo.cfg文件,用来存储配置信息
touch node1/conf/zoo.cfg node2/conf/zoo.cfg node3/conf/zoo.cfg node4/conf/zoo.cfg node5/conf/zoo.cfg
- 创建完成之后查看一下
ls node1/ node2/ node3/ node4/ node5/
第二步(编写配置文件)
- node1节点的配置文件如下:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/home/ubuntu/zookeeper/node1/data/ dataLogDir=/home/ubuntu/zookeeper/node1/log/ clientPort=2181 maxClientCnxns=60 autopurge.snapRetainCount=3 autopurge.purgeInterval=1 server.1=0.0.0.0:2888:3888 server.2=0.0.0.0:2889:3889 server.3=0.0.0.0:2890:3890 server.4=0.0.0.0:2891:3891:observer server.5=0.0.0.0:2892:3892:observer
- node2节点的配置文件如下:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/home/ubuntu/zookeeper/node2/data/ dataLogDir=/home/ubuntu/zookeeper/node2/log/ clientPort=2182 maxClientCnxns=60 autopurge.snapRetainCount=3 autopurge.purgeInterval=1 server.1=0.0.0.0:2888:3888 server.2=0.0.0.0:2889:3889 server.3=0.0.0.0:2890:3890 server.4=0.0.0.0:2891:3891:observer server.5=0.0.0.0:2892:3892:observer
- node3节点的配置文件如下:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/home/ubuntu/zookeeper/node3/data/ dataLogDir=/home/ubuntu/zookeeper/node3/log/ clientPort=2183 maxClientCnxns=60 autopurge.snapRetainCount=3 autopurge.purgeInterval=1 server.1=0.0.0.0:2888:3888 server.2=0.0.0.0:2889:3889 server.3=0.0.0.0:2890:3890 server.4=0.0.0.0:2891:3891:observer server.5=0.0.0.0:2892:3892:observer
- node4节点的配置文件如下:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/home/ubuntu/zookeeper/node4/data/ dataLogDir=/home/ubuntu/zookeeper/node4/log/ clientPort=2184 maxClientCnxns=60 autopurge.snapRetainCount=3 autopurge.purgeInterval=1 server.1=0.0.0.0:2888:3888 server.2=0.0.0.0:2889:3889 server.3=0.0.0.0:2890:3890 server.4=0.0.0.0:2891:3891:observer server.5=0.0.0.0:2892:3892:observer
- node5节点的配置文件如下:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/home/ubuntu/zookeeper/node5/data/ dataLogDir=/home/ubuntu/zookeeper/node5/log/ clientPort=2185 maxClientCnxns=60 autopurge.snapRetainCount=3 autopurge.purgeInterval=1 server.1=0.0.0.0:2888:3888 server.2=0.0.0.0:2889:3889 server.3=0.0.0.0:2890:3890 server.4=0.0.0.0:2891:3891:observer server.5=0.0.0.0:2892:3892:observer
- autopurge配置:
- 客户端在与zookeeper交互过程中会产生非常多的日志,而且zookeeper也会将内存 中的数据作为snapshot保存下来,这些数据是不会被自动删除的,这样磁盘中这样的 数据就会越来越多。不过可以通过这两个参数来设置,让zookeeper自动删除数据
- autopurge.purgeInterval:指定自动清理快照文件和事务日志文件的时间,单位为 h,默认为0表示不自动清理,这个时候可以使用脚本zkCleanup.sh手动清理。如果不 清理则磁盘空间占用越来越大
- autopurge.snapRetainCount:用于指定保留快照文件和事务日志文件的个数,默认 为3
- 不过如果你的集群是一个非常繁忙的集群,然后又碰上这个删除操作,可能会影响 zookeeper集群的性能,所以一般会让这个过程在访问低谷的时候进行,但是遗憾的 是zookeeper并没有设置在哪个时间点运行的设置,所以有的时候我们会禁用这个自 动删除的功能,而做一些其他手段,比如手动或者自动地在凌晨做清理工作。
- Server.ID配置:server.id=IP/Host : port1 : port2
- id:用来配置ZK集群中的各节点,并建议id的值和myid保持一致,【注意】下文会讲 myid的配置
- IP/Host: 服务器的 IP 或者是与 IP 地址做了映射的主机名
- port1:Leader和Follower或Observer交换数据使用
- port2:用于Leader选举
- minSessionTimeout、maxSessionTimeout:
- 一般,客户端连接zookeeper的时候,都会设置一个session timeout,如果超过这个 时间client没有与zookeeper server有联系,则这个session会被设置为过期(如果这个 session上有临时节点,则会被全部删除,这就是实现集群感知的基础)。但是这个时间 不是客户端可以无限制设置的,服务器可以设置这两个参数来限制客户端设置的范 围。
第三步(编写myid文件)
- 例如在上面的日志文件中,最后几行的最前面显示的是“server.id”,这个id就是指zookeeper服务器在集群中的编号,我们需要把这个id写入到 myid文件中,这个myid值在zookeeper集群中的选举过程中会做一个非常大的作用
- myid文件存储在dataDir参数所指定的目录下,因此我们输入下面的命令创建5个myid文件,并将各自的id写入进去
echo "1" > node1/data/myid echo "2" > node2/data/myid echo "3" > node3/data/myid echo "4" > node4/data/myid echo "5" > node5/data/myid
- 查看是否写入成功
cat node1/data/myid node2/data/myid node3/data/myid node4/data/myid node5/data/myid
第四步(启动所有服务器)
- 启动服务器1:
# 我们的zkServer.sh位于该目录下, 使用时换上你们的路径 ~/build/apache-zookeeper-3.6.1-bin/bin/zkServer.sh --config ./node1/conf start
- 启动服务器2:
# 我们的zkServer.sh位于该目录下, 使用时换上你们的路径 ~/build/apache-zookeeper-3.6.1-bin/bin/zkServer.sh --config ./node1/conf start
- 启动服务器3:
# 我们的zkServer.sh位于该目录下, 使用时换上你们的路径 ~/build/apache-zookeeper-3.6.1-bin/bin/zkServer.sh --config ./node1/conf start
- 启动服务器4:
# 我们的zkServer.sh位于该目录下, 使用时换上你们的路径 ~/build/apache-zookeeper-3.6.1-bin/bin/zkServer.sh --config ./node1/conf start
- 启动服务器5:
# 我们的zkServer.sh位于该目录下, 使用时换上你们的路径 ~/build/apache-zookeeper-3.6.1-bin/bin/zkServer.sh --config ./node1/conf start
- 全部启动之后查看它们的状态:可以看到其中node2为leader节点,node1、node3为follower节点,node4、node5为observer节点
第五步(测试)
- 进入2181节点(follpower节点)
- 创建一个nodeservice节点,数据为“xxx000”
- 输入下面的命令从2181节点进入2182节点
connect 0.0.0.0:2182
- 在2182节点中也可以成功的看到内容
- 同理,其他节点的数据也是一致的
四、多台主机之间搭建集群
- 由于资源环境有限,无法用多台机器演示
五、ZooKeeper数据一致性的实现
实际生活中数据一致性的案例
- 车票抢购:例如下面一等座只有4张,多个用户订购时需要持证数据一致性
- 限时抢购:例如下面图片中显示商品“xx件已售”,这个是静态的,其变化在页面刷新之后会改变,当用户真正下单的时候需要与后台保持数据一致性
- 银行转账:最经典的问题,董某向张某转账,需要确保董某的资金减少,张某的资金增加
一致性的三个级别
- 总得来说,我们无法找到一种能够满足分布式系统所有系统属性的分布式一致性解决方案。因此,如何既保证数据的一致性,同时又不影响系统运行的性能,是每一个分布式系统都需要重点考虑和权衡 的
- 于是,一致性级别由此诞生:
- 1.强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但 实现起来往往对系统的性能影响大
- 2.弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不久承诺多久之后数据 能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。例如支付宝体现之后提示几分钟之内到账
- 3.最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里 之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大 型分布式系统的数据一致性上比较推崇的模型
- ZooKeeper是强一致性的
常用的一致性算法
- Paxos:比较复杂,一般不使用
- Raft:Redis、ectd使用
- Zab:zookeeper的算法
Paxos算法
- 阶段一:
- (a) Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的 Prepare请求
- (b) 如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor 已经响应过的所有Prepare请求的编号,那么它就会将它已经 接受过的编号最大 的提案(如果有的话)作为响应反馈给Proposer,同时该Acceptor承诺不再接 受任何编号小于N的提案。
- 阶段二:
- (a) 如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的 响应,那么它就会发送一个针对[N,V]提案的 Accept请求给半数以上的 Acceptor。注意:V就是收到的响应中编号最大的提案的value,如果响应中 不 包含任何提案,那么V就由Proposer自己决定。
- (b) 如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该 Acceptor没有对编号大于N的Prepare请求做出过 响应,它就接受该提案
- 如下图所示:
ZooKeeper的数据一致性方案
- 1. Leader服务器会为事务请求生成一个全局的的递增事务ID(即ZXID),保证每个消息的因果关系的顺序
- 2. Leader服务器会为该事务生成对应的Proposal,进行广播
- 3. Leader服务器会为每一个Follower服务器都各自分配一个单独的队列,然后将需要广播的事务Proposal依次放入这些队列中去,并根据FIFO策略进行消息发送
- 4. 每一个Follower服务器在接收到这个事务Proposal之后,首先以日志形式写入本地磁盘,并且成功写入后反馈给Leader服务器一个Ack响应,ZNODE树
- 5. 当Leader服务器接收超过半数的Follower的Ack响应,Leader自身也会完成对事务的提交。同时就会广播一个Commit消息给所有的Follower服务器以通知进行事务提交。每一个Follower服务器在 接收到Commit消息后,从日志写入到DataTree
Leader服务器崩溃重启
- 当Leader服务器挂掉之后会出现什么问题?
- 第一种:Leader接收的proposal还没有广播给其它的服务器,即ZXID相同
- 第二种:Leader接收的proposal已部分发送给其他的服务器,即ZXID不同
- 总的来说就是:Leader写的数据还没有生效。那么如何处理呢?
- 第一步:选举新的Leader
- 1)旧Leader宕机后,选举新Leader中,旧的Leader重启后不可能再次成为这次选 举的新Leader
- 2)旧Leader宕机后,在剩下的Follower服务器选取新Leader的标准,一定是 (ZXID,myid)最大的那个Follower成为新Leader。(即数据同步最新的那台 Follower服务器)
- 如果你的zxid比我大,那么就不投票给你
- 如果你的zxid比我大,那么我就投票给你
- 如果你的zxid跟我一样,但是myid比我大,那么就投票给你
- 3)ZXID是64位的数字。其中低32位可以靠做是一个简单的单调递增的计数器,高 32位则代表一个Leader从生到死的epoch编号
- 4)新Leader选举出来,从proposal中分析出旧Leader的epoch编号,并递增1, 作为新的ZXID的高32位,然后新ZXID的低32位从0位重新开始计数
- 5)新Leader通过ZXID和所有的Follower机器上的ZXID进行对比,确保数据同步。 保证数据在所有的Follower上与之达成同步。旧Leader上新被提出的事务被抛弃。 当数据达到同步,才将Follower服务器加入可用的Follower服务器列表。然后开 始消息广播
- 第二步:选举成功后数据同步
- 1)Leader等待Follower和Observer连接
- 2)Follower连接leader,将最大的zxid发送给leader
- 3)Leader根据follower的zxid确定同步点
- 4)完成同步后通知follower 已经成为uptodate状态
- 5)Follower收到uptodate消息后,又可以重新接受client的请求进行服务了