如何衡量分布式系统的好坏?

译文 精选
开发
本文会从三个方面考虑分布式设计问题:一致性与可用性、域耦合以及可观察性。

译者 | 崔皓  

策划 | 云昭

分布式系统设计是一个难题,难就难在设计过程中是不会提供直接反馈的。往往有些问题的产生是来源于设计的,例如:可扩展性问题、弹性问题、数据问题。然而,通常的解决方案是治标不治本——仅仅对系统进行修补以使其保持运行,但是潜在的设计问题仍然存在,并且可能在不同的情况下再次爆发。当系统在生产环境中出现故障,再去分析与设计相关的根本原因就需要付出更多的努力,同时会引来大量的组织争论。

和分布式系统代码审查一样,本文会给出简单的清单,列出在审查分布式系统功能(多个系统协同工作)的设计时要注意的事项。

本文会从三个方面考虑分布式设计问题:一致性与可用性、域耦合以及可观察性。前两者经常相互泄漏,因为分布式系统就像一个复杂的网格,每个设计选择都会影响其他多个事物。每个方面的问题都可以作为一个大主题来讨论,因此以下指南代表了对任何设计审查的底线。

根据问题的用例对应上下文,检查完这些基础知识后,就可以针对研究特定方向进行深入研究了。相反,如果在检查中发现问题,那就需要格外小心了。

一致性或可用性

需要提前声明的是本文中的“系统”是指一组独立系统,多个这样的“系统”以不同的方式协作为用户提供最终服务。一致性与可用性是针对多个协作系统而言的。

CPA定理告诉我们,我们可以根据系统的一致性、可用性和分区容错性,然后选择其中任意两个。如果说分区容错生活的一部分,也就是无法避免的。那么CAP 定理的真正选择就会落到一致性和可用性之间了—也就是选择“AP”系统(在分区容错下的可用性)或者个“CP”系统(在分区容错下的一致性)。

软件架构的一个基本规则是所有软件都会失败。假设需要保持三个组件之间的一致性才能使设计的功能正常工作。这种做法会使得该功能变得脆弱,因为如果任何一个组件都有可能出现故障,一旦出现故障该功能就无法正常工作。此时我们需要面对“单点故障”问题了 ——实际上我们有三个有可能出现故障的组件!!!当我们努力保持使整个系统的一致性时,就越容易让它在最轻微的影响下发生故障。保持同步的组件越多,这种情况就越糟糕。

幸运的是,对于这个问题是有解的。

在单个系统中,CP 和 AP是二元选择的(例如 MySQL 是一致的,Cassandra 不是),也就是非此即彼的关系。但在分布式系统中,却并非如此它们之间并不是非黑即白,而是存在灰度地带。每个组件可能会保持一致性(订单、库存和付款),但整个系统角度来看可以设计成保持最终一致性。这种方式也给我们留有余地,从而增强系统的可用性。由此我们的指导方针就是设计整体系统的可用性,即便个别子系统存在不可用的情况,随着时间的推移也可以实现整个系统的可用性。

使用异步消息进行通信

它帮我们消除一致性压力的有力武器,异步消息通信的引入有利于将可用性和特性作为ReactiveManifesto的主要准则。考虑使组件之间的异步通信(通过消息代理传递消息),而不是直接使用请求-响应式 API 调用。如果说同步通信是硅谷的可卡因的话,那么同步API调用就是分布式系统设计的可卡因。可以思考一下——如果两个系统不必保持一致,那么我们为什么要通过同步的方式立即完成调用呢?(正如同步通信模型所要求的那样)。请求-响应模型创建了一种时间耦合形式(“立即服务这个请求!”)在调用者和被调用者之间,如果后者出现不可用的状况,就会导致调用者的调用失败,从而会导致调用者的级联故障。异步通信允许被调用系统按照自己的节奏处理请求,从而减轻可用性的压力。

虽然异步消息传递是一个强大的工具,但在采用它时必须牢记以下几件事。

定义最低可接受的用户体验——对于每个最终用户体验,定义最低的一致的体验。例如,用户赢得了在线游戏,是否必须以全有或全无的方式记入奖励积分、奖励他在排行榜上的新位置、并通知他的所有朋友以及向他发送通知呢?很明显,如果我们越能在核心、一致的体验之外做更多的事情,遇到系统故障的可能性就越小。在讨论需求时,必须对此毫不留情,并且支持它所需的最低要求——其他一切都应该通过异步完成。

