老鱼进阶篇-由浅入深Zookeeper

318 阅读17分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

1、Zookeeper 简介

ZooKeeper 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 Paxos 算法的 ZAB 协议完成的。其主要功能包括:配置维护、域名服务、分布式同步、集群管理等

zookeeper 的官网zookeeper.apache.org

2、ZK一致性及Paxos算法描述

zk一致性主要表现在以下几个方面:

  • 顺序一致性

    从同一个客户端发起的事物请求,会严格的按照请求的先后顺序进行执行记录到zk中。

  • 原子性

    所有事物请求的结果在集群中所有服务器上都是一样的,要么都成功,要么都失败。

  • 单一视图

    客户端无论连接集群中哪台服务器,读取到的数据都是相同的。

  • 可靠性

    事物操作成功后会持久化存储,除非被别的事物操作修改

  • 最终一致性

    一旦一个事务被成功应用,zk 可以保证在一段较短的时间内,客户端最终一定能够从服务端读取到最新的数据。但不能保证实时读取到。

Paxos 算法的执行过程划分为两个阶段:准备阶段 prepare 与接受阶段 accept。

Aprepare 阶段

1) 提案者(Proposer)准备提交一个编号为 N 的提议,于是其首先向所有表决者(Acceptor)发 送 prepare(N)请求,用于试探集群是否支持该编号的提议。

2) 每个表决者(Acceptor)中都保存着自己曾经 accept 过的提议中的最大编号 maxN。当一个表决者接收到其它主机发送来的 prepare(N)请求时,其会比较 N 与 maxN 的值。有以下几种情况:

a) 若 N 小于 maxN,则说明该提议已过时,当前表决者采取不回应或回应 Error 的方式来拒绝该 prepare 请求;

b) 若 N 大于 maxN,则说明该提议是可以接受的,当前表决者会首先将该 N 记录下来,并将其曾经已经 accept 的编号最大的提案 Proposal(myid,maxN,value)反馈给提案者,以向提案者展示自己支持的提案意愿。其中第一个参数 myid 表示该提案的提案者标识 id,第二个参数表示其曾接受的提案的最大编号 maxN,第三个参数表示该提案的真正内容 value。当然,若当前表决者还未曾 accept 过任何提议,则会将Proposal(myid,null,null)反馈给提案者。

c) 在 prepare 阶段 N 不可能等于 maxN。这是由 N 的生成机制决定的。要获得 N 的值,其必定会在原来数值的基础上采用同步锁方式增一。

Baccept 阶段

1) 当提案者(Proposer)发出 prepare(N)后,若收到了超过半数的表决者(Accepter)的反馈,那么该提案者就会将其真正的提案 Proposal(myid,N,value)发送给所有的表决者。

2) 当表决者(Acceptor)接收到提案者发送的 Proposal(myid,N,value)提案后,会再次拿出自己曾经 accept 过的提议中的最大编号 maxN,或曾经记录下的 prepare 的最大编号,让 N与它们进行比较,若 N 大于等于这两个编号,则当前表决者 accept 该提案,并反馈给提案者。若 N 小于这两个编号,则表决者采取不回应或回应 Error 的方式来拒绝该提议。

3) 若提案者没有接收到超过半数的表决者的 accept 反馈,则有两种可能的结果产生。一是放弃该提案,不再提出;二是重新进入 prepare 阶段,递增提案号,重新提出 prepare请求。

4) 若提案者接收到的反馈数量超过了半数,则其会向外广播两类信息:

a) 向曾 accept 其提案的表决者发送“可执行数据同步信号”,即让它们执行其曾接收到的提案;

b) 向未曾向其发送 accept 反馈的表决者发送“提案 + 可执行数据同步信号”,即让它们接受到该提案后马上执行。

3、ZAB协议

ZAB协议,Zookeeper Atomic Broadcast, zk 原子消息广播协议。是专为 ZooKeeper 设计的一种支持崩溃恢复的原子广播协议,在 Zookeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性。

Zookeeper 使用一个单一主进程来接收并处理客户端的所有事务请求,即写请求。当服务器数据的状态发生变更后,集群采用 ZAB 原子广播协议,以事务提案 Proposal 的形式广播到所有的副本进程上。ZAB 协议能够保证一个全局的变更序列,即可以为每一个事务分配一个全局的递增编号 xid。

