拥抱并取代:将ZooKeeper迁移到Kubernetes上

本文介绍了HubSpot公司如何将主机上的ZooKeeper集群迁移到Kubernetes上。HubSpot是一家B2B的SaaS公司,成立于2006年。在8年的时间里,它从零起步,成长为一家收入超过1亿美金的大公司,并且于2014年成功上市。在数字营销领域,HubSpot的网站在全世界的流量排名是第五位,在 SMB(中小企业)SaaS 市场上,它确实做到了主宰一方。
最近我们在完全没有downtime的情况下,将数百个ZooKeeper实例从单个服务器实例上迁移到Kubernetes上。我们的方案是使用强大的Kubernetes特性,比如Endpoint来让整个流程变得简单,本文将分享该方案的高层设计,希望能帮助到那些想要参考我们方案的人。文末列出了重要的网络方面的前提条件。

传统的ZooKeeper迁移

ZooKeeper是很多分布式系统的基础,让用户可以构建强大的平台,构造出集群。在后台,它依赖于相对基础的方案来构建集群:每个服务器实例都有一个config文件,列出所有成员的主机名和数字id,并且所有服务器都有相同的服务器列表,就像这样:

server.1=host1:2888:3888
server.2=host2:2888:3888
server.3=host3:2888:3888

每个服务器有一个唯一的文件,名为myid,告诉它自己对应于上面列表中的那个数字id。

添加或者删除主机时不能违反这个核心规则:每个服务器必须能够达到列在config文件里的服务器列表的quorum。迁移ZooKeeper到新实例上的传统方式是:

  1. 配置并启动一个新主机,在其服务器列表里插入“server.4=host:4…“
  2. 在已有的主机上更新config文件,添加这台新主机并且从服务器列表里移除已经下线的主机
  3. 滚动重启旧主机(在3.4x上没有动态服务器配置功能)
  4. 更新客户端的连接字符串(如果重新解析DNS有问题,可能仅需要变更CNAME记录)

该方案的缺点是需要变更很多config文件,并且滚动重启很多次,用户可能并没有成熟可靠的自动化脚本来完成这一任务。当我们开始考虑将ZooKeeper迁移到Kubernetes上时就在思考这一方案,但是最后想到了另一种更简单的方式。也更加安全,因为在我们的经验里,每次重新选主都有可能(虽然可能性很小)需要很长的时间,这会导致依赖它的系统出问题。

新方案

我们的方案包括将已有的Zookeeper服务器封装到Kuberntes服务里,然后使用相同的ZooKeeper id来完成一对一,服务器到Pod的替换。这样只需要一次滚动重启就可以重新配置已有的ZK实例,然后将服务器一个接一个地关闭。本文不详细介绍如何为ZooKeeper配置Kubernetes拓扑,以及底层的服务就绪检查,因为有多种方案可以实现,各有优缺点。无论顶层拓扑如何,下面讨论的理念都是相同的。我们需要如下五个步骤:

  • 满足前提条件,确保ZooKeeper集群可以开始迁移
  • 在Kubernetes里创建ClusterIP来封装ZooKeeper服务
  • 配置Zookeeper客户端连接到ClusterIP服务
  • 配置ZooKeeper服务器实例通过ClusterIP服务地址执行peer-to-peer事务
  • 将运行在主机上的每个ZooKeeper实例替换成Kubernetes Pod上的ZooKeeper实例
    对于每一步,下文会介绍其基础架构拓扑图。为了便于理解,拓扑图仅仅包含两个Zookeeper实例,尽管实际工作中并不会创建低于三个实例的集群。

满足前提条件

从运行着的ZooKeeper集群开始,我们想要确保主机上的服务能够和Kubernetes集群通信。文末介绍了实现的几种方式。
图1:开始时的状态。两个实例的ZooKeeper和客户端

创建ClusterIP服务

为每个ZooKeeper服务器创建ClusterIP服务以及匹配的Endpoint资源。必须能够pass客户端端口(2181)和内部集群端口(2888,3888)。完成这些之后,就应该可以通过这些服务的主机名连接到ZooKeeper集群了。Kubernetes ClusterIP服务很有用,可以给用户一个固定的IP地址,作为后台Pod的负载均衡器。这里服务到Pod是1:1的映射,因此每个Pod都有一个静态IP地址。
图2:我们的集群,ZooKeeper仍然在物理硬件上,通过ClusterIP服务可以访问到

重新配置客户端

一旦能够通过Kubernetes的ClusterIP服务连接上ZooKeeper集群,就可以重新配置客户端了。如果使用的是ZooKeeper连接字符串里的CNAME记录,就需要变更DNS记录。如果客户端在连接失败时不会重新解析DNS记录,那么需要重启客户端。如果你并没有使用CNAME记录,那么需要重新配置新的连接字符串并且重启所有客户端进程。这时,旧的和新的连接字符串都可以工作。
图3:客户端使用ClusterIP服务实例和ZooKeeper集群通信

重新配置ZooKeeper实例

接下来,我们让ZooKeeper服务器通过这些ClusterIP服务做它们之间的通信。需要变更config文件来记录CLusterIP服务的地址。还需要配置zk_quorum_listen_all_ips标记:如果没有配置这个标记,ZK实例就会尝试绑定到一个并不存在的IP地址,因为这是一个Kube的服务IP。

server.1=zk1-kube-svc-0:2888:3888
server.2=zk2-kube-svc-1:2888:3888
server.3=zk3-kube-svc-2:2888:3888
zk_quorum_listen_all_ips: true

滚动重启这些主机,这时就可以用Pod来替换主机了。
图4:ZooKeeper实例现在通过ClusterIP服务实例和其他ZK实例通信

用Pod替换ZooKeeper主机

一次操作一台服务器做如下操作:

  • 选择一台ZK服务器及其对应的ClusterIP服务
  • 关闭主机上的ZK进程
  • 启动一个Pod,它配置了和关闭的ZK服务器相同的服务器列表和myid文件
  • 等待Pod里的ZK启动,并且和其他ZK节点同步数据
    好了,ZooKeeper集群现在就在Kubernetes上运行了,并且带着之前的所有数据。
    图5:一轮Pod更换后的群集。 ZK1现在在Pod中运行,而ZK2不知道任何更改

网络前提条件

要想上述步骤能够成功,需要搞定一些网络配置。需要确保:

  • 所有连接到ZooKeeper的服务器必须能够路由到Kubernetes的Pod IP地址
  • 所有连接到ZooKeeper的服务器必须能够解析Kubernetes服务主机名
  • 所有需要连接到ZooKeeper的服务器上必须运行Kube-proxy,这样这些服务器才能访问ClusterIP服务。

可以通过几种方法实现上述要求。我们使用的是in-house网络插件,类似于Lyft的 https://github.com/lyft/cni-ipvlan-vpc-k8s插件或者AWS的https://github.com/aws/amazon-vpc-cni-k8s,它可以直接分配AWS VPC IP地址给Pod,而不是用虚拟的overlay网络,因此所有实例都可以路由到任意Pod IP。Overlay网络,比如flannel(https://github.com/coreos/flannel)也应该可以,只要所有服务器都链接到overlay网络上。

Logo

开源、云原生的融合云平台

更多推荐