8

ClickHouse平台跨国无感迁移Zookeeper实践

 2 years ago
source link: https://dbaplus.cn/news-73-4064-1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

ClickHouse平台跨国无感迁移Zookeeper实践

ClickHouse OS 2021-09-29 10:46:51

背景

注:为简化表述,本文中将clickhouse简称为ck, 将zookeeper简称为zk。

我司从去年年底开始启动从香港到新加坡机房的迁移。目前Clickhouse集群所有实例都已经从香港搬迁到了新加坡机房,还剩下其依赖的Zookeeper集群在香港机房,因此我们近期准备将Zookeeper集群平滑搬迁到新加坡机房。

图片

1、目标与挑战

1) zk跨洲搬迁需对用户基本无感知

ck集群发展到现在已经承载了整个公司的实时数据分析需求,还支持了许多在线服务。这要求ck集群不能够停机,在任何时候都是可用的。ck集群每时每刻都在执行数据插入和查询、表变更,而ck在架构设计上重度依赖zk做元数据存储、副本同步和表变更,zk一旦不可用,ck的读写都会受到影响。zk集群的迁移中间必然引起leader的切换,如何在zk集群搬迁的过程中保证读写,这是一个不小的挑战。

2) 热升级+动态配置更新

为了实现上面的目标,我们在迁移的过程中,一方面要从写入层做好重试,避免zk切主过程中的失败。同时,也要尽可能的缩短zk不可用的时间。对zk的操作都要采用热升级的方式,滚动操作,同时因为zk的集群ip都换了,必然要更改很多配置,所有的配置也尽量采用reload的方式,而不是重启服务。

一、整体方案



1.1 第一步:zk从静态配置版本升级到动态配置版本

zk 3.5.0之后支持动态配置特性。利用动态配置特性可方便进行扩容和缩容操作,而不需要对整个zk集群中的所有实例进行滚动重启。但是不巧的是,ck用的zk集群还没有使用动态配置,因此zk集群搬迁的第一步就是将zk集群从静态配置版本平滑升级到动态配置版本,简化后续的扩容和缩容操作。zk动态配置版本详情可参考:

https://backendhouse.github.io/post/zookeeper_dynamic_config/

1.2 第二步:zk扩缩容实现搬迁

第二步,在ck集群升级到动态配置版本之后,通过扩容和缩容操作实现zk集群从香港老机器到新加坡新机器的平滑搬迁:

  • 扩容:将新加坡机房的新机器一台一台加入到zk集群中

  • ck实例修改zk配置:将zk配置全部从老机器换成新机器

  • 缩容:将香港机房的老机器一台一台从zk集群中摘除。

图片

二、遇到的问题和解决方案

为了保证线上zk搬迁过程中不出问题,我们事前进行充分的影响面预估和线下演练,在这个过程中发现了以下问题:

2.1 zk静态配置版本与动态配置版本不兼容

在1.1中,首先将zk中的Follower实例从静态配置升级到动态配置版本时,发现升级中的Follower实例报错:

Follower报错日志如下:

2021-02-25 11:07:03,081 [myid:5] - WARN  [QuorumPeer[myid=5](plain=/0:0:0:0:0:0:0:0:2185)(secure=disabled):Follower@96] - Exception when following the leader  java.io.EOFException          at java.io.DataInputStream.readInt(DataInputStream.java:392)          at org.apache.jute.BinaryInputArchive.readInt(BinaryInputArchive.java:63)          at org.apache.zookeeper.server.quorum.QuorumPacket.deserialize(QuorumPacket.java:85)          at org.apache.jute.BinaryInputArchive.readRecord(BinaryInputArchive.java:99)          at org.apache.zookeeper.server.quorum.Learner.readPacket(Learner.java:158)          at org.apache.zookeeper.server.quorum.Learner.registerWithLeader(Learner.java:336)          at org.apache.zookeeper.server.quorum.Follower.followLeader(Follower.java:78)          at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:1271)  2021-02-25 11:07:03,081 [myid:5] - INFO  [QuorumPeer[myid=5](plain=/0:0:0:0:0:0:0:0:2185)(secure=disabled):Follower@201] - shutdown called  java.lang.Exception: shutdown Follower          at org.apache.zookeeper.server.quorum.Follower.shutdown(Follower.java:201)          at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:1275)

同时,尚未升级的Leader实例报错如下:

