Zookeeper应用场景及深入进阶

目录

一. Zookeeper应⽤场景

(一)数据发布/订阅

1. 配置存储

2. 配置获取

3. 配置变更

(二)命名服务

(三)集群管理

1. 基于Agent的分布式集群管理

2. Zookeeper的两⼤特性

3. 分布式⽇志收集系统

(四)Master选举

(五)分布式锁

1. 排他锁

2. 共享锁

3. ⽺群效应

(六)分布式队列

二. Zookeeper深⼊进阶

(一)ZAB协议

1. 概念

2. ZAB核⼼

3. ZAB协议介绍

4. 基本特性

5. 数据同步

6. 运⾏时状态分析

7. ZAB与Paxos的联系和区别

(二)服务器⻆⾊

1. Leader

2. Follower

3. Observer

(三)服务器启动

1. 服务端整体架构图

2. 单机版服务器启动

3. 集群服务器启动

(五)leader选举

1. 服务器启动时期的Leader选举

2. 服务器运⾏时期的Leader选举


一. Zookeeper应⽤场景

ZooKeeper 是⼀个典型的发布 / 订阅模式的分布式数据管理与协调框架,我们可以使⽤它来进⾏分布式数据的发布与订阅。另⼀⽅⾯,通过对 ZooKeeper 中丰富的数据节点类型进⾏交叉使⽤,配合 Watcher 事件通知机制,可以⾮常⽅便地构建⼀系列分布式应⽤中都会涉及的核⼼功能,如数据发布 / 订阅、命名 服务、集群管理、 Master 选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式应⽤场景 来做下介绍。

(一)数据发布/订阅

