《zk:分布式一致性原理》学习。
ZooKeeper是开放源代码的分布式协调服务,由雅虎创建的、Chubby的开源实现。
1、zk是什么
zk是分布式数据一致性的解决方案(分布式环境下,保证各节点数据一致的方式、框架),基于它可以实现数据发布/订阅(Watcher机制)、负载均衡(动态DNS)、命名服务(ID)、Master选举(选举算法)、分布式锁和分布式队列(不同类型节点+全局唯一ID)等功能。
实现上面所说功能,是因为zk的特性:
顺序一致性:同一客户端发起的事务请求在zk中有序(Leader处理事务请求,并为每个事务分配一个唯一的ZXID)
原子性:事务请求处理结果在整个集群所有机器上是一致的(?)
单一视图:无论客户端连接哪个zk服务器,看到的服务端数据一致
实时性:zk保证一定时间段(?)内,客户端能够读到最新的数据状态
zk将全量数据存储在内存中,适用于读多写少的应用场景(3台机器12~13WQPS)。

zk的基本概念:
集群角色:Leader(读写服务)、Follower(读服务、参与选举)、Observer(读服务)
会话:客户端通过默认2181端口使用TCP连接zk,这个连接就是会话(Session)
数据节点(ZNode)
通常我们说分布式中的节点指的是一台机器实例。zk中节点分为两类:一类是构成集群的机器,我们成为机器节点;另一类是数据模型中的数据单元,我们成为数据节点-ZNode。zk将所有数据存储在内存中,数据模型是一颗树(ZNode Tree),我们通过/来进行路径分割,例如/foo/path1就是一个Znode。每个Znode上都会保存自己的数据内容+属性信息。
Znode分为持久节点和临时节点两类:
1)持久节点指一旦ZNode被创建,除非主动进行ZNode移除,否则这个ZNode将一直在zk上
2)临时节点生命周期和客户端会话绑定
3)zk允许用户为每个节点添加一个特殊属性:SEQUENTIAL,有这个属性的节点被创建时,zk会自动在其节点名后追加一个整数数字(由父节点维护的自增数字,命名服务应用场景)
版本(ZNode版本)、Watcher(事件监听器)、ACL(Access Control Lists 权限控制)
2、ZAB协议
zk没有完全采用Paxos算法,而是使用ZAB(Zookeeper Atomic Broadcast zk原子消息广播协议)作为其数据一致性的算法。通过名字也可以看出这个算法不像Paxos那样通用,是zk自己的算法。
ZAB算法展现形式描述:
所有事务请求由全局唯一的服务器协调处理(有且只有一个Leader)
Leader负责将事务请求分发给所有Follower机器
超过半数Follower机器反馈后,Leader再发送Commit请求
......2PC模式
当产生Leader服务器,同时集群中已经有过半的机器与Leader完成状态同步之后(集群中过半的机器能够和Leader服务器的数据保持一致),ZAB协议就会退出恢复模式,进入消息广播模式。
消息广播模式:
Leader负责消息广播,新加入的服务器进行数据恢复模式(找到Leader服务器,与其进行数据同步,然后参与到消息广播中)。Leader服务器接收客户端事务请求后,产生对应的事务提案并发起广播,非Leader机器收到客户端事务请求后会首先将这个请求转发到Leader处理。
上面描述的广播机制(ZAB算法展现)有点类似2PC过程,但是两者有一些不同之处:
ZAB移除了中断逻辑,所有Follower服务器要么正常反馈Leader提出的事务,要么抛弃(为什么会抛弃?抛弃只在Leader上被提交的事务)
过半的Follower服务器已经反馈Ack之后就开始提交事务,不需要等待所有的Follower
zk如何保证消息处理的顺序性?
Leader广播事务之前首先为这个事务分配一个全局单调递增的唯一ID(事务ID,ZXID)
Leader为每个Follower服务器各自分配一个单独的队列,将事务依次写入队列中FIFO
Follower接收到事务后,先写事务日志到本地磁盘,然后发送ACK
Leader收到过半Ack后,进行Commit广播(同时自身也会对事务进行提交)
1)ZAB基本特性
如果一个事务在一台机器上被处理成功,那么应该在所有机器上都被处理成功。
情况1:事务在Leader上被提交,并得到过半Ack,但发送Commit时Leader挂了。
结果:导致事务只在Leader上提交了,Follower上没有提交。
处理:ZAB协议需要确保哪些已经在Leader服务器上提交的事务最终被所有服务器提交,同时要确保丢弃哪些只在Leader服务器上被提出的事务。
上面处理方式有点绕,简单来说,事务提交表示不可变,所以如果有机器事务A提交了,集群所有的机器这个事务A都要提交(①);但如果事务B只是在被提出,那么恢复后忽略这个提议B(②)。

