Metadata:分布式系统设计要点和建议


这些建议提示都是分布式系统研究人员和从业人员几十年来的集体成果。

提示分为三类:功能、性能和容错:
功能:

  • 应用抽象
  • 减少协调
  • 拥抱单调性

表现:
  • 偏爱偏序而不是全序
  • 杠杆时间
  • 使用间接和代理
  • 模拟估算

容错/可用性(是否继续工作):
  • 根深蒂固的容错能力
  • 保持性能梯度
  • 投资确定性模拟
  • 感受到痛苦


1.应用抽象

  • 将协议从系统中剥离出来,省略不必要的细节,将复杂的系统简化为一个有用的模型,然后在这个抽象的平面上设计解决方案。
  • 在这个抽象的平面上,没有任何干扰,你可以更自由地探索整个设计空间,并找到合适的解决方案。
  • 然后,你再小心翼翼地将解决方案细化到具体平面上。

重要的一点是要抽象思考,不要被细节所困扰。不要只见树木,不见森林。
 
抽象是避免分心、在更高层面提出创新解决方案的有力工具。

在分布式系统领域例子:

  • 简单的 map-reduce 抽象启动了第一代云计算中的数据密集型计算,因为它很好地隐藏了底层资源分配、容错和数据移动的复杂性。
  • Tensorflow 和其他数据流工作概括了 map-reduce。

2.减少协调

  • 协调会带来瓶颈,增加故障风险和故障爆炸半径。因此,在分布式系统中最好尽量避免协调。
  • 尽量少用协调,主要是在控制平面,尽量减少其在数据路径中的存在。
  • 即使作为分布式协调研究人员和 Paxos 专家,我的建议也是减少协调。
  • 协调的成本很高,如果不需要协调也能解决问题,那就尽一切可能不去协调。

乐观并发控制(OCC)就是一个很好的例子。在 OCC 中,每个操作的执行都不需要获取锁,冲突会被检测到并在必要时解决。这就是 "偷懒 "带来丰厚回报的例子(当然是针对低冲突的工作负载)。

减少协调的另一个重要实现方式是将读取路径与正确路径分离,并且在进行读取操作时无需与在项目上进行的写入操作协调。

3.拥抱单调性

  • 单调性可确保在获得新信息时,过去做出的决定和采取的行动不会失效。
  • 单调性是减少协调开销的有效方法。
  • 要在分布式系统中高效、安全地进行协调,几乎总是要将问题转化为单调性问题。
  • 单调性的好处在于,它可以在整个宇宙中 "预复制",从而无需通信即可达成共识。

厄尼-科恩是这方面的大师。他能迅速找到一种方法,将任何问题转化为具有单调性成分的问题,并围绕这一单调性角度来制定协议,从而产生一种高效而有弹性的设计。

例子:

  • 使用不可变数据会使事情变得很轻松。
  • 在操作空间中使用幂等操作也是同样的道理。
  • 多版本和时间戳的使用也是如此。(banq注:UUID全局唯一ID也属于这种)
  • 最后,乔-海勒斯坦(Joe Hellerstein)及其合作者的 CALM(一致性与逻辑单调性)系列工作也是体现单调性的典范。

4.偏好部分顺序而非整体顺序

  • 整体排序意味着每个事件相对于其他事件都有一个唯一的位置。完全排序的成本很高,而且在出现故障时很难维护。
  • 部分排序是指某些事件可能没有确定的相对排序。
  • 在分布式系统中并没有:事件是根据因果关系排序。见这篇文章:分布式系统中的同时性问题
  • 部分排序在处理并发和故障方面灵活有效。部分排序有助于抑制尾部延迟。例如,通过使用法定人数或擦除编码,可以获得 N 的 K 的最佳性能,并消除尾部延迟。

因此,比起总序,更倾向于部分序。

在大型系统中,一个很好的例子就是在事务中使用快照隔离(SI)语义。SI 在工业界很普遍,因为它们不会影响性能(与此同时,学术界坚持发布可序列化事务的工作)。
与可序列化事务相比,SI 允许更多的部分顺序。这就减少了读取的尾部延迟,因为读取不会与写入发生争执,也不会等待写入。
你不需要检查读/写冲突,也不需要为序列化支付额外费用:例如开发人员需要知道如何必要时使用 "SELECT FOR UPDATE "来处理写偏移问题。

5.利用时间提高性能

  • 不要依赖时间来保证正确性,而是根据需要利用时间来提高性能。
  • 使用单调的时间(如混合逻辑时钟(HLC),banq注如UUID全局唯一ID)来实现数据成熟水印,以避免现在的浮躁(Pat Helland 创造了这个很棒的说法)。
  • 新鲜数据/操作具有潜在的波动性和不稳定性。
  • 通过实施数据成熟度水印,我们可以根据稳定的信息做出安全、快速的决策。

请注意,这与拥抱单调性提示是一致的。

著名的是,谷歌 Spanner 引入了使用原子钟和 TrueTime API 来实现严格可序列化的分布式SQL 系统,只需等待小时钟ε清除不确定性,即可实现可线性化读取。

最近利用时间的一个例子来自PolarDB。该系统使用修改时间跟踪表(MTT)来提供强一致性读取。该协议还利用了单调性。

一些无领导共识协议(例如 Accord 和 Tempo)使用时钟同步来建立时间戳稳定性水印来代理依赖性跟踪。一些无领导共识协议还故意对消息施加延迟,以实现粗略地按顺序传递到多个副本,以减少异地复制中的冲突。

更通用的分布式系统领域的示例可以一直追溯到“同步时钟的实际使用”论文 PODC 1991 年租赁是利用时间来减少协调和行使选择性的一个例子