数据发布 / 订阅( Publish/Subscribe )系统,即所谓的配置中⼼,顾名思义就是发布者将数据发布到ZooKeeper 的⼀个或⼀系列节点上,供订阅者进⾏数据订阅,进⽽达到动态获取数据的⽬的,实现配置 信息的集中式管理和数据的动态更新。
发布 / 订阅系统⼀般有两种设计模式,分别是 推( Push 模式和 拉( Pull 模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;⽽拉模式则是由客户端主动发起请求来获取最新数据,通常 客户端都采⽤定时进⾏轮询拉取的⽅式。
ZooKeeper 采⽤的是推拉相结合的⽅式:客户端向服务端注册⾃⼰需要关注的节点,⼀旦该节点的数据发⽣变更,那么服务端就会向相应的客户端发送 Watcher 事件通知,客户端接收到这个消息通知之后, 需要主动到服务端获取最新的数据。
如果将配置信息存放到 ZooKeeper 上进⾏集中管理,那么通常情况下,应⽤在启动的时候都会主动到ZooKeeper 服务端上进⾏⼀次配置信息的获取,同时,在指定节点上注册⼀个 Watcher 监听,这样⼀ 来,但凡配置信息发⽣变更,服务端都会实时通知到所有订阅的客户端,从⽽达到实时获取最新配置信 息的⽬的。
下⾯我们通过⼀个 配置管理 的实际案例来展示 ZooKeeper 数据发布 / 订阅 场景下的使⽤⽅式。
在我们平常的应⽤系统开发中,经常会碰到这样的需求:系统中需要使⽤⼀些通⽤的配置信息,例如机器列表信息、运⾏时的开关配置、数据库配置信息等。这些全局配置信息通常具备以下 3 个特性。
  • 数据量通常⽐较⼩。
  • 数据内容在运⾏时会发⽣动态变化。
  • 集群中各机器共享,配置⼀致。

 接下来我们就以⼀个数据库切换的应⽤场景展开,看看如何使⽤ZooKeeper来实现配置管理:

1. 配置存储

在进⾏配置管理之前,⾸先我们需要将初始化配置信息存储到 Zookeeper 上去,⼀般情况下,我们可以在 Zookeeper 上选取⼀个数据节点⽤于配置信息的存储,例如: /app1/database_confifig

 配置管理的zookeeper节点示意图

我们将需要管理的配置信息写⼊到该数据节点中去,例如:

# 数据库配置信息
#DBCP
dbcp.driverClassName = com.mysql.jdbc.Driver
dbcp.dbJDBCUrl = jdbc : mysql : //127.0.0.1 : 3306/lagou-test
dbcp.username = zm
dbcp.password = 1234
dbcp.maxActive = 30
dbcp.maxIdle = 10

2. 配置获取

集群中每台机器在启动初始化阶段,⾸先会从上⾯提到的 ZooKeeper 配置节点上读取数据库信息,同时,客户端还需要在该配置节点上注册⼀个数据变更的 Watcher 监听,⼀旦发⽣节点数据变更,所有订 阅的客户端都能够获取到数据变更通知。

3. 配置变更

在系统运⾏过程中,可能会出现需要进⾏数据库切换的情况,这个时候就需要进⾏配置变更。借助
ZooKeeper ,我们只需要对 ZooKeeper 上配置节点的内容进⾏更新, ZooKeeper 就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进⾏最新数据的获取。

(二)命名服务

命名服务( Name Service )也是分布式系统中⽐较常⻅的⼀类场景,是分布式系统最基本的公共服务之⼀。在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等 —— 这些 我们都可以统称它们为名字( Name ),其中较为常⻅的就是⼀些分布式服务框架(如 RPC RMI )中 的服务地址列表,通过使⽤命名服务,客户端应⽤能够根据指定名字来获取资源的实体、服务地址和提 供者的信息等。
ZooKeeper 提供的命名服务功能能够帮助应⽤系统通过⼀个资源引⽤的⽅式来实现对资源的定位与使⽤。另外,⼴义上命名服务的资源定位都不是真正意义的实体资源 —— 在分布式环境中,上层应⽤仅仅 需要⼀个全局唯⼀的名字,类似于数据库中的唯⼀主键。
所以接下来。我们来看看如何使⽤ ZooKeeper 来实现⼀套分布式全局唯⼀ ID 的分配机制
所谓 ID ,就是⼀个能够唯⼀标识某个对象的标识符。在我们熟悉的关系型数据库中,各个表都需要⼀个主键来唯⼀标识每条数据库记录,这个主键就是这样的唯⼀ ID 。在过去的单库单表型系统中,通常可以 使⽤数据库字段⾃带的 auto_increment 属性来⾃动为每条数据库记录⽣成⼀个唯⼀的 ID ,数据库会保证 ⽣成的这个 ID 在全局唯⼀。但是随着数据库数据规模的不断增⼤,分库分表随之出现,⽽ auto_increment 属性仅能针对单⼀表中的记录⾃动⽣成 ID ,因此在这种情况下,就⽆法再依靠数据库的 auto_increment 属性来唯⼀标识⼀条记录了。于是,我们必须寻求⼀种能够在分布式环境下⽣成全局唯 ID 的⽅法。
⼀说起全局唯⼀ ID ,相信⼤家都会联想到 UUID 。没错, UUID 是通⽤唯⼀识别码( Universally
Unique Identififier )的简称,是⼀种在分布式系统中⼴泛使⽤的⽤于唯⼀标识元素的标准 确实, UUID是⼀个⾮常不错的全局唯⼀ ID ⽣成⽅式,能够⾮常简便地保证分布式环境中的唯⼀性。⼀个标准的 UUID 是⼀个包含 32 位字符和 4 个短线的字符串,例如 “e70f1357-f260-46ffff-a32d-53a086c57ade” UUID 的优势⾃然不必多说,我们重点来看看它的缺陷。
(1)⻓度过⻓
UUID 最⼤的问题就在于⽣成的字符串过⻓。显然,和数据库中的 INT 类型相⽐,存储⼀个 UUID 需要花费更多的空间。
(2) 含义不明
上⾯我们已经看到⼀个典型的 UUID 是类似于 “e70f1357-f260-46ffff-a32d-53a086c57ade” 的⼀个字符串。根据这个字符串,开发⼈员从字⾯上基本看不出任何其表达的含义,这将会⼤⼤影响问题排查和开 发调试的效率。
所以接下来,我们结合⼀个分布式任务调度系统来看看如何使⽤ ZooKeepe 来实现这类全局唯⼀ ID 的⽣成。 之前我们已经提到,通过调⽤ ZooKeeper 节点创建的 API 接⼝可以创建⼀个顺序节点,并且在 API 回值中会返回这个节点的完整名字。利⽤这个特性,我们就可以借助 ZooKeeper 来⽣成全局唯⼀的 ID 了,如下图:

全局唯⼀ ID ⽣成的 ZooKeeper 节点示意图
说明,对于⼀个任务列表的主键,使⽤ ZooKeeper ⽣成唯⼀ ID 的基本步骤:
1. 所有客户端都会根据⾃⼰的任务类型,在指定类型的任务下⾯通过调⽤ create ()接⼝来创建⼀个
顺序节点,例如创建 “job-” 节点。
2. 节点创建完毕后, create ()接⼝会返回⼀个完整的节点名,例如 “job-0000000003”
3. 客户端拿到这个返回值后,拼接上 type 类型,例如 “type2-job-0000000003” ,这就可以作为⼀个
全局唯⼀的 ID 了。
ZooKeeper 中,每⼀个数据节点都能够维护⼀份⼦节点的顺序顺列,当客户端对其创建⼀个顺序⼦节点的时候 ZooKeeper 会⾃动以后缀的形式在其⼦节点上添加⼀个序号,在这个场景中就是利⽤了 ZooKeeper 的这个特性。

(三)集群管理

1. 基于Agent的分布式集群管理

随着分布式系统规模的⽇益扩⼤,集群中的机器规模也随之变⼤,那如何更好地进⾏集群管理也显得越来越重要了。所谓集群管理,包括集群监控与集群控制两⼤块,前者侧重对集群运⾏时状态的收集,后 者则是对集群进⾏操作与控制。
在⽇常开发和运维过程中,我们经常会有类似于如下的需求:
  • 如何快速的统计出当前⽣产环境下⼀共有多少台机器
  • 如何快速的获取到机器上下线的情况
  • 如何实时监控集群中每台主机的运⾏时状态
在传统的基于 Agent 的分布式集群管理体系中,都是通过在集群中的每台机器上部署⼀个 Agent ,由这个 Agent 负责主动向指定的⼀个监控中⼼系统(监控中⼼系统负责将所有数据进⾏集中处理,形成⼀系 列报表,并负责实时报警,以下简称 监控中⼼ )汇报⾃⼰所在机器的状态。在集群规模适中的场景 下,这确实是⼀种在⽣产实践中⼴泛使⽤的解决⽅案,能够快速有效地实现分布式环境集群监控,但是 ⼀旦系统的业务场景增多,集群规模变⼤之后,该解决⽅案的弊端也就显现出来了。
(1)⼤规模升级困难
以客户端形式存在的 Agent ,在⼤规模使⽤后,⼀旦遇上需要⼤规模升级的情况,就⾮常麻烦,在升级成本和升级进度的控制上⾯临巨⼤的挑战。
(2) 统⼀的 Agent ⽆法满⾜多样的需求
对于机器的 CPU 使⽤率、负载( Load )、内存使⽤率、⽹络吞吐以及磁盘容量等机器基本的物理状态,使⽤统⼀的 Agent 来进⾏监控或许都可以满⾜。但是,如果需要深⼊应⽤内部,对⼀些业务状态进⾏监 控,例如,在⼀个分布式消息中间件中,希望监控到每个消费者对消息的消费状态;或者在⼀个分布式 任务调度系统中,需要对每个机器上任务的执⾏情况进⾏监控。很显然,对于这些业务耦合紧密的监控 需求,不适合由⼀个统⼀的 Agent 来提供。
(3) 编程语⾔多样性
随着越来越多编程语⾔的出现,各种异构系统层出不穷。如果使⽤传统的 Agent ⽅式,那么需要提供各种语⾔的 Agent 客户端。另⼀⽅⾯, 监控中⼼ 在对异构系统的数据进⾏整合上⾯临巨⼤挑战。

2. Zookeeper的两⼤特性

  1. 客户端如果对 Zookeeper 的数据节点注册 Watcher 监听,那么当该数据节点的内容或是其⼦节点
列表发⽣变更时, Zookeeper 服务器就会向订阅的客户端发送变更通知。
   2. 对在 Zookeeper 上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被
⾃动删除
利⽤其两⼤特性,可以实现集群机器存活监控系统,若监控系统在 /clusterServers 节点上注册⼀个Watcher 监听,那么但凡进⾏动态添加机器的操作,就会在 /clusterServers 节点下创建⼀个临时节 点: /clusterServers/[Hostname] ,这样,监控系统就能够实时监测机器的变动情况。
下⾯通过分布式⽇志收集系统这个典型应⽤来学习 Zookeeper 如何实现集群管理。

3. 分布式⽇志收集系统

分布式⽇志收集系统的核⼼⼯作就是收集分布在不同机器上的系统⽇志,在这⾥我们重点来看分布
式⽇志系统(以下简称 ⽇志系统 )的收集器模块。
在⼀个典型的⽇志系统的架构设计中,整个⽇志系统会把所有需要收集的⽇志机器(我们以 ⽇志源机器 代表此类机器)分为多个组别,每个组别对应⼀个收集器,这个收集器其实就是⼀个后台机器(我们 收集器机器 代表此类机器),⽤于收集⽇志。
对于⼤规模的分布式⽇志收集系统场景,通常需要解决两个问题:
  • 变化的⽇志源机器
在⽣产环境中,伴随着机器的变动,每个应⽤的机器⼏乎每天都是在变化的(机器硬件问题、扩容、机房迁移或是⽹络问题等都会导致⼀个应⽤的机器变化),也就是说每个组别中的⽇志源机器通常是在不断变化
  • 变化的收集器机器
⽇志收集系统⾃身也会有机器的变更或扩容,于是会出现新的收集器机器加⼊或是⽼的收集器机器退出的情况。
 ⽆论是⽇志源机器还是收集器机器的变更,最终都可以归结为如何快速、合理、动态地为每个收集器分配对应的⽇志源机器。这也成为了整个⽇志系统正确稳定运转的前提,也是⽇志收集过程中最⼤的 技术挑战之⼀,在这种情况下,我们就可以引⼊ zookeeper 了,下⾯我们就来看 ZooKeeper 在这个场景 中的使⽤。
使⽤ Zookeeper 的场景步骤如下
注册收集器机器
使⽤ ZooKeeper 来进⾏⽇志系统收集器的注册,典型做法是在 ZooKeeper 上创建⼀个节点作为收集器的根节点,例如 /logs/collector (下⽂我们以 收集器节点 代表该数据节点),每个收集器机器在启动的时 候,都会在收集器节点下创建⾃⼰的节点,例如 /logs/collector/[Hostname]

任务分发
待所有收集器机器都创建好⾃⼰对应的节点后,系统根据收集器节点下⼦节点的个数,将所有⽇志源机器分成对应的若⼲组,然后将分组后的机器列表分别写到这些收集器机器创建的⼦节点(例
/logs/collector/host1 )上去。这样⼀来,每个收集器机器都能够从⾃⼰对应的收集器节点上获取⽇志源机器列表,进⽽开始进⾏⽇志收集⼯作。
状态汇报
完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能。因此,针对这个问题,我们需要有⼀个收集器的状态汇报机制:每个收集器机器在创建完⾃⼰的专属节点后,还需要 在对应的⼦节点上创建⼀个状态⼦节点,例如 /logs/collector/host1/status ,每个收集器机器都需要定 期向该节点写⼊⾃⼰的状态信息。我们可以把这种策略看作是⼀种⼼跳检测机制,通常收集器机器都会 在这个节点中写⼊⽇志收集进度信息。⽇志系统根据该状态⼦节点的最后更新时间来判断对应的收集器 机器是否存活。
动态分配
如果收集器机器挂掉或是扩容了,就需要动态地进⾏收集任务的分配。在运⾏过程中,⽇志系统始终关注着 /logs/collector 这个节点下所有⼦节点的变更,⼀旦检测到有收集器机器停⽌汇报或是有新的收集 器机器加⼊,就要开始进⾏任务的重新分配。⽆论是针对收集器机器停⽌汇报还是新机器加⼊的情况, ⽇志系统都需要将之前分配给该收集器的所有任务进⾏转移。为了解决这个问题,通常有两种做法:
· 全局动态分配
这是⼀种简单粗暴的做法,在出现收集器机器挂掉或是新机器加⼊的时候,⽇志系统需要根据新的收集器机器列表,⽴即对所有的⽇志源机器重新进⾏⼀次分组,然后将其分配给剩下的收集器机器。
· 局部动态分配
全局动态分配⽅式虽然策略简单,但是存在⼀个问题:⼀个或部分收集器机器的变更,就会导致全局动态任务的分配,影响⾯⽐较⼤,因此⻛险也就⽐较⼤。所谓局部动态分配,顾名思义就是在⼩范围内进 ⾏任务的动态分配。在这种策略中,每个收集器机器在汇报⾃⼰⽇志收集状态的同时,也会把⾃⼰的负 载汇报上去。请注意,这⾥提到的负载并不仅仅只是简单地指机器 CPU 负载( Load ),⽽是⼀个对当前 收集器任务执⾏的综合评估,这个评估算法和 ZooKeeper 本身并没有太⼤的关系,这⾥不再赘述。
全局动态分配⽅式虽然策略简单,但是存在⼀个问题:⼀个或部分收集器机器的变更,就会导致全局动态任务的分配,影响⾯⽐较⼤,因此⻛险也就⽐较⼤。所谓局部动态分配,顾名思义就是在⼩范围内进 ⾏任务的动态分配。在这种策略中,每个收集器机器在汇报⾃⼰⽇志收集状态的同时,也会把⾃⼰的负 载汇报上去。请注意,这⾥提到的负载并不仅仅只是简单地指机器 CPU 负载( Load ),⽽是⼀个对当前 收集器任务执⾏的综合评估,这个评估算法和 ZooKeeper 本身并没有太⼤的关系,这⾥不再赘述。
在这种策略中,如果⼀个收集器机器挂了,那么⽇志系统就会把之前分配给这个机器的任务重新分配到那些负载较低的机器上去。同样,如果有新的收集器机器加⼊,会从那些负载⾼的机器上转移部分任务 给这个新加⼊的机器。
上述步骤已经完整的说明了整个⽇志收集系统的⼯作流程,其中有两点注意事项:
节点类型
/logs/collector 节点下创建临时节点可以很好的判断机器是否存活,但是,若机器挂了,其节点会被删除,记录在节点上的⽇志源机器列表也被清除,所以需要选择持久节点来标识每⼀台机器,同时在节 点下分别创建 /logs/collector/[Hostname]/status 节点来表征每⼀个收集器机器的状态,这样,既能实 现对所有机器的监控,同时机器挂掉后,依然能够将分配任务还原。
⽇志系统节点监听
/logs/collector 节点下创建临时节点可以很好的判断机器是否存活,但是,若机器挂了,其节点会被删除,记录在节点上的⽇志源机器列表也被清除,所以需要选择持久节点来标识每⼀台机器,同时在节 点下分别创建 /logs/collector/[Hostname]/status 节点来表征每⼀个收集器机器的状态,这样,既能实 现对所有机器的监控,同时机器挂掉后,依然能够将分配任务还原。
⽇志系统节点监听
若采⽤ Watcher 机制,那么通知的消息量的⽹络开销⾮常⼤,需要采⽤⽇志系统主动轮询收集器节点的策略,这样可以节省⽹络流量,但是存在⼀定的延时。

(四)Master选举

Master 选举是⼀个在分布式系统中⾮常常⻅的应⽤场景。分布式最核⼼的特性就是能够将具有ᇿ⽴计算能⼒的系统单元部署在不同的机器上,构成⼀个完整的分布式系统。⽽与此同时,实际场景中往往也需 要在这些分布在不同机器上的ᇿ⽴系统单元中选出⼀个所谓的 ⽼⼤ ,在计算机中,我们称之为 Master
在分布式系统中, Master 往往⽤来协调集群中其他系统单元,具有对分布式系统状态变更的决定权。例如,在⼀些读写分离的应⽤场景中,客户端的写请求往往是由 Master 来处理的;⽽在另⼀些场景中, Master 则常常负责处理⼀些复杂的逻辑,并将处理结果同步给集群中其他系统单元。 Master 选举可以 说是 ZooKeeper 最典型的应⽤场景了,接下来,我们就结合 ⼀种海量数据处理与共享模型 这个具体例 ⼦来看看 ZooKeeper 在集群 Master 选举中的应⽤场景。
在分布式环境中,经常会碰到这样的应⽤场景:集群中的所有系统单元需要对前端业务提供数据,⽐如⼀个商品 ID ,或者是⼀个⽹站轮播⼴告的⼴告 ID (通常出现在⼀些⼴告投放系统中)等,⽽这些商品 ID 或是⼴告 ID 往往需要从⼀系列的海量数据处理中计算得到 —— 这通常是⼀个⾮常耗费 I/O CPU 资源 的过程。鉴于该计算过程的复杂性,如果让集群中的所有机器都执⾏这个计算逻辑的话,那么将耗费⾮ 常多的资源。⼀种⽐较好的⽅法就是只让集群中的部分,甚⾄只让其中的⼀台机器去处理数据计算,⼀ 旦计算出数据结果,就可以共享给整个集群中的其他所有客户端机器,这样可以⼤⼤减少重复劳动,提 升性能。 这⾥我们以⼀个简单的⼴告投放系统后台场景为例来讲解这个模型。

 整个系统⼤体上可以分成客户端集群、分布式缓存系统、海量数据处理总线和 ZooKeeper四个部分。

⾸先我们来看整个系统的运⾏机制。图中的 Client 集群每天定时会通过 ZooKeeper 来实现 Master 选举。选举产⽣ Master 客户端之后,这个 Master 就会负责进⾏⼀系列的海量数据处理,最终计算得到⼀个数 据结果,并将其放置在⼀个内存 / 数据库中。同时, Master 还需要通知集群中其他所有的客户端从这个 内存 / 数据库中共享计算结果。
接下去,我们将重点来看 Master 选举的过程,⾸先来明确下 Master 选举的需求:在集群的所有机器中选举出⼀台机器作为 Master 。针对这个需求,通常情况下,我们可以选择常⻅的关系型数据库中的主 键特性来实现:集群中的所有机器都向数据库中插⼊⼀条相同主键 ID 的记录,数据库会帮助我们⾃动 进⾏主键冲突检查,也就是说,所有进⾏插⼊操作的客户端机器中,只有⼀台机器能够成功 —— 那么, 我们就认为向数据库中成功插⼊数据的客户端机器成为 Master
借助数据库的这种⽅案确实可⾏,依靠关系型数据库的主键特性能够很好地保证在集群中选举出唯⼀的⼀个 Master 。但是我们需要考虑的另⼀个问题是,如果当前选举出的 Master 挂了,那么该如何处理? 谁来告诉我 Master 挂了呢?显然,关系型数据库没法通知我们这个事件。那么,如果使⽤ ZooKeeper 否可以做到这⼀点呢? 那在之前,我们介绍了 ZooKeeper 创建节点的 API 接⼝,其中⼀个重要特性便 是:利⽤ ZooKeeper 的强⼀致性,能够很好保证在分布式⾼并发情况下节点的创建⼀定能够保证全局唯 ⼀性,即 ZooKeeper 将会保证客户端⽆法重复创建⼀个已经存在的数据节点。也就是说,如果同时有多 个客户端请求创建同⼀个节点,那么最终⼀定只有⼀个客户端请求能够创建成功。利⽤这个特性,就能 很容易地在分布式环境中进⾏ Master 选举了。

 在这个系统中,⾸先会在 ZooKeeper 上创建⼀个⽇期节点,例如“2020-11-11

客户端集群每天都会定时往 ZooKeeper 上创建⼀个临时节点,例如 /master_election/2020-11-
11/binding 。在这个过程中,只有⼀个客户端能够成功创建这个节点,那么这个客户端所在的机器就成为了 Master 。同时,其他没有在 ZooKeeper 上成功创建节点的客户端,都会在节
/master_election/2020-11-11 上注册⼀个⼦节点变更的 Watcher ,⽤于监控当前的 Master 机器是否存活,⼀旦发现当前的 Master 挂了,那么其余的客户端将会重新进⾏ Master 选举。
从上⾯的讲解中,我们可以看到,如果仅仅只是想实现 Master 选举的话,那么其实只需要有⼀个能够保证数据唯⼀性的组件即可,例如关系型数据库的主键模型就是⾮常不错的选择。但是,如果希望能够快 速地进⾏集群 Master 动态选举,那么就可以基于 ZooKeeper 来实现。

(五)分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的⼀种⽅式。如果不同的系统或是同⼀个系统的不同主机之间共享了⼀个或⼀组资源,那么访问这些资源的时候,往往需要通过⼀些互斥⼿段来防⽌彼此之 间的⼲扰,以保证⼀致性,在这种情况下,就需要使⽤分布式锁了。
在平时的实际项⽬开发中,我们往往很少会去在意分布式锁,⽽是依赖于关系型数据库固有的排他性来实现不同进程之间的互斥。这确实是⼀种⾮常简便且被⼴泛使⽤的分布式锁实现⽅式。然⽽有⼀个不争 的事实是,⽬前绝⼤多数⼤型分布式系统的性能瓶颈都集中在数据库操作上。因此,如果上层业务再给 数据库添加⼀些额外的锁,例如⾏锁、表锁甚⾄是繁重的事务处理,那么就会让数据库更加不堪重负
下⾯我们来看看使⽤ ZooKeeper 如何实现分布式锁,这⾥主要讲解排他锁和共享锁两类分布式锁。

1. 排他锁

排他锁( Exclusive Locks ,简称 X 锁),⼜称为写锁或ᇿ占锁,是⼀种基本的锁类型。如果事务 T1 对数据对象 O1 加上了排他锁,那么在整个加锁期间,只允许事务 T1 O1 进⾏读取和更新操作,其他任 何事务都不能再对这个数据对象进⾏任何类型的操作 —— 直到 T1 释放了排他锁
从上⾯讲解的排他锁的基本概念中,我们可以看到,排他锁的核⼼是如何保证当前有且仅有⼀个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到。
下⾯我们就来看看如何借助 ZooKeeper 实现排他锁:
定义锁
在通常的 Java 开发编程中,有两种常⻅的⽅式可以⽤来定义锁,分别是 synchronized 机制和 JDK5 提供的ReentrantLock 。然⽽,在 ZooKeeper 中,没有类似于这样的 API 可以直接使⽤,⽽是通过 ZooKeeper 上的数据节点来表示⼀个锁,例如 /exclusive_lock/lock 节点就可以被定义为⼀个锁,如图:

获取锁
在需要获取排他锁时,所有的客户端都会试图通过调⽤ create ()接⼝,在 /exclusive_lock 节点下创建临时⼦节点 /exclusive_lock/lock 。在前⾯,我们也介绍了, ZooKeeper 会保证在所有的客户端中,最 终只有⼀个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户 端就需要到 /exclusive_lock 节点上注册⼀个⼦节点变更的 Watcher 监听,以便实时监听到 lock 节点的变 更情况。
释放锁
定义锁 部分,我们已经提到, /exclusive_lock/lock 是⼀个临时节点,因此在以下两种情况下,都有可能释放锁。 · 当前获取锁的客户端机器发⽣宕机,那么 ZooKeeper 上的这个临时节点就会被移除。 · 正常执⾏完业务逻辑后,客户端就会主动将⾃⼰创建的临时节点删除。 ⽆论在什么情况下移除了 lock 点, ZooKeeper 都会通知所有在 /exclusive_lock 节点上注册了⼦节点变更 Watcher 监听的客户端。这些 客户端在接收到通知后,再次重新发起分布式锁获取,即重复 获取锁 过程。整个排他锁的获取和释放 流程,如下图:

2. 共享锁

共享锁(Shared Locks,简称S锁),⼜称为读锁,同样是⼀种基本的锁类型。

如果事务 T1 对数据对象 O1 加上了共享锁,那么当前事务只能对 O1 进⾏读取操作,其他事务也只能对这个数据对象加共享锁 —— 直到该数据对象上的所有共享锁都被释放。
共享锁和排他锁最根本的区别在于,加上排他锁后,数据对象只对⼀个事务可⻅,⽽加上共享锁后,数据对所有事务都可⻅。
下⾯我们就来看看如何借助 ZooKeeper 来实现共享锁。
定义锁 和排他锁⼀样,同样是通过 ZooKeeper 上的数据节点来表示⼀个锁,是⼀个类似于
“/shared_lock/[Hostname]- 请求类型 - 序号 的临时顺序节点,例如 /shared_lock/host1-R-
0000000001 ,那么,这个节点就代表了⼀个共享锁,如图所示:

 ② 获取锁

在需要获取共享锁时,所有客户端都会到 /shared_lock 这个节点下⾯创建⼀个临时顺序节点,如果当前是读请求,那么就创建例如 /shared_lock/host1-R-0000000001 的节点;如果是写请求,那么就创建例 /shared_lock/host2-W-0000000002 的节点。
判断读写顺序
通过 Zookeeper 来确定分布式读写顺序,⼤致分为四步
     1. 创建完节点后,获取 /shared_lock 节点下所有⼦节点,并对该节点变更注册监听。
     2. 确定⾃⼰的节点序号在所有⼦节点中的顺序。
     3. 对于读请求:若没有⽐⾃⼰序号⼩的⼦节点或所有⽐⾃⼰序号⼩的⼦节点都是读请求,那么表
明⾃⼰已经成功获取到共享锁,同时开始执⾏读取逻辑,若有写请求,则需要等待。对于写请求:若⾃⼰不
是序号最⼩的⼦节点,那么需要等待。
     4. 接收到 Watcher 通知后,重复步骤 1
释放锁 ,其释放锁的流程与ᇿ占锁⼀致。

3. ⽺群效应

上⾯讲解的这个共享锁实现,⼤体上能够满⾜⼀般的分布式集群竞争锁的需求,并且性能都还可以 ——这⾥说的⼀般场景是指集群规模不是特别⼤,⼀般是在 10 台机器以内。但是如果机器规模扩⼤之后,会 有什么问题呢?我们着重来看上⾯ 判断读写顺序 过程的步骤 3 ,结合下⾯的图,看看实际运⾏中的情况。

 针对如上图所示的情况进⾏分析

1. host1 ⾸先进⾏读操作,完成后将节点 /shared_lock/host1-R-00000001 删除。
2. 余下 4 台机器均收到这个节点移除的通知,然后重新从 /shared_lock 节点上获取⼀份新的⼦节
点列表。
3. 每台机器判断⾃⼰的读写顺序,其中 host2 检测到⾃⼰序号最⼩,于是进⾏写操作,余
下的机器则继续等待。
4. 继续 ...
可以看到, host1 客户端在移除⾃⼰的共享锁后, Zookeeper 发送了⼦节点更变 Watcher 通知给所有机器,然⽽除了给 host2 产⽣影响外,对其他机器没有任何作⽤。⼤量的 Watcher 通知和⼦节点列表获取 两个操作会重复运⾏,这样不仅会对 zookeeper 服务器造成巨⼤的性能影响影响和⽹络开销,更为严重 的是,如果同⼀时间有多个节点对应的客户端完成事务或是事务中断引起节点消失, ZooKeeper 服务器 就会在短时间内向其余客户端发送⼤量的事件通知,这就是所谓的 ⽺群效应
上⾯这个 ZooKeeper 分布式共享锁实现中出现⽺群效应的根源在于,没有找准客户端真正的关注点。我们再来回顾⼀下上⾯的分布式锁竞争过程,它的核⼼逻辑在于:判断⾃⼰是否是所有⼦节点中序号最⼩ 的。于是,很容易可以联想到,每个节点对应的客户端只需要关注⽐⾃⼰序号⼩的那个相关节点的变更 情况就可以了 —— ⽽不需要关注全局的⼦列表变更情况。
可以有如下改动来避免⽺群效应。
改进后的分布式锁实现:
⾸先,我们需要肯定的⼀点是,上⾯提到的共享锁实现,从整体思路上来说完全正确。这⾥主要的改动在于:每个锁竞争者,只需要关注 /shared_lock 节点下序号⽐⾃⼰⼩的那个节点是否存在即可,具体实 现如下。
1. 客户端调⽤ create 接⼝常⻅类似于 /shared_lock/[Hostname]- 请求类型 - 序号的临时顺序节点。
2. 客户端调⽤ getChildren 接⼝获取所有已经创建的⼦节点列表(不注册任何 Watcher )。
3. 如果⽆法获取共享锁,就调⽤ exist 接⼝来对⽐⾃⼰⼩的节点注册 Watcher 。对于读请求:向⽐⾃⼰序号⼩的最后⼀个写请求节点注册 Watcher 监听。对于写请求:向⽐⾃⼰序号⼩的最后⼀个节点注 Watcher 监听。
4. 等待 Watcher 通知,继续进⼊步骤 2
 此⽅案改动主要在于:每个锁竞争者,只需要关注 /shared_lock 节点下序号⽐⾃⼰⼩的那个节点是否存在即可。

注意 相信很多同学都会觉得改进后的分布式锁实现相对来说⽐较麻烦。确实如此,如同在多线程并发编程实践中,我们会去尽量缩⼩锁的范围 —— 对于分布式锁实现的改进其实也是同样的思路。那么对于开 发⼈员来说,是否必须按照改进后的思路来设计实现⾃⼰的分布式锁呢?答案是否定的。在具体的实际 开发过程中,我们提倡根据具体的业务场景和集群规模来选择适合⾃⼰的分布式锁实现:在集群规模不 ⼤、⽹络资源丰富的情况下,第⼀种分布式锁实现⽅式是简单实⽤的选择;⽽如果集群规模达到⼀定程 度,并且希望能够精细化地控制分布式锁机制,那么就可以试试改进版的分布式锁实现。

(六)分布式队列

分布式队列可以简单分为两⼤类: ⼀种是常规的 FIFO 先⼊先出队列模型 ,还有⼀种是 等待队列元素聚集后统⼀安排处理执⾏的 Barrier 模型
FIFO 先⼊先出
FIFO First Input First Output ,先⼊先出), FIFO 队列是⼀种⾮常典型且应⽤⼴泛的按序执⾏的队列模型:先进⼊队列的请求操作先完成后,才会开始处理后⾯的请求。
使⽤ ZooKeeper 实现 FIFO 队列,和之前提到的共享锁的实现⾮常类似。 FIFO 队列就类似于⼀个全写的共享锁模型,⼤体的设计思路其实⾮常简单:所有客户端都会到 /queue_fififo 这个节点下⾯创建⼀个临时 顺序节点,例如如 /queue_fififo/host1-00000001

 创建完节点后,根据如下4个步骤来确定执⾏顺序。

1. 通过调⽤ getChildren 接⼝来获取 /queue_fififo 节点的所有⼦节点,即获取队列中所有的元素。
2. 确定⾃⼰的节点序号在所有⼦节点中的顺序。
3. 如果⾃⼰的序号不是最⼩,那么需要等待,同时向⽐⾃⼰序号⼩的最后⼀个节点注册 Watcher
听。
4. 接收到 Watcher 通知后,重复步骤 1

 Barrier:分布式屏障

Barrier 原意是指障碍物、屏障,⽽在分布式系统中,特指系统之间的⼀个协调条件,规定了⼀个队列的元素必须都集聚后才能统⼀进⾏安排,否则⼀直等待。这往往出现在那些⼤规模分布式并⾏计算的应⽤ 场景上:最终的合并计算需要基于很多并⾏计算的⼦结果来进⾏。这些队列其实是在 FIFO 队列的基础 上进⾏了增强,⼤致的设计思想如下:开始时, /queue_barrier 节点是⼀个已经存在的默认节点,并且 将其节点的数据内容赋值为⼀个数字 n 来代表 Barrier 值,例如 n=10 表示只有当 /queue_barrier 节点下的 ⼦节点个数达到 10 后,才会打开 Barrier 。之后,所有的客户端都会到 /queue_barrie 节点下创建⼀个临 时节点,例如 /queue_barrier/host1 ,如图所示。

 创建完节点后,按照如下步骤执⾏。

1. 通过调⽤ getData 接⼝获取 /queue_barrier 节点的数据内容: 10
2. 通过调⽤ getChildren 接⼝获取 /queue_barrier 节点下的所有⼦节点,同时注册对⼦节点变更的
Watcher 监听。
3. 统计⼦节点的个数。
4. 如果⼦节点个数还不⾜ 10 个,那么需要等待。
5. 接受到 Wacher 通知后,重复步骤 2

. Zookeeper深⼊进阶

(一)ZAB协议

1. 概念

在深⼊了解 zookeeper 之前,很多同学可能会认为 zookeeper 就是 paxos 算法的⼀个实现,但事实上,zookeeper 并没有完全采⽤ paxos 算法,⽽是使⽤了⼀种称为 Zookeeper Atomic Broadcast ZAB Zookeeper 原⼦消息⼴播协议)的协议作为其数据⼀致性的核⼼算法。
ZAB 协议并不像 Paxos 算法那样 是⼀种通⽤的分布式⼀致性算法,它是⼀种特别为 zookeeper 专⻔设计的⼀种 ⽀持崩溃恢复的原⼦⼴播协议
zookeeper 中,主要就是依赖 ZAB 协议来实现分布式数据的⼀致性,基于该协议, Zookeeper 实现了⼀种主备模式的系统架构来保持集群中各副本之间的数据的⼀致性,表现形式就是 使⽤⼀个单⼀的主进 程来接收并处理客户端的所有事务请求,并采⽤ ZAB 的原⼦⼴播协议,将服务器数据的状态变更以事务 Proposal 的形式⼴播到所有的副本进程中, ZAB 协议的主备模型架构保证了同⼀时刻集群中只能够有⼀ 个主进程来⼴播服务器的状态变更,因此能够很好地处理客户端⼤量的并发请求。但是,也要考虑到主 进程在任何时候都有可能出现崩溃退出或重启现象,因此 ,ZAB 协议还需要做到当前主进程当出现上述异 常情况的时候,依旧能正常⼯作。

