服务器的 “正确” CPU 利用率水平是什么?如果你查看一个设计良好、运行良好的服务的监控仪表板,我们希望看到的 CPU 利用率在一天或两天的平均值应该是多少?
这是一个非常一般性的问题,并且不清楚它是否应该有一个单一的答案。也就是说,长期以来,我一般认为更高的利用率总是更好:我们应该尽量接近 100% 的利用率。为什么?因为低于 100% 的任何利用率都代表未使用的硬件容量,这意味着我们在浪费资源。如果一个服务没有将其 CPU 最大化,我们可以将其迁移到一个更小的实例上,或者在该节点上运行一些额外的工作。
事实证明,这种简单的直觉很少是完全正确的。
假设我们达到了理想状态,我们的服务运行接近 100% 的利用率。如果我们意外地变得热门,接收到意外的流量激增,会发生什么?或者如果我们想部署一个在每个请求中需要额外 CPU 的新功能呢?
如果我们处于 100% 的利用率,然后发生某些事情增加了负载,那么我们就麻烦了!在 100% 的利用率下运行使我们没有空间来吸收增量负载。我们要么以某种方式降级,要么不得不匆忙增加应急容量,或者两者兼而有之。
这个简单的例子是一个非常普遍现象的实例:效率的提高往往与韧性相互权衡,而我们对系统的优化越深入,这种权衡往往变得越糟糕。
超过某个点,使系统更高效将意味着使其韧性降低,反之,构建鲁棒性往往会使系统效率降低(至少在短期内)。这并不是说没有双赢的情况;有时可以将帕累托前沿向外移动;修复 “愚蠢” 的性能错误有时会产生这种效果。然而,在某个努力水平之后,你将被迫做出权衡。
在这里所说的 “韧性”,重要的是要注意,我的意思比 “可靠性” 或 “保持运行的能力” 要广泛得多;我指的是 “吸收或应对变化的能力” 的更一般的概念 —— 各种变化,包括错误或故障的变化,但也包括产品需求的变化、市场的变化、组织或团队组成的变化,等等。
这种权衡的例子#
这种权衡发生在超出容量规划的领域。它几乎适用于任何技术系统或组织的几乎每个层面。以下是我观察到的其他一些地方:
冗余#
运行多个服务实例是非常常见的,负载均衡器的设置使得如果任何实例失败,负载将透明地路由到其他实例。在复杂的组织中,这种模式可能应用于整个数据中心的层面,架构允许整个数据中心失败,并将其负载路由到其他数据中心。
为了使这一切正常工作,每个实例必须有足够的备用容量来吸收来自故障实例的增量负载。在稳定状态下,在没有故障的情况下,该容量必须处于闲置状态,或者在最好的情况下,服务于可以在瞬间被丢弃的低优先级工作。我们想要的冗余越多,我们在稳定状态下必须保持的闲置容量就越多。
优化#
复杂的性能优化通常通过利用问题领域的特定属性或结构来实现。通过将不变性嵌入数据结构和代码组织中,通常可以获得巨大的性能提升。然而,越是依赖特定假设,就越难以更改,这使得高度优化的代码往往更难以演变或添加功能。
作为一个具体的例子,在我对 Sorbet 的反思中,我谈到我们决定类型推断将是局部的和单遍的,并将这一假设嵌入我们的代码和数据结构中。这个选择带来了显著的效率提升,但在某种意义上使系统变得更脆弱:一些竞争类型系统具有的许多功能在 Sorbet 代码库中将是不可实现或难以实现的,因为这些假设。我仍然相信这是该项目的正确选择,但值得承认这些权衡。
Hillel Wayne,提到一个类似的属性为 “聪明的代码”,他将其定义为
利用对问题的知识的代码。
他也谈到这种意义上的聪明代码往往是高效的,但有时是脆弱的。
序列化格式#
很难找到比 “直接将内存中的structs
复制到磁盘” 更高效的序列化格式;几乎不需要代码,并且保存或加载数据的序列化成本几乎为零。
然而,这导致了一个脆弱的系统 —— 即使添加一个新字段也需要重写数据或其他特殊处理。并且在不同字节序或字长的机器之间共享数据变得具有挑战性。
另一方面,将所有数据写为 JSON 或某种类似的通用容器为添加新字段或新模块之间的通信创造了无尽的灵活性,而不会影响现有代码,但代价是在线和 CPU 工作中序列化和反序列化数据流的开销巨大。
分布式系统#
我最喜欢的系统论文之一是COST论文,它考察了许多大数据平台,并观察到它们中的许多具有与可用硬件 (近) 线性扩展的理想特性,但代价是效率比调优的单线程实现低得多。
我发现这是一个常见的权衡。分布式计算框架灵活且具有韧性,能够通过扩展来处理近乎任意的工作负载。它们可以通过扩展来处理某人部署的低效代码,并透明地处理硬件故障。需要处理更多数据?只需添加更多硬件(通常是透明的,使用某种自动扩展)。
另一方面,经过精心编码的单节点解决方案往往会更快(有时快 10-100 倍!),但更脆弱:如果数据集不再适合一个节点,或者如果你需要执行 10 倍更昂贵的分析,或者如果团队中的新工程师无意中在紧密的内部循环中提交了慢代码,整个系统可能会崩溃或无法执行其工作。
小团队与大型组织#
小团队 —— 包括 “一人团队”—— 可以非常高效和富有生产力。团队越小,沟通开销越少,保持每个工程师头脑中丰富的共享背景就越容易。写文档的需求更少,关于变更的沟通需求更少,花在新成员入职和培训上的时间更少,等等。小团队可以通过 “认真思考并努力尝试” 的策略走得比大团队更远,通常需要依赖较少的工具,如代码检查器和小心的防御性抽象设计。
在适当的情况下,小团队在仔细设计和经验丰富的工程师的帮助下,有时可以大致匹配 10 倍于其规模的团队的原始产出 —— 这是效率的巨大提升!
然而,小团队对组织、技术环境或项目业务需求的变化要脆弱得多,韧性较差。如果一个人离开一个 4 人的团队,你的带宽就减少了 25%—— 更糟糕的是,团队几乎没有招聘和培训新成员的经验,许多知识和文档仅存在于剩余成员的头脑中。
同样,业务重点的变化或新产品的推出需要团队的系统提供更多功能或其他开发,超出团队的支持能力相对容易,而快速扩展团队将面临所有相同原因的挑战。
自动化与人工流程#
一般来说,让机器执行任务比让人手动执行更高效 —— 更便宜、更快,且通常更可靠。
然而,人类是无尽的适应者,而机器(包括物理机器和软件系统)则更脆弱,固守其方式。一个有人的系统在应对突发情况或意外事件时有更多的选择。
即使我们保留所有相同的人,但通过自动化来加速他们工作的某些部分,我们也面临自动化依赖的风险,即人类过于依赖自动化并不恰当地信任,或者他们在没有自动化的情况下的功能能力萎缩,以至于在需要时无法适当地 “手动” 介入。
余量#
许多这些具体观察实际上是关于余量(不是软件产品)的观察。
具有健康余量的系统 —— 至少在短期内,经过简单分析 —— 按定义是低效的:这些余量是时间或资源,正在 “闲置”,本可以产生输出。
不过,从更广泛的角度来看,这些余量是韧性的关键:系统中的 “余地” 使系统能够应对小的干扰或意外;开发人员或操作人员可以利用这些余量介入并处理意外负载或在问题变得灾难性或外部可见之前解决潜在问题。
一个没有余量的系统在正常工作时是高效的,但在任何变化的情况下都很脆弱,并会迅速崩溃。
结论#
我试图触及一些具体的例子,在这些例子中,效率和可靠性是相互对立的,并且相互权衡或至少在相反的方向上施加压力。希望我已经说服你这是一个广泛的现象,即使在可能没有严格权衡的情况下,这两个价值观也往往会相互对立并暗示不同的决策。
不幸的是,仅仅这个观察通常并不能告诉我们该如何处理任何_特定_系统。如果我们查看某个系统,初步分析表明它在输入的使用上效率低下,我们无法在不更仔细观察的情况下判断它是一个功能失调和糟糕设计选择的巢穴,还是这种表面上的低效率支持着巨大的冗余、灵活性和余量,使其能够应对可能出现的任何变化。我们需要更仔细地观察,几乎总是需要特定团队和特定问题的领域专业知识。
此外,有时会有免费的午餐。一些设计选择或决策确实将帕累托前沿向外移动,而不仅仅是沿着它移动。它们在优化良好的系统中可能很少见,但我们不能忽视它们的可能性。许多系统尚未得到很好的优化!
此外,设计空间中的最佳点因系统而异。有时,可靠性或韧性至关重要,我们有理由容忍巨大的第一阶低效率。然而,有时,极端效率是正确的目标:也许我们的利润微薄到足以成为我们唯一的选择,或者也许我们对我们领域的稳定性和对我们系统的需求有足够的信心,以至于我们相信不会面临任何过于剧烈的变化。
因此,主要是我所能做的就是呼吁我们作为工程师、设计师和系统观察者,意识到这种权衡及其影响。当我们指责一个系统浪费和低效时,值得暂停思考一下这种 “浪费” 可能带来了什么。当我们着手优化一个系统时,暂停一下,了解当前系统中的关节和灵活性在哪里,哪些是至关重要的,并尽力保留这些。当我们为一个系统、团队或组织设定效率的指标或目标时,让我们意识到,缺乏对立压力的情况下,我们可能也在要求系统变得更加脆弱和脆弱。