本文最初发布于 Lena Hall 个人博客,经原作者 Lena Hall 授权,由 InfoQ 中文站翻译并分享。
导读: 本文基于 Lena Hall 的 O’Reilly Velocity 2019 主题演讲,进行了解读。分布式系统是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。内聚性是指每一个数据库分布节点高度自治,有本地的数据库管理系统。透明性是指每一个数据库分布节点对用户的应用来说都是透明的,看不出是本地还是远程。在分布式数据库系统中,用户感觉不到数据是分布的,即用户不须知道关系是否分割、有无副本、数据存于哪个站点以及事务在哪个站点上执行等。说了这么多,但实际上,分布式系统一点都不复杂,简单的说就是:将整个个软件视为一个系统(不管它有多复杂),然后将整个系统分割为一系列的进程,每个进程完成一定的功能。再将这些进程分散到不同的机器上,分散后,选择若干种通信协议将它们连接起来。这就完成了分布式系统的构建。人们常常把分布式系统自然而然的和并行计算联系起来。然而这并不正确。实际上,分布式系统并不一定是并行的。Lena Hall 给我们详细阐述了“完美无瑕”的分布式系统应该是什么样的,以及我们应该如何朝着这一目标做哪些工作。
本文阐述了我们所做工作带来的影响,我们所面临的复杂性和障碍,以及对于构建更好的分布式系统的重要因素,尤其是当其他关键领域依赖并建立我们所创造的基础之上时。
目前可用的系统已经提供了许多解决方案,封装了许多分布式算法,实现了自动化并抽象出一些复杂性。使用这些系统的工程师并不一定非要拥有开发它们所需的大量知识不可。尽管对于工程师新手来说,了解这些系统所依赖的基础知识越来越没有什么必要了,但在某些情况下,了解幕后工作对于做出正确的决策并解决出现的困难问题还是必不可少的。
基础知识仍然很重要吗?
为什么说重视基础知识很重要?今天,我们正处于这样的一个阶段,分布式系统在医疗系统、自主设备、传输自动化和其他生命攸关系统中的应用越来越广泛,在这些场景中,错误的代价越来越高,而正确性变得日益重要起来。
错误的代价并不是指你的系统今天不可用的时间长短。它是关于你的系统发生宕机或故障时,给用户或他们的用户带来的代价。我们应该负起责任,并永远记住我们为什么这样做,我们真正要解决的问题是什么。
每个行业都希望通过结合他们和我们的研究和解决方案来取得更大的进步。而我们的工作能够帮助其他领域更好地实现他们的目标。对我们来说,了解如何放宽某些限制或者微调某些权衡也是非常有用的。
了解核心的内容,是一个强大的工具,可以让我们从容驾驭不断变化的选项和工具的复杂性,并帮助我们构建正确的解决方案,以改进我们现有的选项。
但事实证明,要做到这一点并不是那么容易。在我们前进的道路上,荆棘载途,艰难险阻。
理论和实践存在巨大的差距
理解“正确”对我们的系统意味着什么,对我们来说,不啻为一个挑战。大多数理论材料都很不“接地气”,众所周知,它们很难被理解并掌握。不止如此,它往往还不包括那些将这一理论付诸实践所需的信息。要想应用到生产系统中,就必须修改理论算法,并对它们进行调整以适应实际环境。其中许多并没有透露对实际解决方案很重要的具体细节。甚至对协议细节稍有误解,也会破坏协议的正确性。因此,我们需要做更多的额外工作,以保证实践时,协议仍然是正确的。
正确性难以验证和维持
在实际环境中验证并维持分布式系统的正确性,是一项具有挑战性的工作。它可能听上去很完美,但在实践中,却可能是低效的、或者难以实现的。这在理论上听起来不切实际,但在实践中却是完全可以接受的。须知在算法逻辑及其实现中,很多地方都有可能出错,而且还难以检测。
正确性并不总是优先考虑的事
另一方面,正确性并不总是优先考虑的事。还有截止期限、竞争和客户都需要更快的解决方案。终端系统还可能会出现仓促而不能正确修复的情况。这意味着团队可能没有足够的时间来做这件事:正确地讨论并系统地解决罕见的“间歇性”错误背后的真正原因,这些错误在未来还会再次发生,导致出现更多的错误。
我们该如何改进?
我们可以从多个方面改进。其中之一就是:强调并集中精力提高正确性,以确保我们能够构建并维护系统,使其始终按照我们所希望的那样运行。
另一个就是,提高对它们如何工作的理解,因为这有助于我们减少复杂性和可能出现的错误,并使我们更有准备迎接即将到来的挑战。
当我们不直接实现分布式算法和概念时,我们肯定会依赖基于那些构建的系统。在我们构建的内容与其他领域相互交叉时,理解基本概念和权衡就变得极为重要了。
如果向你承诺了一些性能和一致性,那么你该如何确保这些承诺就是在你需要的确切水平上提供的呢?
简单问题复杂化
当几台计算机相互通信时,琐碎的问题就变成了棘手的问题,而且它们还会累积起来。分布式系统难以理解,也难以实现,而且在实践中很难保持它们的正确性。
我认为有很多方法可以说明其原因。最近,我有机会为生物信息学领域的人解释,他们想知道为什么需要在分布式环境中的重要属性之间进行权衡。
排序
我想到的第一件事就是对事件进行排序。在一台机器上进行排序很容易,但当消息通过网络发送时,就变得很难了!我们不能依赖物理时间戳,因为不同机器上的物理时钟往往有所偏移。对于分布式系统中的排序,我们经常应用逻辑时钟,或者简单地说,在节点之间传递的计数器。
由于分布式系统的异步特性,我们并不能很容易地为所有事件建立竞争顺序,因为其中一些事件是并发的!我们所能做的就是找出哪些事件是并发的,哪些事件在彼此之前发生。即使是这样一个简单的任务,我们也需要做出一些决定。
例如,如果系统告诉我们事件是有序的,但实际上它们是并发的,那么,这种情况我们是否可以接受呢?
或者,我们是否需要确切地知道,当我们可以对事件进行排序时,事件真的不是同时发生的吗?
协议
我们不能简单地对并发事件进行排序,有时候,我们仍然需要决定操作的顺序、值、值序列或其他任何东西。
事实证明,让几台机器选择相同的东西是另一种情况。在这种情况下,我们必须要问自己的问题,并决定什么对我们是合适的。
例如,二阶段提交(Two Phase Commit)的是一种解决方案,在这种解决方案中,我们的节点可以就某些方面达成一致。
译注:在计算机网络以及数据库领域内,二阶段提交(Two-phase Commit)是指,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被称为是一种协议。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的 ACID 特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者) 的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
只要我们的节点不出故障,它就可以进行工作。
如果一些节点发生崩溃,为了防止出现数据不一致的可能性,系统就会阻塞,直到崩溃的节点返回,这种情况也有可能永远不会发生;或者需要非常、非常长的时间。
所以,这个算法是安全的,但它不是实时的。
如果这还不是我们所能接受的,那么我们是不是真的需要一个系统来回应呢?
在这种情况下,我们可能有另一种潜在的解决方案:三阶段提交(Three Phase Commit),当节点出现故障时,它就不会发生阻塞。
译注: 三阶段提交(Three-phase commit),是在计算机网络及数据库的范畴下,使得一个分布式系统内的所有节点能够执行事务的提交的一种分布式算法。三阶段提交是为解决两阶段提交协议的缺点而设计的。与二阶段提交不同的是,三阶段提交是 “非阻塞” 协议。三阶段提交在二阶段提交的第一阶段与第二阶段之间插入了一个准备阶段,使得原先在二阶段提交中,参与者在投票之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的 “不确定状态” 所产生的可能相当长的延时的问题得以解决。
但是,当有网络分区时,情况又会怎么样呢?
网络的两个相互隔离的分区在超时后可能会做出两个不同的决定,系统最终将会处于不一致的状态。
所以,这时候我们得到的是相反的结果:系统是实时响应的,但并不安全,因为不同的节点可以决定不同的值。
如果我们对这两个选项中的任何一个都能接受,那就太好了!
如果我们希望数据始终保持一致性,并且系统还要能够响应,那么我们应该做什么呢?
不可能结果
不可能结果 证明,实际上,在完全异步的环境中,总是终止并最终作出决定的确定性算法是不存在的,甚至可能出现一次故障。
译注: 不可能结果(Impossibility result)是分布式领域的专业术语,在一个完全异步的消息传递分布式系统中,如果一个进程有故障,那么一致性问题是无法得到解决的。简单来说,就是在一定的限制条件下问题能否被解决,那么任务的不可能结果就只有两种情况:能和不能。
从这个结果中,我们可以学到的主要内容是:如果我们想在实践中解决协议,我们就必须重新考虑我们的假设,以反映更为现实的期望!例如,我们可以设定最大消息延迟的上限,并确定系统可接受的失败次数。
如果我们改变假设,那么我们就可以用多种方式来解决分布式协议!
Paxos 算法
最著名的解决方案是 Paxos 算法,众所周知,这个算法晦涩难懂,而且难以正确实施。
它实际上是可行的,但只有在大多数节点都必须启动,且最大消息延迟是有限的情况下才可行。
在 Paxos 中,任何节点都可以提出一个值,在经过“准备”和“建议”阶段后,所有节点都应该就相同的值达成一致。
大多数节点都需要启动,因为如果在每个阶段中 Quorums 相交,总有有至少一个节点记得最近的建议是什么,这就阻止了对旧值达成一致。
为了提高算法的效率,在实际应用中,对出事算法进行了许多优化。基于它们所选择的权衡,共识算法也有许多可能的变体。
例如,Leader 完成了多少工作。拥有一个强大的 Leader 可能是好事,但也有可能是坏事。这要取决于失败的频率和重新选举的难度。另一种权衡是,我们可以容忍多少节点出故障,以及 Quorum 数量应该有多少。
有些被低估的标准是算法在实践中的可理解性和可实施性。Raft 之所以广受欢迎,是因为它更容易理解,现在已经应用于许多广泛使用的项目中。
译注: Paxos 算法是 Leslie Lamport(就是 LaTeX 中的 “La”,此人现在在微软研究院)于 1990 年提出的一种基于消息传递的一致性算法。这个算法被认为是类似算法中最有效的。Paxos 算法解决的问题是一个分布式系统如何就某个值(决议)达成一致。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个 “一致性算法” 以保证每个节点看到的指令一致。
新的权衡仍在不断发现
但更有趣的是,尽管共识和协议的话题并不新鲜,但我们仍然发现了许多新的优化和权衡。
在经典的 Paxos 算法中,大多数节点都需要启动以确保所有的 Quorum 相交。但事实证明,我们可以重新考虑并简化大多数 Quorum 要求。事实证明,只有准备阶段和建议阶段的 Quorum 相交就足矣,这为我们提供了更多的灵活性来对每个阶段的 Quorum 大小和性能进行实验!
重点是,揭示迄今为止的新性能和可用性权衡,有助于我们在实践中扩大选择范围。共识只是一个构件块,但它可以用来解决许多常见问题,如原子广播(atomic broadcast)、分布式锁(distributed locks)、强一致性复制等等。
请查看 Heidi Howard 博士的论文 “Distributed Consensus Revised”,该篇论文是这方面最好的论文之一。
一致性复制?
复制(Replication)是当今任何分布式系统的重要组成部分。
我们实际上可以用共识来实现强一致性复制,但它的缺点之一就是性能。另一方面当然是一致性复制,这种复制速度非常快,但在客户端却可以看到不一致的数据。在实践中,我们通常会希望获得更好的性能,同时还要保持更强的一致性,这可能有些棘手。
因此,在某些情况下,我们可以提出比共识更快的解决方案,而且比最终的一致性更加一致。
其中一个有趣的例子是 Aurora,它避免了对 I/O 和其他一些操作达成共识。它们使用 Quorum 进行写入,但不用它们来阅读。实际上,副本可能会存储不同的值,但当客户端执行读取操作时,由于数据库维护一致性点,因此它可以直接查看已知数据一致的节点,并将正确的数据返回给客户端。
无冲突可复制数据类型
另一个有趣的例子是无冲突可复制数据类型(Conflict Free Replicated Data Type,CRDT)。它们可以提供强大的最终一致性:快速读写操作,甚至在网络分区期间仍然保持可用,而无需使用共识或同步。然而,只有在我们能够有解决任何并发冲突的规则时才有可能。
换句话说,如果可以使用某些函数合并并发更新,这些可以按任意顺序应用它们,并且还可以按照我们想要的任意次数应用它们,而不会破坏结果,那么我们就只能使用这种技术。
这是一个完全可以接受的示例,其中所有更新都是附加的,因此它们完全满足这一要求。
另一方面,这一点并不明显,因为我们没有明确的规则来解决这种同时更新的冲突。
Azure Cosmos DB 使用 CRDT 在并发多主多区域写入的后台来解决冲突。Redis 和 Riak 也使用 CRDT。
故障检测
如果我们在分布式系统中进入另一个主题,我们总会发现需要进行更多的权衡。
故障检测器 是分布式系统中发现节点崩溃的关键技术之一。它们可以应用于协议问题、Leader 选举、组员协议和其他领域。
我们可以通过故障检测器的“完备性”、“正确度”来衡量它们的效率。
完备性显示了系统中的一些或所有节点是否发现所有故障。正确度度量故障检测器在怀疑另一个节点故障可能出现的错误级别。
事实证明,即使是不可靠的故障检测器在实际系统中也是非常有用的,因为我们可以通过添加一种故障知识传播到所有节点的“八卦”机制,来提高它们的完备性。
为什么所有这一切都很重要?
权衡可能是多种多样的,如果我们知道如何使用它们,知道在哪里寻找它们,那么我们就可以变得非常灵活。
许多产品都是围绕算法和权衡而构建的。这些产品为我们做出了某些选择,我们通过使用某些产品来做出选择。未受教育的选择可能会导致延迟和数据丢失。对于某些系统来说,这点可能会导致客户流失和巨额资金。对于其他系统来说,它可能导致反应缓慢,或者操作顺序错误,从而构成了实际的生命威胁。理解你的权衡对于做出正确的选择、了解正确的含义以及在现实中验证系统的正确性非常重要。
验证与维护现实中的正确性
在我们明确了抉择和权衡之后,我们该如何在实际系统中维护正确性呢?
系统模型检查是验证分布式逻辑的安全性和活性(尤其是安全性)的常用选项之一。模型检查很有用,因为它探索了系统最终可能出现的所有状态。现在有相当多的工具,如 TLA+ 就非常有名,除此之外还有更多的新兴技术,如语义感知模型检查。
为了验证分布式系统的实际运行实现的正确性,仅靠模型检查是不够的。
很少有项目会发布关于如何维护其系统的正确性并对其进行验证的相关信息。但其中一些项目做到了。
例如,Kafka 的各种系统测试 每天都在运行,世界上的任何人都可以检查并查看 哪些工作符合预期,哪些工作不正常。
Cassandra 对他们的综合测试方法写了一篇很棒的文章。
我真的希望,会有更多的产品、项目和系统能够对他们投入测试和正确性验证方面的工作更加开放。
如果我们看一下准备运行生产机分布式系统需要做些什么,就会发现有很多事情需要去做。
当然,对于小范围场景并确保多个服务协同工作良好,单元测试和集成测试是必不可少的。但这些还不够!我们可以使用更多的技术。模糊测试和基于属性的测试为你的系统提供了随机生成的输入,以确保基本属性基于其规范是正确的。实际上,我在 Microsoft 从事过一个关于模糊测试的项目,总体来说,这是一个非常吸引人的话题。性能测试对收集各种组件的延迟和吞吐量的数据非常有用。故障注入有助于检查系统在故障情况下是否可用,以及预期的系统属性是否仍然保持正确。
综上所述,导致最严重错误的背后的大量原因,仍然是异常处理逻辑。
有些事情,我们无法完全解决。我们需要接受这样一个事实:在我们编写完所有的测试和检查之后,无论如何,错误都会存在。我们是人类,再加上上下文切换,因此我们不可能知晓每一件事,有太多的活动部分了。我们永远没有探索过新的领域,如果我们害怕离开所熟悉的领域,那么我们就永远不会取得进展。但是,我们绝对可以为处理意外错误做出更好的准备,找到模式,并试图解决导致错误的原因。这就是为什么检测代码很重要,可观察性也很重要。当我们意识到这种可能性并为解决生产错误奠定基础时,就不那么可怕了。
题外话
产品变化很快,描述它们的一致性、弹性和性能的术语极其繁多。基本概念和权衡仍然存在,并不断积累。它们在孤立的情况下并没有什么用处,但是了解它们,对于做出正确的选择和在实践中保持正确性是必不可少的。当我们的系统在特定级别的响应性和安全性有强烈要求的场景中受到信任时,正确性尤为重要。
如果你正在构建某些内容时,请扪心自问:这会被误解吗?复杂性就像项目周围的一堵防弹墙一样,它使解释、构建、使用和改进变得困难。试着让你构建的系统能够被其他人理解,因为理解有助于正确性的提高。
正确性并不容易,也不是免费的。你必须致力于此,并将其作为优先事项。不仅仅是一个愿意这样做的工程师的水平上,而是整个组织的水平。不要相信你的系统知识工作:测试它,验证它,并在事情失败时做好准备,向你的用户和客户展示你在演示系统方面投入了哪些技术和努力。
想想那些与你工作相关但没有得到足够重视的重要领域吧,如果你有机会和其他领域工作的其他人聊天,那就去做,了解他们在工作中所面临的挑战,以及他们正在做出的权衡。虚心求教,与他们分享你的工作。这会帮助你成长为一个更优秀的工程师。
作者介绍:
Lena Hall,Microsoft 高级软件工程师,从事大数据和分布式系统方向。曾在 Microsoft 研究院工作。同时也是一名技术演讲者、机器学习 ML4ALL 的组织者、Kafka 峰会的董事。
原文链接:
Eventually Perfect Distributed Systems
评论