2. ZAB核⼼

ZAB协议的核⼼是定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理⽅式

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

3. ZAB协议介绍

ZAB协议包括两种基本的模式:崩溃恢复消息⼴播

进⼊崩溃恢复模式:

当整个服务框架启动过程中,或者是 Leader 服务器出现⽹络中断、崩溃退出或重启等异常情况时, ZAB协议就会进⼊崩溃恢复模式,同时选举产⽣新的 Leader 服务器。当选举产⽣了新的 Leader 服务器,同 时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后, ZAB 协议就会退出恢复模式,其 中,所谓的 状态同步 就是指数据同步,⽤来保证集群中过半的机器能够和 Leader 服务器的数据状态保 持⼀致。
进⼊消息⼴播模式:
当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进⼊ 消息⼴播模式, 当⼀台同样遵守 ZAB 协议的服务器启动后加⼊到集群中,如果此时集群中已经存在⼀ Leader 服务器在负责进⾏消息⼴播,那么加⼊的服务器就会⾃觉地进⼊ 数据恢复模式:找到 Leader 所在的服务器,并与其进⾏数据同步,然后⼀起参与到消息⼴播流程中去。 Zookeeper 只允许唯⼀的⼀。
Leader 服务器来进⾏事务请求的处理, Leader 服务器在接收到客户端的事务请求后,会⽣成对应的事务提议并发起⼀轮⼴播协议,⽽如果集群中的其他机器收到客户端的事务请求后,那么这些⾮ Leader 服务器会⾸先将这个事务请求转发给 Leader 服务器。
接下来我们就重点讲解⼀下 ZAB 协议的消息⼴播过程和崩溃恢复过程
消息⼴播
ZAB 协议的消息⼴播过程使⽤原⼦⼴播协议,类似于⼀个⼆阶段提交过程,针对客户端的事务请求,Leader 服务器会为其⽣成对应的事务 Proposal ,并将其发送给集群中其余所有的机器,然后再分别收集 各⾃的选票,最后进⾏事务提交。
ZAB 的⼆阶段提交过程中,移除了中断逻辑,所有的 Follower 服务器要么正常反馈 Leader 提出的事务Proposal ,要么就抛弃 Leader 服务器,同时, ZAB 协议将⼆阶段提交中的中断逻辑移除意味着我们可以 在过半的 Follower 服务器已经反馈 Ack 之后就开始提交事务 Proposal 了,⽽不需要等待集群中所有的 Follower 服务器都反馈响应,但是,在这种简化的⼆阶段提交模型下,⽆法处理因 Leader 服务器崩溃退 出⽽带来的数据不⼀致问题,因此 ZAB 采⽤了崩溃恢复模式来解决此问题,另外,整个消息⼴播协议是 基于具有 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 消息后,也会完成对事务的提 交。
崩溃恢复
ZAB 协议的这个基于原⼦⼴播协议的消息⼴播过程,在正常情况下运⾏⾮常良好,但是⼀旦在 Leader 服务器出现崩溃,或者由于⽹络原因导致 Leader 服务器失去了与过半 Follower 的联系,那么就会进⼊崩溃 恢复模式。在 ZAB 协议中,为了保证程序的正确运⾏,整个恢复过程结束后需要选举出⼀个新的 Leader 服务器,因此, ZAB 协议需要⼀个⾼效且可靠的 Leader 选举算法,从⽽保证能够快速地选举出新的 Leader ,同时, Leader 选举算法不仅仅需要让 Leader ⾃身知道已经被选举为 Leader ,同时还需要让集 群中的所有其他机器也能够快速地感知到选举产⽣出来的新 Leader 服务器。

