面向状态的一致性:为什么我们不再寻找唯一正确的答案
每个分布式系统都有不同类型的状态。我们早期犯的错误是问错了问题。我们一直在问:“集群应该使用哪种一致性模型?”有用的问题实际上是:“这块特定状态实际上需要什么样的一致性保证?”这两个听起来相似,但其实并不一样。第一个假设整个系统存在单一答案。第二个假设并不存在单一答案——而正是第二个假设构成了这篇文章的核心,最终以一张表的形式解释了大部分内容。我们在构建集群消息代理的过程中,艰难地发现了这个差异。其底层的教训与MQTT关系不大。任何持有多种状态的分布式系统最终都会遇到这个问题。掌握这个问题后,设计的其他部分几乎会自然而然地变得简单。事件发生时:两个Pod相隔五分钟被内核的OOM处理程序终止。内存限制:512Mi。并没有什么特殊的——这是一个普通无状态服务的正常容器限制。第一个假设是显而易见的:负载均衡不均衡,可能导致重新连接级联。看似合理,结果却是错误的。活跃连接,三个Pod,同一时间窗口:Pod A: 8 ██ Pod B: 174 ████████████████ Pod C: 1,039 ████████████████████████████████████████ 同一时间窗口,工作集内存,三个Pod:Pod A: ~270MB ████████████████████████████ Pod B: ~310MB ████████████████████████████████ Pod C: ~360MB █████████████████████████████████████ 这种内存并不是负载不均衡所产生的形状。一个Pod服务的连接数是另一个的130倍,内存却几乎相同。要么测量不准确,要么思维模型存在问题。调查发现,指标没有问题。阅读负责会话状态的实际代码路径揭示了真正的原因:在每个Pod启动时,一个持久化钩子加载了每个客户端的存储会话状态——整个车队的,而不仅仅是会重新连接到该特定Pod的那部分。一次无过滤的读取,在启动时调用一次,返回的每一行都成为了一个活的内存对象。为什么会有人这样写代码?在一个无状态集群后面,使用非粘性负载均衡器,确实无法提前知道哪些客户端将会重新连接到该Pod。因此,看似最简单的正确实现是:加载所有内容,无论在哪里,让任何连接找到其状态在等待。这并不是一个打字错误,而是一个设计悄然假设节点应该准备好服务任何可能到来的客户端。一个小的本地实例,恰好是开头提到的错误问题的缩影:它优化了“集群可以服务任何人”,而不是“这块特定状态有这个特定要求”。 这个链条通常不会以架构问题的形式出现:错误的抽象 ↓ 错误的保证 ↓ 错误的架构 ↓ 错误的扩展。错误的抽象:将“谁服务这个客户端”看作每个节点都需要的答案,而不仅仅是一个。错误的保证:到处复制,“以防万一。”错误的架构:没有对节点负责的边界。错误的扩展:成本随车队规模增长,而不是实际责任增长。一个OOM只是这个链条恰好变得可见的地方。它也可能表现为缓慢的内存泄漏,无法解释的扩展限制,或者在车队增长时神秘变得更风险的滚动更新。这个问题显现出的问题:简单地说,它不再看起来像内存错误,而是看起来像建模错误:我们假设这个状态需要在任何可能的客户到达的地方进行复制。它完全不需要那样——它只需要一个拥有者,由任何节点可以自行计算的规则决定。这是一个超越这个事件的问题:不是“我们如何在更少的内存中装下它”,而是“这个状态实际上需要什么?我们是否给了它超出需求的东西?”在修复之前命名错误:我们陷入的默认情况也值得一个名字,尽管没有人故意选择它。我们开始将这种设计习惯称为统一一致性:将一种一致性策略应用到整个系统,让每个状态部分继承它,而不考虑该特定状态实际上需要什么。为了明确,我们并不是说一种具体的算法——统一一致性并不是你在论文中会找到的技术。我们所指的是习惯:到处默认为系统已经信任的一致性模型,而不询问每个状态是否真正需要那么多。错误的假设,现实 ──────────────── ─────── ─────────────────── 每块状态 每块分布式状态 有其专属的语义 需要相同的保证。 ───────► "如果两个节点短暂不一致会发生什么?" 没有人通过决定“我们将应用统一一致性”来设计系统。这是默认发生的结果,一点一点小的决策,当一开始的问题没有提出时。它不是一个稻草人论者——这是我们实际所做的事情,OOM
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