本章内容
- 识与解的区别
- 心智模型的作用
- 分布式系统的结构与行为
- 分布式系统的正确性、可扩展性与可靠性
- 构建分布式系统的必要性
每一个现代应用都是分布式应用。无论你在构建 Web 应用、移动应用、可穿戴应用、云服务还是游戏,问题不在于应用是否分布式,而在于它分布式到什么程度。分布式系统是由一组协作的、并发的组件构成,这些组件通过网络收发消息彼此通信:
- 分布式系统的行为由其组件的行为及其交互涌现出来。
- 分布式系统的复杂性由其组件自身的复杂性以及它们交互的精细程度涌现出来。
为什么要把应用做成分布式?毕竟分布式应用出了名地难以构建,为什么还要费这番周折?
作为工程师,我们用正确性(correctness) 、可扩展性(scalability)和可靠性(reliability)来衡量系统是否达标。换言之,系统必须在负载增加与故障不可避免的情况下仍能提供其既定功能。尽管单个组件可以实现功能,但没有任何单个组件能应对无界负载或挺过不可避免的故障。我们需要不止一个组件(如多个进程或节点)——我们需要一个分布式系统。
诚然,分布式系统很复杂,但对大多数软件工程师来说,理解分布式系统已不再是可选项。本书将为你打下扎实基础,使你能够自信地推理分布式系统,从而构建可用、可扩展且可靠的分布式系统。
注 在我理解分布式系统的旅程中,曾多次出现让人兴奋、增添自信的“啊哈!”时刻。在本书中,我会把这些“啊哈!”时刻分享给你,希望对你有所帮助。
1.1 软件工程与心智模型
软件工程可能令人愉悦,同时也可能令人沮丧。在复杂的分布式系统中,这种张力尤为明显。
对我来说,软件工程最令人沮丧的时刻,是我陷入困惑的时候。不幸的是,在这个行业里,困惑几乎是常态。其他学科已经发展出稳健的心智模型与交流方式,而软件工程仍在追赶。比如土木工程依托于以物理为根基、以数学表达并用标准化图示沟通的心智模型;而在软件工程中,我们却常常在日常使用的概念与术语上磕磕绊绊。你不妨提出一长串问题:“什么是微服务?”“什么是云?”“什么是云原生?”“什么是 Serverless?”你是否有明确答案?即便有,其他工程师是否同意你的看法?
我们常常只有模糊的概念,难以高效、有效地沟通。对我而言,这是极大的挫败。我不想通过对系统“探针式”地反复试探、观察、解释,然后在下一次试探时又无意推翻刚刚建立的认知。
我想要一个准确而简明的心智模型——一种简练的内部表征,使我能够自信地对系统进行推理。在本书中,你将建立准确而简明的心智模型,以自信地推理分布式系统;你还将学会构建心智模型,用确定性替代困惑,用自信替代犹疑。
【顿悟时刻】从“知道”到“理解”
知道不等于理解。知道复杂分布式系统,与理解复杂分布式系统,是两回事。
这一差别在诸多领域都很明显。以棋类为例:学习规则并不难,但掌握取胜的战略与战术却需要时间与投入。
国际象棋正是这种分野的完美示例:学会规则所需时间很短,但真正理解并熟练运用策略与战术则要长得多。这也是为何国际象棋常被视为衡量人类与机器“智慧”的尺度。尽管它只有一副棋盘、六种棋子与两位棋手,棋局却极其复杂而精妙。
分布式系统亦然。随着本书的推进,我们的目标不仅是积累更多知识,更要获得更深的理解——构建可靠的心智模型。
1.1.1 心智模型:推理之基
本书名 Think Distributed Systems 强调了心智模型的重要性——它们是推理的基石。在深入分布式系统的心智模型之前,先来了解心智模型本身:什么是心智模型?什么样的心智模型才算好?
模型是对目标系统的某种表征;心智模型是对目标系统的内部表征,是理解(能看懂)与沟通(能讲清)的基础。
例如,图 1.1 展示了一个简单的电路:右侧是系统本身,左侧是与之对应的心智模型(内部表征)。我们对系统的推理,正是建立在这一心智模型之上。
图 1.1 心智模型与系统
可以用一组“事实”来更直观地理解系统与心智模型的关系:
- 系统——可视为构成客观真实的一组事实。
- 心智模型——可视为构成主观认知的一组事实。
一个好的心智模型应当既正确又完备:
- 正确性:模型中的每一个事实都是真实系统中的事实。换言之,正确的心智模型不包含谬误。
- 完备性:模型包含系统中所有相关的事实。换言之,完备的心智模型不遗漏相关要点。
1.1.2 正确的心智模型
正确的心智模型要求:模型中的每个事实都与所表征的系统一致。若图 1.1 所示系统中的灯泡电阻为 3.0 欧姆,而模型也声明灯泡电阻为 3.0 欧姆,则该模型在这一点上是正确的。若模型写成 2.0 欧姆,则是不正确的。
1.1.3 完备的心智模型
没有任何模型能穷尽系统的所有事实。因此,我们将完备性定义为:覆盖系统中所有相关的事实;“相关”与具体应用情境有关,亦即“相关性”取决于观察者的目标。
如果我关心的是图 1.1 所示电路中通过灯泡的电流大小,那么该心智模型既正确又完备。但如果我关心的是灯泡的亮度,那么该模型就是不完备的,因为我缺少“发光效率”(把电能转换为可见光的能力)这一相关事实。
1.2 软件系统的心智模型
软件系统是虚拟构造,而非物理构造。我们可以把分布式软件系统看作一组通过网络交换消息而彼此通信的组件。然而,这些构造是虚拟的;无论怎么寻找,你都不会把它们当作物理构件找到。取而代之的是,软件系统表现得仿佛由这些构造组成。软件系统的可观测行为与我们对其建立的模型在表达力上是等价的;我们会看到多种等价模型,它们对可观测行为的刻画并无孰优孰劣。不同模型都是有效的!
注 我当然可以争辩说,软件现象最终可还原为物理现象,比如硅中的电子运动。但我赞同 Edward A. Lee 在 Plato and the Nerd: The Creative Partnership of Humans and Technology(MIT Press,2017)中的观点:“尽管软件归根结底是硅中的电子,但物理与软件之间隔着太多层,以至于这种联系对理解而言变得毫无意义。”
警惕类比(Beware the Analogy)
类比——基于感知到的相似性来比较两个概念——是构建心智模型时流行的教与学工具。类比承诺了可迁移性,让我们得以用熟悉的领域来推理一个新领域。比如,将电路类比为水在管道中流动,有助于澄清电流、电压与电阻等概念。
但需要提醒的是:采用类比的老师通常知道相似性在哪儿终止、差异性从哪儿开始,而学生未必有这份洞见。这会导致误解,比如把电“想象”为像水那样“流动”,从而过度简化并扭曲物理现实。
因此,尽管类比有助于理解某个概念,但按定义它描述的是另一个概念的模型。务必批判性地评估类比的边界,避免得出错误结论。
1.3 不同类型的模型
这些年来,我广泛阅读分布式系统相关资料,却愈发沮丧:几乎每篇博客、每本书、每篇论文对分布式系统的定义与描述都不一样。我常常在不一致的海洋中迷失,难以调和这些差异。
这段经历让我意识到:描述分布式系统没有唯一方式。方式多不胜数——非形式化或形式化的描述;把系统表述为动作机(action machine) 、状态机(state machine) 、状态–动作机(state-action machine) ,或进程代数(process algebra) ……清单还可以继续列下去。
这种多样性增加了复杂度,但也带来了深度——每一种心智模型都提供了独特视角。学习多种心智模型有助于我们获得全局性理解,并帮助我们识别自身思考中的遗漏或不一致。
1.3.1 用不同模型描述同一方面
我的第一个体会是:有时,描述分布式系统的不同方式确实是等价的,因为一种描述可以转换为另一种描述(见图 1.2)。
根据前文的定义,分布式系统是一组并发组件,这些组件通过网络收发消息彼此通信。我们可以把网络视为通信总线并同时作为在途消息(in-flight)缓冲区;也可以只把网络看作通信总线,而让每个组件各自维护一个用于跟踪在途消息的缓冲区。这两种描述是等价的:都能表达消息丢失、消息重复、消息乱序。
一方面,我们可以把分布式系统描述为:网络承担在途消息的缓冲(见图 1.3)。
图 1.3 由网络充当在途消息缓冲区
另一方面,我们也可以把分布式系统描述为:组件承担在途消息的缓冲(见图 1.4)。
图 1.4 由组件充当在途消息缓冲区
两种模型都不是错误的。它们都能捕捉系统的相关方面,如消息处于在途、或消息被丢失、重复、重排。模型的选择通常取决于偏好。
1.3.2 用不同模型描述系统的不同方面
我的第二个体会是:有时,描述分布式系统的不同方式并不等价,因为它们的关注点不同。某些描述刻意忽略系统的某些方面,另一些描述则将其纳入(见图 1.5)。
图 1.5 用不同模型描述系统的不同方面(各模型的“事实集合”部分重叠)
在其视频课程《Introduction to TLA+, the Temporal Logic of Actions》(lamport.azurewebsites.net/video/video…)中,Leslie Lamport 描述了事务提交(Transaction Commit)协议——对所有分布式事务协议的一个抽象,以及两阶段提交(Two-Phase Commit, 2PC)协议——分布式事务协议的一个实现。(第 6 章将深入探讨分布式事务协议与 2PC。)
其中,第一个模型忽略消息交换,而第二个模型将消息交换纳入考虑。于是,前者无法表示消息丢失、重复、重排;后者则可以。
注 在第 6 章与第 10 章中,我们将进一步讨论分布式事务协议、共识协议以及不同的分布式系统模型,并更详细地考察它们各自的特征。
盲人摸象
著名的“盲人摸象”寓言常用来说明:理解复杂或抽象主题时,从多种视角加以考量的重要性。故事里,六个盲人第一次遇到大象,各自触摸到象的不同部位(如鼻子、耳朵、尾巴),于是就按各自触到的部分来描述大象。寓意是:每个人的理解都是不完整且受限于其视角的;只有把所有视角合在一起,才能更完整地把握对象。
同样地,学习多种心智模型或不同的思维方式,有助于你获得更整体的理解;接触多样视角还能帮助你发现自己思考中的误解。
当你遇到对同一分布式系统的不同模型时,并不意味着其中一些就是不正确的。阅读帖子、文章或论文时,尽量理解作者选择了哪一种模型,以及为何选择它:是什么让该模型更适合作者要表达的观点?
请记住,这种做法可能相当具有挑战性,甚至令人沮丧。你也许需要暂时放下自己辛苦建立的心智模型,转而采用作者的心智模型。但作为回报,你会获得更全面的理解。
1.4 关于分布式系统的思考
本节先非形式化地讨论如何构建可理解的分布式系统及其机制的心智模型;在后续章节,我将形式化地讨论如何构建更为完备的心智模型。
分布式系统是一组协作的、并发的组件的集合,这些组件通过网络发送与接收消息彼此通信。每个组件对自身的本地状态拥有独占访问权,其他组件无权访问。网络也对其自身的本地状态拥有独占访问权,其中包括在途消息(见图 1.6)。
一种准确而简明地理解分布式系统行为的方式,是把它看作状态机(若要强调状态之间的转换步骤,也称状态—动作机)。系统的行为可表示为一系列状态,每一步(step)都将系统从一个状态推进到下一个状态(见图 1.7)。
在最一般的层面上,系统以离散步骤推进。每一步要么由某个组件执行,要么由网络执行。可以将这些步骤划分为:
- 外部步骤——例如接收消息或发送消息
- 内部步骤——例如执行本地计算或访问本地状态
总之,我们将如此看待系统:任一时刻,恰有一个组件或网络完成恰好一个步骤。
图 1.6 将分布式系统视为一组并发且通信的组件(未示出网络的本地状态)
图 1.7 将系统行为视为一条状态序列
注 在各类博客、论文和书籍中,组件常被称为 actors、agents、processes 或 nodes。相应地,步骤也常被称为 actions 或 events。
1.4.1 正确性(Correctness)
随着职业发展,我意识到自己对软件系统正确性的理解相当模糊:系统若“按预期工作”,那就是正确的(当然)。但这种定义无法给出判断系统是否正确的客观标准。我希望能够严格地思考系统的保证、能力与边界。
Leslie Lamport 在《Proving the Correctness of Multiprocess Programs》(lamport.azurewebsites.net/pubs/provin…)中给出了我最喜欢的定义:将系统的正确性刻画为其安全性与活性性质:
- 安全性(Safety) ——非形式地说,保证“坏事永不发生”。
- 活性(Liveness) ——非形式地说,保证“好事最终会发生”。
若系统的每一种可能行为都满足其安全性与活性保证(见图 1.8),则称系统是正确的。
图 1.8 安全性与活性
在分布式事务的语境中,安全性保证同一事务的任意两个参与者不会得出相互冲突的决策;而活性保证每个参与者最终都会作出决策。设一个分布式事务跨越两个参与者,即两个键值存储 P1 与 P2。
代码清单 1.1 一个分布式事务
Begin Transaction
AT participant P1 SET b TO "foo"
AT participant P2 SET a TO "bar"
Commit Transaction
图 1.9 展示了该分布式事务的行为空间——即所有可能行为。此处,一个状态反映了参与者 P1 与 P2 处的事务状态。事务可能处于三种状态之一:进行中(working) 、已提交(committed)与已回滚(aborted) 。一次状态转换仅更新一个参与者:将其从 working 变为 committed 或 aborted(请记住——恰一组件、恰一步骤)。
系统的安全性保证断言:当一方回滚时,P1 与 P2 不会出现一方提交、一方回滚的情形。活性保证断言:P1 与 P2 最终都会提交或回滚。二者合起来断言:最终 P1 与 P2 将达成一致决策——要么都提交,要么都回滚。换言之,安全性避免系统进入不一致状态,活性避免系统卡死。
起初,我觉得安全性/活性听上去过于学院派(委婉说法:能不能别这么“折磨人”?)。但我很快体会到,它们是极有价值的思维工具,能让我更有效、更高效地推理系统。
注 关注我们如何选择性地思考系统:虽然这里有两个键值库,但我们不以键与值的角度去思考,而只关心参与者 P1 与 P2 的事务状态,忽略了其他方面。比如,我们忽略了事务涉及的键与值。当然,我们也完全可以改从键与值的角度去建模,并纳入其他方面,如相关键上的锁。在此给出该模型,是在声明:这些被忽略的方面与当前讨论无关。你可能不同意——这并不意味着谁对谁错,只是我们的心智模型、关注点与兴趣不同。
1.4.2 可扩展性与可靠性
在《Defining Liveness》(mng.bz/jZdz)中,Bowen Alpern 与 Fred Schneider 证明:系统的任何性质都可表示为某个安全性性质与某个活性性质的交集。但我并不总用安全性/活性去思考系统的所有方面——尤其是可扩展性与可靠性。
这并不罕见:不同的心智模型会竞争我们的注意力。即便这些模型在表达力上等价(能以不同方式表达同一方面),我们也常会偏好其中之一。因此,我习惯将可扩展性与可靠性放到响应性(responsiveness)的框架下去思考——非形式地定义为系统满足服务级别目标的能力:
- 可扩展性(Scalability) :系统在负载存在的情况下保持响应性的能力。
- 可靠性(Reliability) :系统在故障存在的情况下保持响应性的能力。
1.4.3 响应性(Responsiveness)
响应性可以用四个相关概念来形式化:
- 服务级别指标(SLI) :对系统行为的量化观测。
- 服务级别目标(SLO) :作用于 SLI 的谓词(返回真/假),用于判定系统行为是否达成目标。
- 错误率(Error rate) :在固定时间区间内,未达成目标的观测数占总观测数之比。
- 错误预算(Error budget) :我们可接受的错误率上限。
基于这些要素,我们可将系统的响应性定义为:系统使其错误率保持在错误预算之下的能力。
提示 关于这些概念的详细讨论,推荐阅读 Google 的《Site Reliability Engineering》系列(sre.google/books)。
【顿悟时刻】应用相关性(Application-Specific)
包括可扩展性与可靠性在内的正确性保证都是与应用相关的。作为软件工程师,你可以定义系统的保证,并决定什么行为是理想的、可容忍的或不可容忍的。换句话说:你认为“坏掉了”的,可能在我看来仍属“可接受”。
【顿悟时刻】涌现性(Emergent)
包括可扩展性与可靠性在内的正确性保证都是涌现属性:正确性、可扩展性与可靠性并不溯源于单个组件。单个组件无法化解无穷负载,也无法单独化解一次崩溃故障。相反,可扩展性与可靠性是一组组件及其交互共同作用的结果。
1.5 两个重要观念
此处我想强调两个对我理解分布式系统影响深远的“大点子”:系统的系统以及全局视角与局部视角。它们将作为本书后文对“服务”(甚至——斗胆说一句——微服务)进行清晰、精确定义的基础。
1.5.1 系统的系统
分布式系统是一组并发且通信的组件。尽管这一定义没有明说,但它往往促使我们把组件视为原子(不可再分)实体。然而在软件系统中,多数“组件”并非原子实体,而是高阶实体,即子系统(见图 1.10)。
图 1.10 将分布式系统视为一组并发且通信的子系统
如何理解这样一种情形:一群并发通信的组件表现出某种特定行为,而当我们以更高抽象层级观察时,这群组件又仿佛是单个组件在行为?匈牙利哲学家 Arthur Koestler 在其 1967 年著作 The Ghost in the Machine 中引入了 holon 一词,指一种既是整体又是更大整体之部分的实体。根据观察层级不同,holon 可以被看作原子实体,或被看作由多个 holon 组织而成的高阶实体。由 holon 组成的层级结构称为 holarchy(见图 1.11)。
图 1.11 Holon 与 Holarchy
在物理学、生物学、社会学等诸多领域都能见到 holon 与 holarchy,用于描述由更小、相互关联部分构成的复杂系统。尽管它们在软件工程中不算常用,但我发现它们对思考分布式系统极为有用。大多数复杂的分布式系统并不存在“干净利落”的层级,而是呈现复杂而混杂的组织形态。holon 与 holarchy 能表达这些复杂而“凌乱”的组织形式,使我们可以按需分组、放大或缩小视野,以准确而简练地捕捉系统的相关方面。
举例来说,设想一个多租户、带复制的数据库管理系统。该系统通过某种共识算法(如 Paxos、Raft 或 Viewstamped Replication)进行数据复制。再假设系统有两个租户(各分配一套数据库)和三个数据库节点。
根据我们的视角或想要表达的观点不同,可以有不同看法:一方面,我们可以把整个数据库集群视为一个原子实体;或者,把它视为由三台相互作用的数据库节点组成的高阶实体。另一方面,我们也可以把系统视为两个原子实体——两套数据库(每个租户一套);或者,把每套数据库视为由多台相互作用的数据库节点组成的高阶实体。
我们可以选择哪些组件当作原子实体、哪些当作高阶实体,以及在分析中纳入哪些组件。即便是同一个分布式系统,这种选择也会随语境而变(见图 1.12)。
图 1.12 用两种不同的 holarchy 表示同一系统
1.5.2 全局视角 vs. 局部视角
回到全局视角与局部视角。回忆一下,我们先前从一个全知观察者的角度看系统,也就是能够观察到整个系统的状态:这就是全局视角。单个组件并无此特权;它只能观察到自身状态以及它与网络之间的通信通道,因此只有受限的局部视角。在全局视角下,我们可以观察到整个系统——也就是确定系统状态(见图 1.13)。
图 1.13 全局视角
在局部视角下,组件只能观察自身——只能确定自己的状态。若要确定全局系统状态,组件必须与其他组件配合,后者需要通过网络发送其记录的本地状态(见图 1.14)。
图 1.14 C1 的视角
通常我们把通信想成组件 C1 与组件 C2 直接进行消息交换。但务必要理解:C1 并非直接与 C2 通信,而是在与系统的其余部分通信。C1 向网络发送一条收件人标记为 C2 的消息;网络可能把消息转发给 C2,也可能不,或者转发给其他组件。
1.6 分布式系统有限公司
在深入分布式系统主题时,我建议你把它们想象成一栋办公楼里的公司(见图 1.15)。我们称这家公司为 “分布式系统有限公司”(Distributed Systems Incorporated) 。
这栋大楼代表系统,每个房间代表一个并发(独立)组件。(本书后文将更细致地讨论并发。)房间之间通过穿过邮政室、连接各房间的气动传输管道间接相连,它们即构成网络。办公楼与外界通过一个邮箱相连,负责处理所有进出站消息。
【定义】 气动传输管道(pneumatic tubes)是一种利用压缩空气在管网中传送装有小物件或文件的容器的系统。19 世纪末至 20 世纪中期,它们在办公室中常被用于在楼宇或园区的不同位置之间传递信息。
我觉得这个心智模型很有帮助,因为它让我能更容易地思考和讨论分布式系统。分布式系统有限公司把一个无形、抽象的“赛博系统”映射为一个有形、具体的物理系统,同时忠实地捕捉了系统的核心机制:
图 1.15 分布式系统有限公司
- 组件即房间——每个房间及其“住户”代表分布式系统中的一个并发组件。组件拥有本地状态,会收发消息,并执行一组任务,共同形成系统整体行为。
- 网络即气动管道——大楼中连接各房间的气动管道代表承载消息收发的网络。
- 外部接口即邮箱——大楼的邮箱负责所有入站/出站消息,代表系统与外界通信的外部接口。
此外,该模型还能覆盖许多关切点。比如:如果邮政室的 Bob 丢失、重复或者重排了消息,会怎样?如果员工休息 15 分钟、去度假,或者离职,又会怎样?这些状况恰好对应分布式系统中的常见问题:
-
崩溃语义(Crash semantics) ——把员工的请假等价为系统的不同故障模式:
- 短暂缺席:员工短时休息 ⇒ 瞬时故障。
- 较长缺席:员工去度假 ⇒ 间歇性/阶段性故障。
- 永久缺席:员工离职 ⇒ 永久性故障。
-
消息投递语义(Message-delivery semantics) ——把邮政室的差错等价为网络的不同投递语义:
- 消息丢失:消息未被投递。
- 消息重复:同一消息被多次投递。
- 消息乱序:消息的投递次序被打乱。
借助这些例子,你可以分析后果并探索应对措施。设想这样一个场景:某位员工向另一位员工发送请求,却一直未收到回复。第一位员工如何判断:是请求丢了,对方根本没收到,还是对方收到了请求,但回复在途中丢了?如果第一位员工再次发送请求,就可能导致同一请求被处理两次,从而引发不一致。若不谨慎处理,这些情况都会损害公司提供理想客户服务的能力。
你可能会惊讶于这个模型的适用范围之广。如果你使用 Kubernetes(kubernetes.io),这个流行的分布式容器编排平台,可以试着构建“Kubernetes有限公司”的比喻;如果你使用 Apache Kafka(kafka.apache.org),这个流行的分布式流处理平台,也可以试着构建“Kafka有限公司”。把每种技术都映射到这栋办公楼的类比上,能把抽象概念具象化,从而帮助你可视化并推理分布式系统的复杂性。
1.7 驾驭复杂性(Navigating complexity)
即便不深入分布式系统的各种复杂细节,我们也已经能收获一些“啊哈!”时刻。本节先做非形式化的预览,后续章节再进一步展开。
1.7.1 简单却复杂(Simple yet complex)
尽管“分布式系统有限公司”里的每位员工只处理简单的任务、遵循简单的规则,但由此产生的整体行为却可能很复杂。换言之,把简单组件拼在一起,并不必然得到简单系统。
1.7.2 涌现行为(Emergent behavior)
“分布式系统有限公司”里那些有趣的整体行为,并不能追溯到单个员工:
单个员工无法为公司的可扩展性负责——一个人一天能完成的工作量总有极限;
单个员工也无法为公司的可靠性负责——他/她可能因为生病、休假或离职而缺席。
这些有趣的行为是涌现出来的:由个体的行动及其交互共同导致。这正是“用不可靠组件构建可靠系统”理念的基石。
1.7.3 视角切换(Changing perspective)
我们常会不自觉地从黑盒与白盒两种角度思考系统。但从黑盒转到白盒(或反之)其实是分辨率的变化,而不是视角的变化。
如图 1.16 所示,无论看黑盒还是白盒,我们都站在全知观察者的立场:能观察到整个系统的状态,也就是拥有全局视角。
图 1.16 黑盒 vs 白盒模型:全局视角
然而在图 1.17 中,系统内部的单个组件没有这种“上帝视角”。组件只能观察自身状态,因此只有局部视角。换个视角表达,我们就能准确而简练地刻画分布式系统的核心挑战:全局思考,局部行动。
注 尽管图 1.17 的画面看起来有点“孤单落寞”,我更愿意把“分布式系统有限公司”想象成文化积极、团队默契、员工满意度拉满的地方。
图 1.17 局部视角
1.7.4 全局思考,局部行动(Think globally; act locally)
打造分布式系统的核心挑战,是设计一个全局算法,并让系统中的每个组件各自实现本地算法。图 1.18 展示了一个常见难题:脑裂(split-brain) 。在该场景中,全局正确性依赖于只有一个领导者做决策。但如何确保不会出现多人都以为自己是领导者?参与者是谁?他们各自拥有哪些本地知识?他们采取了哪些本地步骤来确保全局保证得以满足?换言之,挑战在于:在每个组件只知本地、只取本地的前提下,仍然让系统作为一个有机整体正确运作。
图 1.18 脑裂(Splitbrain)
1.8 超越代码思考(Thinking above the code)
翻阅本书,你会注意到几乎没有代码示例。这不是疏漏,而是刻意选择:简短、静态的代码片段,难以刻画复杂、动态的系统。取而代之,我们选择超越代码层来思考,构建更贴合问题本质的定制心智模型。
以竞态条件(race condition)为例。许多材料用一小段代码来“引入并定义”竞态:两个线程同时更新同一个变量。这种做法的问题是:它会让人误以为竞态只发生在“单变量更新”层面,并把抽象概念的总结留给读者自己推断——等于把理解负担转嫁给读者,而不是清晰传达完整的心智模型。
能否做得更好?让我们构建一个不依赖代码的极简模型:
- 一个进程是原子动作序列。
设进程 P 的原子动作序列为 a, b, c;进程 Q 的原子动作序列为 x, y, z。
进程的执行方式是:做完序列中的第一个动作,再继续执行剩余动作。 - 两个进程的并发组合记作 (P | Q) ,表示从 P 与 Q 的动作中,**所有可能交错(interleaving)**的集合。
在这个框架下,竞态条件可以这样定义:当 (P | Q) 的某个子集(即部分交错序列)被判定为不正确时,就出现了竞态(见图 1.19)。至关重要的是:你就是“正确性”的裁判——本书后文会详细说明。
图 1.19 关于竞态的推理
该模型不限于“线程访问单一变量”,而是向下到协作式多任务、向上到抢占式多任务,再到单机单核、单机多核、乃至多机都适用。
在数据库系统中,我们常说:若对每一种可能交错,其执行结果都等价于下面两种顺序组合之一(见图 1.20),则 (P | Q) 是正确的(即可串行化,serializability):
- (P • Q) —— 顺序组合:P 先于 Q 执行
- (Q • P) —— 顺序组合:Q 先于 P 执行
图 1.20 关于可串行化的推理
当我们找对了心智模型,就能以前所未有的清晰度来推理我们创造的系统。
小结(Summary)
-
心智模型是对目标系统的内部表征,是理解与沟通的基础。
-
追求对分布式系统的深度理解,优于只掌握其概念名词。
-
分布式系统是一组通过网络收发消息而彼此通信的并发组件。
-
设计分布式系统的核心挑战:在组件仅有局部知识的前提下,构建一个整体一致、协同运作的系统。
-
从根本上说,我们关心的是系统的保证,并据此进行推理:
- 正确性(由安全性与活性刻画)
- 可扩展性与可靠性(满足服务级别目标的响应性)
-
分布式系统可以类比为一家公司:房间代表并发组件,气动管道代表网络,邮箱代表外部接口。