ZooKeeper(一)概述和ZAB协议

ZooKeeper(一)概述和ZAB协议

一. 概述

1.1 什么是ZooKeeper?

  • Apache ZooKeeper 由 Apache Hadoop 子项目发展而来。
  • ZooKeeper是一个开源的分布式协调服务,由雅虎创建,Google Chubby 的开源实现。
  • 设计目标是将那些复杂易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
  • ZooKeeper 为分布式应用提供了高效且可靠的分布式协调服务,提供了诸如统一命名服务配置关联分布式锁等分布式的基础服务。
  • ZooKeeper并没有直接采用 Paxos 算法,而是一种被称为 ZAB(ZooKeeper Atomic Broadcast)的一致性协议。

ZooKeeper 是一个典型的分布式数据一致性的解决方案,应用可以基于它实现诸如数据发布/订阅负载均衡命名服务分布式协调/通知集群管理Master选举分布式锁分布式队列等功能。

1.2 特性

  • 顺序一致性:从同一客户端发起的事务请求,将会严格的按照发送顺序被应用到ZooKeeper中。
  • 原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,要么整个集群所有机器都成功应用了某个事务,要么都没有应用。
  • 单一视图:无论客户端连接哪个ZooKeeper服务器,看到的服务端数据模型都是一致的。
  • 可靠性:一旦服务端成功应用了一个事务,并完成对客户端的响应,事务引起的服务端状态一直保留到下个事务进行变更。
  • 实时性:ZooKeeper仅保证在一定时间段内,客户端最终一定能够从服务端读取到最新的数据状态。

1.3 设计目标

  1. 简单的数据模型:分布式程序通过一个共享的、树型结构的名字空间来相互协调
  2. 可以构建集群:一般3~5台机器可以组成一个可用的ZooKeeper集群,每台机器都会在内存中维护当前的服务器状态,机器之间互相都保持着通信。ZooKeeper客户端会与任意一台机器创建一个TCP连接,一旦连接断开,客户端会自动连接到其他机器。
  3. 顺序访问:ZooKeeper为客户端的每个更新请求分配一个全局唯一的递增编号,反映了事务操作的先后顺序。
  4. 高性能:ZooKeeper将全量数据存储在内存中,直接服务于客户端的所有非事务请求,非常适合于读操作为主的场景。

1.4 核心概念

  • 集群角色:

    • 最典型的集群模式是Master/Slave(主备模式),能够处理所有写操作的是Master机器,所有通过异步复制方式获取最新数据,并提供读服务的机器是Slave机器。
    • ZooKeeper并没有采用这种模式,而是引入了新的三个角色:
      • Leader:集群中所有机器通过选举过程选定一个Leader,其为客户端提供读和写服务。
      • Follower:提供读服务。
      • Observer:提供读服务,不参与Leader选举过程,也不参与写操作的“过半写成功”策略,因此Observer可以在不影响写性能的情况下提高集群的读性能。
  • 会话(Session):

    • 在ZooKeeper中,一个客户端连接是指客户端和服务器之间的一个TCP长连接。
    • ZooKeeper对外的服务端口默认是2181,客户端启动时首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始,客户端通过这个连接进行心跳检测与服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接收响应,同时还能通过该连接接收服务器的Watch事件通知。
    • sessionTimeout值用来设置一个客户端会话的超时时间。因服务器压力过大、网络故障或客户端主动断开连接等导致的连接断开,只要在sessionTimeout规定时间内重新连上一台服务器,之前创建的会话仍会有效。
  • 数据节点(ZNode):分布式中节点即每台机器,在ZooKeeper中,节点分为两类:

    • 机器节点:构成集群的机器
    • 数据节点:数据模型中的数据单元,即ZNode,又可以分为
      • 持久节点:一旦此ZNode被创建,除非主动进行移除操作,否则将一直保存。
      • 临时节点:生命周期和客户端会话绑定,一旦客户端会话失效,该客户端创建的所有临时节点都会被移除。

    数据模型是一棵树,由斜杠(/)进行分割的路径,就是一个ZNode,如 /foo/path1 。每个ZNode上都会保存自己的数据内容,以及一系列属性信息。

    ZooKeeper允许用户为每个节点添加一个特殊的属性:SEQUENTIAL,当节点标记此属性,在节点创建时会自动在节点名后追加一个由父节点维护的自增整型数字。

  • 版本:ZooKeeper的每个ZNode都会维护一个叫做 Stat 的数据结构,Stat中记录了节点的三个数据版本:

    • version:当前ZNode的版本
    • cversion:当前ZNode子节点的版本
    • aversion:当前ZNode的ACL版本
  • Watcher:即事件监听器,用户可以在指定节点上注册一些Watcher,在一些特定事件触发时,ZooKeeper服务端会将事件通知到感兴趣的客户端上。

  • ACL:ZooKeeper采用 Access Control Lists 策略来进行权限控制,类似于UNIX文件系统的权限控制,定义了5种权限:

    • CREATE:创建子节点的权限
    • READ:获取节点数据和子节点列表的权限
    • WRITE:更新节点数据的权限
    • DELETE:删除子节点的权限
    • ADMIN:设置节点ACL的权限