① Server1 广播P1(提案1)、P2(提案2)、C1(提案1提交)、P3(提案3)、C2(提案2提交)
Server1发出C2后崩溃 -> ZAB要确保Server2/Server3最终都要提交提案2=> p2
② Server1 提出P3后崩溃,其他集群没有收到这个提案,那么要确保集群恢复后丢弃P3
针对上面两种情况,ZAB要确保提交被Leader提交的事务、丢弃被跳过的提案。
2)为什么选出的新Leader拥有最大的事务ID(ZXID)就可以保证了呢?
完成Leader选举、正式开始工作(接收客户端事务请求)前,Leader服务器会首先确认事务日志中所有Commit的事务都被过半的机器提交了。
数据同步过程:
Leader为每个Follower准备一个队列,将没有被Follower提交的事务以提案P方式发送,同时紧接着发送Commit消息(注意,这里不等Ack了,直接发送提交消息),等到所有Follower将未提交事务处理完后,Leader将Follower机器加入到真正可用的列表中。
ZAB协议中ZXID是一个64位数字,低32位是一个单调递增的计数器(新的事务+1),高32位代表Leader周期epoch编号(Leader更换时+1,同时低32位置0重新开始)。接上文“ZAB基本特性”提到的P3,如果后面Server1好了,重新加入集群,发现自己有一个P3提案没有被提交,因为P3包含的是上一个Leader周期(说明此刻处于一个新的Leader周期中)的epoch,新的Leader发送最大的提交事务P2到server1上,P2<P3,Leader要求Server1丢弃P3。
3)深入ZAB协议
ZAB协议是zk框架的核心,规定任何时候都需要保证只有一个主进程负责消息广播,如果主进程崩溃,需要选举出一个新的主进程。随着时间推移,zk集群中会出现一个主进程序列P1、P2、... Pn-1、Pn,n标识主进程的周期epoch。
ZAB协议包括崩溃恢复和消息广播两个阶段,细分可以分为:发现、同步和广播三个阶段:
发现:Leader选举过程,找出主进程(Leader)
同步:Leader有最大的事务ID,看一下自己的Follower有没有跟上,让他们都跟上提交;同时让自己没有的提案,Follower也要销毁。保证Leader数据为准同步完成。
广播:同步完成后,开始正式接收客户端新的事务请求处理。
4)ZAB & Paxos
ZAB并不是Paxos算法的典型实现,可以说它是zk独有的算法。
两者都存在类似Leader的角色(协调者、提案发起人)
Leader都会等待超过半数Follower后进行提交
两者都存在一个Leader周期的概念(epoch & Ballot)
两者的设计目标不同,ZAB用于构建高可用分布式数据主备系统,例如zk;Paxos更通用广泛,是用于构建分布式一致性状态机系统。
3、ZK的使用
集群与单机两种运行模式。
集群模式:
集群中每台机器都需要感知整个集群的组成(类似于ES的节点配置),配置文件zoo.cfg中进行配置:server.id=host:port:port,一行代表一个机器。
id是ServerID,是一个变量而不是id这个字母,例如server.1=1.1.1.1:1201:1202
所有机器上的zoo.cfg内容都应该是一致的,并且每台机器的id要不同,id范围1-255
在dataDir所配置的目录中创建myid的文件,文件第一行写上一个数字,和zoo.cfg中机器编号对应
bin目录下zkServer.sh脚本启动
单机模式机器列表中只有server.1这一项,其他的跟集群相同。
4、zk的应用
发布/订阅
zk是典型的发布/订阅的分布式数据管理与协调框架,可以使用它来进行分布式数据的发布和订阅。例如配置管理,监听配置变更。发布者将数据发布到zk的一个或一系列节点上,订阅者进行数据订阅,从而达到动态获取数据的目的,实现配置和数据的动态更新。
系统的全局配置通常具有以下三个特征:
1)数据量比较小
2)内容会在运行时动态变更
3)集群中各个机器共享
首先在zk上选出一个数据节点进行配置的存储,如下图所示。

将配置信息写入到该数据节点中。集群中每台机器启动时会从zk配置节点上读取配置信息,同时注册一个数据变更的Watcher。系统运行中出现配置变更时zk会帮我们将改动通知发送到各个客户端。
负载均衡(zk实现动态DNS方案)

首先在zk上创建节点进行域名配置。与配置信息的监听原理差不多。
命名服务

使用zk生成唯一ID。每个客户端会根据自己的任务类型创建一个顺序节点。zk会返回一个节点的名称,客户端使用任务类型+名称就可以作为唯一的ID。zk中每个数据节点都维护一份子节点的顺序队列,当客户端对其创建一个顺序子节点时,zk会自动以后缀的形式给子节点添加一个序号。
分布式协调/通知
分布式锁

zk通过数据节点标识一个锁。需要获取锁时,所有客户端试图通过create()接口在指定路径的节点下创建临时子节点,zk会保证最终只有一个客户端能够创建成功,那么可以认为这个客户端获取了锁。同时,没有获取到锁的客户端注册一个子节点变更的Watcher监听。




