最近由于业务需求,测试各种组件的高可用性。由于我们的环境在AWS 北京部署。只有两个Aviable Zone(可用区)。

注释:有两个数据中心,相互需要做容灾的需求,和本文测试的情况是相同的。

而Zookeeper需要3个以上的单数节点同时工作,并且,必须保证半数以上的节点存活,还能正常提供服务。

那么,针对只有两个AZ的情况,不管怎么规划,都有概率遇到存在半数以上的AZ挂掉,导致整个Zookeeper不可用的情况。

Zookeeper 跨区高可用方案_zookeeper

所以,我们能做的就是,在这个AZ挂掉之后,我们怎么尽快处理,并恢复环境。

我们准备两个软件安装好,参数配置好的机器。在可用区1完全挂掉之后,可以手动启动两个备用节点。将可用区2的Zookeeper数量增加过半数。就可以在可用区2恢复Zookeeper的服务。

参考下图:

Zookeeper 跨区高可用方案_multi-az_02

以上的设想,是否能实现呢?

那我们今天就来测试一下。

1. 一共准备了5台机器,作为测试

Zookeeper 跨区高可用方案_multi-az_03

2. Zookeeper的下载与安装。

2.1 Zookeeper官方下载地址

https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/


2.2 下载软件

wget https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz

2.3 详细Zookeeper安装步骤,请参考:

https://blog.51cto.com/hsbxxl/1971241

2.4 zoo.cfg的配置 #cat zoo.cfg

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/data/zookeeper/data
dataLogDir=/data/zookeeper/log
clientPort=2181
autopurge.snapRetainCount=3
autopurge.purgeInterval=6
server.1=172.31.9.73:2888:3888
server.2=172.31.20.233:2888:3888
server.3=172.31.26.111:2888:3888
server.4=172.31.17.68:2888:3888
server.5=172.31.16.33:2888:3888

2.5  根据zoo.cfg创建data和log两个文件夹

mkdir -p /data/zookeeper/data 
mkdir -p /data/zookeeper/log

2.6 根据节点号码,修改文件

echo 1 > /data/zookeeper/data/myid

3. 一共准备了5台EC2进行测试,并且都已经安装好Zookeeper

但是只启动三台,另两个机器作为standby

下图可以看到,已经有三台启动zookeeper,

注意,在Zookeeper启动的过程中,必须保证三台及以上,zookeeper集群才能正常工作

Zookeeper 跨区高可用方案_zookeeper_04

4. 接下来,我开始逐个机器关机,看zookeeper的状态

当前leader在zk3上,我们先关闭zk1,再关闭zk3,看Leader会不会飘到zk2上

4.1 在zk1上执行kill,杀掉进程

[root@ip-172-31-9-73 ~]# jps
12438 Jps
7545 QuorumPeerMain
[root@ip-172-31-9-73 ~]# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /root/zookeeper-3.4.14/bin/../conf/zoo.cfg
Mode: follower
[root@ip-172-31-9-73 ~]# kill -9 7545

4.2 在zk5上通过zkCli链接zk3,并可以查询数据。

在zk1上kill掉进程之后,理论上,还有zk2和zk3存活,但是zkCli的连接显示已经报错。

[root@ip-172-31-16-33 bin]# ./zkCli.sh -server 172.31.26.111:2181
Connecting to 172.31.26.111:2181
......
[zk: 172.31.26.111:2181(CONNECTED) 0] ls /
[zk-permanent, zookeeper, test]
[zk: 172.31.26.111:2181(CONNECTED) 1] 2019-06-23 07:28:06,581 [myid:] - INFO  [main-SendThread(ip-172-31-26-111.cn-north-1.compute.internal:2181):ClientCnxn$SendThread@1158] - Unable to read additional data from server sessionid 0x30000c504530000, likely server has closed socket, closing socket connection and attempting reconnect
......
2019-06-23 07:28:09,822 [myid:] - INFO  [main-SendThread(ip-172-31-26-111.cn-north-1.compute.internal:2181):ClientCnxn$SendThread@1025] - Opening socket connection to server ip-172-31-26-111.cn-north-1.compute.internal/172.31.26.111:2181. Will not attempt to authenticate using SASL (unknown error)
2019-06-23 07:28:09,824 [myid:] - INFO  [main-SendThread(ip-172-31-26-111.cn-north-1.compute.internal:2181):ClientCnxn$SendThread@879] - Socket connection established to ip-172-31-26-111.cn-north-1.compute.internal/172.31.26.111:2181, initiating session
2019-06-23 07:28:09,825 [myid:] - INFO  [main-SendThread(ip-172-31-26-111.cn-north-1.compute.internal:2181):ClientCnxn$SendThread@1158] - Unable to read additional data from server sessionid 0x30000c504530000, likely server has closed socket, closing socket connection and attempting reconnect