二. ZAB 协议

2.1 什么是ZAB协议?

ZooKeeper并没有完全采用 Paxos 算法,而是使用了一个称为 ZooKeeper Atomic Broadcast(ZAB,ZooKeeper原子消息广播协议)的协议来作为其数据一致性的核心算法。

ZAB协议是一种支持崩溃恢复的原子广播协议,最初只是为雅虎公司内部一些高吞吐量、低延迟、健壮、简单的分布式系统场景设计的,并不像Paxos算法那样通用和可扩展。

基于该协议,ZooKeeper实现了一种主备模式的系统架构来保持集群中各副本之间的数据一致性。ZooKeeper使用一个单一的主进程来接收并处理客户端所有的事务请求,通过ZAB的原子广播协议将服务器的数据状态以事务Proposal的形式广播到所有的副本进程。该主备模型保证了同一时刻集群中只能有一个主进程来广播服务器的状态变更,因此能够很好的处理客户端大量的并发请求。并且支持分布式环境中对于顺序状态的需求(有些状态变更必须依赖于比它早生成的状态变更),保证了一个全局的变更序列被顺序应用。因为主进程随时可能崩溃或重启,还要做到主进程有问题时仍能正常工作。

所有事务请求必须由一个全局唯一的服务器来协调处理,即Leader服务器,余下的服务器成为Follower服务器。Leader服务器负责将一个客户端事务请求转换为一个事务 Proposal(提议),并将该提议分发给集群中所有的Follower服务器。之后Leader服务器等待所有Follower服务器的反馈,一旦超过半数进行了正确的反馈,Leader服务器再次向所有的Follower服务器分发Commit消息,要求将前一个提议提交。

2.2 协议介绍

ZAB协议包含两种基本模式:

  • 崩溃恢复:
    • 当整个服务框架在启动过程,或是Leader服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB进入该模式选举产生新的Leader服务器。
    • 选出新的Leader,且集群中已有过半的机器与新的Leader完成状态同步(指数据同步,用来保证过半机器与Leader保持一致)之后,ZAB退出该模式。
  • 消息广播:
    • 集群中有过半的Follower服务器完成了与Leader服务器的状态同步,服务框架进入该模式。
    • 当一台遵守ZAB协议的服务器启动并加入集群时,若此时已存在Leader服务器,它会自觉的进入数据恢复模式:找到Leader所在服务器,并与其进行数据同步,然后一起参与到消息广播流程中。

(1)消息广播

消息广播的过程类似于二阶段提交:

ZAB没有中断逻辑,所以Follower服务器反馈Ack后就可以提交事务Proposal,这种简化模型自然存在Leader单点故障引起的数据不一致问题。ZAB协议通过崩溃恢复模式来解决这个问题。

