Bootstrap

zookeeper迁移实践

背景

由于公司管理层决定要将云平台从阿里云切换成AWS,所以部署在阿里云上的所有中间件服务都需要迁移,包括kafka, zookeeper等。由于zookeeper集群保存着重要的状态信息,而且迁移过程中极易出错,在此记录,方便有需要的读者参考。

迁移要求

对zookeeper集群的迁移,需要保证两点:

1)数据不能丢

2)服务不能中断

即对于业务方来说,迁移过程应该是尽量无感知的,不能影响到业务。

实施方案

zookeeper集群的迁移,针对不同的zk部署版本,迁移过程也有所区别。如果zk版本 >= 3.5,那么zk集群支持动态配置,此时迁移过程会简单很多(参考zookeeper官网介绍

而对于zk < 3.5之前的版本,可以有两种解决方案:

(1)停机迁移

(2)平滑迁移

方案一 停机迁移

这个方案最简单,操作方便,先把旧节点上的zk进程停掉,然后将zk数据目录copy一份到新节点,然后启动新节点进程即可。但缺点是迁移期间无法继续提供服务,而且停机时间视数据目录大小copy耗时而定。因此该方案适用于非线上环境zookeeper集群迁移。

方案二 平滑迁移

该方案的基本思路是先扩展zookeeper集群到新节点,然后下线旧节点,在整个迁移过程中zk集群可以持续提供服务。因此,平滑迁移可以保证服务不中断,适合线上环境执行,但缺点是操作复杂,容易出错。

具体迁移流程

由于我们目前部署的zk集群版本为3.4.12,为了保证线上服务不中断,我们采用平滑迁移方案。

假定迁移前集群由 server1, server2, server3 组成,迁移后集群由 server4, server5, server6 组成。原有节点配置如下所示:

server.1=localhost:2881:3881
server.2=localhost:2882:3882
server.3=localhost:2883:3883

迁移流程如下:

1)新增ID为4,5,6的节点(注意,新增节点id需要大于旧节点id),并依次启动server4, server5, server6,其中server4需要进行二次重启。

server4的配置如下:

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 
#需要在启动server6后进行第二次重启并打开剩余节点注释
#server.6=localhost:2886:3886

server5的配置如下:

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 
server.6=localhost:2886:3886

server6的配置如下:

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
server.6=localhost:2886:3886

即新增节点的启动顺序为 server4(列表中不包含server6) -> server5 -> server6 -> 重启server4(包含完整列表)。

2)对1,2,3节点的配置文件,分别补充新增节点列表,e.g.

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

server.6=localhost:2886:3886

3)对原节点1,2,3中的follower节点,分别进行重启

可以使用命令 echo mntr|nc localhost 2181 来查看当前集群节点的角色。

假设现在节点2是leader,那么分别重启节点1和节点3(此时除了节点2,其余节点都能看到集群有6个member)

4)对原节点1,2,3中的leader节点,进行重启

重启原leader节点之前,先确认follower节点已经同步完成 echo mntr|nc localhost 2181

zk_followers 5

zk_synced_followers 5

重启旧leader后,会选举出新的leader(一般是id最大的节点),假设为节点6。

至此,我们拥有了一个6个节点组成的新zk集群,其中3个旧节点,3个新节点。

5)对外发布新节点4,5,6组成的连接串(即业务方将使用这个串进行新集群交互)

这一步很重要,它可以让业务方通过新地址串连接zk集群,方便我们后续下线旧节点。比如我们目前的集群有6个节点组成,但是我们对外发布 e.g. ip4:2181,ip5:2181,ip6:2181 的新节点连接串,这样业务方就不再感知旧节点,后续我们下线旧节点时就不会对业务方造成影响。

6)确认业务方替换完新连接串后(注意,因为新连接串只包含4,5,6节点,所以如果业务方替换完后,不会再有业务方机器连接旧节点):

A.先下线节点1 (此时可用节点为5个,满足6个节点集群需要至少4个可用的要求),剩余两个可用的旧节点用来滚动替换,最终建成一个3个新节点的集群。

B.剩余2,3,4,5,6节点分别将节点1的配置从zoo.cfg中删除/注释,然后按先follower,后leader的顺序重启各个节点,这样形成5个节点的zk集群。

C.对于5个节点的集群,可以容忍两个节点失效,因此,先下线旧节点2,然后对于剩余的3,4,5,6节点分别将节点2的配置从zoo.cfg中删除/注释,然后按先follower,后leader的顺序重启各个节点,这样形成4个节点的集群。