2021-02-26 19:35:08,065 [myid:6] - WARN  [LearnerHandler-/xx.xx.xx.xx:52906:LearnerHandler@644] - ******* GOODBYE /xx.xx.xx.xx:52906 ********  2021-02-26 19:35:08,066 [myid:6] - INFO  [WorkerReceiver[myid=6]:FastLeaderElection$Messenger$WorkerReceiver@285] - 6 Received version: b00000000 my version: 0  2021-02-26 19:35:08,066 [myid:6] - INFO  [WorkerReceiver[myid=6]:FastLeaderElection@679] - Notification: 2 (message format version), 5 (n.leader), 0xb0000000c (n.zxid), 0x2 (n.round), LOOKING (n.state), 5 (n.sid), 0xb (n.peerEPoch), LEADING (my state)b00000000 (n.config version)  2021-02-26 19:35:08,067 [myid:6] - ERROR [LearnerHandler-/xx.xx.xx.xx:52908:LearnerHandler@629] - Unexpected exception causing shutdown while sock still open  java.io.IOException: Follower is ahead of the leader (has a later activated configuration)          at org.apache.zookeeper.server.quorum.LearnerHandler.run(LearnerHandler.java:398)  

经过定位,发现问题出在静态版本的Leader实例与动态版本的Follower实例不可共存。在串行升级zk的过程中,为了尽量减少zk集群不可用时间,我们先升级完所有Follower, 最后再升级Leader。当一个Follower实例从静态配置版本升级到动态配置版本之后,此时Leader还处于静态配置版本,其config version是0; 而Follower此时处于动态版本,config version大于0。

Follower启动之后会请求Leader,请求数据中带上config version. Leader收到请求之后会对Follower的config version做校验,如果发现对方config version大于自己,便抛异常(Follower is ahead of the leader)并主动关闭连接。

   if (learnerInfoData.length >= 20) {                      long configVersion = bbsid.getLong();                      if (configVersion > leader.self.getQuorumVerifier().getVersion()) {                          throw new IOException("Follower is ahead of the leader (has a later activated configuration)");                      }                  }  

而Follower读到EOF之后也会抛异常(EOFException),并不断重试。

摆在眼前的解决方案有两种:

  • 方案一:串行升级改成并行升级,避免静态版本与动态版本的实例同时存在。

  • 方案二:由于静态版本和动态版本同时存在的时间很短,zk增加一个临时版本,该版本中去掉Leader对Follower的config version检查,绕过上述问题。

考虑到zk集群中数据较多,zk实例启动时间较长,并行升级会导致zk集群在2-4min内不可用。一旦出现问题发生回滚,zk集群不可用时间还会翻倍,风险较大。于是我们摒弃方案一,选择方案二:先将静态版本串行升级到去掉config version检查的静态版本,再升级到动态版本。

2.2 ck无法动态加载zk配置

在线下演练过程中发现,在1.2中,当修改clickhouse配置文件中的zk server列表后,配置变更并不会被ck动态加载。而clickhouse上的应用已经如此之多,其中不乏一些2B业务或对实时性要求非常强的业务,通过重启ck集群去加载新的zk server列表显然不可接受。

最终的解决方案是增加clickhouse对zk配置动态加载的支持,从而避免重启ck集群影响用户。目前这一优化已经合并到社区,PR:https://github.com/ClickHouse/ClickHouse/pull/14678

2.3 zk实例重启导致少量ck查询失败

ck查询一般不涉及zk交互,因此zk搬迁大部分情况下不影响ck查询。但是在线下演练过程中发现,当clickhouse开启了开关optimize_trivial_count_query之后, 对应PR: https://github.com/ClickHouse/ClickHouse/pull/7510,执行一些简单的select count查询会被zk搬迁所影响。

追查代码发现,optimize_trivial_count_query开启后,对于简单的select count查询,ck会跳过常规的查询过程,转而从metadata中获取总行数(在此过程中会访问zk),以此来提高select count的查询速度。因此,在zk集群搬迁之前,我们将clickhouse集群中的开关optimize_trivial_count_query设置为0,待zk搬迁完成之后再将其开启。

2.4 zk实例重启导致写入ck失败

在向ck写入数据时,ck依赖zk集群分配blockid,并将数据从当前副本同步到配偶副本。因此zk搬迁过程中必定会影响ck写入。而我们要做的便是将影响写入的时间段尽量缩短;同时一旦发现写入失败针对同一个分片下的副本不断重试,保证zk集群恢复时,ck写入也能自动恢复。

目前有Flink、Spark和clickhouse_sinker三个入口往ck写数据,在zk搬迁之前我们需要提前确保它们写ck的过程已经有了重试机制。

2.5 zk实例重启导致ck表变更失败