4.3 我们继续 kill掉zk3上的进程,只保留zk2上的进程。但是我们已经无法确认zk2是Leader还是Follow,或者说,他是否还保留有数据。

[root@ip-172-31-26-111 bin]# jps
4183 QuorumPeerMain
4648 Jps
[root@ip-172-31-26-111 bin]# kill -9 4183
[root@ip-172-31-26-111 bin]# jps
4658 Jps

4.4 zk3上进程kill掉之后,链接就不只是上面的报错了,而是直接连接拒绝

[root@ip-172-31-16-33 bin]# ./zkCli.sh -server 172.31.26.111:2181
Connecting to 172.31.26.111:2181
......
Welcome to ZooKeeper!
2019-06-23 07:35:18,411 [myid:] - INFO  [main-SendThread(ip-172-31-26-111.cn-north-1.compute.internal:2181):ClientCnxn$SendThread@1025] - Opening socket connection to server ip-172-31-26-111.cn-north-1.compute.internal/172.31.26.111:2181. Will not attempt to authenticate using SASL (unknown error)
JLine support is enabled
2019-06-23 07:35:18,533 [myid:] - INFO  [main-SendThread(ip-172-31-26-111.cn-north-1.compute.internal:2181):ClientCnxn$SendThread@1162] - Socket error occurred: ip-172-31-26-111.cn-north-1.compute.internal/172.31.26.111:2181: Connection refused
[zk: 172.31.26.111:2181(CONNECTING) 0] 2019-06-23 07:35:19,639 [myid:] - INFO  [main-SendThread(ip-172-31-26-111.cn-north-1.compute.internal:2181):ClientCnxn$SendThread@1025] - Opening socket connection to server ip-172-31-26-111.cn-north-1.compute.internal/172.31.26.111:2181. Will not attempt to authenticate using SASL (unknown error)
2019-06-23 07:35:19,640 [myid:] - INFO  [main-SendThread(ip-172-31-26-111.cn-north-1.compute.internal:2181):ClientCnxn$SendThread@1162] - Socket error occurred: ip-172-31-26-111.cn-north-1.compute.internal/172.31.26.111:2181: Connection refused

4.5 可以看到zk2上的进程还在,

# jps
5155 QuorumPeerMain
5211 Jps

4.6 并且通过下面命令,可以检查到zk2 的2181端口还在提供服务

# echo ruok | nc localhost 2181
imok

4.7 但是其他命令是没有正常输出的,只有echo ruok | nc localhost 2181输出ok。

# echo ruok | nc 172.31.16.33 2181
imok[root@ip-172-31-16-33 bin]# echo conf | nc 172.31.16.33 2181
This ZooKeeper instance is not currently serving requests
# echo dump | nc 172.31.16.33 2181
This ZooKeeper instance is not currently serving requests

4.8  ZooKeeper 四字命令

ZooKeeper 四字命令

功能描述

conf

输出相关服务配置的详细信息。

cons

列出所有连接到服务器的客户端的完全的连接 / 会话的详细信息。包括“接受 / 发送”的包数量、会话 id 、操作延迟、最后的操作执行等等信息。

dump

列出未经处理的会话和临时节点。

envi

输出关于服务环境的详细信息(区别于 conf 命令)。

reqs

列出未经处理的请求

ruok

测试服务是否处于正确状态。如果确实如此,那么服务返回“imok ”,否则不做任何相应。

stat

输出关于性能和连接的客户端的列表。

wchs

列出服务器 watch 的详细信息。

wchc