整个消息广播协议基于具有FIFO特性的TCP协议来进行网络通信,很容易保证消息接收和发送的顺序性。Leader为每个事务Proposal分配一个全局单调递增的事务ID(ZXID),每个事务按照ZXID的先后顺序进行排序和处理。

Leader服务器为每个Follower服务器各自分配一个队列,将需要广播的事务Proposal依次放入队列中,根据FIFO的策略进行消息发送。每个Follower服务器收到事务Proposal后,首先以事务日志的形式写入到本地磁盘中,在成功写入后反馈给Leader一个Ack响应。收到超过半数的Ack响应后,Leader会广播一个Commit消息给所有Follower服务器以通知进行事务提交,同时Leader完成自己的事务提交。

(2)崩溃恢复

进入崩溃恢复模式后,需要选举出一个新的Leader服务器,这需要一个高效且可靠的选举算法,不仅需要让新Leader自己知道已被选举为Leader,还要让集群中所有机器快速感知到这点。

ZAB协议要确保那些已经在Leader服务器上提交的事务最终被所有服务器提交。

假设一个事务已在Leader服务器提交,但在将Commit消息发送给所有Follower之前,Leader挂掉了:

ZAB协议要确保丢弃那些只在Leader服务器上被提出的事务。

相反,在崩溃恢复过程中出现一个需要被丢弃的提案,在崩溃恢复结束后需要跳过该事务Proposal:

针对以上两点,如果Leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最高编号(ZXID)的事务Proposal,就可以保证新选举出的Leader一定具有所有已经提交的提案,还可以省区Leader服务器检查Proposal的提交和丢弃工作这一步。

完成Leader选举后,在正式工作前,Leader服务器首先要确认事务日志中所有的Proposal是否都已经被集群中过半的机器提交了,即是否完成数据同步

  • Leader为每个Follower准备一个队列,将没有被各Follower同步的事务以Proposal消息的形式逐个发送给Follower,并每个都跟着发送一个Commit消息。
  • 等到所有Follower都将其尚未同步的事务Proposal从Leader上同步过来并成功应用到本地数据库后,Leader将该Follower加如到真正可用的Follower列表,并开始之后流程。

如何处理需要丢弃的事务Proposal?

ZXID时一个64位的数字,低32位可以看作简单的递增计数器,高32位则代表了Leader周期epoch的编号,每当选举产生一个新的Leader服务器,就会从Leader上取出其本地日志中最大事务Proposal的ZXID,解析出epoch值并加1,将此编号作为新的epoch,并将低32位置0。

这样可以区分开不同的Leader周期变化,从而避免不同的Leader服务器错误的使用相同的ZXID提出不同的事务Proposal。

当一个包含了上个周期尚未提交的事务Proposal的服务器启动时,其肯定无法成为Leader。因为当前集群一定包含一个Quorum集合,集合中的机器一定包含了更高epoch的事务Proposal,因此该机器肯定非最高编号事务。当其连接上Leader后,Leader根据自己记录的最后被提交的Proposal对比,并要求Follower进行一个回退操作—回退到一个确实已经被集群过半机器提交的最新的事务Proposal(如上图4-4 Server1连接后,会被要求去除P3)。

2.3 深入剖析

未完待续…

(1) 系统模型

(2) 问题描述

(3) 算法描述

(4) 运行分析

2.4 ZAB与Paxos算法的异同

  • 二者都存在一个Leader进程角色,负责协调多个Follower进程的运行。

  • Leader进程都会等待超过半数的Follower进程正确反馈后,才会将提案提交。

  • ZAB协议中标识Leader周期的epoch值,Paxos算法中叫Ballot。

二者的本质区别在于设计目标不同,ZAB协议用于构建一个高可用的分布式数据主备系统,而Paxos算法则用于构建一个分布式的一致性状态机系统。

三. 简单使用

3.1 部署与运行

