边界:构建可靠系统的一些考虑点

  • Tyler Treat
  • 薛命灯

2017 年 1 月 2 日

话题:语言 & 开发架构

复杂的系统一般是由很多离散的部件组成,这些部件会出现故障,而且有时候会有多个部件同时出现故障,所以复杂的系统通常是运行在故障模式下。在一个采用了微服务架构的系统里,一个功能可能需要调用多个服务,因此每个部件的可用性决定了整个系统的可用性。这是弹性工程背后的核心逻辑。假设一个功能依赖三个服务,每个服务分别具有 90%、95% 和 99% 的可靠性,那么部分可用性就介于 99.995% 和 84% 之间(假设失效是单独发生的)。弹性工程意味着在设计时要把失效作为常规的考虑因素。

预测失效是弹性工程的第一步,而第二步是拥抱它们。告诉客户,可预知的失效好过未知或非期望的失效。回压是另一种弹性模式。从根本上说,回压就是要对资源强加限制。比如队列长度限制、带宽限制、流量控制、消息速度限制、消息大小限制等等。如果不显式地进行限制,它们就会变成隐式的(比如服务器的内存会被耗尽,不过因为这种限制是隐式的,所以无法准确地预测在什么时候会发生什么问题)。使用无边界的队列或其它一些隐式的限制就好比有人声称知道自己什么时候可以戒掉酒瘾,因为人总有一死,或许到了那个时候就不会再喝酒了。

速率限定不仅能够防止那些糟糕的 actor 破坏你的系统,它同时也是为了防止你自己对系统造成破坏。队列限制和消息大小限制是最有趣的,因为它们让很多开发者感到疑惑,同时也让他们感到沮丧,因为他们还没有完全搞清楚它们背后的动机。它们其实也是速率限定的另一个形式,或者说回压。下面我们拿消息大小限制作为例子。

假设我们有一个分布式系统,系统里的 actor 可以给其它 actor 发送消息,接收消息的 actor 对收到的消息进行处理,当然它们也可能往外发送消息。好的软件工程师都知道,分布式计算的第八个谬论是“均等网络”。所以,并不是所有的 actor 都使用相同的硬件、软件或者网络。我们有运行 Ubuntu 的拥有 128G 内存的服务器,有运行 macOS 的拥有 16G 内存的笔记本电脑,有运行 Android 的拥有 2G 内存的移动客户端,还有 512M 内存的物联网设备,在这些设备上面运行着各种各样的软件和网络接口。

如果我们不对消息的大小进行限制,那么我们就是在制造隐式的限制(上面我们对此做过讨论)。换句话说,你和你的交互方正在遵循一种无言的协议,双方都无法请求退出。因为任何一个 actor 都可以发送任意大小的消息,那么下游的消费者必须直接或者间接地支持任意大小的消息。我们怎么可能对任意大小的东西进行测试呢?我们做不到。我们只有两种选择:要么做出显式的限制,要么保持这种隐式的限制。如果选择了前者,我们可以定义我们的行为边界,并且对其进行测试。而后者要求我们基于未定义的生产规模进行测试,这是在拿系统可靠性作为赌注。第二种情况的限制依然存在,只不过被隐藏了起来。如果我们不让它们变成显式的,很容易在生产环境受到 DoS 攻击。在云基础设施环境中,因为它们的多租户特性,这些限制变得尤为重要。这些限制可以防止糟糕的 actor(包括你自己)拖垮服务,或者垄断基础设施和系统资源。

在我们的异构 actor 系统里,我们针对移动设备和 Web 浏览器进行消息限制,它们一般都是单线程或内存受限的消费者。如果没有显式的消息大小限制,客户端可能会因为请求过多的数据或接收无法处理的数据而崩溃,这就是为什么有些协议虽然没有明确规定但必须存在。

让我们从企业的角度来看待这些问题。假设有另一个系统:美国国家高速公路系统。美国交通局通过 Federal Bridge Gross Weight Formula 来防止大量的汽车对道路和桥梁造成破坏。这里存在着相同的工程问题,只不过规则和基础设施不太一样。

2007 年 8 月,明尼阿波里斯市的洲际 35W 密西西比河大桥坍塌,这个事故引起了人们对于卡车重量和大桥承受力之间关系的思考。2008 年 11 月,美国国家交通安全局给出大桥坍塌的几个原因:有缺陷的加固板、不精确的勘察、过重的建材以及高峰时期的车流重量。

交通局依赖地磅来确保卡车重量与官方允许的重量合规,并对超重的车辆进行处罚。

官方规定的最大重量是 80000 磅。超过这个重量的卡车依然可以在高速上行驶,不过行程会受到限制。超重许可只会被发放给那些无法拆分至符合官方标准的货物,而且除了卡车以外没有其他方式可以运载这些货物。

重量限制需要被硬性规定,这样工程师们在建造道路、桥梁和其它基础设施时就有章可循。计算机系统也一样。这也就是为什么很多计算机系统硬性规定了很多限制。例如,Amazon 就对他们的 Simple Queue Service 做了明确的限制——标准队列最多可承载 12 万个不落地的消息,而 FIFO 队列是 2 万个,而且消息大小被限制在 256K 以内。Amazon Kinesis、Apache Kafka、NATS 和 Google App Engine 所使用的消息都限制在 1M 以内。系统设计者可以通过这些限制来优化他们的基础设施,并降低多租户环境存在的风险——虽然置之不理会让资源计划变得更简单。

不管是队列、消息大小、查询或者流量,不对它们进行限制是一种弹性工程反模式。不对它们进行显式地限制,故障会以不可预期和非期望的方式发生。要记住,限制其实是时刻存在的,只是有时候它们被隐藏起来了。通过显式的限制,可以让故障的发生更加地可预期,而且发生故障的平均时间变长,而从故障中恢复的时间变短,只要在事先多做一些稍微复杂一点的工作。

事先做出显式的限制,好过让系统在不可预期的情况下发生故障。后者虽然在前期会少作一些工作,但从长期来看会带来更多的问题。要求开发者们直接做出显式的限制,他们会因此认真思考他们的 API 和业务逻辑,并设计出稳定、可伸缩、高性能的交互系统。

查看英文原文:Take It to the Limit: Considerations for Building Reliable Systems


感谢郭蕾对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注 我们。

语言 & 开发架构