表变更操作包括创建/删除表、新增/删除/修改字段。ck集群在执行表变更时必然会访问ck集群,因此zk搬迁过程中会影响ck集群中的表变更。因为ck表变更相对于ck写入与查询来说频率较低。因此在zk搬迁过程中,我们对ck平台进行功能降级,即此时不支持ck表变更操作,以此避免zk实例重启导致ck表变更失败。

三、最终的搬迁方案



3.0 初始状态

假设香港机器:A1, A2, A3, A4, A5,新加坡机器:B1, B2, B3, B4, B5。

初始状态下,zk集群部署于香港机房,初始版本为静态配置版本。

我们的目标是将zk集群搬迁到新机房,最终版本为动态配置版本。

3.1 升级到动态配置版本

版本升级路线:静态版本 -> (不带config version检查的)静态版本 -> 动态版本

1 )静态版本 -> 不带(config version检查的)静态版本

串行升级,先升级Follower, 最后升级Leader。每升级完一台机器,检查集群中Leader/Follower状态,检查ck查询和写入是否有异常。等待zk实例完成启动后,接着再升级下一个zk实例。

预期影响:

  • 升级Follower实例时,ck到该zk实例上的连接被断开,部分ck写入可能会报zookeeper session expired错误,ck重连其他zk实例后恢复正常,恢复时间不超过40s。

  • 升级Leader实例时,zk会进入选举周期并会产生新的Leader, ck写入会报table in readonly mode错误,新Leader产生之后ck写入恢复正常,恢复时间不超过3min。

回滚方案:直接并行回滚到初始的静态版本。

2) 不带(config version检查的)静态版本 -> 动态版本

升级步骤、预期影响面、回滚方案同3.1.1。

3.2 动态扩缩容

1) 扩容:将新加坡新机器加入到zk集群中

串行扩容步骤:

  • 在新机器B1上部署zk实例, 其配置中包含A1-A5和B1。通过reconfig -add 6=B1:2888:3888;2181将B1加入到集群中。接着检查当前所有zk实例的本地配置是否已更新,检查Leader/Follower状态,检查ck读写是否有异常,确认无问题后扩容下一台机器。

  • 在新机器B2上部署zk实例, 其配置中包含A1-A5和B1-B2。通过reconfig -add 7=B2:2888:3888;2181将B2加入到集群中。检查步骤同上。

  • 重复执行以上步骤,直到所有新机器都已经加入到zk集群中。

预期影响:无。

回滚步骤:在串行扩容过程中,如果有任何一步出现异常,则将新实例通过reconfig -remove <id>命令从集群中摘掉。

2)修改ck配置:将zk配置改成新加坡新机器

修改ck配置,将zookeeper-servers从旧机器A1-A5改成新机器B1-B5,并下发到所有ck实例。netstat命令检查ck是否与新zk实例建立连接。

<zookeeper-servers>        <node index="0">            <host>A1</host>            <port>2181</port>        </node>        <node index="1">            <host>A2</host>            <port>2181</port>        </node>        <node index="2">            <host>A3</host>            <port>2181</port>        </node>        <node index="3">            <host>A4</host>            <port>2181</port>        </node>        <node index="4">            <host>A5</host>            <port>2181</port>        </node>  </zookeeper-servers>  

预期影响:无。

回滚:一旦检查过程中发现异常,将ck配置回滚并重新下发。

3 )缩容:将香港老机器从zk集群中摘掉

串行缩容过程中,应当遵循先缩容Follower实例,最后缩容Leader实例的顺序,具体步骤如下:

  • 缩容A1: 通过reconfig -remove 1=A1:2888:3888;2181命令,将老机器A1从集群中摘除。接着检查当前所有zk实例的本地配置是否自动更新,检查Leader/Follower状态,检查ck读写是否出现异常。确认无问题后,将老机器A1上的zk实例下线。

  • 缩容A2, 操作同上。

  • 重复执行以上操作,直到所有香港老机器都已经从zk集群中摘除。

预期影响:同3.1.1。

回滚:在串行缩容过程中,如果有任何一步出现异常,通过reconfig -add <id>=<ip>:2888:3888;2181命令将待下线实例重新加入zk集群中。

四、总结

通过线下环境中充分的zk搬迁演练,我们得以及时发现zk搬迁中出现的各种问题,并一一加以解决。最终在ck用户基本无感知的情况下,完成了zk集群从香港到新加坡的平滑迁移。

作者丨ClickHouse OS 来源丨公众号:ClickHouse OS(ID:gh_9a12382e5262) dbaplus社群欢迎广大技术人员投稿,投稿邮箱:[email protected]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK