如何实现一致的主节点操作?

一致性问题

一致性问题是分布式系统面临的一大类基础问题。由于分布式系统由多个独立的节点所组成,不同的节点在物理上拥有自己的状态,如果想要对外以统一的门面提供服务,就需要在不同的节点之间同步特定的状态。例如,集群对主节点的共识,集群对日志追加的顺序的一致性,以及集群对消息是否已送达的判断等等。

上世纪末,著名的分布式系统专家 Leslie Lamport 发表了 The Part-Time Parliament 论文,提出了 Paxos 分布式共识算法。Paxos 算法解决了分布式系统内多个节点对一个决议如何达成共识的问题,且是迄今为止唯一经过业界验证的算法。后来的 Raft 算法或者是 Heidi Howard 的工作,都是在 Paxos 算法的框架下进行转述、特化或者完善原始论文中未尽的具体场景讨论。

当然,我们今天要讲的内容并不是这些复杂的、数学的算法流程和正确性证明,也不是讨论如何将 Paxos 算法落地,而是聊一聊如何在基础的 Paxos 算法的具体实现上,解决现实世界里更加复杂的、组合的一致性问题。

主节点选举

Paxos 算法提供了一种基础的解决分布式系统内多个节点对一个决议达成共识的方案,业界遵循论文的论述,很快也提出了相应的实现,例如 Apache ZooKeeper 等。

这些系统专注于解决分布式共识的问题,为了保证正确地实现已经很复杂的分布式共识算法,这些系统往往并不直接提供复杂的业务可用的操作接口,而是提供诸如 Get/Set 这样的原语接口。

同时,正如前面所提到的,Paxos 原始论文并没有仔细的讨论现实中存在的类型各异的失败情况,也并不关心现实世界里重要的可用性保障问题,如何权衡可用性和合理的应对不同类型的失败情况,在 ZooKeeper、Consul、Etcd 等实现里采用了不同的处理方式,这也导致了业务实现有现实意义的一致性动作时,常常还要考虑到所基于的系统的实现制约。

一个常见的一致性需求是,分布式系统为了保证可用性,通常要对同类型的节点上线多个副本,而其中担任类似管理者、协调者角色的节点,多个副本中又只能有一个对外提供服务,否则就会出现脑裂问题或者需要在业务层重新实现一遍分布式共识算法。

需求存在的地方,就会有解决方案。多个副本中对外服务的那一个副本,我们可以认为是这个副本集合中的主节点,而在一个候选集中选出一个成为主节点,是一个分布式系统内的决议。这样,我们就能利用现有的分布式共识系统,以在一个候选集中选出一个主节点建模问题,把问题转换为分布式系统内的一个决议的共识问题。

ZooKeeper 作为投产最久的分布式共识系统之一,对这个选主问题给出了一个参考实现,其主要内容如下。

1.首先,根据先验知识,所有副本将约定名字的资源作为主节点的标志,并发地尝试在 ZooKeeper 里创建该名字的临时节点。

2.根据共识系统的能力,只有一个副本持有的客户端的请求会成功,这个副本就成为集群的主节点,而其他的副本则开始监听临时节点的状态。

3.主节点开始对外提供服务,直到它下线或者宕机。此时,临时节点由于会话失效而被 ZooKeeper 集群删除。

4.其他的副本监听到这一事件,重复 1 的步骤,新的创建节点成功的副本成为集群的主节点。

在其他没有临时节点概念的系统中,例如 Etcd 里面,是使用资源 TTL + 主节点定时刷新 TTL 来完成类似的功能。

上面这个算法有一个明显的问题,即所有的副本都监听同一个资源,而同时只会选举出一个主节点。这就意味着当原先的主节点下线或宕机的时候,所有其他的副本都会收到通知并尝试创建临时节点。这一方面会造成不必要的通信开销,因为只有一个客户端请求会成功;另一方面在选主需要某种形式的公平性的时候,这样的竞争方式可能造成部分节点饿死的不公平表现。

因此,针对这个问题,ZooKeeper 提出了主节点选举的优化算法。

