我们的服务发现机制捕获了自身的故障,然后把自己关掉了
我们有一个三副本的集群,它不断与自己产生分歧。后台任务一连跑了两遍、三遍。答案不在日志里,而在我们自己的代码里。对等节点发现例程里有一个 catch 块,里面的注释大意是:"multicast 失败,已禁用发现。"我们的服务发现机制正在捕获自身的故障,然后悄无声息地把自己关掉,而且它从生产环境上线第一天起就一直在这么干。
这是一个关于为什么一个在机架服务器上完全正确的集群协议,一旦被放到托管平台上就成了错误工具的故事,也是关于为什么修复之道是删掉它、而不是修好它的故事。
集群究竟是用来干什么的
一台 auth 服务器为了可用性会以多个副本运行。这些副本大体上互相独立:任何一个都能校验 token 或核对密码。但有几类任务必须只跑一次,而不是每个副本各跑一次。删除过期记录的数据保留扫描。触发客户 webhook 的那一轮处理。任何向外部伸手、会产生副作用的操作。对这些任务,你需要集群就两件事达成一致:成员有哪些,以及其中哪一个是运行单例工作的 leader。
我们最初的答案是经典做法:gossip。每个节点与它的对等节点闲聊,成员关系是"谁可达"这一事实涌现出来的属性,整个群体再从达成共识的集合里选出一个 leader。这是个漂亮的设计。但它也假定各节点能彼此找到对方,而旧代码找到它们的方式是 multicast:在本地网段上喊一嗓子,看看谁应答。
它为什么会无声无息地坏掉
托管的 Kubernetes 不承载 multicast。几乎任何不是你自己搭建的云网络也都不承载。喊声发出去,每一次都没有任何回音。于是发现"失败"了,catch 块被触发,每个节点都得出结论:自己在这世上孑然一身。
在单副本下这无伤大雅。在三副本下,这就是构造性的脑裂:三个节点,每个都确信自己是唯一的成员,每个都把自己选为 leader,每个都运行那份单例工作。保留扫描在三个节点上都跑了。webhook 那一轮处理把每个客户的 webhook 都触发了三次。这一切都没有抛出任何错误,因为从每个孤独节点的视角看,一切都好好的。这个 bug 不是崩溃;而是三个程序各自在一个被它们理解错了的世界里表现得完美无瑕。
修复之道是干脆完全不再去发现对等节点
正是下面这个重新构想,让整件事坍缩成了寥寥数行:我们一直在用一个聒噪的协议,去重建一个平台早已为我们以强一致性存好的事实。我们运行在一个会向我们提供强一致存储的云上。如果已经存在一个所有人都能认同的单一处所,那么 leader 选举根本不需要对等节点之间的共识。
于是领导权变成了一个 blob lease。一个 blob,一个 lease。谁持有这个 lease,谁就是 leader。lease 有超时时间,所以一个挂掉的 leader 会停止续约,lease 便自行释放,留给下一个接手者。没有对等节点发现,没有 quorum 计算,没有 multicast,也没有一个伺机把它禁用掉的 catch 块。协调与事件则承载在一张充当节点间总线的小表上。
那个我没料到自己会爱上的特性:你可以打开 Storage Explorer,亲眼看见集群的"心思"。此刻是谁持有 lease。总线上排着哪些东西。成员关系不再是你从某个协议的行为里去推断出来的东西,而成了一行你能直接读到的数据。当那个决定"谁来运行你那些破坏性任务"的东西是一个你能盯着看的值时,调试就不再是考古了。
我们故意删掉一个精巧之物的那一段
既然都动手了,我们顺便删掉了一个基于 conflict-free replicated data type 构建的分布式限流器,那是一个无需协调就能把各节点的计数器合并成一个全局计数的 CRDT。它确实精巧。但它解决的也是一个我们已经搬走了的问题。全局限流应该归属于边缘,归属于请求进来的地方,而不该用一个需要一整段文字才能解释清楚的数据结构在集群内部重建出来。删掉。
删掉能正常工作的代码会让人觉得像是一种损失,直到你算清自己从此不必再维护什么。我们移除了 gossip 层、multicast 假设、那个自我禁用的 catch 块,还有 CRDT,并用一个 lease 加一张表取代了这一切。代码行数下降了,而系统可能处于的状态数也随之下降了。
抽象出来的那条教训
gossip 与 multicast 式的成员关系,在裸金属上、或在你掌控的扁平网络上,是正确的工具。而在一个其网络压根承载不了它们所依赖的那套机制的平台上,它们就是错误的工具,而且其故障模式不是一声响亮的崩溃;而是一次看起来还在正常工作的无声降级。在你把一个分布式系统的模式移植到托管基础设施上之前,先问问它对网络做了什么假设,再问问平台早已免费给了你什么。我们的平台早已运行着一个强一致存储。在那个存储里放一个 lease,就是一个比我们一直背负的那套协议更简单、也更正确的 leader 选举,而且这一个我们还能盯着看。
如果你在运营 auth,那么那些保管你的密钥、决定谁来运行你那些破坏性任务的部件,应该是你所拥有的东西里最无聊、也最便于查看的。我们把我们的做成了无聊的。这是一次升级。