4. 基本特性

根据上⾯的内容,我们了解到, ZAB 协议规定了如果⼀个事务 Proposal 在⼀台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。接下来我们看看在崩溃恢复过程中,可能 会出现的两个数据不⼀致性的隐患及针对这些情况 ZAB 协议所需要保证的特性。
(1) ZAB 协议需要确保那些已经在 Leader 服务器上提交的事务最终被所有服务器都提交
假设⼀个事务在 Leader 服务器上被提交了,并且已经得到过半 Folower 服务器的 Ack 反馈,但是在它将 Commit 消息发送给所有 Follower 机器之前, Leader 服务器挂了,如图所示

图中的消息 C2 就是⼀个典型的例⼦:在集群正常运⾏过程中的某⼀个时刻, Server1 Leader 服务器,其先后⼴播了消息 P1 P2 C1 P3 C2 ,其中,当 Leader 服务器将消息 C2 C2 Commit Of Proposal2 的缩写,即提交事务 Proposal2 )发出后就⽴即崩溃退出了。针对这种情况, ZAB 协议就需要 确保事务 Proposal2 最终能够在所有的服务器上都被提交成功,否则将出现不⼀致。
(2) ZAB 协议需要确保丢弃那些只在 Leader 服务器上被提出的事务
如果在崩溃恢复过程中出现⼀个需要被丢弃的提案,那么在崩溃恢复结束后需要跳过该事务 Proposal ,如图所示。