1.首先,根据先验知识,所有副本将约定名字的资源作为主节点的标志,并发地尝试在 ZooKeeper 里创建该名字的临时顺序节点,其中,顺序编号由 ZooKeeper 支持生成,保证全局递增。临时顺序节点形如 LockNode_0, LockNode_1,...,LockNode_n。

2.副本尝试获取所有的顺序节点,并根据顺序编号排序,如果自己对应的编号是顺序节点中最小的,那么自己就成为集群的主节点对外提供服务。

3.其他副本监听顺序编号升序排序后恰好在自己前一位的节点,如果它失去联系,则重复 2 的步骤检查自己是否成为新的主节点,如果不是,重复 3 的步骤。

在其他没有临时顺序节点的系统中,例如 Etcd 里面,是通过资源的 revision 来判断对应的顺序关系和监听手段的。

主节点操作

上一节里,我们介绍了分布式系统里相同类型节点的不同副本如何选出主节点对外服务。但是,这个过程只管选出主节点,却没有描述选出主节点之后,主节点会怎么行动。

在现实世界里,需要选主的节点往往是要维护某种集群状态的,如果这个状态需要持久化,那就涉及到主节点修改集群元数据的问题。例如,FLINK 里的 JobManager 需要将自己的通信地址发布到一个约定名字的资源上,以让 TaskManager 发现自己,同时还需要管理 Checkpoint 元数据。

通常来说,我们希望只有集群的主节点能够执行这样的操作。这是因为如果一个非主节点将自己的通信地址发布到约定名字的资源上,在主节点不周期性发布自己的通信地址的情况下,TaskManager 将永远无法发现真正的主节点,而周期性发布通信地址会增加不必要的开销。

我们将只允许主节点进行的操作,例如修改持久化的集群元数据,称为主节点操作。实现主节点操作一言以蔽之,就是节点只在自己是主节点的时候,提议的操作才会真正的执行。延续上面的例子,如果 JobManager 失去了主节点权限,那么它发布地址或修改 Checkpoint 数据的操作就会被拒绝执行。

这一点的实现并不那么容易,因为根据上一节里提到的选举算法,节点认定自己成为主节点并开始对外服务是个纯客户端行为,虽然它会在拉取分布式共识系统的数据后作出判断,但分布式共识系统完全可能异步地出现状态变化,导致刚刚拉取的数据不再具有时效性。

根据 Curator 的技术注解 TN-10 所述,这将导致在分布式共识系统上根据 LockNode 的信息已经不再是主节点的节点,由于客户端没有及时收到推送,仍然错误地认为自己是主节点。同时,新的主节点收到推送,也认为自己是主节点,开始对外服务。这时,就出现了集群脑裂的问题。

无状态的服务可以以任意数量对外服务,因为每个副本是对等的,而如上所述,需要选主的节点往往是要维护某种集群状态的。服务内容里不涉及修改集群状态的,脑裂并不是一个大的问题;但是一旦涉及修改集群状态,脑裂就可能会导致集群状态不一致。既然我们将修改集群状态定义为主节点操作,那么实际非主的节点的操作就应该被拒绝以避免集群状态不一致。

说到这里,思维灵活的同学应该可以发现,问题的症结在于,实际决定主节点权限的信息在分布式共识系统服务端,而做出自己是不是主节点的判断的,却是副本上的客户端。为了解决上面的问题,我们应该将主节点操作和当前副本的主节点权限检查放在一个事务里原子地执行。这就是我们实现主节点操作的核心思路。

正如主节点选举在不同的分布式共识系统上的实现有细微的不同一样,主节点操作的实现在不同的分布式共识系统上的实现也会有差别。我们以 FLINK 实现主节点操作为例,讲解主节点操作基于 Kubernetes 的 ConfigMap 和 ZooKeeper 可以如何实现。

基于 ConfigMap 的实现

基于 ConfigMap 的实现是一个在很高抽象层次上的实现,它严重依赖于 ConfigMap 的一致性保证,包括修改时全局递增的 revision 和读取 ConfigMap 内容时所有内容快照版本相同的一致性。

