构建具有跨域容灾能力的Zookeeper服务
Zookeeper是Apache的顶级项目,为应用提供高效、高可用的分布式协调服务,包括数据发布/订阅、负载均衡、命名服务、分布式协调/通知及分布式锁等。其具有使用便捷、性能高及稳定性良好的特点,被广泛使用在很多的开源组件中,例如Kafka、Hadoop、Dubbo等,这些服务使用Zookeeper作为配置或者注册中心等。
1、跨域容灾的基本概念
高可用性是计算机系统比较重要的运行指标,系统在运行过程要因为避免软硬件故障造成服务不可用。保证系统高可用性的架构设计的核心准则是组件的冗余及故障的自动转移,除了通过多个节点构成集群来实现冗余外,还要考虑集群要在多个机房、多区域(同城及异地)等实现灾备等多个要素,保证从单个节点到数据中心故障甚至在自然灾难情况造成区域不可用情况下服务可以正常运行。
目前有多种高可用方案,包括集群的分布式架构、同城多机房、异地多机房等,其中”两地三中心”架构是目前业界典型的保证高可用性的跨地域容灾方案。
两地三中心包括了生产中心、同城灾备中心和异地灾备中心,其中:
生产中心,对外提供服务
同城灾备中心,第一级容灾保护,通常在离生产中心几十公里距离建立的同城灾备中心,应用可在不丢失数据的情况下切换到同城灾备中心
异地灾备中心,在离生产中心几百或上千公里外建立的异地灾备中心,应对区域性重大灾难,实现周期性异步复制灾备,这是两地三中心容灾方案的第二级容灾保护
在两地三中心方案中核心的问题包括:数据同步、网络时延及故障切换等问题
1)数据同步,各中心中数据要通过同步来保证一致性,可以分为实时同步及异步同步两种方式,要根据场景选择不同的方式,例如金融类的场景要实现实时,但是数据分析类的应用可接收数据有一定的延迟来选择异步。
2)网络时延,同城机房时延在1ms~3ms,异地灾备中心通常与生产中心的距离较远时延会达到百毫秒。在业务运行过程中要跨机房的服务调用会给业务的可用性带来较大挑战。
3)故障切换与恢复,对于高可用性要求高的服务要求故障时快速切换,手动进行服务切换无法满足高可用的要求,故障后要实现业务的自动切换,保证最小化影响时间。
2.系统架构
Apache Zookeeper是分布式集群服务,由一组主机组成。在生产环境中通常由3台以上组成高可用的一致性分布式系统,系统架构如下所示:
客户端程序会选择和集群中任意一台服务器创建TCP连接,并且客户端和服务器断开连接后可以连接其他服务器。在集群中Zookeeper Server分为三种角色:
Leader,由选举产生,负责进行投票的发起和决议,更新系统状态
Follower,用于接受客户端请求并向客户端返回结果,在选举过程中参与投票。可以将Follower配置为只读服务(readonlymode.enabled)
Observer,为了扩展系统并提高读取速度,接受客户端连接及读写请求,并将写请求转发给Leader,但Observer不参与投票过程,只同步Leader的状态。
客户端(Client),请求的发起方
Zookeeper配置多个实例共同构成一个集群来对外提高服务以达到水平扩展的目的,每个实例中的数据是相同的,并且均可以对外提供读写服务,对于客户端来说每个服务器实例是相同的。
2.1 选举过程
在Zookeeper中客户端可以读取任意服务实例的数据,因此要保证集群中每个实例中数据的一致性,客户端的请求分为两种:事务性的会话请求,即更改集群的数据的请求,包括节点的创建、节点数据的写入、ACL的配置、节点的删除等;读类型请求,读取节点、读取节点数据、获取ACL信息、配置节点Watch等,事务性的会话请求影响集群中实例的数据的一致性。
对于事务性的会话请求,Zookeeper集群的服务端会将请求统一转发给Leader服务器进行操作,Leader服务器内部执行该事务性的会话请求的过程中会将数据同步写到其他角色服务器,从而保证会话请求的执行顺序,进而保证整个Zookeeper集群的数据一致性。因此Zookeeper集群中Leader是保证分布式数据一致性的关键所在,集群中的服务实例启动后由选举机制来决定Leader,当Leader服务器宕机后,整个集群将暂停对外服务,从而进入新一轮Leader选举。Zookeeper使用ZAB协议(Paxos算法的扩展)作为数据一致性算法,在协议中有以下状态:
Looking,系统刚启动或者Leader崩溃后正在处于选举状态
Following,Follower节点所处状态,与Leader进行数据同步
Leading,Leader所处状态,当前集群中有一个Leader为主进程
Observing,观察状态,同步Leader的状态,但是不参与选举
节点状态之间的转换如下图所示:
Zookeeper启动时所有节点的初始状态为Looking或者Observing,这个时候集群会尝试选举一个Leader节点(Observer节点不参与选举)。选举出的Leader节点切换为Leading状态,当节点发现集群中已经选举出Leader则该节点会切换到Following状态,然后和Leader节点保持同步。
当Follower节点与Leader失去联系时Follower节点会切换到Looking状态,进入下一轮选举。在Zookeeper的整个生命周期中每个节点会在Looking、Following及Leading状态之间不断转换,选举过程如下图所示:
选举开始后处于Looking状态的节点向其他节点发送选举自己为Leader的请求信息,在信息中包括:
服务器ID,编号越大则在选举算法中的权重越大
数据ID,存放的最大数据ID,值越大则权重越大
逻辑时钟(Epoch),投票的次数,同一轮投票过程中的逻辑时钟值是相同的,每投完一轮则这个数据就会增加,根据接收到的其他服务器返回的投票信息中的对比,根据不同的值做出不同的判断
选举状态,节点所处的状态
算法核心是所有实例发起投票的过程,当某节点获取的投票数超过半数,则成为Leader,一种极端情况是偶数个节点时两个服务实例都获取半数投票,就会发生脑裂的情况,因此在构建集群时启动奇数个实例,保证会仅有一个实例称为Leader。
当集群中Leader服务器出现宕机或者不可用情况时,整个集群无法对外提供服务,则要进入下一轮Leader选举过程,非Observer服务器将自身服务器状态变更为Leader,选举过程与初始启动后进行的选举过程相同。
2.2 事务性会话请求处理流程
客户端向集群中任意实例发起事务性会话请求后,该实例会将请求转发至Leader,由Leader进行请求的处理,执行流程如下图所示:
客户端进行事务性(写数据)请求时会指定Zookeeper集群中Server节点,如果该节点为Follower或者Observer节点,则将写请求转发给Leader。Leader通过内部的协议进行原子广播,直到一半以上的Server节点都成功写入数据,这次写请求便算是成功,然后Leader会通知相应的Follower节点写请求成功,该节点向Client返回写入成功响应。
2.3 读请求数据处理
读流程比较简单,由于Zookeeper集群中所有Server节点都拥有相同的数据,所以读取时可以在任意Server点上,读取过程如下图所示:
客户端连接到集群中某个节点,获取数据后直接返回
3.跨域容灾方案
Zookeeper集群有多个服务实例,超过一半节点正常工作,则集群就能够正常运行,服务本身就是高可用的。但是要考虑数据中心(机房)故障时如何保证整个集群能够正常对外提供服务。解决多数据中心容灾的方案有单机房增加节点、同城多机房、两地三中心(异地容灾)等,针对以上几种方案的对比,如下表所示(后面篇幅会进行详尽分析):
方案 |
优点 |
缺点 |
备注 |
单机房多节点 |
难度易 |
无容灾 |
不可行 |
同城双机房 |
难度易,无时延 |
无法保证可用性 |
不可行 |
同城多机房 |
难度易,无时延 |
无异地容灾 |
可行,但是无异地容灾 |
两地三中心 (统一集群) |
难度中等 满足高可用的要求 |
1)网络情况复杂,当时延较大时可能出现不可用的情况 2)性能稍差 |
可行,经测试网络时延不影响集群运行 |
两地三中心(Observer) |
难度中等 满足高可用的要求 |
性能比统一集群的方式高,但是 生产中心故障后Zookeeper集群不可用,需要手动进行Observer切换 |
不可行 |
两地三中心 (独立集群) |
难度高 满足高可用 |
引入集群外数据同步工具 无自动故障切换 |
可行,但是实现复杂 |
其中同城多机房、两地三中心跨地域部署及独立部署方案,均可以达到跨域容灾的目的,目前Zookeeper 原生的客户端SDK在选定zk节点不可达时不会进行failover到可用节点,因此使用zookeeper作为基础服务的服务都会进行扩展,包括定制化HostProvider、使用框架curator或者在zk连不上时重试其他节点等实现自动故障切换。
1)单机房多节点
对于单机房增加节点将其部署在不同的机柜中可以一定程度提高高可用性,避免多个节点同时出现问题
节点选举及事务性请求都需要半数以上成功则算是成功,因此多增加节点对写请求及选举过程的影响都比较大,并且这种方案不能解决整个机房出现故障的容灾问题。
2)同城多机房
单机房做不到容灾,在同城进行多机房的部署,如下图所示:
多机房部署要考虑某机房故障情况下,保证有半数以上的Zookeeper节点正常工作,如果仅两个机房的情况下无法保证,上例中三机房部署是可以实现任意一个机房故障情况下,满足正常节点上大于半数以上,保证机房容灾。
3)两地三中心(跨域部署)
多机房级别的容灾对于一般的业务是足够的,但是无法解决服务的异地容灾问题,这里就需要两地三中心的方案来实现,如下图所示:
异地部署最大的问题是网络传输时延比较大,导致集群的投票节点的决策实现比较长,在事务性中需要半数节点同意提案则认为请求成功,因此影响请求的处理性能及集群的运行。
在生产环境中两地三中心是最常见、容灾性最好的部署方案,在Zookeeper中要充分考虑投票的过半原则,假定zk集群中集群总数为N
地区1中心1,将其作为生产中心,其分配的节点也要少于1半,分配节点数量
地区1中心2 ,生产中心分配完成之后,与异地数据中心平分节点
地区2中心1,则剩余节点
4)两地三中心优化方案
异地的网络比较复杂,很容易出现由于网络故障而重新选举导致整个集群的不可用,因此可以选择在生产中心进行集群部署,其他中心使用Observer节点,部署示例如下所示:
Observer节点启动从Leader中同步数据,不参与选举及投票,因此对集群的事务性请求及选举没有影响。其仅提供读请求,因此在实现异地容灾的情况下一定程度上提升读性能。
但是存在的问题是Observer不支持事务性请求,并且Leader故障后无法同步数据(需要验证),不能对外提供服务。因此这种方案需要在生产中心节点故障后,将手动的将容灾中心的Observer切换成Participant模式,使其参与选举,对外提供事务性请求的服务。
5) 两地三中心(独立部署)
以上方案各自存在着一定的问题,包括时延导致不稳定、写请求性能低、无法实现异地容灾等,还有一种方案是将各中心的集群独立部署,运行过程中进行数据的实时同步
服务使用生产中心的Zookeeper集群,其他灾中心的集群可异步同步数据,在保证集群性能的同时也实现了容灾。但是Zookeeper集群的客户端还不支持生产集群故障后自动Failover到灾备中心集群,无法做到故障的实时切换,并且还要引入集群外的数据同步工具。
4.跨域容灾演练
在双中心容灾架构下,如果双方权重相等,无法做出判断,权重不等情况下,如果权重大的机房受灾则剩余一个权限不够也无法起到作用,因此在两地三中心方案中通过第三个数据中心进行仲裁,这也是异地容灾的核心。这里进行两地三中心(统一集群部署)的容灾测试,核心关注点:
数据是否同步
跨域(不同城)情况下,时延是否会造成Leader选举异常
读写性能
客户端应用在生产中心集群故障时,是否实现自动故障切换
4.1跨域容灾部署
这里选用资源池A和资源池B作为两地的机房,资源池A使用可用区1和可用区2作为同城区域中心,部署主机配置采用<4 VCore,16GB>,后续会在资源池A可用区1的主机上部署kafka集群作为应用服务。
1)申请主机,分别在移动云的资源池A和资源池B共申请5台虚拟机,开启公网,操作系统BC-Linux 7.6,申请成功后进行网络时延测试
可用区 |
zk id |
|
资源池A1可用区1 |
36.***.***.65 |
1 |
资源池A1可用区1 |
36.***.***.42 |
2 |
资源池A2可用区1 |
36.***.***.202 |
3 |
资源池B3可用区1 |
36.***.***.32 |
4 |
资源池B3可用区1 |
36.***.***.198 |
5 |
资源池A1到资源池A2的网络时延,基本在2ms左右
# ping -c 10 -i 1 36.***.***.202
PING 36.***.**.202 (36.***.***.202) 56(84) bytes of data.
64 bytes from 36. ***.***.202: icmp_seq=1 ttl=53 time=2.57 ms
64 bytes from 36. ***.***.202: icmp_seq=9 ttl=53 time=1.69 ms
......
--- 36. ***.***.202 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9015ms
rtt min/avg/max/mdev = 1.652/1.786/2.578/0.273 ms
资源池A2到资源池B3的网络延迟在30ms左右
# ping -c 10 -i 1 36.***.***.32
PING 36. ***.***.32 (36. ***.***.32) 56(84) bytes of data.
64 bytes from 36. ***.***.32: icmp_seq=1 ttl=44 time=32.1 ms
64 bytes from 36. ***.***.32: icmp_seq=2 ttl=44 time=28.5 ms
.....
--- 36.***.***.32 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9013ms
rtt min/avg/max/mdev = 28.446/28.847/32.168/1.109 ms
跨地域的主机之间的网络时延时间在可接受的范围内,不会影响整个集群的运行,并且zookeeper服务是以读请求为主,可选择就近节点进行数据读取,跨域部署不会影响读性能,可以保证性能在可接受的范围。
1)部署jdk及zookeeper,jdk部署不再介绍,zookeeper的zoo.cfg核心配置:
server.1=36. ***.***.65:2888:3888
server.2=36. ***.***.42:2888:3888
server.3=36. ***.***.202:2888:3888
server.4=36. ***.***.32:2888:3888
server.5=36. ***.***.198:2888:3888
启动命令:
# cd /opt/apache-zookeeper-3.5.9-bin
# bin/zkServer.sh start
启动后在三个可用区中可以连接对应的zk Server,如下所示:
ZK集群启动成功,目前Leader阶段为资源池B3可用区1
1)测试1
连接任意zkServer,然后创建ZK Node:/example,数据后: hello zk,成功后连接任意节点,均可以查看到/example,集群启动成功。
2)测试2,通过JDK API测试zk客户端,核心代码如下:
String zkConnectStr = "36.***.***.65:2181,36.***.***.42:2181,36.***.**.202:2181,36.***.***.32:2181,36.***.***.198:2181";
final CountDownLatch latch = new CountDownLatch(1);
ZooKeeper zookeeper = new ZooKeeper(zkConnectStr,4000, event -> {
if(Watcher.Event.KeeperState.SyncConnected == event.getState()) {
System.out.println("event:" + event);
latch.countDown();
}
});
//zookeeper.create("/example2", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
byte[] data = zookeeper.getData("/example", false, new Stat());
System.out.println("Data for /example: " + new String(data, "UTF-8"));
latch.await();
System.out.println(zookeeper.getState());
zookeeper.close();
运行后信息获取正常。
4.2容灾故障演练
1) 模拟生产中心故障,停止两个zk Server(资源池A1,生产中心的两个节点),资源池A1的zk server连接失败,其余节点的zk连接正常
当Leader故障后Follower快速切换成Leader。
2)客户端应用的自动故障切换,部署Kafka集群,配置server.properties
zookeeper.connect=36.***.***.65:2181,36.***.***.42:2181,36.***.**.202:2181,36.***.***.32:2181,36.***.***.198:2181
kafka集群启动命令执行如下:
# bin/kafka-server-start.sh -daemon config/server.properties
集群启动后:
# bin/kafka-topics.sh --create --topic test --bootstrap-server 36.***.***.65:9092
Created topic test.
# bin/kafka-topics.sh --describe --topic test --bootstrap-server 36.***.***.65:9092
Topic: test TopicId: lCh3qQqyQFqssv2761SmOg PartitionCount: 1 ReplicationFactor: 1 Configs: segment.bytes=1073741824
Topic: test Partition: 0 Leader: 0 Replicas: 0 Isr: 0
进行benchmark测试:
# bin/kafka-producer-perf-test.sh --topic test --producer.config config/producer.properties --num-records 100000000 --record-size 1024 --throughput -1
410806 records sent, 82161.2 records/sec (80.24 MB/sec), 330.3 ms avg latency, 516.0 ms max latency.
......
502335 records sent, 100467.0 records/sec (98.11 MB/sec), 304.5 ms avg latency, 415.0 ms max latency.
3) 模拟zk故障对服务的影响,停掉leader zk,运行日志如下所示:
test --producer.config config/producer.properties --num-records 100000000 --record-size 1024 --throughput -1 bin/kafka-producer-perf-test.sh --topic
557475 records sent, 111495.0 records/sec (108.88 MB/sec), 276.4 ms avg latency, 346.0 ms max latency.
546060 records sent, 109212.0 records/sec (106.65 MB/sec), 278.8 ms avg latency, 333.0 ms max latency.
.....
502335 records sent, 100467.0 records/sec (98.11 MB/sec), 304.5 ms avg latency, 415.0 ms max latency.
运行正常,再停掉相同可用区的zk server,对服务不会造成影响。再停掉资源池A可用区2的zk server,由于zk故障个数超过了1/2,zk集群故障。
参考链接
1)Zookeeper简介:http://itpcb.com/a/262343
2)集群的多机房部署:https://zq99299.github.io/note-architect/hc/05/08.html