在图所示的集群中,假设初始的 Leader 服务器 Server1 在提出了⼀个事务 Proposal3 之后就崩溃退出了,从⽽导致集群中的其他服务器都没有收到这个事务 Proposal3 。于是,当 Server1 恢复过来再次加 ⼊到集群中的时候, ZAB 协议需要确保丢弃 Proposal3 这个事务。
结合上⾯提到的这两个崩溃恢复过程中需要处理的特殊情况,就决定了 ZAB 协议必须设计这样⼀个
Leader 选举算法:能够确保提交已经被 Leader 提交的事务 Proposal ,同时丢弃已经被跳过的事务 Proposal 。针对这个要求,如果让 Leader 选举算法能够保证新选举出来的 Leader 服务器拥有集群中所 有机器最⾼编号(即 ZXID 最⼤)的事务 Proposal ,那么就可以保证这个新选举出来的 Leader ⼀定具有 所有已经提交的提案。更为重要的是,如果让具有最⾼编号事务 Proposal 的机器来成为 Leader ,就可 以省去 Leader 服务器检查 Proposal 的提交和丢弃⼯作的这⼀步操作了。

5. 数据同步

完成 Leader 选举之后,在正式开始⼯作(即接收客户端的事务请求,然后提出新的提案)之前,
Leader 服务器会⾸先确认事务⽇志中的所有 Proposal 是否都已经被集群中过半的机器提交了,即是否完成数据同步。下⾯我们就来看看 ZAB 协议的数据同步过程。
所有正常运⾏的服务器,要么成为 Leader ,要么成为 Follower 并和 Leader 保持同步。 Leader 服务器需要确保所有的 Follower 服务器能够接收到每⼀条事务 Proposal ,并且能够正确地将所有已经提交了的 事务 Proposal 应⽤到内存数据库中去。具体的, Leader 服务器会为每⼀个 Follower 服务器都准备⼀个队 列,并将那些没有被各 Follower 服务器同步的事务以 Proposal 消息的形式逐个发送给 Follower 服务器, 并在每⼀个 Proposal 消息后⾯紧接着再发送⼀个 Commit 消息,以表示该事务已经被提交。等到 Follower 服务器将所有其尚未同步的事务 Proposal 都从 Leader 服务器上同步过来并成功应⽤到本地数 据库中后, Leader 服务器就会将该 Follower 服务器加⼊到真正的可⽤ Follower 列表中,并开始之后的其 他流程。