在上面提到的两个假设的前提下,我们只要将集群状态也用 ConfigMap 管理起来,就可以实现状态修改的主节点操作了。

逻辑上,JobManager 持有一个保存作业元数据的 ConfigMap 资源。每个 JobManager 的副本使用 fabric8 客户端库封装的功能,基于 ConfigMap 的锁来进行选主,选主的元数据保存在约定名字的 ConfigMap 的 Annotation 上。

具体地说,当 JobManager 对应的 ConfigMap 的锁告知当前 JobManager 副本成为主节点后,该主节点获取 ConfigMap 的一个快照,并检查对应的锁所指示的主节点是不是自己,如果是,则发布自己的通信地址到 ConfigMap 中,并提起客户端请求将新的 ConfigMap 同步到 Etcd 集群中。

在此过程中,如果主节点对应的副本发生过变化,那么服务端的 ConfigMap 的 ResourceVersion 必然大于刚才节点客户端接收到的 ResourceVersion。此时,该节点提出的请求将被拒绝,这就实现了主节点操作的语义。

同理可以在同一个 ConfigMap 下发布属于 JobManager 领域的其他元数据信息,例如 Checkpoint 信息。

关于这个方案的更多细节,可以查阅正在实现中的 FLIP-144 提案。

基于 ZooKeeper 的实现

FLINK 最早的托管式部署是部署在 YARN 上的,也就没办法利用 ConfigMap 的现成抽象。在没有 Kubernetes 的岁月里,许多系统的分布式共识都是基于 ZooKeeper 来做的,ZooKeeper 的方案是最成熟方案。

我在 FLINK 社区曾经设计过一个基于 ZooKeeper 的主节点操作的设计文档,该设计文档对应也在某司实现落地了。社区的基于 ZooKeeper 的一致性保证的实现是有问题的,在网络不稳定的情况下,应当是主节点操作的部分操作由于上面提到的问题将无法保证。同时,由于没有正确地处理不会导致临时节点丢失的频发网络闪断问题(ConnectionLossException),导致集群的稳定性更加难以保证。不少厂商在使用 FLINK 时不得不进行二次开发,至少手动打上 FLINK-10052 的补丁以改善这个问题。

基于 ZooKeeper 实现主节点操作比起基于 ConfigMap 来的复杂度来源于没有 ConfigMap 这样的聚合资源对象。ConfigMap 能存储很多信息,而整体又是一个原子变更的资源对象。在 ZooKeeper 上,连主节点竞选都要手动实现,粒度是 ZNode 的粒度,上层的原子性要重新实现。这就是 ZooKeeper 面临的特有问题。

回顾一下,实现主节点操作的核心思路是,将主节点操作和当前副本的主节点权限检查放在一个事务里原子地执行。

假设我们使用临时顺序节点的方案,主节点权限实际上是由当前副本关联的临时顺序节点是否有这一组临时顺序节点最小的顺序编号来标识的。进一步的,由于顺序标号是递增的,且副本成为主节点的时刻其对应的顺序编号必然是最小的,我们只需要记录成为主节点的时刻对应的顺序编号,并检查该节点是否存在,即可检查主节点权限。

另一方面,ZooKeeper 通过在服务端实现多个操作的聚合事务,提供了多个操作原子性提交的能力。有了检查主节点权限的手段,也有把这一动作和其他数据操作原子化实施的方法,我们就可以组合起来实现主节点操作的语义了。

具体地说,JobManager 的所有副本根据前文提到的选举算法选举主节点,在成功创建临时顺序节点时记录自己的顺序编号。当需要进行主节点操作时,例如发布自己的通信地址信息,则在同一个事务里检查临时顺序节点的存在性和写通信地址信息节点。其他的主节点操作也类似。

在此过程中,如果主节点对应的副本发生过变化,检查临时顺序节点的存在性会返回不存在,对应的操作也就会被拒绝。这就实现了主节点操作的语义。