ZooKeeper使用Java语言编写,所以需要1.6以上版本的Java环境。ZooKeeper包含集群和单机两种运行模式。

(1)部署

  1. 下载安装包并解压:http:zookeeper.apache.org/releases.html

  2. 配置文件 zoo.cfg

    %ZK_HOME%/conf 目录下 zoo_sample.cfg 重命名为 zoo.cfg

    1
    2
    3
    4
    5
    6
    7
    8
    9
    tickTime=2000
    dataDir=/var/lib/zookeeper/
    clientPort=2181
    initLimit=5
    syncLimit=2
    # server.id=host:port:port 用来感知集群由哪些机器构成,id表示机器序号,dataDir目录下需要有一个myid文件,内容为一个数字对应此id
    server.1=<IP1>:2888:3888
    server.2=<IP2>:2888:3888
    server.3=<IP3>:2888:3888
    • 集群模式下所有cfg文件都应是一致的,最好使用代码仓库管理起来。
    • id取值范围为 :1~255。
  3. 创建 myid 文件,在dataDir指定目录下,内容为数字id

  4. 为所有机器配置2和3步内容

  5. 启动服务器:使用 %ZK_HOME%/bin 目录下的 zkServer.sh 脚本启动

    1
    2
    3
    4
    $ sh zkServer.sh start
    JMX enabled by default
    Using config: /opt/zookeeper-3.4.3/bin/../conf/zoo.cfg
    Starting zookeeper ... STARTED
  6. 验证服务器

    1
    2
    $ telnet 127.0.0.1 2181
    stat

单机模式与集群模式只是在 zoo.cfg 的配置上有些差异,server.id 只有一项 server.1

(2)运行和停止

启动服务:

  • Java命令行:在 %ZK_HOME% 目录下执行命令 java -cp zookeeper -3.4.3 jar:lib/ slf4j-api-1.6. 1. jar:lib/slf4j- log4j12-1.6.1.jar:lib/log4j-1.2.15.jar:conf org. apache. zookeeper. server.quorum.QuorumPeerMain conf/zoo.cfg 注意log4j和slf4j版本
  • 自带的启动脚本

停止服务:sh zkServer.sh stop

(3)常见异常

  • 端口被占用:java.net.BindException: Address already in use 异常是因为2181端口被其他进程占用,关闭对应进程并重启ZK即可。
  • 磁盘没有剩余空间:java.io.IOException: No space left on device 遇到此异常,ZK会立即执行Failover策略,从而退出进程。清理磁盘空间,为了避免后续再次遇到此问题,最好加上对ZK服务器磁盘使用的监控和ZK日志的自动清理。
  • 无法找到 myid 文件:缺少此文件,创建即可。
  • 集群中其他机器Leader选举端口未开: Cannot open channel to 2 at election adress /xx.xx.xx.xx:3888 这是由于启动过程中,虽然当前机器启动了,但其他机器还未启动完。ZK使用3888端口进行Leader选举过程的投票通信。

3.2 客户端脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 进入bin目录,确认是否连接上ZK服务器
$ sh zkCli.sh
# 连接指定ZK服务器
$ sh zkCli.sh -server ip:port

# 创建一个ZK节点
# s或e分别指定节点特性顺序或临时节点,不加则默认为持久节点;acl进行权限控制,默认不加控制
create [-s] [-e] path data acl
# 在根节点下创建一个叫/zk-book的节点,内容为123
create /zk-book 123

# 读取
# ls列出ZK指定节点下所有子节点
ls path [watch]
ls /
# get命令获取指定节点的数据内容和属性信息
get path [watch]
get /zk-book

# 更新
# 更新指定节点的数据内容,version参数指定本次更新基于ZNode哪个数据版本进行
set path data [version]
set /zk-book 456

# 删除,要删除节点,其不能包含子节点
delete path [version]

参考:

🔗 《从Paxos到Zookeeper-分布式一致性原理与实践》