通过这个示例,我们可以根据在线游戏的各个与用户相关的环节进行拆解,并且进行设计。奖励得分不一定在游戏结束的时候才给予,而是可以在游戏过程中通过异步的方式更新积分。同理:排行榜、和发送通知的功能也可以异步进行。这些东西需要在需求设计阶段就定义好,只要在满足用户最低要求的游戏体验的情况下,将各个游戏步骤进行拆解异步就好了。

保证最终一致性:无论单个用户操作还是客户端请求都可以跨多个组件修改数据,因此需要通过设计应保证所有系统将在某个指定时间对用户请求达成共识——即使是通过分布式回滚的方式进行。

系统地保证 SLA 的一致性:它是非常有价值的,可能有一个最终一致性的计划,但是如果没有设置和执行设定时间框架的机制,就不可能从缓慢的处理中检测到故障。由于我们无法确定事件在处理过程中是否引发错误或消息是否在网络中丢失,因此需要通过明确的时间硬绑定来维持最终的一致性保证。

域耦合  

好分布式系统设计会在正确的抽象级别将不同的事物进行拆分。拆分之后的分隔线被称为域边界,并且使用独特的沟通语言和域特有的功能接口来对域边界进行标识。例如,消息代理域会封装消息、交付保证、存储媒体等信息。支付域会封装交易、支付网关等信息。

虽然这这里会提到到微服务中界限上下文的概念,但这一概念是广泛适用的。例如,支付可以是一个域,其中支付网关和交易可以支付域的子域。

根据域设计的概念来进行分布式系统架构,其主要思想是在同一级别对域进行解耦,同时在父域级别建立内聚。例如,在上面的示例中,试图让支付网关和事务在实现时尽可能相互分离,不过作为同一域的一部分应该要求术语和数据模型保持一致性。

创建域边界:随着微服务采用率的大幅上升,通过底层技术分离的方式来识别组件域的方式就显得尤为重要来。识别哪些组件属于哪个域以及外部系统如何与这些系统通信也是很重要的。我们可能会使用多种服务来处理货物(跟踪服务、扫描服务、审计服务等),但外部用户应该使用有聚合的“物流”域实体和API来访问域中的功能组件。构建域边界的最好工具是API 网关,它可以透过域本身抽象出更高级别API 背后的内部细节。

使用标准领域语言在系统之间进行通信:两个组件之间的交流应该通过:现有实体 + 实体的状态 +实体可能执行操作 的方式进行,不要去创建一些不属于两个域的构造然后让它们进行通信。可以使用通信双方的构造来实现这一点,这取决于我们想要事件还是消息。如果您发现两个组件之间的通信需要创建某种特殊语言,那么就意味着组件的拆分方式存在问题,或者需要通过中间组件,该组件存在于这两个组件之间,利用中间组件的方式让两个组件进行通信。

将多域/多组件操作分离到工作流中——有一种域耦合发生的常见方式,即当一个组件开始对多组件工作流进行端到端控制。这意味着当前域知道各种其他域、以及它们的行为和其边界之外的“工作流”的性质。这种意识使组件与现有工作流耦合,因此难以扩展。

如果一个功能需要调用多个组件,应该将此功能从对应的域核心服务中分离出来,并将其划为有状态的组件。该组件可以应用到专用的编排服务或某种BPM 系统中。有状态的组件意味着可以从一个地方获得重试、错误报告、SLA等诸多信息。

如果我们不能对所有事情都使用显式编排,使用编排来构建工作流也是一个可行的选择,但需要使用某种方式来跟踪任务完成SLA,以减少长期工作流的脆弱性。

跨组件建模业务流程:上述观点的一个推论是,我们应该对业务流程进行端到端建模,而不应该考虑技术边界。由于我们已经通过转移编排到工作流的方式来解耦域,因此在狭窄的团队边界内构建工作流是没有意义的。应该使工作流尽可能多地包含整个业务流程——这将建立业务知识的中央存储库,并提供对运营状态的可见性。