讨论的时候还有一种候选方案是将信息放置在竞选的临时顺序节点上,这样就不需要协调两个节点了。但是这个方案的问题在于,节点信息的发布是需要接收方监听节点的。如果不是一个固定的节点,那接收方就要监听一系列的节点变更,这无疑是一个不可忽视的开销和系统复杂度。此外,单个节点的容量是有限的,即使通信地址信息能够通过这个方式解决,Checkpoint 元数据和 JobGraph 元数据等却不一定能很好的支持起来。

后记

主节点操作是我在工作中实现的第一个完整的 feature,此前我大概写过多封社区讨论邮件和一篇介绍文章来推动和分享这项工作。

实际上,它的实现并不复杂。但是,为了确保这个实现的有效性和错误边界,需要开发者了解分布式共识算法,了解不同程度一致性的保障方式,同时,还需要校对现有系统和客户端库代码上的技术细节,减少实现上的 BUG 导致的功能缺陷。

技术会友,阿里巴巴的技术专家王阳推动了 fabric8 社区开发主节点选举的功能,并主导了 FLIP-144 的进程,在提高分布式系统的一致性保障上,我和他有多次线上和线下的讨论,核心思路和多个重要的技术细节是共同讨论得出的结果。Curator 的创始人 Jordan Zimmerman 是一个充满热情的分布式系统黑客,在社区的邮件讨论中对主节点操作的问题分析和尝试性的实现给了我很好的启发和信心。ZooKeeper 社区的 Michael Han、Ted Dunning 和 Enrico Olivelli 等优秀的程序员参与了议题的讨论,提出了自己创造性的意见。

一致性问题是分布式系统面临的一大类基础问题,分布式共识算法是解决这一问题的一个有效的方案。在跟从事相关研究的同学,以及工作内容直接涉及一致性的同学的讨论里,通过重复的介绍其中种种具体问题的定义和越来越抽象地描述问题的解法,我对主节点操作的介绍也从具体的组件细节和交互流程演变为了【实现主节点操作的核心思路是,将主节点操作和当前副本的主节点权限检查放在一个事务里原子地执行】这样一个简练的表述。

基础架构、基础技术是从业务需求里面总结共性并标准化流程、插件化差异得到的,更加基础的支持则是以上层的基础技术为业务提炼得到的。主节点操作的共性在于主节点操作和当前副本的主节点权限检查的原子执行,它不仅仅是基于 ZooKeeper 的实现或者基于 ConfigMap 的实现,只要有相应的需求背景和可迁移的技术手段,都可以实现类似的功能。

一致性问题是分布式系统面临的一大类基础问题,这里所讲的问题和方案甚至都摸不到其理论的一边一角,整个学习和思考的过程更可以说是越进行越发现越多自己不知道的内容。但是,理论最终要落地投产才能发挥价值。坚持从务实的角度入手,广泛地进行技术讨论,并提取出其中的核心要点,回过头来再去分享和讨论,进一步的思考和抽象,如此往复。虽然我不知道这样的做法能带来具体怎么样的结果,但总不会是什么坏的事情。

参考链接

•Paxos 共识算法 https://lamport.azurewebsites.net/pubs/lamport-paxos.pdf

•Raft 算法 https://raft.github.io/raft.pdf

•Heidi Howard 的工作 https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-935.pdf

•选主问题的参考实现 https://zookeeper.apache.org/doc/r3.6.2/recipes.html#sc_leaderElection

•Curator 的技术注解 TN-10 https://cwiki.apache.org/confluence/display/CURATOR/TN10

•FLIP-144 提案 https://cwiki.apache.org/confluence/display/FLINK/FLIP-144%3A+Native+Kubernetes+HA+for+Flink#FLIP144:NativeKubernetesHAforFlink

•基于 ZooKeeper 的主节点操作的设计文档 (https://docs.google.com/document/d/1cBY1t0k5g1xNqzyfZby3LcPu4t-wpx57G1xf-nmWrCo/edit?usp=sharing

•ZooKeeper 社区的邮件讨论 https://lists.apache.org/x/thread.html/594b66ecb1d60b560a5c4c08ed1b2a67bc29143cb4e8d368da8c39b2@%3Cuser.zookeeper.apache.org%3E

  • 0
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:书香水墨 设计师:CSDN官方博客 返回首页
评论 1

打赏作者

_tison

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值