6. 运⾏时状态分析

ZAB协议的设计中,每个进程都有可能处于如下三种状态之⼀

   · LOOKING Leader 选举阶段。
   · FOLLOWING Follower 服务器和 Leader 服务器保持同步状态。
   · LEADING Leader 服务器作为主进程领导状态。
所有进程初始状态都是 LOOKING 状态,此时不存在 Leader ,接下来,进程会试图选举出⼀个新的
Leader ,之后,如果进程发现已经选举出新的 Leader 了,那么它就会切换到 FOLLOWING 状态,并开始和 Leader 保持同步,处于 FOLLOWING 状态的进程称为 Follower LEADING 状态的进程称为 Leader ,当 Leader 崩溃或放弃领导地位时,其余的 Follower 进程就会转换到 LOOKING 状态开始新⼀轮的 Leader 举。
⼀个 Follower 只能和⼀个 Leader 保持同步, Leader 进程和所有的 Follower 进程之间都通过⼼跳检测
机制来感知彼此的情况。若 Leader 能够在超时时间内正常收到⼼跳检测,那么 Follower 就会⼀直与该Leader 保持连接,⽽如果在指定时间内 Leader ⽆法从过半的 Follower 进程那⾥接收到⼼跳检测,或者 TCP 连接断开,那么 Leader 会放弃当前周期的领导,并转换到 LOOKING 状态,其他的 Follower 也会选择 放弃这个 Leader ,同时转换到 LOOKING 状态,之后会进⾏新⼀轮的 Leader 选举。

7. ZABPaxos的联系和区别

联系:

  ① 都存在⼀个类似于 Leader 进程的⻆⾊,由其负责协调多个 Follower 进程的运⾏。
  ② Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将⼀个提议进⾏提交。
  ③ 在 ZAB 协议中,每个 Proposal 中都包含了⼀个 epoch 值,⽤来代表当前的 Leader 周期,在 Paxos算法中,同样存在这样的⼀个标识,名字为 Ballot
区别:
  Paxos 算法中,新选举产⽣的主进程会进⾏两个阶段的⼯作,第⼀阶段称为读阶段,新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第⼆阶段称为写阶段,当前主进程开始提出⾃ ⼰的提议。
   ZAB 协议在 Paxos 基础上添加了同步阶段,此时,新的 Leader 会确保 存在过半的 Follower 已经提交了之前的 Leader 周期中的所有事务 Proposal 。这⼀同步阶段的引⼊,能够有效地保证 Leader 在新的周 期中提出事务 Proposal 之前,所有的进程都已经完成了对之前所有事务 Proposal 的提交。
  总的来说, ZAB 协议和 Paxos 算法的本质区别在于,两者的设计⽬标不太⼀样, ZAB 协议主要⽤于构建⼀个⾼可⽤的分布式数据主备系统,⽽ Paxos 算法则⽤于构建⼀个分布式的⼀致性状态机系统。

(二)服务器⻆⾊

1. Leader

Leader服务器是Zookeeper集群⼯作的核⼼,其主要⼯作有以下两个:

   (1) 事务请求的唯⼀调度和处理者,保证集群事务处理的顺序性。
   (2) 集群内部各服务器的调度者。
请求处理链
使⽤责任链来处理每个客户端的请求是 Zookeeper 的特⾊, Leader 服务器的请求处理链如下:
可以看到,从 prepRequestProcessor FinalRequestProcessor 前后⼀共 7 个请求处理器组成了 leader服务器的请求处理链
(1) PrepRequestProcessor 。请求预处理器,也是 leader 服务器中的第⼀个请求处理器。在 Zookeeper中,那些会改变服务器状态的请求称为事务请求(创建节点、更新数据、删除节点、创建会话等), PrepRequestProcessor 能够识别出当前客户端请求是否是事务请求。对于事务请求, PrepRequestProcessor 处理器会对其进⾏⼀系列预处理,如创建请求事务头、事务体、会话检查、 ACL 检查和版本检查等。
(2) ProposalRequestProcessor 。事务投票处理器。也是 Leader 服务器事务处理流程的发起者,对于⾮事务性请求, ProposalRequestProcessor 会直接将请求转发到 CommitProcessor 处理器,不再做 任何处理,⽽对于事务性请求,处理将请求转发到 CommitProcessor 外,还会根据请求类型创建对应的 Proposal 提议,并发送给所有的 Follower 服务器来发起⼀次集群内的事务投票。同时,
ProposalRequestProcessor 还会将事务请求交付给 SyncRequestProcessor 进⾏事务⽇志的记录。
(3) SyncRequestProcessor 。事务⽇志记录处理器。⽤来将事务请求记录到事务⽇志⽂件中,同时
会触发 Zookeeper 进⾏数据快照。
(4) AckRequestProcessor 。负责在 SyncRequestProcessor 完成事务⽇志记录后,向 Proposal 的投票收集器发送 ACK 反馈,以通知投票收集器当前服务器已经完成了对该 Proposal 的事务⽇志记录。
(5) CommitProcessor 。事务提交处理器。对于⾮事务请求,该处理器会直接将其交付给下⼀级处理器处理;对于事务请求,其会等待集群内 针对 Proposal 的投票直到该 Proposal 可被提交,利⽤
CommitProcessor ,每个服务器都可以很好地控制对事务请求的顺序处理。
(6) ToBeCommitProcessor 。该处理器有⼀个 toBeApplied 队列,⽤来存储那些已经被
CommitProcessor 处理过的可被提交的 Proposal 。其会将这些请求交付给 FinalRequestProcessor 处理器处理,待其处理完后,再将其从 toBeApplied 队列中移除。
(7) FinalRequestProcessor 。⽤来进⾏客户端请求返回之前的操作,包括创建客户端请求的响应,针对事务请求,该处理器还会负责将事务应⽤到内存数据库中。

2. Follower

Follower服务器是Zookeeper集群状态中的跟随者,其主要⼯作有以下三个:

   (1) 处理客户端⾮事务性请求(读取数据),转发事务请求给 Leader 服务器。
   (2) 参与事务请求 Proposal 的投票。
   (3) 参与 Leader 选举投票。