D.对于4个节点的集群,可以容忍一个节点失效,此时要注意不能直接下线节点3,否则后续重启节点4,5,6时会造成集群不可用。因此对于剩余的4,5,6节点分别将节点3的配置从zoo.cfg中删除/注释,然后按先follower,后leader的顺序重启各个节点,这样最终形成3个新节点的集群,此时下线最后一个旧节点3。

7) done,此时最终形成节点4,5,6组成的新集群,数据跟旧集群一致。

注意事项

1.新增节点必须按照节点顺序依次启动,不能同时启动新增节点,关键因素就是先要保持原有的leader不变,不能因为新增节点导致重新选举。

这里我们要解释下为什么第一次启动节点4时配置列表中不能包含节点6,假设我们包含节点6,那么节点4看到集群包含6个节点,需要至少4个节点选择一致才能确定leader节点,而这时节点1,2,3会有一致选择,而节点4,5,6会有一致选择,都达不到确定leader节点的要求。

2.新增节点的id必须大于原有节点id。这是因为zookeeper在两个节点之间建立连接时,只允许id较大的节点向id较小的节点发起连接,如果id较小的节点向id较大的节点发起连接,会被舍弃,如下图所示:

假设新增节点id比原有节点id小,会出现什么情况呢?

答案就是会出现两个leader,并且最后会导致数据不一致,如下图所示:

而真实数据为原来leader,节点5的数据为

/xiaojiang = 'born-in-beihai'

/cangcang = 'born-in-hangzhou'

造成这种状况的根本原因,在于重启原有leader节点前,现有集群节点没有全部处于sync状态。而新增较小的节点id,使得在各个新增节点重启过程中出现较大id节点(原有节点)无法感知较小id节点加入集群的情况:

举个例子,原来有节点4,5,6(leader为5),新增节点1,2,3,那么启动完新增节点后,整个zk集群还是只有原来的4,5,6节点处于sync状态,此时节点1,2,3都处于寻找leader状态,并且都投票给节点3(相同情况下,编号大的胜出)。那么当重启节点4后,节点1,2,3,4选出了节点4作为leader(选票数超过6个节点的一半),而此时节点5,6仍然组成以5为leader的集群,这时两个集群都能提供服务,那么很明显数据将出现不一致。而后面继续重启节点6和节点5时,leader仍为节点4(仍然获得超过集群一半以上节点的支持),那么最后整个集群将以leader(节点4)的数据作为依据,因而先前在节点5作为leader期间变更的数据将丢失!!! 这无疑会是灾难性的后果。

3.在对外提供新集群连接串时,不要包含老节点地址。

业务方客户端在使用zk连接串时,都会随机挑一个地址进行尝试,如果无法连接则尝试下一个地址。因此我们只提供新地址组成的连接串,可以方便我们后续判断是否所有业务方都已使用新串进行连接。

4.在进行迁移的第6步进行滚动下线的D步骤时,要注意:不能先下线节点3然后再依次进行重启新节点。这是因为对于4个节点集群只能容忍一个节点失效,如果先下线节点3,那么后续重启任何一个节点都将使得zk集群在重启期间不可用。

遇到的问题

1.zk server上有保留着已建立连接的established信息(netstat -ntap),但是对应server却没有相应的连接。

这个问题比较诡异,不好排查,不影响操作流程。在zookeeper官网上找到一个原因可供参考:

Some broken network infrastructure may lose the FIN packet that is sent from closing client. These never closed client sockets cause OS resource leak.

解决方案:

官方文档有提到一个配置(clientTcpKeepAlive,3.6.1开始支持)

Setting this to true sets the TCP keepAlive flag on the client sockets. Enabling this option terminates these zombie sockets by idle check.

2.迁移过程中zk server中可能会看到类似的日志:

Too many connections from /172.23.xxx.xxx - max is 60

这个warning日志表示当前server接受的客户端连接数超过了默认给每个客户端IP使用的连接数,如果想调整这个参数,重新设置即可,e.g.

# set max client connects 
maxClientCnxns=300

总结

3.5版本前的zk迁移比较繁琐,容易出错,在迁移前也查阅了一些资料,但都没有详细的迁移步骤,在参考相关资料及自身搭建测试环境验证后,顺利完成了线上环境zk集群的迁移。为了避免后来人走弯路出错导致集群数据不一致或者服务中断,留下经过验证的一些迁移步骤,希望对大家有所帮助。

大家有更好的迁移计划也欢迎留言探讨~