当 Zookeeper 客户端连接到 Zookeeper 集群的一个节点后,若客户端提交的是读请求,那么当前节点就直接根据自己保存的数据对其进行响应;如果是写请求且当前节点不是Leader,那么节点就会将该写请求转发给 Leader,Leader 会以提案的方式广播该写操作,只要有超过半数节点同意该写操作,则该写操作请求就会被提交。然后 Leader 会再次广播给所有订阅者,即 Learner,通知它们同步数据。

3.1 zk集群的三类角色

  • Leader: 事务请求的唯一处理者,也可以处理读请求
  • Follower: 可以直接处理客户端的读请求,并向客户端响应;但其不会处理事务请求,其只会将客户端事务请求转发给Leader来处理;对Leader发起的事务提案具有表决权;同步 Leader 中的事务处理结果;Leader 选举过程的参与者,具有选举权与被选举权。
  • Observer: 不参与 Leader 选举的 Follower,在 Leader 选举过程中没有选举权与被选举权;同时,对于 Leader 的提案没有表决权。用于协助 Follower 处理更多的客户端读请求。Observer 的增加,会提高集群读请求处理的吞吐量,但不会增加事务请求的通过压力,不会增加 Leader 选举的压力。

3.2 三个重要数据

  • zxid: 其为一个 64 位长度的 Long 类型,其中高 32 位表示 epoch,低 32 位表示 xid。
  • Epoch: 每个 Leader 选举结束后都会生成一个新的 epoch,并会通知到集群中所有其它 Server,包含 Follower 与 Observer。
  • xid: 唯一的事物id流水号。

3.3 三种模式

  • 恢复模式

    集群启动过程中,或 Leader 崩溃后,系统都需要进入恢复模式,以恢复系统对外提供服务的能力。其包含两个重要阶段:Leader 选举与初始化同步。

  • 广播模式

    初始化广播与更新广播。

  • 同步模式

    初始化同步与更新同步。

3.4 四种状态

  • LOOKING

    选举状态

  • FOLLOWING

    follower工作时的状态

  • LEADING

    leader工作时的状态

  • OBSERVING

    Observer工作时的状态

3.5 同步模式与广播模式

恢复模式两个阶段为Leader选举与初始化广播(同步),下面主要介绍初始化广播。

3.5.1 初始化广播(同步)

当集群完成Leader选举之后,此时的Leader还不是真正的leader状态,是一个准Leader, 需要经过以下流程后才会成为集群中真正的leader,步骤如下:

  1. 为了保证集群中leader和leaner的消息同步,leader为每个learner创建一个FIFO的队列,用于消息同步。
  2. leader将没有进行同步的事物消息封装成一个Proposal提案。
  3. leader将这个proposal通过队列逐个发给learner,并在此提案后面紧跟一个COMMIT消息指令,通知learner可以进行消息同步。
  4. leaner收到消息提案后直接将消息保存到本地的事物日志中,并向leader恢复ACK消息。
  5. leader服务器在收到learner的ACK后就将此leaner加入到真正可用的follower或者observer中,如果leader没有收到确认ack,则不会将其添加到可用列表中。

3.5.2 消息广播

当集群完成了初始化同步之后,集群进入了正常工作状态。此时如果集群中的learner收到了来自客户端的事物请求,会将该请求转发至Learder服务器,然后再执行以下的步骤:

  1. Leader收到事物请求之后,会为每个事物生成一个唯一的64位的自增ID,也就是zxid。通过比较azxid的大小即可以实现事物的有序性管理,然后将事物请求封装成一个提案Proposal。
  2. Leader根据follower提供的follower队列列表,将提案依次发送至各个follower服务器。
  3. follower收到请求提案Proposal后,首先会将提案的zxid与本地事物日志中的最大的zxid进行比较,如果当前提案的zxid大于保存的最大zxid, 则选择接受该提案,并将当前提案保存到本地事物日志中,并像leader回复确认的ACK。
  4. 当Leader收到过半的ACKs时,就会向所有的follower发送COMMIT消息,向所有的Observer发送该提案,通知他们可以进行数据同步。
  5. 当follower收到COMMIT消息后,会将本地事物日志中的提案信息正式更新到本地。当Observer收到提案后,直接接受并保存当前提案信息。
  6. 无论是 Follower 还是 Observer,在同步完成后都需要向 Leader 发送成功 ACK。

