一、简介
ZooKeeper 是用来协调(同步)分布式进程的服务,提供了一个简单高性能的协调内核,用户可以在此之上构建更多复杂的分布式协调功能。脑裂通常会出现在集群环境中,比如 ElasticSearch、Zookeeper 集群,而这些集群环境有一个统一的特点,就是它们有一个大脑,比如 ElasticSearch 集群中有 Master 节点,Zookeeper 集群中有 Leader 节点。
二、奇数节点
zookeeper 容错指的是:当宕掉几个 zookeeper 节点服务器之后,剩下的个数必须大于宕掉的个数,也就是剩下的节点服务数必须大于 n/2,这样 zookeeper 集群才可以继续使用,无论奇偶数都可以选举 leader。例如5台 zookeeper 节点机器最多宕掉 2 台,还可以继续使用,因为剩下 3 台大于 5/2。那么为什么最好为奇数个节点呢?是在以最大容错服务器个数的条件下,会节省资源。比如,最大容错为 2 的情况下,对应的zookeeper 服务数,奇数为5,而偶数为6,也就是6个zookeeper 服务的情况下最多能宕掉2个服务,所以从节约资源的角度看,没必要部署6(偶数)个 zookeeper 服务节点。
zookeeper 集群有这样一个特性:集群中只要有过半的机器是正常工作的,那么整个集群对外就是可用的。也就是说如果有 2 个 zookeeper 节点,那么只要有1个zookeeper 节点死了,那么 zookeeper 服务就不能用了,因为1没有过半,所以2个 zookeeper 的死亡容忍度为0;同理,要是有3个 zookeeper,一个死了,还剩下2个正常的,过半了,所以3个 zookeeper 的容忍度为1;同理也可以多列举几个:2->0; 3->1; 4->1; 5->2; 6->2 就会发现一个规律,2n 和 2n-1 的容忍度是一样的,都是 n-1,所以为了更加高效,何必增加那一个不必要的 zookeeper 呢。
根据以上可以得出结论:从资源节省的角度来考虑,zookeeper 集群的节点最好要部署成奇数个。
三、集群脑裂场景说明
对于一个集群,想要提高这个集群的可用性,通常会采用多机房部署,比如现有一个由6台 zkServer 所组成的一个集群,部署在了两个机房:
正常情况下,此集群只会有一个 Leader,那么如果机房之间的网络断了之后,两个机房内的 zkServer 还是可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个 Leader。
这就相当于原本一个集群,被分成了两个集群,出现了两个"大脑",这就是所谓的"脑裂"现象。对于这种情况,其实也可以看出来,原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么解决等等问题。刚刚在说明脑裂场景时有一个前提条件就是没有考虑过半机制,所以实际上 Zookeeper 集群中是不会轻易出现脑裂问题的,原因在于过半机制。
zookeeper 的过半机制:在领导者选举的过程中,如果某台 zkServer 获得了超过半数的选票,则此 zkServer 就可以成为 Leader 了。
举个简单的例子:如果现在集群中有 5 台 zkServer,那么 half=5/2=2,那么也就是说,领导者选举的过程中至少要有三台 zkServer 投了同一个 zkServer,才会符合过半机制,才能选出来一个 Leader。
问题1:zookeeper 选举的过程中为什么一定要有一个过半机制验证?因为这样不需要等待所有 zkServer 都投了同一个 zkServer 就可以选举出来一个Leader 了,这样比较快,所以叫快速领导者选举算法。
问题2:过半机制中为什么是大于,而不是大于等于呢?这就是更脑裂问题有关系了,比如回到上文出现脑裂问题的场景。
当机房中间的网络断掉之后,机房1内的三台服务器会进行领导者选举,但是此时过半机制的条件是 "节点数 > 3",也就是说至少要4台 zkServer 才能选出来一个 Leader,所以对于机房1来说它不能选出一个 Leader,同样机房2也不能选出一个 Leader,这种情况下整个集群当机房间的网络断掉后,整个集群将没有 Leader。
而如果过半机制的条件是 "节点数 >= 3",那么机房1和机房2都会选出一个Leader,这样就出现了脑裂。这就可以解释为什么过半机制中是大于而不是大于等于,目的就是为了防止脑裂。
假设我们现在只有5台机器,也部署在两个机房:
此时过半机制的条件是 "节点数 > 2",也就是至少要3台服务器才能选出一个 Leader,此时机房件的网络断开了,对于机房1来说是没有影响的,Leader 依然还是 Leader,对于机房2来说是选不出来 Leader 的,此时整个集群中只有一个Leader。所以可以简单总结得出,有了过半机制,对于一个 Zookeeper 集群来说,要么没有 Leader,要么只有1个 Leader,这样 zookeeper 也就能避免了脑裂问题。
四、集群脑裂问题处理
4.1、什么是脑裂
简单点来说,脑裂 (Split-Brain) 就是比如当你的 cluster 里面有两个节点,它们都知道在这个 cluster 里需要选举出一个 master。那么当它们两个之间的通信完全没有问题的时候,就会达成共识,选出其中一个作为 master。但是如果它们之间的通信出了问题,那么两个结点都会觉得现在没有 master,所以每个都把自己选举成 master,于是 cluster 里面就会有两个 master。
对于 Zookeeper 来说有一个很重要的问题,就是到底是根据一个什么样的情况来判断一个节点死亡 down 掉了。 在分布式系统中这些都是有监控者来判断的,但是监控者也很难判定其他的节点的状态,唯一一个可靠的途径就是心跳,Zookeeper 也是使用心跳来判断客户端是否仍然活着。
使用 ZooKeeper 来做 Leader HA 基本都是同样的方式:每个节点都尝试注册一个象征 leader 的临时节点,其他没有注册成功的则成为 follower,并且通过 watch 机制 (这里有介绍) 监控着leader所创建的临时节点,Zookeeper 通过内部心跳机制来确定 leader 的状态,一旦 leader 出现意外 Zookeeper 能很快获悉并且通知其他的 follower,其他 flower 在之后作出相关反应,这样就完成了一个切换,这种模式也是比较通用的模式,基本大部分都是这样实现的。但是这里面有个很严重的问题,如果注意不到会导致短暂的时间内系统出现脑裂,因为心跳出现超时可能是 leader 挂了,但是也可能是 zookeeper 节点之间网络出现了问题,导致 leader 假死的情况,leader其实并未死掉,但是与 ZooKeeper 之间的网络出现问题导致Zookeeper 认为其挂掉了然后通知其他节点进行切换,这样 follower 中就有一个成为了 leader,但是原本的 leader 并未死掉,这时候 client 也获得 leader 切换的消息,但是仍然会有一些延时,zookeeper 需要通讯需要一个一个通知,这时候整个系统就很混乱可能有一部分 client 已经通知到了连接到新的 leader 上去了,有的 client 仍然连接在老的 leader 上,如果同时有两个 client 需要对 leader 的同一个数据更新,并且刚好这两个 client 此刻分别连接在新老的 leader 上,就会出现很严重问题。
这里做下小总结:假死:由于心跳超时(网络原因导致的)认为 leader 死了,但其实 leader 还存活着。脑裂:由于假死会发起新的 leader 选举,选举出一个新的 leader,但旧的 leader 网络又通了,导致出现了两个 leader ,有的客户端连接到老的 leader,而有的客户端则连接到新的 leader。
4.2、原因
主要原因是 Zookeeper 集群和 Zookeeper client 判断超时并不能做到完全同步,也就是说可能一前一后,如果是集群先于 client 发现,那就会出现上面的情况。同时,在发现并切换后通知各个客户端也有先后快慢。一般出现这种情况的几率很小,需要 leader 节点与 Zookeeper 集群网络断开,但是与其他集群角色之间的网络没有问题,还要满足上面那些情况,但是一旦出现就会引起很严重的后果,数据不一致。
4.3、如何解决
要解决 Split-Brain 脑裂的问题,一般有下面几种种方法:
- Quorums (法定人数) 方式: 比如3个节点的集群,Quorums = 2, 也就是说集群可以容忍1个节点失效,这时候还能选举出1个 lead,集群还可用。比如4个节点的集群,它的 Quorums = 3,Quorums 要超过3,相当于集群的容忍度还是1,如果2个节点失效,那么整个集群还是无效的。这是 zookeeper 防止"脑裂"默认采用的方法。
- 采用 Redundant communications (冗余通信)方式:集群中采用多种通信方式,防止一种通信方式失效导致集群中的节点无法通信。
- Fencing (共享资源) 方式:比如能看到共享资源就表示在集群中,能够获得共享资源的锁的就是 Leader,看不到共享资源的,就不在集群中。
- 仲裁机制方式。
- 启动磁盘锁定方式。
要想避免 zookeeper"脑裂"情况其实也很简单,在 follower 节点切换的时候不在检查到老的 leader 节点出现问题后马上切换,而是在休眠一段足够的时间,确保老的 leader 已经获知变更并且做了相关的 shutdown 清理工作了然后再注册成为 master 就能避免这类问题了,这个休眠时间一般定义为与 zookeeper 定义的超时时间就够了,但是这段时间内系统可能是不可用的,但是相对于数据不一致的后果来说还是值得的。
4.4、方案
1、Quorums
zooKeeper 默认采用了 Quorums 这种方式来防止"脑裂"现象,即只有集群中超过半数节点投票才能选举出 Leader。这样的方式可以确保 leader 的唯一性,要么选出唯一的一个 leader,要么选举失败。在 zookeeper 中 Quorums 有3个作用:
集群中最少的节点数用来选举 leader 保证集群可用。
通知客户端数据已经安全保存前集群中最少数量的节点数已经保存了该数据。一旦这些节点保存了该数据,客户端将被通知已经安全保存了,可以继续其他任务。而集群中剩余的节点将会最终也保存了该数据。
假设某个 leader 假死,其余的 followers 选举出了一个新的 leader。这时,旧的leader 复活并且仍然认为自己是 leader,这个时候它向其他 followers 发出写请求也是会被拒绝的。因为每当新 leader 产生时,会生成一个 epoch 标号(标识当前属于那个 leader 的统治时期),这个epoch是递增的,followers 如果确认了新的leader 存在,知道其 epoch,就会拒绝 epoch 小于现任 leader epoch 的所有请求。那有没有 follower 不知道新的 leader 存在呢,有可能,但肯定不是大多数,否则新 leader 无法产生。Zookeeper 的写也遵循 quorum 机制,因此,得不到大多数支持的写是无效的,旧 leader 即使各种认为自己是 leader,依然没有什么作用。
2、冗余心跳线
添加冗余的心跳线,例如双线条线,尽量减少“裂脑”发生机会。
3、启用磁盘锁
正在服务一方锁住共享磁盘,"裂脑"发生时,让对方完全"抢不走"共享磁盘资源。但使用锁磁盘也会有一个不小的问题,如果占用共享盘的一方不主动"解锁",另一方就永远得不到共享磁盘。现实中假如服务节点突然死机或崩溃,就不可能执行解锁命令。后备节点也就接管不了共享资源和应用服务。于是有人在 HA 中设计了"智能"锁。即正在服务的一方只在发现心跳线全部断开(察觉不到对端)时才启用磁盘锁。平时就不上锁了。
4、设置仲裁机制
例如设置参考 IP(如网关 IP),当心跳线完全断开时,2个节点都各自 ping 一下 参考 IP,不通则表明断点就出在本端,不仅"心跳"、还兼对外"服务"的本端网络链路断了,即使启动(或继续)应用服务也没有用了,那就主动放弃竞争,让能够 ping 通参考 IP 的一端去起服务。更保险一些,ping 不通参考 IP 的一方干脆就自我重启,以彻底释放有可能还占用着的那些共享资源。
评论