6. 使用间接和代理
间接和代理是扩展分布式系统的首要资源。“计算机科学中的任何问题都可以通过添加另一级间接来解决。” 因此,当您遇到瓶颈时,请尝试引入间接和代理。

示例包括使用负载平衡器和缓存。
使用缓存时,要警惕不稳定性陷阱(参见提示 #9)。Brooker 有一篇关于此问题的精彩文章,DynamoDB 就是为了避免这种情况而设计的。虽然缓存等组件可以提高性能,但 DynamoDB 不允许它们隐藏在没有缓存的情况下会执行的工作,从而确保系统始终具备处理意外情况的能力。

7.模拟估算

  • 轻量级形式化方法有助于设计正确的容错协议。但是,如果没有良好的性能,正确的协议也是无用的。Marc Brooker 在他的 "形式化方法只解决了我一半的问题"一文中很好地解释了这一点。
  • 在设计协议时,我们需要考虑性能和成本。
  • 模拟是对性能(延迟、吞吐量)和成本(容量和货币成本)进行事后估算的好方法。
  • 这里值得强调的是货币成本,因为它在设计实际系统时非常重要。

您可以用 Python 或其他编程语言编写模拟程序。

  • 关键是要有一个原型。
  • 模拟是快速失败的好方法。
  • 你可以利用模拟来尽早发现瓶颈,然后使用前面提到的提示(如使用间接/代理、部分顺序),看看是否能缓解瓶颈。
  • 使用模拟还有助于规划容量和延迟预算,并以此为基础倒推工作。
  • 通过仿真,您还可以在不同的工作负载模式下尝试使用系统模式,看看该协议是否是正确的选择。
  • 如果性能不佳,就回到绘图板,利用抽象和 TLA+ 等工具进一步探索设计空间。

如今,形式化方法也开始添加一些统计检查支持,这种模式有助于估算哪种协议变体最适合实现。

8.植入容错功能

  • 容错应该在设计阶段就根深蒂固,而不是事后才想起来,作为一个附属品添加进去。
  • 事后加装容错是有问题的。你会错失良机,在协议设计中走弯路。
  • 事后加装容错会带来性能/可扩展性问题、复杂性和成本。
  • 它还会给数据的一致性和耐用性带来许多问题。
  • 附加容错还可能使你容易陷入不稳定性陷阱(参见提示 9)。

AWS 非常重视静态稳定性。静态稳定性是指数据计划在数据平面受到干扰时保持平衡的特性。

其他建立容错的例子来自无状态或软状态服务设计。如果你能拥有这些服务,那就再好不过了,因为你可以根据需要重启这些组件或进行实例化,而不必担心协调问题。
(banq注:无状态比有状态更容错,更健壮,因为没有上下文状态信息,丢失也没有关系,但是找到状态的类型很重要,而有状态设计是针对特定上下文数据,更快地完成工作)

9.保持性能梯度
避免陨变故障(metastability failures亚稳态失效)非常重要。
亚稳态意味着负载吞吐量永久过载,即使在移除触发器之后也是如此。
亚稳定性就是避免自己做 DOS:避免不对称的请求响应工作。

什么情况下会出现这种情况?如果过度优化普通情况,就会发生这种情况。为了避免这种情况,应尽量避免性能上的巨大差异,并在系统中保持性能梯度(阿列克谢的术语)。

DynamoDB 提供了一个很好的示例来说明如何保持性能梯度。
DynamoDB 的杀手级功能是其可预测性。虽然缓存等组件可以提高性能,但DynamoDB 不允许它们隐藏在它们不在的情况下将执行的工作,从而确保系统始终能够处理意外情况。

在这里以 SQL 与 NoSQL 为例: SQL 的声明性是一个主要优势,但也是操作问题的常见根源。

  • 这是因为 SQL 掩盖了有关运行程序的最重要的实际问题之一:我们要求计算机执行多少工作?
  • 这说明了抽象与实现之间的紧张关系。
  • 抽象(提示#1)不应隐藏性能等重要特征。
  • 关于这个主题,请注意垃圾收集开始时间

10.投资于确定性模拟

  • 在受控的确定性环境中模拟分布式系统,可以发现潜在的正确性和恢复问题。
  • 确定性仿真还有助于调查代码是否符合协议。
  • 最后,确定性模拟还有助于通过单盒测试提高开发人员的开发速度,因为开发人员可以更快、更有信心地添加功能。

AWS 开发的 Turmoil 就是一个很好的例子,它提供了一个基于 Rust/Tokio 的确定性模拟环境。Turmoil 可帮助团队识别拐角案例(包括涉及故障和分区的案例),以确定性的方式重放这些案例,并快速找出问题所在。

FoundationDB 为确定性模拟测试提供了一个很好的用例:"甚至在构建数据库本身之前,FDB 团队就已经构建了一个确定性数据库仿真框架,该框架可以在单个物理进程中使用合成工作负载模拟交互进程网络,并模拟磁盘/进程/网络故障和恢复。FDB 依靠这种随机的确定性模拟框架,在单个盒子中对其分布式数据库(使用真实代码)的正确性进行故障注入和端到端测试"。

再比如,基于模型的测试用例生成可以识别在开发过程中可能并不明显的死角和漏洞。你可以运行这些死角案例,甚至可以对你的实现进行线性驱动测试,以检查代码的一致性。

(banq注:限定上下文环境,然后将这个事物放入这个限定仿真上下文环境中测试,其实这是由于人追求确定性心理引发的,会让人变成上帝一样疯狂,上下文环境是那么容易从上而下制造出来吗?事物是从下而上涌现出来的)