3.6 恢复模式三个原则

  • leader主动出让原则

    若集群中 Leader 收到的 Follower 心跳数量没有过半,此时 Leader 会自认为自己与集群的连接已经出现了问题,其会主动修改自己的状态为 LOOKING,去查找新的 Leader。为了防止集群出现脑裂。而其它 Server 由于有过半的主机认为已经丢失了 Leader,所以它们会发起新的 Leader选举,选出一个新的 Leader。

  • 已经同步处理的消息不能丢失

    Leader 收到超过半数 Follower 的 ACKs 后,就向各个 Follower 广播 COMMIT 消息,批准各个 Server 执行该写操作事务。当各个 Server 在接收到 Leader 的 COMMIT 消息后就会在本地执行该写操作,然后会向客户端响应写操作成功。但是如果在非全部Follower 收到 COMMIT 消息之前 Leader 就挂了,这将导致一种后果:部分 Server 已经执行了该事务,而部分 Server 尚未收到 COMMIT 消息,所以其并没有执行该事务。当新的 Leader 被选举出,集群经过恢复模式后需要保证所有 Server 上都执行了那些已经被部分 Server 执行过的事务。

  • 被丢弃的消息不能被重现

    当在 Leader 新事务已经通过,其已经将该事务更新到了本地,但所有 Follower 还都没有收到 COMMIT 之前,Leader 宕机了(比前面叙述的宕机更早),此时,所有 Follower 根本就不知道该 Proposal 的存在。当新的 Leader 选举出来,整个集群进入正常服务状态后,之前挂了的 Leader 主机重新启动并注册成为了 Follower。若那个别人根本不知道的 Proposal还保留在那个主机,那么其数据就会比其它主机多出了内容,导致整个系统状态的不一致。所以,该 Proposa 应该被丢弃。类似这样应该被丢弃的事务,是不能再次出现在集群中的,应该被清除。

3.7 Leader选举

集群启动过程中,或 Leader 宕机后,集群就进入了恢复模式。恢复模式中最重要的阶段就是 Leader 选举

myid也称为 ServerId,这是 zk 集群中服务器的唯一标识。例如,有三个 zk 服务器,那么编号分别是 1,2,3。

LogicClock: 逻辑时钟,Logicalclock,是一个整型数,该概念在选举时称为 logicalclock,而在选举结束后称为 epoch。即 epoch 与 logicalclock 是同一个值,在不同情况下的不同名称。

