共计 6576 个字符,预计需要花费 17 分钟才能阅读完成。
ZooKeeper 是一个开源分布式协调服务、分布式数据一致性解决方案。可基于 ZooKeeper 实现命名服务、集群管理、Master 选举、分布式锁等功能。
一、高可用
为了保证 ZooKeeper 的可用性,在生产环境中我们使用 ZooKeeper 集群模式对外提供服务,并且集群规模至少由 3 个 ZooKeeper 节点组成。
1. 集群至少由 3 个节点组成
ZooKeeper 其实 2 个节点也可以组成集群并对外提供服务,但我们使用集群主要目的是为了高可用。如果 2 个节点组成集群,其中 1 个节点挂了,另外 ZooKeeper 节点不能正常对外提供服务。因此也失去了集群的意义。
如果 3 个节点组成集群,其中 1 个节点挂掉后,根据 ZooKeeper 的 Leader 选举机制是可以从另外 2 个节点选出一个作为 Leader 的,集群可以继续对外提供服务。
2. 并非节点越多越好
- 节点越多,使用的资源越多
- 节点越多,ZooKeeper 节点间花费的通讯成本越高,节点间互连的 Socket 也越多。影响 ZooKeeper 集群事务处理
- 节点越多,造成脑裂的可能性越大
3. 集群规模为奇数
集群规模除了考虑自身成本和资源外还要结合 ZooKeeper 特性考虑:
- 节省资源
3 节点集群和 4 节点集群,我们选择使用 3 节点集群;5 节点集群和 6 节点集群,我们选择使用 5 节点集群。以此类推。因为生产环境为了保证高可用,3 节点集群最多只允许挂 1 台,4 节点集群最多也只允许挂 1 台 (过半原则中解释了原因)。同理 5 节点集群最多允许挂 2 台,6 节点集群最多也只允许挂 2 台。
出于对资源节省的考虑,我们应该使用奇数节点来满足相同的高可用性。
- 集群可用性
当集群中节点间网络通讯出现问题时奇数和偶数对集群的影响
4. 集群配置
ZooKeeper 集群配置至少需要 2 处变更:
01 增加集群配置
在{ZK_HOME}/conf/zoo.cfg 中增加集群的配置,结构以 server.id=ip:port1:port2 为标准。
比如下面配置文件中表示由 3 个 ZooKeeper 组成的集群:
server.1=localhost:2881:3881
server.2=localhost:2882:3882
server.3=localhost:2883:3883
02 配置节点 id
zoo.cfg 中配置集群时需要指定 server.id,这个 id 需要在 dataDir(zoo.cfg 中配置)指定的目录中创建 myid 文件,文件内容就是当前 ZooKeeper 节点的 id。
二、集群角色
ZooKeeper 没有使用 Master/Slave 的概念,而是将集群中的节点分为了 3 类角色:
- Leader
在一个 ZooKeeper 集群中,只能存在一个 Leader,这个 Leader 是集群中事务请求唯一的调度者和处理者,所谓事务请求是指会改变集群状态的请求;Leader 根据事务 ID 可以保证事务处理的顺序性。
如果一个集群中存在多个 Leader,这种现象称为「脑裂」。试想一下,一个集群中存在多个 Leader 会产生什么影响?
相当于原本一个大集群,裂出多个小集群,他们之间的数据是不会相互同步的。「脑裂」后集群中的数据会变得非常混乱。
- Follower
Follower 角色的 ZooKeeper 服务只能处理非事务请求;如果接收到客户端事务请求会将请求转发给 Leader 服务器;参与 Leader 选举;参与 Leader 事务处理投票处理。
Follower 发现集群中 Leader 不可用时会变更自身状态,并发起 Leader 选举投票,最终集群中的某个 Follower 会被选为 Leader。
- Observer
Observer 与 Follower 很像,可以处理非事务请求;将事务请求转发给 Leader 服务器。
与 Follower 不同的是,Observer 不会参与 Leader 选举;不会参与 Leader 事务处理投票。
Observer 用于不影响集群事务处理能力的前提下提升集群的非事务处理能力。
三、Leader 选举
Leader 在集群中是非常重要的一个角色,负责了整个事务的处理和调度,保证分布式数据一致性的关键所在。既然 Leader 在 ZooKeeper 集群中这么重要所以一定要保证集群在任何时候都有且仅有一个 Leader 存在。
如果集群中 Leader 不可用了,需要有一个机制来保证能从集群中找出一个最优的服务晋升为 Leader 继续处理事务和调度等一系列职责。这个过程称为 Leader 选举。
1. 选举机制
ZooKeeper 选举 Leader 依赖下列原则并遵循优先顺序:
01 选举投票必须在同一轮次中进行
如果 Follower 服务选举轮次不同,不会采纳投票。
02 数据最新的节点优先成为 Leader
数据的新旧使用事务 ID 判定,事务 ID 越大认为节点数据约接近 Leader 的数据,自然应该成为 Leader。
03 比较 server.id,id 值大的优先成为 Leader
如果每个参与竞选节点事务 ID 一样,再使用 server.id 做比较。server.id 是节点在集群中唯一的 id,myid 文件中配置。
不管是在集群启动时选举 Leader 还是集群运行中重新选举 Leader。集群中每个 Follower 角色服务都是以上面的条件作为基础推选出合适的 Leader,一旦出现某个节点被过半推选,那么该节点晋升为 Leader。
2. 过半原则
ZooKeeper 集群会有很多类型投票。Leader 选举投票;事务提议投票;这些投票依赖过半原则。就是说 ZooKeeper 认为投票结果超过了集群总数的一半,便可以安全的处理后续事务。
- 事务提议投票
假设有 3 个节点组成 ZooKeeper 集群,客户端请求添加一个节点。Leader 接到该事务请求后给所有 Follower 发起「创建节点」的提议投票。如果 Leader 收到了超过集群一半数量的反馈,继续给所有 Follower 发起 commit。此时 Leader 认为集群过半了,就算自己挂了集群也是安全可靠的。
- Leader 选举投票
假设有 3 个节点组成 ZooKeeper 集群,这时 Leader 挂了,需要投票选举 Leader。当相同投票结果过半后 Leader 选出。
- 集群可用节点
ZooKeeper 集群中每个节点有自己的角色,对于集群可用性来说必须满足过半原则。这个过半是指 Leader 角色 + Follower 角色可用数大于集群中 Leader 角色 + Follower 角色总数。
假设有 5 个节点组成 ZooKeeper 集群,一个 Leader、两个 Follower、两个 Observer。当挂掉两个 Follower 或挂掉一个 Leader 和一个 Follower 时集群将不可用。因为 Observer 角色不参与任何形式的投票。
所谓过半原则算法是说票数 > 集群总节点数 /2。其中集群总节点数 / 2 的计算结果会向下取整。
在 ZooKeeper 源代码 QuorumMaj.java 中实现了这个算法。下面代码片段有所缩减。
public boolean containsQuorum(HashSet<Long> set) {
/** n 是指集群总数 */
int half = n / 2;
return (set.size() > half);
}
回过头我们看一下奇数和偶数集群在 Leader 选举的结果
所以 3 节点和 4 节点组成的集群在 ZooKeeper 过半原则下都最多只能挂 1 节点,但是 4 比 3 要多浪费一个节点资源。
四、场景实战
我们以两个场景来了解集群不可用时 Leader 重新选举的过程。
1.3 节点集群重选 Leader
假设有 3 节点组成的集群,分别是 server.1(Follower)、server.2(Leader)、server.3(Follower)。此时 server.2 不可用了。集群会产生以下变化:
集群不可用
因为 Leader 挂了,集群不可用于事务请求了。
02 状态变更
所有 Follower 节点变更自身状态为 LOOKING,并且变更自身投票。投票内容就是自己节点的事务 ID 和 server.id。我们以 (事务 ID, server.id) 表示。
假设 server.1 的事务 id 是 10,变更的自身投票就是(10, 1);server.3 的事务 id 是 8,变更的自身投票就是(8, 3)。
03 首轮投票
将变更的投票发给集群中所有的 Follower 节点。server.1 将(10, 1)发给集群中所有 Follower,包括它自己。server.3 也一样,将(8, 3)发给所有 Follower。
所以 server.1 将收到(10, 1)和(8, 3)两个投票,server.3 将收到(8, 3)和(10, 1)两个投票。
04 投票 PK
每个 Follower 节点除了发起投票外,还接其他 Follower 发来的投票,并与自己的投票 PK(比较两个提议的事务 ID 以及 server.id),PK 结果决定是否要变更自身状态并再次投票。
对于 server.1 来说收到(10, 1)和(8, 3)两个投票,与自己变更的投票比较后没有一个比自身投票(10, 1)要大的,所以 server.1 维持自身投票不变。
对于 server.3 来说收到(10, 1)和(8, 3)两个投票,与自身变更的投票比较后认为 server.1 发来的投票要比自身的投票大,所以 server.3 会变更自身投票并将变更后的投票发给集群中所有 Follower。
05 第二轮投票
server.3 将自身投票变更为(10, 1)后再次将投票发给集群中所有 Follower。
对于 server.1 来说在第二轮收到了(10, 1)投票,server.1 经过 PK 后继续维持不变。
对于 server.3 来说在第二轮收到了(10, 1)投票,因为 server.3 自身已变更为(10, 3)投票,所以本次也维持不变。
此时 server.1 和 server.3 在投票上达成一致。
06 投票接收桶
节点接收的投票存储在一个接收桶里,每个 Follower 的投票结果在桶内只记录一次。ZooKeeper 源码中接收桶用 Map 实现。
下面代码片段是 ZooKeeper 定义的接收桶,以及向桶内写入数据。Map.Key 是 Long 类型,用来存储投票来源节点的 server.id,Vote 则是对应节点的投票信息。节点收到投票后会更新这个接收桶,也就是说桶里存储了所有 Follower 节点的投票并且仅存最后一次的投票结果。
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
07 统计投票
接收到投票后每次都会尝试统计投票,投票统计过半后选举成功。
投票统计的数据来源于投票接收桶里的投票数据,我们从头描述这个场景,来看一下接收桶里的数据变化情况。
server.2 挂了后,server.1 和 server.3 发起第一轮投票。
server.1 接收到来自 server.1 的(10, 1)投票和来自 server.3 的(8, 3)投票。
server.3 同样接收到来自 server.1 的(10, 1)投票和来自 server.3 的(8, 3)投票。此时 server.1 和 server.3 接收桶里的数据是这样的:
server.3 经过 PK 后认为 server.1 的选票比自己要大,所以变更了自己的投票并重新发起投票。
server.1 收到了来自 server.3 的(10, 1)投票;server.3 收到了来自 sever.3 的(10, 1)投票。此时 server.1 和 server.3 接收桶里的数据变成了这样:
基于 ZooKeeper 过半原则:桶内投票选举 server.1 作为 Leader 出现 2 次,满足了过半 2 > 3/2 即 2>1。
最后 sever.1 节点晋升为 Leader,server.3 变更为 Follower。
2. 集群扩容 Leader 启动时机
ZooKeeper 集群扩容需要在 zoo.cfg 配置文件中加入新节点。扩容流程在 ZooKeeper 扩容中介绍。这里我们以 3 节点扩容到 5 节点时,Leader 启动时机做一个讨论。
假设目前有 3 个节点组成集群,分别是 server.1(Follower)、server.2(Leader)、server.3(Follower),假设集群中节点事务 ID 相同。配置文件如下。
server.1=localhost:2881:3881
server.2=localhost:2882:3882
server.3=localhost:2883:3883
01 新节点加入集群
集群中新增 server.4 和 server.5 两个节点,首先修改 server.4 和 server.5 的 zoo.cfg 配置并启动。节点 4 和 5 在启动后会变更自身投票状态,发起一轮 Leader 选举投票。server.1、server.2、server.3 收到投票后由于集群中已有选定 Leader,所以会直接反馈 server.4 和 server.5 投票结果:server.2 是 Leader。server.4 和 server.5 收到投票后基于过半原则认定 server.2 是 Leader,自身便切换为 Follower。
节点 server.1、server.2、server.3 配置
server.1=localhost:2881:3881
server.2=localhost:2882:3882
server.3=localhost:2883:3883
#节点 server.4、server.5 配置
server.1=localhost:2881:3881
server.2=localhost:2882:3882
server.3=localhost:2883:3883
server.4=localhost:2884:3884
server.5=localhost:2885:3885
02 停止 Leader
server.4 和 server.5 的加入需要修改集群 server.1、server.2、server.3 的 zoo.cfg 配置并重启。但是 Leader 节点何时重启是有讲究的,因为 Leader 重启会导致集群中 Follower 发起 Leader 重新选举。在 server.4 和 server.5 两个新节点正常加入后,集群不会因为新节点加入变更 Leader,所以目前 server.2 依然是 Leader。
我们以一个错误的顺序启动,看一下集群会发生什么样的变化。修改 server.2zoo.cfg 配置文件,增加 server.4 和 server.5 的配置并停止 server.2 服务。停止 server.2 后,Leader 不存在了,集群中所有 Follower 会发起投票。当 server.1 和 server.3 发起投票时并不会将投票发给 server.4 和 server.5,因为在 server.1 和 server.3 的集群配置中不包含 server.4 和 server.5 节点。相反,server.4 和 server.5 会把选票发给集群中所有节点。也就是说对于 server.1 和 server.3 他们认为集群中只有 3 个节点。对于 server.4 和 server.5 他们认为集群中有 5 个节点。
根据过半原则,server.1 和 server.3 很快会选出一个新 Leader,我们这里假设 server.3 晋级成为了新 Leader。但是我们没有启动 server.2 的情况下,因为投票不满足过半原则,server.4 和 server.5 会一直做投票选举 Leader 的动作。截止到现在集群中节点状态是这样的:
03 启动 Leader
现在,我们启动 server.2。因为 server.2zoo.cfg 已经是 server.1 到 serverv.5 的全量配置,在 server.2 启动后会发起选举投票,同时 serverv.4 和 serverv.5 也在不断的发起选举投票。当 server.2 的选举轮次和 serverv.4 与 serverv.5 选举轮次对齐后,最终 server.2 会变更自己的状态,认定 server.5 是 Leaader。
意想不到的事情发生了,出现两个 Leader:
ZooKeeper 集群扩容时,如果 Leader 节点最后启动就可以避免这类问题发生,因为在 Leader 节点重启前,所有的 Follower 节点 zoo.cfg 配置已经是相同的,他们基于同一个集群配置两两互联,做投票选举。
作者:史天舒