分布式系统中的乐观和错误假设


避免协调是让我们构建的分布式系统超越单机性能的一个基本要素。当我们构建避免协调的系统时,我们最终构建的组件会假设其他组件在做什么。这一点也很重要。如果两个组件不能在每一步操作后都互相检查,那么它们就需要对其他组件正在进行的行为做出假设。

有一种方法是设置假设前提,前置条件,这种前提可分为乐观假设和悲观假设。本文讨论的是这种先入为主的假设有时很多余,甚至自己绊自己。

在思考分布式系统的设计时,明确每个组件所做的假设、该假设是乐观的还是悲观的,以及如果假设错误会发生什么情况,是非常有用的。

在悲观假设和乐观假设之间做出选择,会对系统的可扩展性和性能产生巨大影响。

假设与协调性:

  • 乐观假设会避免或延迟协调,
  • 而悲观假设则需要或寻求协调。

假设与自以为是:
  • 乐观的假设认为自己会按计划行事。
  • 而悲观的假设则会铤而走险,确保自己能够得逞。

为了具体说明这一点,让我们举几个例子:

示例 1:缓存
分布式缓存几乎总是假设其所保存的数据是否发生了变化。与 CPU2 不同,分布式缓存通常并不一致,但我们仍希望它们最终保持一致。我们所说的最终一致性是指,如果写入流停止,缓存最终都会趋于包含相同的数据。换句话说,不一致的时间相对较短。

确保这一特性(即不一致性是短暂的)的最常见方法可能就是使用生存时间(TTL)。

简单地说,这意味着缓存只在一定的固定时间内保存项目。TTL 为项目的陈旧程度提供了一个强 上限。这是一种简单、强大且广受欢迎的机制。

但这也是一种悲观的假设:假设缓存中数据条目发生了变化,缓存就会做额外的工作。
在每条目的写入率较低的系统中,这种悲观假设的错误概率要比正确率高得多。(再写入较低情况下,没有必要有这种悲观假设)

TTL 是悲观假设以后制造出的新问题,另外一个缺点是:当缓存无法与源对话时,它就会清空缓存。这是不可避免的:如果缓存无法与源联系,那么缓存就无法提供有强约束的滞后性(或任何其他强滞后性保证)。

因此,悲观的 TTL 假设具有很强的可用性劣势:如果网络分区或授权停机时间超过 TTL,缓存命中率将降为零。

示例 2:乐观并发控制OCC
乐观并发控制及其与基于锁的悲观方法之间的权衡是分布式数据库中的一个经典话题(也许是最经典的话题)。

  • OCC乐观并发控制是一种实现隔离(如 ACID I)事务的方法,它假定其他并发事务不会发生冲突,并在最后时刻检测该假定是否错误。
  • 而像经典的两阶段锁2PC这样的悲观方法,则是在假定其他事务会发生冲突的基础上进行大量的协调工作,值得趁早发现,以避免重复工作并做出明智的调度决策。

一般来说,当乐观假设正确时,OCC 系统的协调程度低于悲观系统;当乐观假设错误时,OCC 系统的协调程度高于悲观系统。

对这两种方法进行比较是一个很难解决的一阶问题,但更复杂的是,在乐观和悲观之间做出选择还会导致一系列二阶问题。
例如:争执事务的数量取决于并发事务的数量,而并发事务的数量在悲观系统中取决于锁等待时间,在乐观系统中取决于重试率。
在这两种系统中,过去的争用和未来的争用之间都会产生直接的反馈回路。

例 3:租赁锁
租约是分布式系统中广泛使用的一种基于时间的锁。

在大多数系统中,租约取代了许多协调步骤。一个组件获得一个租约,然后将租约作为许可来执行多项任务,而不必担心其他组件正在执行与之冲突的任务,或可能会出现分歧等。由于不用担心冲突,持有租约的组件可以避免协调,全速前进。

租约是悲观主义(我假定其他事情会与我的工作发生冲突,所以我要阻止它们)和乐观主义(我假定我可以在接下来的一段时间内不需要协调就能继续工作)的有趣结合。

但是如果悲观这种假设是错误的呢?
那么所有的心跳、更新和租约存储都是白费功夫。其他组件在等待租约时浪费的工作时间也是如此。


结论
我喜欢对系统行为进行推理的一种方式是编写 "此组件假设...... "这样的句子。(banq注:寻找上下文的假设前提条件是关键)

对于缓存在的 TTL 例子,我们可以写出这样的语句(banq注:以逻辑命题的形式=形式逻辑):

  1. 本组件假定客户可以看到陈旧的数据,只要陈旧度是有界限的,以及
  2. 该缓存假定其保存的项目已发生变化,因此应在每次 TTL 到期后进行检查,以及
  3. 该缓存假定客户端宁愿经历不可用或更高的延迟,也不愿看到比 TTL 约束更陈旧的项目。

这些声明是帮助我们构建系统行为思维的工具。第三条--可用性与陈旧性的权衡--尤其强大,因为它通常是人们在选择严格的 TTL 时的一个隐藏假设。

通过将每种假设分为悲观型(需要协调)或乐观型(避免协调),我们还可以对协调的最佳时机进行结构化思考,并确保我们在选择需要协调的时间和原因时保持一致。