在集群启动过程中的 Leader 选举过程(算法)与 Leader 断连后的 Leader 选举过程稍微有一些区别,基本相同。

  • 集群启动中的 Leader 选举

    在集群初始化阶段,当第一台服务器 Server1 启动时,其会给自己投票,然后发布自己的投票结果。投票包含所推举的服务器的 myid 和 ZXID,使用(myid, ZXID)来表示,此时 Server1的投票为(1, 0)。由于其它机器还没有启动所以它收不到反馈信息,Server1 的状态一直属于Looking,即属于非服务状态。当第二台服务器 Server2 启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,选举过程如下:

    (1) 每个 Server 发出一个投票。此时 Server1 的投票为(1, 0),Server2 的投票为(2, 0),然后各自将这个投票发给集群中其他机器。

    (2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自 LOOKING 状态的服务器。

    (3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行 PK,PK规则如下:

    a. 优先检查 ZXID。ZXID 比较大的服务器优先作为 Leader。

    b. 如果 ZXID 相同,那么就比较 myid。myid 较大的服务器作为 Leader 服务器。

    对于 Server1 而言,它的投票是(1, 0),接收 Server2 的投票为(2, 0)。其首先会比较两者的 ZXID,均为 0,再比较 myid,此时 Server2 的 myid 最大,于是 Server1 更新自己的投票为(2, 0),然后重新投票。对于 Server2 而言,其无须更新自己的投票,只是再次向集群中所有主机发出上一次投票信息即可。

    (4) 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息。对于 Server1、Server2 而言,都统计出集群中已经有两台主机接受了(2, 0)的投票信息,此时便认为已经选出了新的 Leader,即 Server2。

    (5) 改变服务器状态。一旦确定了 Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为 FOLLOWING,如果是 Leader,就变更为 LEADING

    (6) 添加主机。在新的 Leader 选举出来后 Server3 启动,其想发出新一轮的选举。但由于当前集群中各个主机的状态并不是LOOKING,而是各司其职的正常服务,所以其只能是以Follower 的身份加入到集群中。

  • 宕机后的leader选举

    Zookeeper 运行期间,Leader 与非 Leader 服务器各司其职,即便当有非 Leader 服务器宕机或新加入时也不会影响 Leader。但是若 Leader 服务器挂了,那么整个集群将暂停对外服务,进入新一轮的 Leader 选举,其过程和启动时期的 Leader 选举过程基本一致。

    假设正在运行的有 Server1、Server2、Server3 三台服务器,当前 Leader 是 Server2,若某一时刻 Server2 挂了,此时便开始新一轮的 Leader 选举了。选举过程如下:

    (1) 变更状态。Leader 挂后,余下的非 Observer 服务器都会将自己的服务器状态由FOLLOWING 变更为 LOOKING,然后开始进入 Leader 选举过程。

    (2) 每个 Server 会发出一个投票,仍然会首先投自己。不过,在运行期间每个服务器上的 ZXID 可能是不同,此时假定 Server1 的 ZXID 为 111,Server3 的 ZXID 为 333;在第一轮投票中,Server1 和 Server3 都会投自己,产生投票(1, 111),(3, 333),然后各自将投票发送给集群中所有机器。

    (3) 接收来自各个服务器的投票。与启动时过程相同。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自 LOOKING 状态的服务器。

    (4) 处理投票。与启动时过程相同。针对每一个投票,服务器都需要将别人的投票和自己的投票进行 PK。对于 Server1 而言,它的投票是(1, 111),接收 Server3 的投票为(3, 333)。其首先会比较两者的 ZXID,Server3 投票的 zxid 为 333 大于 Server1 投票的 zxid 的 111,于是Server1 更新自己的投票为(3, 333),然后重新投票。对于 Server3 而言,其无须更新自己的投票,只是再次向集群中所有主机发出上一次投票信息即可。

    (5) 统计投票。与启动时过程相同。对于 Server1、Server2 而言,都统计出集群中已经有两台主机接受了(3, 333)的投票信息,此时便认为已经选出了新的 Leader,即 Server3。

    (6) 改变服务器的状态。与启动时过程相同。一旦确定了 Leader,每个服务器就会更新自己的状态。Server1 变更为 FOLLOWING,Server3 变更为 LEADING。

3.8 高可用集群容灾设计

无论是写操作投票,还是 Leader 选举投票,都必须过半才能通过,也就是说若出现超过半数的主机宕机,则投票永远无法通过。基于该理论,由 5 台主机构成的集群,最多只允许 2 台宕机。而由 6 台构成的集群,其最多也只允许 2 台宕机。即6 台与5 台的容灾能力是相同的。基于此容灾能力的原因,建议使用奇数台主机构成集群,以避免资源浪费

对于一个高可用的系统,除了要设置多台主机部署为一个集群避免单点问题外,还需要考虑将集群部署在多个机房、多个楼宇。对于多个机房、楼宇中集群也是不能随意部署的,下面就多个机房的部署进行分析。在多机房部署设计中,要充分考虑“过半原则”,也就是说,尽量要确保 zk 集群中有过半的机器能够正常运行

  • 三级房部署

    在生产环境下,三机房部署是最常见的、容灾性最好的部署方案

    三机房部署中要求每个机房中的主机数量必须少于集群总数的一半。这样可以保证,三个机房中若有一个机房断电或断网,其它两个机房中的机器总数仍是过半的,集群仍可以正常对外提供服务。当然,若两个机房出现了问题,那么整个集群就瘫痪了。这种情况出现的概率要远低于一个机房出问题的情况。

  • 双机房部署

    zk 官网没有给出较好的双机房部署的容灾方案。只能是让其中一个机房占有超过半数的主机,使其做为主机房,而另一机房少于半数。当然,若主机房出现问题,则整个集群会瘫痪。

3.9 常见应用场景简述

  • 配置维护
  • 命名服务
  • DNS服务
  • 分布式同步
  • 集群管理
  • 分布式锁
  • 分布式队列