leader ⼀样, Follower 也采⽤了责任链模式组装的请求处理链来处理每⼀个客户端请求,由于不
需要对事务请求的投票处理,因此 Follower 的请求处理链会相对简单,其处理链如下。

Leader 服务器的请求处理链最⼤的不同点在于, Follower 服务器的第⼀个处理器换成了
FollowerRequestProcessor 处理器,同时由于不需要处理事务请求的投票,因此也没有了
ProposalRequestProcessor 处理器。
(1) FollowerRequestProcessor
其⽤作识别当前请求是否是事务请求,若是,那么 Follower 就会将该请求转发给 Leader 服务器,
Leader 服务器在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进⾏处理。
(2) SendAckRequestProcessor
其承担了事务⽇志记录反馈的⻆⾊,在完成事务⽇志记录后,会向 Leader 服务器发送 ACK 消息以表明⾃身完成了事务⽇志的记录⼯作。

3. Observer

Observer ZooKeeper 3.3.0 版本开始引⼊的⼀个全新的服务器⻆⾊。从字⾯意思看,该服务器充当了⼀个观察者的⻆⾊ —— 其观察 ZooKeeper 集群的最新状态变化并将这些状态变更同步过来。
Observer 服务器在⼯作原理上和 Follower 基本是⼀致的,对于⾮事务请求,都可以进⾏ᇿ⽴的处理,⽽对于事务请求,则会转发给 Leader 服务器进⾏处理。和 Follower 唯⼀的区别在于, Observer 不参与任 何形式的投票,包括事务请求 Proposal 的投票和 Leader 选举投票。简单地讲, Observer 服务器只提供 ⾮事务服务,通常⽤于在不影响集群事务处理能⼒的前提下提升集群的⾮事务处理能⼒。
另外, Observer 的请求处理链路和 Follower 服务器也⾮常相近 , 其处理链如下

另外需要注意的⼀点是,虽然在图中可以看到, Observer 服务器在初始化阶段会将
SyncRequestProcessor 处理器也组装上去,但是在实际运⾏过程中, Leader 服务器不会将事务请求投票发送给 Observer 服务器。

(三)服务器启动

1. 服务端整体架构图

Zookeeper服务器的启动,⼤致可以分为以下五个步骤 

   1. 配置⽂件解析
   2. 初始化数据管理器
   3. 初始化⽹络 I/O 管理器
   4. 数据恢复
   5. 对外服务

2. 单机版服务器启动

单机版服务器的启动其流程图如下:

 上图的过程可以分为预启动初始化过程。

(1) 预启动

     1. 统⼀由 QuorumPeerMain 作为启动类。⽆论单机或集群,在 zkServer.cmd zkServer.sh中都配置了 QuorumPeerMain 作为启动⼊⼝类。
     2. 解析配置⽂件 zoo.cfg zoo.cfg 配置运⾏时的基本参数,如 tickTime dataDir
clientPort 等参数。
     3. 创建并启动历史⽂件清理器 DatadirCleanupManager 。对事务⽇志和快照数据⽂件进⾏定时清理。
     4. 判断当前是集群模式还是单机模式启动。若是单机模式,则委托给 ZooKeeperServerMain 进⾏启动。
     5. 再次进⾏配置⽂件 zoo.cfg 的解析。
     6. 创建服务器实例 ZooKeeperServer Zookeeper 服务器⾸先会进⾏服务器实例的创建,然后对该服务器实例进⾏初始化,包括连接器、内存数据库、请求处理器等组件的初始化。

(2)初始化

     1. 创建服务器统计器 ServerStats ServerStats Zookeeper 服务器运⾏时的统计器。
     2. 创建 Zookeeper 数据管理器 FileTxnSnapLog FileTxnSnapLog Zookeeper 上层服务器和底层数据存储之间的对接层,提供了⼀系列操作数据⽂件的接⼝,如事务⽇志⽂件和快照数据⽂件。 Zookeeper 根据 zoo.cfg ⽂件中解析出的快照数据⽬录 dataDir 和事务⽇志⽬录 dataLogDir 来创建FileTxnSnapLog
     3. 设置服务器 tickTime 和会话超时时间限制。
     4. 创建 ServerCnxnFactory 。通过配置系统属性 zookeper.serverCnxnFactory 来指定使⽤ Zookeeper ⾃⼰实现的 NIO 还是使⽤ Netty 框架作为 Zookeeper 服务端⽹络连接⼯⼚。
     5. 初始化 ServerCnxnFactory Zookeeper 会初始化 Thread 作为 ServerCnxnFactory 的主线程,然后再初始化 NIO 服务器。
     6. 启动 ServerCnxnFactory 主线程。进⼊ Thread run ⽅法,此时服务端还不能处理客户端请求。
     7. 恢复本地数据。启动时,需要从本地快照数据⽂件和事务⽇志⽂件进⾏数据恢复。
     8. 创建并启动会话管理器。 Zookeeper 会创建会话管理器 SessionTracker 进⾏会话管理。
     9. 初始化 Zookeeper 的请求处理链。 Zookeeper 请求处理⽅式为责任链模式的实现。会有多个请求处理器依次处理⼀个客户端请求,在服务器启动时,会将这些请求处理器串联成⼀个请求处理链。
     10. 注册 JMX 服务。 Zookeeper 会将服务器运⾏时的⼀些信息以 JMX 的⽅式暴露给外部。
     11. 注册 Zookeeper 服务器实例。将 Zookeeper 服务器实例注册给 ServerCnxnFactory ,之后 Zookeeper 就可以对外提供服务。
 ⾄此,单机版的 Zookeeper 服务器启动完毕。

3. 集群服务器启动

单机和集群服务器的启动在很多地⽅是⼀致的,其流程图如下:

上图的过程可以分为 预启动、初始化、 Leader 选举、 Leader Follower 启动期交互、 Leader
Follower 启动 等过程。

(1)预启动

 1. 统⼀由QuorumPeerMain作为启动类。

2. 解析配置⽂件zoo.cfg

3. 创建并启动历史⽂件清理器 DatadirCleanupFactory
4. 判断当前是集群模式还是单机模式的启动。在集群模式中,在 zoo.cfg ⽂件中配置了多个服务器 地址,可以选择集群启动。

(2)初始化 

1. 创建 ServerCnxnFactory
2. 初始化 ServerCnxnFactory
3. 创建 Zookeeper 数据管理器 FileTxnSnapLog
4. 创建 QuorumPeer 实例。 Quorum 是集群模式下特有的对象,是 Zookeeper 服务器实例
ZooKeeperServer )的托管者, QuorumPeer 代表了集群中的⼀台机器,在运⾏期间,
QuorumPeer 会不断检测当前服务器实例的运⾏状态,同时根据情况发起 Leader 选举。
5. 创建内存数据库 ZKDatabase ZKDatabase 负责管理 ZooKeeper 的所有会话记录以及
DataTree 和事务⽇志的存储。
6. 初始化 QuorumPeer 。将核⼼组件如 FileTxnSnapLog ServerCnxnFactory ZKDatabase
注册到 QuorumPeer 中,同时配置 QuorumPeer 的参数,如服务器列表地址、 Leader 选举算法和会话超时时间限制等。
7. 恢复本地数据。
8. 启动 ServerCnxnFactory 主线程

(3)Leader选举

1. 初始化 Leader 选举。
集群模式特有, Zookeeper ⾸先会根据⾃身的服务器 ID SID )、最新的
ZXID lastLoggedZxid )和当前的服务器 epoch currentEpoch )来⽣成⼀个初始化投票,在
初始化过程中,每个服务器都会给⾃⼰投票。然后,根据 zoo.cfg 的配置,创建相应 Leader 选举算法
实现, Zookeeper 提供了三种默认算法( LeaderElection AuthFastLeaderElection
FastLeaderElection ),可通过 zoo.cfg 中的 electionAlg 属性来指定,但现只⽀持
FastLeaderElection 选举算法。在初始化阶段, Zookeeper 会创建 Leader 选举所需的⽹络 I/O
QuorumCnxManager ,同时启动对 Leader 选举端⼝的监听,等待集群中其他服务器创建连接。
2. 注册 JMX 服务。
3. 检测当前服务器状态
运⾏期间, QuorumPeer 会不断检测当前服务器状态。在正常情况下, Zookeeper 服务器的状态
LOOKING LEADING FOLLOWING/OBSERVING 之间进⾏切换。在启动阶段, QuorumPeer 的初始
状态是 LOOKING ,因此开始进⾏ Leader 选举。
4. Leader 选举
ZooKeeper Leader 选举过程,简单地讲,就是⼀个集群中所有的机器相互之间进⾏⼀系列投
票,选举产⽣最合适的机器成为 Leader ,同时其余机器成为 Follower 或是 Observer 的集群机器⻆
⾊初始化过程。关于 Leader 选举算法,简⽽⾔之,就是集群中哪个机器处理的数据越新(通常我们根
据每个服务器处理过的最⼤ ZXID 来⽐较确定其数据是否更新),其越有可能成为 Leader 。当然,如
果集群中的所有机器处理的 ZXID ⼀致的话,那么 SID 最⼤的服务器成为 Leader ,其余机器称为
Follower Observer