事件优先与消息:解耦域更喜欢使用 pub-sub(发布订阅) 模型的事件功能,而不是目标消息。虽然这取决于许多其他因素,但它增强了对其他域的不可知性,因为发布者不关心pub-sub 模型中的消费者,因此消除了发布者和消费者域之间的耦合。

可观察性  

用Charity Majors的话来说,可观察性可以用来回答有关系统的新问题,同时无需窥视系统内部的能力。我认为可观察性分为两种:技术和业务。我们应该能够解释系统的技术状态,并且可以通过业务指标来确定它的运行状态。

使用事件数据来构建度量:任何系统的基本工作单元是事件,系统会用自己的领域语言来解释事件。事件可以是任何类型(例如ORDER_CREATED、REQUEST_RECEIVED、ERROR_RESPONSE_RETURNED)。一个有追求的想法是,我们会将整个系统信息建模,并通过发送事件的方式与外部系统进行通讯,同时对事件进行保存和分析用以获取更多的信息,而不是通过常见的日志跟踪方式来获取信息。拥有原始事件数据的好处是可以与领域建模方法同步,并且可以随时使用原始数据并获取新的指标。这比浏览非结构化文本日志、Spans、traces和任意Prometheus/Statsd 类型指标的组合要好得多。

将原始数据存储在一个中心位置:遵守上面给出的可观察性定义的唯一方法是存储来自系统的原始数据,因为当我们开始只处理预定义的指标时,就被它们束缚而失去了回答“新”问题的能力。反对使用原始数据的一个常见观点就是需要更多的容量存储它们,但可以采取一些方法缓解容量的问题(例如:采样等),反过来看原始数据的存储在面对新的故障时,为系统提供的诊断能力就显得无价了。在分布式系统中,将可观察性数据集中存放显然比存放在分布式孤岛中要好的多,这样更加利于提升对整体系统的洞察力。

一般准则  

使用异步框架来实现——我之前写过关于如何使用异步编程来扩展系统。因此,在调用远程系统时,应该确保以异步方式进行,以免阻塞应用程序线程。这需要在早期的实现/设计中考虑,如果应用程序框架不允许这样做,或者应用程序的基本框架不是为此而设计的,那么您就不走运了。如果您可以选择异步,请始终接受它——当意外的流量激增时,您的系统和您的团队会感谢您。

了解你的调用者——根据定义,没有人会为分布式系统负责。因此,我们应该对内部系统采取尽可能多的预防措施,就像对外部系统采取的措施一样。如果可以的话,其中至少要强制执行速率限制,同时要了解您的调用者。这将有助于在出现问题时,隔离问题的根源——当系统着火时使用才使用IP 地址识别源头就太晚了。

知道何时失败——我已经谈了很多关于如何让系统在不同条件下保持可用性的问题了。但在某些情况下,失败总比做错事要好。在许多情况下,一致性是完全可以接受的选择,我们应该小心不要过度补偿它们。

译者介绍

崔皓,51CTO社区编辑,资深架构师,拥有18年的软件开发和架构经验,10年分布式架构经验。曾任惠普技术专家。乐于分享,撰写了很多热门技术文章,阅读量超过60万。《分布式架构原理与实践》作者。

责任编辑:薛彦泽
相关推荐

2023-05-12 08:23:03

分布式系统网络

2023-02-11 00:04:17

分布式系统安全

2024-01-09 08:00:58

2023-05-29 14:07:00

Zuul网关系统

2022-05-11 13:55:18

高可用性分布式弹性

2017-10-27 08:40:44

分布式存储剪枝系统

2023-10-26 18:10:43

分布式并行技术系统

2020-09-21 09:15:12

系统

2024-03-18 08:50:20

分布式系统机制

2018-09-10 13:02:48

人工智能AI创业公司

2017-10-17 08:33:31

存储系统分布式

2024-03-19 11:41:12

2024-03-01 12:16:00

分布式系统服务

2019-07-17 22:23:01

分布式系统负载均衡架构

2017-12-05 09:43:42

分布式系统核心

2023-04-26 08:01:09

分布式编译系统

2023-10-08 10:49:16

搜索系统分布式系统

2019-06-19 15:40:06

分布式锁RedisJava

2018-12-14 10:06:22

缓存分布式系统

2019-08-05 07:58:01

分布式架构系统
点赞
收藏

51CTO技术栈公众号