通过 session 列出服务器 watch 的详细信息,它的输出是一个与watch 相关的会话的列表。

wchp

通过路径列出服务器 watch 的详细信息。它输出一个与 session相关的路径。

4.9 正常情况下,以上命令可以输出:

# echo dump | nc 172.31.20.233 2181

SessionTracker dump:
org.apache.zookeeper.server.quorum.LearnerSessionTracker@77714302
ephemeral nodes dump:
Sessions with Ephemerals (0):

# echo conf | nc 172.31.20.233 2181

clientPort=2181
dataDir=/data/zookeeper/data/version-2
dataLogDir=/data/zookeeper/log/version-2
tickTime=2000
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
serverId=2
initLimit=10
syncLimit=5
electionAlg=3
electionPort=3888
quorumPort=2888
peerType=0

# echo envi| nc 172.31.20.233 2181

Environment:
zookeeper.version=3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
host.name=ip-172-31-20-233.cn-north-1.compute.internal
java.version=1.8.0_212
java.vendor=Oracle Corporation
java.home=/usr/java/jdk1.8.0_212-amd64/jre
java.class.path=/root/zookeeper-3.4.14/bin/../zookeeper-server/target/classes:/root/zookeeper-3.4.14/bin/../build/classes:/root/zookeeper-3.4.14/bin/../zookeeper-server/target/lib/*.jar:/root/zookeeper-3.4.14/bin/../build/lib/*.jar:/root/zookeeper-3.4.14/bin/../lib/slf4j-log4j12-1.7.25.jar:/root/zookeeper-3.4.14/bin/../lib/slf4j-api-1.7.25.jar:/root/zookeeper-3.4.14/bin/../lib/netty-3.10.6.Final.jar:/root/zookeeper-3.4.14/bin/../lib/log4j-1.2.17.jar:/root/zookeeper-3.4.14/bin/../lib/jline-0.9.94.jar:/root/zookeeper-3.4.14/bin/../lib/audience-annotations-0.5.0.jar:/root/zookeeper-3.4.14/bin/../zookeeper-3.4.14.jar:/root/zookeeper-3.4.14/bin/../zookeeper-server/src/main/resources/lib/*.jar:/root/zookeeper-3.4.14/bin/../conf:
java.library.path=/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
java.io.tmpdir=/tmp
java.compiler=<NA>
os.name=Linux
os.arch=amd64
os.version=4.14.123-86.109.amzn1.x86_64
user.name=root
user.home=/root
user.dir=/root/zookeeper-3.4.14/bin

5. 这个时候,我去启动另外两个备用节点,zk4,zk5.这个两个节点都是第一次启动。

Zookeeper 跨区高可用方案_multi-az_05

6. 再次连接到zookeeper上,可以看到,至少数据还是没有丢失的

[root@ip-172-31-16-33 bin]# ./zkCli.sh -server 172.31.16.33:2181
Connecting to 172.31.16.33:2181
......
[zk: 172.31.16.33:2181(CONNECTED) 0] ls /
[zk-permanent, zookeeper, test]

7. 通过以上测试,似乎是达到我们预期的结果。唯一的一点小问题,就是:我们有3个节点,为什么关闭1个,剩余两个,就不能正常运行了呢?

其实,这里是有个“想当然”的小问题。

我们以为,只启动三个. 其实,Zookeeper集群,识别的是5个, 为什么呢?

Zookeeper靠什么去识别集群中有几个节点呢?当然不是靠“想当然”。一定是有配置文件告诉它。Zookeeper,只有两个配置文件zoo.cfg和myid。

那就只有zoo.cfg会影响到它了。

8. 我将zoo.cfg做如下修改之后。只开启3个节点,在关闭一个节点之后,还是可以正常运行的。

注释掉server2和server5

# cat zoo.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/data/zookeeper/data
dataLogDir=/data/zookeeper/log
clientPort=2181
autopurge.snapRetainCount=3
autopurge.purgeInterval=6
server.1=172.31.9.73:2888:3888
#server.2=172.31.20.233:2888:3888
server.3=172.31.26.111:2888:3888
server.4=172.31.17.68:2888:3888
#server.5=172.31.16.33:2888:3888

9. 关闭server4之后,还有server2和server3活着。

[root@ip-172-31-26-111 ~]# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /root/zookeeper-3.4.14/bin/../conf/zoo.cfg
Mode: leader
[root@ip-172-31-9-73 ~]# zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /root/zookeeper-3.4.14/bin/../conf/zoo.cfg
Mode: follower

10. 总结,如果考虑两个AZ的情况下,zookeeper节点数多的AZ出现灾难情况,我们如何快速恢复?

(假设Server1/Server2在1AZ,Server3/Server4/Server5在2AZ)

10.1. 在Zookeeper节点少的AZ,多准备2台配置好zookeeper的EC2,并关机待使用。Server4/Server5具体zoo.cfg配置如下

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/data/zookeeper/data
dataLogDir=/data/zookeeper/log
clientPort=2181
autopurge.snapRetainCount=3
autopurge.purgeInterval=6
server.3=172.31.26.111:2888:3888
server.4=172.31.17.68:2888:3888
server.5=172.31.16.33:2888:3888

10.2.  Server1/Server2/Server3,是正常运行的节点,配置如下:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/data/zookeeper/data
dataLogDir=/data/zookeeper/log
clientPort=2181
autopurge.snapRetainCount=3
autopurge.purgeInterval=6
server.1=172.31.9.73:2888:3888
server.2=172.31.20.233:2888:3888
server.3=172.31.26.111:2888:3888

10.3.  灾难发生,Server1/Server2所在的1AZ挂掉的情况下,需要人工介入,将Server3的配置更改为如下配置,并重启Server3的zookeeper服务,然后启动Server4/Server5,一定要先启动Server3,注意顺序。

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/data/zookeeper/data
dataLogDir=/data/zookeeper/log
clientPort=2181
autopurge.snapRetainCount=3
autopurge.purgeInterval=6
server.3=172.31.26.111:2888:3888
server.4=172.31.17.68:2888:3888
server.5=172.31.16.33:2888:3888

10.4 日常运行状态

Zookeeper 跨区高可用方案_zookeeper_06

10.5 检查已经创建的znode信息

./zkCli.sh -server 172.31.16.33:2181 ls /
Connecting to 172.31.16.33:2181
[zk-permanent, zookeeper, test]

10.6 关闭Server1/Server2,注意顺序,先关闭follow,如果先关闭leader,会发生切换。我们期望的是Server3最后以follow的身份存活。


11. 最终可以看到测试结果,一切都是按照我们“想当然”的方向发展。

Zookeeper 跨区高可用方案_zookeeper_07

12. 最后验证zookeeper中的znode数据,还是都存在的。

./zkCli.sh  -server 172.31.16.33:2181 ls /
Connecting to 172.31.16.33:2181
[zk-permanent, zookeeper, test]

13. 其实数据一直是在这个路径下,只要有一个节点还保留,就会保存下去。

# ls /data/zookeeper/data/
myid  version-2  zookeeper_server.pid

注意:一定要保证Server4/Server5的下面两个路径是空的,不然会出现,Server4/Server5识别的是之前的陈旧信息。

/data/zookeeper/data/version-2
/data/zookeeper/log/version-2

14. 说到这里,我们可以理解到,Zookeeper的全部数据,都是存放在下面两个路径中。如果需要做备份,可以直接在OS层面,做cp备份即可。

dataDir=/data/zookeeper/data
dataLogDir=/data/zookeeper/log

衍生一个想法,就是如果想做跨Region,北京(主环境)到宁夏(容灾环境)的zookeeper的高可用怎么做呢?

我们可以考虑将北京的zookeeper的数据文件定期备份,并导入到宁夏的环境。

具体步骤:

<1. 在宁夏启动一个Zookeeper集群,并配置好,然后关闭zookeeper服务,清空掉数据文件夹。

<2. 在北京,通过脚本定期检查zookeeper各个节点状态,从一个运行健康的节点,定期备份数据到S3的一个bucket,为每个文件加上时间戳。

<3. 通过S3的Cross Region Replication,同步到宁夏。

<4. 然后在宁夏,从S3读取备份文件,并还原到灾备的zookeeper中。