(4) LeaderFollower启动期交互过程

到这⾥为⽌, ZooKeeper 已经完成了 Leader 选举,并且集群中每个服务器都已经确定了⾃⼰的⻆⾊ ——通常情况下就分为 Leader Follower 两种⻆⾊。下⾯我们来对 Leader Follower 在启动期间的交互 进⾏介绍,其⼤致交互流程如图所示。

1. 创建 Leader 服务器和 Follower 服务器。完成 Leader 选举后,每个服务器会根据⾃⼰服务器的⻆⾊创建相应的服务器实例,并进⼊各⾃⻆⾊的主流程。
2. Leader 服务器启动 Follower 接收器 LearnerCnxAcceptor 。运⾏期间, Leader 服务器需要和所有其余的服务器(统称为 Learner )保持连接以确集群的机器存活情况, LearnerCnxAcceptor 负责接 收所有⾮ Leader 服务器的连接请求。
3. Learner 服务器开始和 Leader 建⽴连接。所有 Learner 会找到 Leader 服务器,并与其建⽴连接。
4. Leader 服务器创建 LearnerHandler Leader 接收到来⾃其他机器连接创建请求后,会创建⼀个LearnerHandler 实例,每个 LearnerHandler 实例都对应⼀个 Leader Learner 服务器之间的连 接,其负责 Leader Learner 服务器之间⼏乎所有的消息通信和数据同步。
5. Leader 注册。 Learner 完成和 Leader 的连接后,会向 Leader 进⾏注册,即将 Learner 服务器的基本信息( LearnerInfo ),包括 SID ZXID ,发送给 Leader 服务器。
6. Leader 解析 Learner 信息,计算新的 epoch Leader 接收到 Learner 服务器基本信息后,会解析出该 Learner SID ZXID ,然后根据 ZXID 解析出对应的 epoch_of_learner ,并和当前 Leader 服务器 epoch_of_leader 进⾏⽐较,如果该 Learner epoch_of_learner 更⼤,则更新 Leader epoch_of_leader = epoch_of_learner + 1 。然后 LearnHandler 进⾏等待,直到过半 Learner 已经 Leader 进⾏了注册,同时更新了 epoch_of_leader 后, Leader 就可以确定当前集群的 epoch 了。
7. 发送 Leader 状态。计算出新的 epoch 后, Leader 会将该信息以⼀个 LEADERINFO 消息的形式发送给 Learner ,并等待 Learner 的响应。
8. Learner 发送 ACK 消息。 Learner 接收到 LEADERINFO 后,会解析出 epoch ZXID ,然后向 Leader反馈⼀个 ACKEPOCH 响应。
9. 数据同步。 Leader 收到 Learner ACKEPOCH 后,即可进⾏数据同步。
10. 启动 Leader Learner 服务器。当有过半 Learner 已经完成了数据同步,那么 Leader Learner 服务器实例就可以启动了

(5)LeaderFollower启动

1. 创建启动会话管理器。
2. 初始化 Zookeeper 请求处理链,集群模式的每个处理器也会在启动阶段串联请求处理链。
3. 注册 JMX 服务。

⾄此,集群版的Zookeeper服务器启动完毕

(五)leader选举

Leader选举概述

Leader选举是zookeeper最重要的技术之⼀,也是保证分布式数据⼀致性的关键所在。

Zookeeper 集群中的⼀台服务器出现以下两种情况之⼀时,需要进⼊ Leader 选举。
   (1) 服务器初始化启动。
   (2) 服务器运⾏期间⽆法和 Leader 保持连接。
下⾯就两种情况进⾏分析讲解。

1. 服务器启动时期的Leader选举

若进⾏ Leader 选举,则⾄少需要两台机器,这⾥选取 3 台机器组成的服务器集群为例。在集群初始化阶段,当有⼀台服务器 Server1 启动时,其单ᇿ⽆法进⾏和完成 Leader 选举,当第⼆台服务器 Server2 启动 时,此时两台机器可以相互通信,每台机器都试图找到 Leader ,于是进⼊ Leader 选举过程。选举过程 如下

(1)每个Server发出⼀个投票

由于是初始情况, Server1 (假设 myid 1 )和 Server2 假设 myid 2 )都会将⾃⼰作为 Leader 服务器来进⾏投票,每次投票会包含所推举的服务器的 myid ZXID ,使⽤ (myid, ZXID) 来表示,此时 Server1 投票为 (1, 0) Server2 的投票为 (2, 0) ,然后各⾃将这个投票发给集群中其他机器

(2)接受来⾃各个服务器的投票

集群的每个服务器收到投票后,⾸先判断该投票的有效性,如检查是否是本轮投票、是否来⾃
LOOKING 状态的服务器。

(3)处理投票

针对每⼀个投票,服务器都需要将别⼈的投票和⾃⼰的投票进⾏ PK PK 规则如下
    · 优先检查 ZXID ZXID ⽐较⼤的服务器优先作为 Leader
    · 如果 ZXID 相同,那么就⽐较 myid myid 较⼤的服务器作为 Leader 服务器。
  现在我们来看 Server1 Server2 实际是如何进⾏投票处理的。对于 Server1 来说,它⾃⼰的投票是( 1 0 ),⽽接收到的投票为( 2 0 )。⾸先会对⽐两者的 ZXID ,因为都是 0 ,所以⽆法决定谁是 Leader 。接下来会对⽐两者的 myid ,很显然, Server1 发现接收到的投票中的 myid 2 ,⼤于⾃⼰,于 是就会更新⾃⼰的投票为( 2 0 ),然后重新将投票发出去。⽽对于 Server2 来说,不需要更新⾃⼰的 投票。

(4)统计投票

每次投票后,服务器都会统计所有投票,判断是否已经有过半的机器接收到相同的投票信息。对于Server1 Server2 服务器来说,都统计出集群中已经有两台机器接受了(
2 0 )这个投票信息。这⾥我们需要对 过半 的概念做⼀个简单的介绍。所谓 过半 就是指⼤于集群机器数量的⼀半,即⼤于或等于 n/2+1 )。对于这⾥由 3 台机器构成的集群,⼤于等于 2 台即为达到 过半 要求。
那么,当 Server1 Server2 都收到相同的投票信息(2 0 )的时候,即认为已经选出了 Leader

(5)改变服务器状态

⼀旦确定了 Leader ,每个服务器就会更新⾃⼰的状态:如果是 Follower ,那么就变更为
FOLLOWING ,如果是 Leader ,那么就变更为 LEADING

2. 服务器运⾏时期的Leader选举

ZooKeeper 集群正常运⾏过程中,⼀旦选出⼀个 Leader ,那么所有服务器的集群⻆⾊⼀般不会再发⽣变化 —— 也就是说, Leader 服务器将⼀直作为集群的 Leader ,即使集群中有⾮ Leader 机器挂了或是有 新机器加⼊集群也不会影响 Leader 。但是⼀旦 Leader 所在的机器挂了,那么整个集群将暂时⽆法对外 服务,⽽是进⼊新⼀轮的 Leader 选举。服务器运⾏期间的 Leader 选举和启动时期的 Leader 选举基本过 程是⼀致的。
我们还是假设当前正在运⾏的 ZooKeeper 机器由 3 台机器组成,分别是 Server1 Server2
Server3 ,当前的 Leader Server2 。假设在某⼀个瞬间, Leader 挂了,这个时候便开始了 Leader 选举。
       (1) 变更状态
Leader 挂后,余下的⾮ Observer 服务器都会将⾃⼰的服务器状态变更为 LOOKING ,然后开始进⼊
Leader 选举过程。
       (2) 每个 Server 会发出⼀个投票
在运⾏期间,每个服务器上的 ZXID 可能不同,此时假定 Server1 ZXID 123 Server3 ZXID 122 ;在第⼀轮投票中, Server1 Server3 都会投⾃⼰,产⽣投票 (1, 123) (3, 122) ,然后各⾃将投票发送给 集群中所有机器。
   (3) 接收来⾃各个服务器的投票,与启动时过程相同
   (4) 处理投票。与启动时过程相同,此时, Server1 将会成为 Leader
   (5) 统计投票。与启动时过程相同
   (6) 改变服务器的状态。与启动时过程相同
本文为拉钩教育课堂笔记整理。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

enterpc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值