ps.本文为《从Paxos到Zookeeper 分布式一致性原理与实践》笔记之一
ZooKeeper
ZooKeeper曾是Apache Hadoop的一个子项目,是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、master选举、分布式锁和分布式队列等。
ZooKeeper是Google的Chubby一个开源的实现,由雅虎创建,是Hadoop和Hbase的重要组件。
ZooKeeper没有直接采用paxos算法,而是采用了一种被称为ZAB(Zookeeper Atomic Broadcast)的一致性协议
ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
ZooKeeper可以保证如下分布式一致性特性
顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到Zookeeper中;
原子性:所有事务的请求结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么在整个集群中所有机器上都成功应用了某一个事务,要么都没有应用,没有中间状态;
单一视图:无论客户端连接的是哪个Zookeeper服务器,其看到的服务端数据模型都是一致的。
可靠性:一旦服务端成功应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。
实时性:Zookeeper仅仅保证在一定的时间内,客户端最终一定能够从服务端上读到最终的数据状态。
ZooKeeper的四个设计目标
zk致力于提供一个高性能、高可用、且具有严格的顺序访问控制能力(主要是写操作)的分布式协议,最后者能使得zk能实现一些复杂的同步语义。
简单的数据模型:能够通过一个共享的、树型结构的名字空间来进行相互协调。这里是树形结构的名字空间是指zk服务器内存中的一个数据结构,由其一系列的ZNode数据节点组成,他们的层级关系就像文件系统的目录结构,不过zk将其全量数据存储在内存中,以达到高吞吐。
可以构建集群:zk集群通常由一组机器组成,集群中的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都保持着通信。集群中只要超过一般的机器可以正常工作,zk就可以对外提供服务。zk客户端会选择和集群中任意一台机器共同来创建一个tcp链接,如果连接断开,客户端会自动连接到服务机器的其他机器。
顺序访问:对于来自客户端的每个更新请求,Zookeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序。
高性能:Zookeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景。
ZooKeeper的基本概念
集群角色
最典型的集群就是Master/Slave模式(主备模式),此情况下把所有能够处理写操作的机器称为Master机器,把所有通过异步复制方式获取最新数据,并提供读服务的机器为Slave机器。
Zookeeper不采用主备模式,它引入了Leader、Follower、Observer三种角色,Zookeeper集群中的所有机器通过Leaser选举过程来选定一台被称为Leader的机器,Leader服务器为客户端提供读和写服务,Follower和Observer提供读服务,但是Observer不参与Leader选举过程,不参与写操作的"过半写成功"策略,Observer可以在不影响写性能的情况下提升集群的性能。
leader:
是整个集群工作机制中的核心,其主要工作有:1、事务请求的唯一调度和处理者,保证集群事务处理的顺序性。2、集群内部各服务器的调度者。follower:
是zookeeper集群状态的跟随者,其主要工作是:1、处理客户端的非事务请求,转发事务请求给leader服务器。2、参与事务请求proposal的投票3、参与leader选举投票observer
和follower唯一的区别在于,observer服务器只提供非事务服务,不参与任何形式的投票,包括事务请求proposal的投票和leader选举投票。通常在不影响集群事务处理能力的前提下提升集群的非事务处理能力。
会话
- 指客户端会话,一个客户端连接是指客户端和服务端之间的一个TCP长连接,Zookeeper对外的服务端口默认为2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来自服务器的Watch事件通知。
数据节点
- 我们常说的节点指的是集群中的机器节点,zk中节点有两类,第一类指构成集群的机器,称为机器节点,第二类是指数据模型中的数据单元,称为数据节点-Znode,Zookeeper将所有数据存储在内存中,数据模型是一棵树,由斜杠/进行分割的路径,就是一个ZNode,如/foo/path1,每个ZNode都会保存自己的数据内存,同时还会保存一些列属性信息。
- ZNode分为持久节点和临时节点两类,持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上,而临时节点的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
- 另外,Zookeeper还允许用户为每个节点添加一个特殊的属性:SEQUENTIAL。一旦节点被标记上这个属性,那么在这个节点被创建的时候,Zookeeper会自动在其节点后面追加一个整形数字,其是由父节点维护的自增数字。
- 临时,持久和顺序,如此,数据节点一共有四种类型:
- 持久节点
- zk中最常见的一种节点类型。除非主动删除,否则一直保留
- 持久顺序节点
- 基本和持久节点一致,额外的特性表现在顺序性上。持久顺序节点在创建节点的时候,zk会自动给它的名字加上数字后缀,表示在该父节点下的创建顺序,后缀上线是整型最大值。
- 临时节点
- 临时节点的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
- 临时顺序节点
- 同临时节点,再加上顺序特性。
- 持久节点
stat
- 数据节点中除了有数据内容外,还有一个stat对象来记录节点的状态信息:
版本
- 对于每个ZNode,Zookeeper都会为其维护一个叫作Stat的数据结构,Stat记录了这个ZNode的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)、aversion(当前ZNode的ACL版本)。
- 类似于乐观锁和cas,保证原子性操作
事务操作
- 在ZooKeeper中,能改变ZooKeeper服务器状态的操作称为事务操作。一般包括数据节点创建与删除、数据内容更新和客户端会话创建与失效等操作。对应每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用ZXID表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些事务操作请求的全局顺序。
watcher
- watcher是事件监听器,Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端。该机制是zk实现分布式协调服务的重要特性。
- 其逻辑如下图:
- 当客户端向服务端注册watcher时,也会将watcher对象存储在客户端的watcherManager中,当服务端出发watcher时间后,向客户端发送通知,客户端从watcherManager中去处watcher对象来执行回调逻辑。
ACL
Zookeeper采用ACL(Access Control Lists)策略来进行权限控制,类似于unix文件系统的权限控制,- 权限模式
- ip模式
- 通过ip地址粒度来进行权限控制。
- digest
- 类似于username:password形式配置权限标识来控制,
- world
- 即任何人都可以访问
- super
- 一种特殊的digest模式,只不过权限是超级用户。
- ip模式
- 权限:
- CREATE:创建子节点的权限。
- READ:获取节点数据和子节点列表的权限。
- WRITE:更新节点数据的权限。
- DELETE:删除子节点的权限。
- ADMIN:设置节点ACL的权限。
- 值得注意的是,CREATE和READ都是针对子节点的权限控制。
- 权限模式
ZAB协议
ZAB并不是一种通用的分布式一致性算法,它是一种特别为Zookeeper设计的崩溃可恢复的原子消息广播算法。
核心处理方式
- 所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为leader服务器,而余下的其他服务器则成为follower服务器。
- leader服务器负责将一个客户端事务请求转换成一个事务proposal,并将该proposal分发给集群中所有的follower服务器。之后leader服务器需要等待所有follower服务器的反馈,一旦超过半数的follower服务器进行了正确的反馈后,那么leader就会再次向所有的follower服务器分发commit消息,要求其将前一个proposal进行提交。
协议内容
ZAB有两种基本的模式:崩溃恢复和消息广播。
当整个服务框架启动过程中或Leader服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB协议就会进入恢复模式并选举产生新的Leader服务器。
当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式,那么整个服务框架就可以进入消息广播模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够与leader服务器的数据状态保持一致。
Leader选举算法不仅仅需要让Leader自身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产生的新的Leader服务器。
当一台同样遵守ZAB协议的服务器启动并加入集群后,如果已经存在leader,那么它会自觉的找到leader,与其进行数据同步,然后一起参与消息广播。
如果follower服务器接收到客户端的事务请求,那么他们会将这个事务请求转发给leader服务器。
当Leader服务器出现崩溃或者机器重启、集群中已经不存在过半的服务器与Leader服务器保持正常通信时,那么在重新开始新的一轮的原子广播事务操作之前,所有进程首先会使用崩溃恢复协议来使彼此到达一致状态,于是整个ZAB流程就会从消息广播模式进入到崩溃恢复模式。
一个机器要成为leader,要获得过半机器的支持,而由于每台机器都可能崩溃,因此整个过程可能出现多个leader,一个机器也可能多次成为leader。
消息广播
ZAB协议的消息广播过程使用原子广播协议,类似于一个二阶段提交过程,针对客户端的事务请求,Leader服务器会为其生成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务提交。
此处ZAB的二阶段提交和一般的二阶段提交略有不同,ZAB移除了二阶段提交中的事务中断的逻辑,follower服务器要么正常反馈,要么抛弃leader。好处是我们不需要等待所有follower都反馈响应才能提交事务,坏处是集群无法处理leader崩溃而带来的数据不一致的问题。后者需要崩溃恢复模式来解决这个问题。
整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,因此能够很容易保证消息广播过程中消息接受与发送的顺序性。
整个消息广播过程中,Leader服务器会为每个事务生成对应的Proposal来进行广播,并且在广播事务Proposal之前,Leader服务器会先为这个Proposal分配一个全局单调递增的唯一ID,称之为事务ID(ZXID),由于ZAB协议需要保证每个消息严格的因果关系,因此必须将每个事务Proposal按照其ZXID的先后顺序来进行排序和处理。
在广播过程中,leader会为每一个follower分配一个单独的队列,然后将需要广播的事务proposal依次放入,并且根据FIFO策略进行消息发送。每个follower接收到proposal之后,都会首先将其以事务日志的形式写入本地磁盘,写入成功后反馈leader一个ack响应。当leader收到超过半数的follower的ack响应之后,就会广播一个commit消息给所有follower以通知其进行事务提交,同时leader自身也完成事务的提交。每个follower在接收到commit之后,也会完成对事务的提交。
崩溃恢复
- 当整个服务框架启动过程中或Leader服务器出现网络中断、崩溃退出与重启等异常情况无法与半数以上的follower联系时,ZAB协议就会进入恢复模式。
崩溃恢复下的两种情况和所要保证的特性
ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交。
- 如果leader在崩溃前发出了proposal1,proposal2,commit1(proposal1的commit),proposal3,commit2(说明leader自己已经commit了proposal2),那么ZAB需要确保恢复后proposal2在所有服务器上都被提交成功,否则会出现不一致。
ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务
- 如果leader服务器A在崩溃前发出了proposal1,proposal2,commit1(proposal1的commit),proposal3,commit2(说明leader自己已经commit了proposal2),那么ZAB需要确保恢复后,A重新加入集群(大概率不是leader了)后,要舍弃proposal3这个事务。
leader选举算法
在崩溃恢复过程中需要处理的特殊情况,就决定了ZAB协议必须设计这样的
- 能够确保提交已经被Leader提交的事务的Proposal,同时丢弃已经被跳过的事务Proposal。
- 如果让Leader选举算法能够保证新选举出来的Leader服务器拥有集群所有机器中最高编号(ZXID最大)的事务Proposal,那么就可以保证这个新选举出来的Leader一定具有所有已经提交的更改。
- 更为重要的是如果让具有最高编号事务的Proposal机器成为Leader,就可以省去Leader服务器查询Proposal的提交和丢弃工作这一步骤了。
数据同步
- 完成Leader选举后,在正式开始工作前,Leader服务器首先会确认日志中的所有Proposal是否都已经被集群中的过半机器提交了,即是否完成了数据同步。
- 基于上文讲到的两种情况,数据同步会有不同的处理:
- 同步事务的提交:
- leader为每一个follower都准备一个队列,并将那些没有被各follower同步的事务以proposal消息的形式逐个发送给follower,并在每个proposal消息后面紧跟一个commit消息表示该事务已经被leader提交。等到某个follower同步了所有之前尚未同步的事务并将其成功应用到本地数据库,leader会将该follower加入到可用follower列表中。
处理丢弃的事务
下面分析ZAB协议如何处理需要丢弃的事务Proposal的,ZXID是一个64位的数字,其中低32位可以看做是一个简单的单调递增的计数器,针对客户端的每一个事务请求,Leader服务器在产生一个新的事务Proposal时,都会对该计数器进行加1操作;而高32位则代表了Leader周期epoch的编号,每当选举产生一个新的Leader时,就会从这个Leader上取出其本地日志中最大事务Proposal的ZXID,并解析出epoch值,然后加1,之后以该编号作为新的epoch,低32位从0来开始生成新的ZXID。
ZAB协议通过epoch号来区分Leader周期变化的策略,能够有效地避免不同的Leader服务器错误地使用不同的ZXID编号提出不一样的事务Proposal的异常情况。当一个包含了上一个Leader周期中尚未提交过的事务Proposal的服务器启动时,其肯定无法成为Leader,因为当前集群中一定包含了一个Quorum(过半)集合,该集合中的机器一定包含了更高epoch的事务的Proposal,因此这台机器的事务Proposal并非最高,也就无法成为Leader。
当这台机器以follower身份连上leader之后,leader会根据自己最后被提交的proposal来和这台机器的proposal作比较,发现需要舍弃的事务后,leader会要求该台机器进行回滚操作,回滚到某个被半数机器执行的最新的事务版本。
ZAB和paxos的联系和区别
联系
- 都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程的运行。
- Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提议进行提交。
- 在ZAB协议中,每个Proposal中都包含了一个epoch值,用来代表当前的Leader周期,在Paxos算法中,同样存在这样的一个标识,名字为Ballot。
区别
- Paxos算法中,新选举产生的主进程会进行两个阶段的工作,第一阶段称为读阶段,新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第二阶段称为写阶段,当前主进程开始提出自己的提议。
- ZAB协议在Paxos基础上添加了同步阶段,此时,新的Leader会确保存在过半的Follower已经提交了之前的Leader周期中的所有事务Proposal。
- ZAB协议主要用于构建一个高可用的分布式数据主备系统,而Paxos算法则用于构建一个分布式的一致性状态机系统。