这篇博文描述了 Uber 通过更好的负载平衡来高效利用硬件的过程。本文描述的工作持续了一年多,涉及多个团队的工程师,并带来了显著的效率节约。本文介绍了技术解决方案以及我们找到这些解决方案的过程——从很多方面来看,这段旅程比目的地更艰难。
背景更好的负载平衡:实时动态子集 | Uber Blog是一篇相关的博客文章,早于本文描述的工作。我们不会重复背景——我们建议浏览一下那里的服务网格概述。我们还将重复使用同一套词典。这篇文章重点介绍通过上述服务网格进行通信的工作负载。这涵盖了我们绝大多数无状态工作负载。
问题陈述2020 年,我们开始着手提高 Uber 多租户平台的整体效率。具体来说,我们专注于减少运行无状态服务所需的容量。在这篇博文中,我们将介绍各个团队如何做出理性决策导致资源使用效率低下,我们如何分析问题和不同的方法,以及如何通过改善负载分配,让团队安全地提高 CPU 利用率并降低成本。这篇文章只关注 CPU,因为这是我们的主要限制。
首先,介绍一些背景信息:在 Uber,大多数容量决策都是分散的。虽然我们的平台团队提供了推荐目标和自动扩展器等工具,但采用具体目标的最终决定权在每个产品团队/组织手中。存在预算流程来限制无限制分配。
在预算过程中,我们注意到利用率过低,这在我们看来是不合理的。然而,提高利用率的尝试却遭到了产品团队的担忧——他们担心提高利用率会危及系统的可靠性并影响其可用性/延迟目标,这是理所当然的。
问题的原因被认为是网络负载平衡不理想。许多工作负载的任务 CPU 使用率高于平均水平。这些异常值在正常运行期间运行良好,但在故障转移期间却举步维艰——并且为了不违反 SLA,我们的平均利用率下降了。
图 1:典型的“不平衡图”。每条线代表一个容器的 CPU 使用率。
图 2:一个不太明显的情况:容器利用率分布在整个带内,但有些容器的利用率比其他容器高。
影响的不对称性负载不平衡的一个重要方面是其影响的不对称性。想象一下,在 100 个工作负载中,有 5 个未得到充分利用。这会影响效率,但成本相对较低——我们没有尽可能高效地使用 5% 的机器。
如果情况反过来,同样的 5 个工作负载被过度利用,情况就会更加严重。我们可能会影响客户体验,并可能影响系统的可靠性。避免这些热点的简单解决方案是降低整个集群的平均利用率。现在,这将产生更为显著的影响:95% 的工作负载未得到充分利用,这意味着(财务)资源的浪费更为严重。
森林和树木由于异常值很容易发现,我们最初专注于逐一修复和追踪它们,试图尽快找出每个问题的根源并单独修复它们。这些单独修复的结果并不总是如预期的那样。我们的一些更改的影响低于预期,或者只影响了系统的一小部分。同样,后来的其他更改也带来了意想不到的显著改进。这是因为有几个独立的问题在起作用。这个“问题森林”导致工作基本上是连续的——只有在较大的问题得到修复后,我们才会发现一个新的、较小的问题。
回想起来,如果分析更加严谨,我们本可以减轻“意外”部分的影响——我们可以更深入地了解系统,并提前收集更多样本。不过,工作的顺序可能是一样的——只有通过这个过程,我们才能学会如何理解和衡量系统。
衡量影响可能令人惊讶的是,直到最后,该项目最有争议的方面之一就是衡量影响。讨论涉及来自不同团队和组织的人员,他们在不同时间加入和离开该项目。每个参与方对问题、其优先级和潜在解决方案都有宝贵但略有不同的看法。
持续测量影响本身就非常复杂。显然,我们应该测量异常值——我们很快决定使用给定工作负载中第 p99 个使用率最高的任务的 CPU 利用率。经过讨论,我们同意使用平均值作为基准,将 p99/平均值作为不平衡指标。
然而,这一点却出奇的模糊:
工作负载在多个区域的多个集群中运行。p99/平均值应该在所有实例中计算还是针对每个集群单独计算?如果是针对每个集群,我们如何权衡结果?这个决定会极大地影响最终的数字。工作负载在多个区域运行,但与区域不同,我们的区域具有很强的隔离性——将流量发送到哪里不受网络控制。因此,网络团队可能关心与业务不同的指标。典型的工作负载具有周期性模式——服务可能在一周中的某一天最繁忙,而在其他时间利用率不足。我们应该只测量峰值时的不平衡还是全天测量?如果在峰值时,应该将多长时间视为峰值?我们只关心每周一次的峰值吗?我们的工作负载通常以主动-主动模式运行,每个区域都有一些备用容量以应对潜在的故障转移。负载不平衡在这些故障转移期间最为严重——我们应该只在那时测量它吗?如果是这样,我们的测量频率将降低——通常,我们每周会得到一个简单的样本。工作负载很嘈杂。服务推出通常会导致不平衡峰值(因为新容器的出现和预热)。有些工作负载可能推出得很快(每个增量),但每天通过 CD 管道推出数十次。其他工作负载的速度要慢得多,单次推出可能需要数小时。这两种类型的推出都可能与高峰时间重叠。除此之外,还有“非典型事件”,如临时性能下降、流量耗尽、负载测试或与事件相关的问题。大多数工作负载遵循“标准”模式,但一些(更关键的)服务已被划分为具有单独路由配置的自定义分片。同样,一小部分基本工作负载也可以通过自定义对等路由访问。最后,另一小部分服务在专用主机上运行。这些维度可能会影响我们的跟踪。一旦我们确定了每个工作负载指标,问题就会扩展到多服务:
我们如何在最终得分中衡量个人的工作量?每个服务的层级(优先级)如何影响其在最终得分中的权重?不同的工作负载具有不同的周期模式,这会影响得分吗?工作负载通常具有每周和每日峰值,但这些峰值并不是同时出现的。我们能否将最终指标分解为子部分来追踪各个区域或集群的不平衡?指标必须实时可用,以便进行开发和监控——在这里,我们关心的是尽可能高的精度,通常是亚分钟级。然而,同一个指标必须在很长一段时间内(数年)可用,我们需要将数据汇总为以天为单位的块,同时牢记所有先前的加权考虑因素。
实际数字:最终,我们创建了一个“持续不平衡指标”。对于每个工作负载,我们计算每分钟的 p99(例如 5 个内核)和平均(例如 4 个内核)CPU 利用率。结合容器数量,我们可以计算出“浪费的内核”。对于上述示例,10 个容器将导致 10*4=40(内核)使用率, (5-4)*10=10浪费内核,结果指标为 1+10/40=1.25。这直观地映射到人类在实时调试时可以进行的“标准”p99/平均计算 125%。
图3:不平衡的理论定义。
随着时间的推移,这实际上成为两条曲线下面积的比率:p99 和平均利用率。
图 4:实时仪表板上的持续不平衡指标。
这种方法的好处是,由于浪费和利用率是以绝对核心数计算的,因此我们能够以自定义的任意维度聚合它们:每个服务、每个服务-每个集群、每个服务组、每个集群、每个区域。同样,任何时间窗口(小时、天、周)自然都可以工作——就像对一系列整数求和一样简单。此外,该指标自然会给“繁忙”时段更高的权重——高峰期的不平衡比非高峰期的不平衡更为关键。缺点是很难向人们解释该指标,但我们发现“加权 p99/平均值”的近似值是可以接受的。
另一种方法是计算“每周 p99 与 p99”之比和“每周平均值与平均值之比”,这种方法更容易根据单个服务进行解释,但对随机事件(耗尽、故障转移、负载测试、部署)的敏感性较高,因此会产生噪音。此外,跨服务加权不太直接。
上述指标在 Grafana 中以实时指标的形式提供,在 Hive 中以长期存储的形式提供。我们需要编写自定义管道来每天对指标进行预处理,以实现可视化。
不同的切片值得一提的是,衡量负载不平衡的一个特别问题:如何对数据进行切片会极大地影响结果。人们很容易从小切片(集群、区域、地区)开始,然后“平均”不平衡。遗憾的是,这在实践中行不通。例如,可能有两个集群的(平均)p99/平均值比率为 110%,但当查看整个工作负载时,不平衡可能会高得多——在我们的案例中高达 140%。同样,将两个不平衡程度较高的集群组合在一起可能会导致不平衡程度较低。
解决问题第一步:首先获取(黑客)数据我们首先构建了 Grafana 仪表板以实现实时可观察性。这使我们能够实时测量每个服务的影响,但无助于了解根本原因。虽然假设负载平衡存在问题,但我们并不真正知道。最初的问题是缺乏可观察性,我们面临两个问题。
首先,由于基数问题,我们的负载均衡器不会按每个后端实例发出统计数据。由于许多服务运行着数千个容器和数百个程序,这会导致我们的代理内存使用量激增,甚至中型服务的统计数据也无法查询。幸运的是,那个夏天的一个实习项目增加了一项功能,可以在新的指标命名空间(保持现有统计数据不变)上以可选方式发出统计数据(节省代理内存使用量)。结合汇总规则,我们现在可以自省大多数服务(只要我们一次只为其中几个启用额外的可见性)。
其次,我们失去了在计算和网络堆栈中唯一标识实例的能力。当时,我们可以看到每个目标的 CPU 使用率,但无法轻松地将其映射到容器。由于我们的 IP 目标范围很广,并且端口使用情况动态,因此主机:端口的可用“唯一标识符”会破坏我们的指标(再次是基数)。关于适当解决方案的讨论之前已经停滞了几个季度。最终,网络堆栈实施了一个基于对 IP 地址进行排序并发出基于整数的实例 ID 的短期解决方案。这些在部署中并不稳定,但结合一些更复杂的脚本,我们能够获得所需的数据。
这一步提供了重要的教训:
始终首先获取数据精心策划、有针对性的孤立黑客攻击可能非常有用你不需要完美的可观察性就能得出正确的结论手动分析在深入了解了这个问题之后,我们挑选了一些大型服务并尝试分析其根本原因。令人惊讶的是,负载平衡并没有问题——在 1 分钟的窗口内(我们当时的 CPU 统计分辨率),RPS 分布几乎完美。每个容器接收到的请求数量几乎相等,大多数应用程序的差异低于 0.1%。然而,在同一个窗口内,CPU 利用率差异很大。
经过数周的调查,我们能够量化几个独立的原因:
一些重要的流量强制不平衡来源。例如,我们的许多系统都是“城市感知”的,每个城市始终位于一个区域。这自然会将不同数量的流量推向每个区域,随着城市的醒来和入睡,流量比例会不断变化。服务跨多个硬件 SKU 运行,包括集群内和跨集群。即使是理论上相同的硬件也表现出明显的性能差异。部分不平衡被留在“未知”桶中。大部分不平衡最终被证明是我们的可观察性问题。我们目前将其余部分(不到原始不平衡的 20%)归因于嘈杂的邻居。
下图展示了我们 2020 年最大的服务之一的初步分析。
图 5:理解不平衡。
被迫建立长期聚合那时,我们想从任何容易实现的事情开始。更好的负载平衡:实时动态子集 | Uber 博客给了我们一些可以调整的旋钮。然而,这并不容易,反而带来了一个新问题。
图 6:单个服务每周 CPU 利用率的模式。
我们的服务呈现出繁重的日周期和周周期(见上文)。除此之外,我们经常看到由故障、部署、故障转移或临时事件引起的峰值。推出更改后,只有大规模改进(20% 以上)才能被人为发现,但我们的更改太过细微。
这导致了上文中解释的可观察性决策。我们构建了管道,以基于稳定且可抵御峰值的指标来聚合长期数据。除此之外,我们还可以按集群、区域、地区或服务组划分指标——这反过来又让我们能够调查更多“可疑”行为。
一些预先存在的旋钮让我们减少了服务网格引起的负载不平衡部分,但这只是整体问题的一小部分。
可能的解决方案显然,第一步是查看底层硬件配置和操作系统设置。启动了几个单独的线程来查看这些内容。
解决硬件异构性需要更复杂的过程。有许多可行的方法,包括:
修改CFS 参数,使得尽管底层硬件不同,但集群中的每个主机看起来都相同。这个选项很有吸引力,但最终被放弃了,因为它对各种软件堆栈(如GOMAXPROCS)的影响不明确。回想起来,这也阻止了我们利用cpu-sets 进行配置。修改主机到集群的放置以实现统一的集群。修改每个服务集群的位置以保证稳定但不统一的主机选择。转向云式主机管理,每个团队将选择特定类型的硬件。许多可能的服务网格变化以实现更好的负载平衡。图 7:选项矩阵(故意模糊处理)
在众多可能的选项中,我们出于多种原因选择了对服务网格进行更改。从技术上讲,我们层上的更改不需要更改数据中心的物理布局,也不需要对每个服务进行迁移。从战术上讲,我们还可以将更改快速交付给大多数服务。
更改硬件虽然硬件 SKU 内部存在根本差异,但我们发现硬件、固件和低级软件存在许多问题。这些问题涉及操作系统设置、CPU 调节器设置、固件版本、驱动程序版本、CPU 微代码版本,甚至内核版本与 Intel HWP 不兼容。造成这种情况的一个普遍根本原因是,从历史上看,一旦硬件被引入并出现在队列中,除非出现问题,否则它就不会受到影响。然而,随着时间的推移,这导致了机器之间的偏差。
Uber 在混合云/私有环境中运行,因此我们自然也会遇到特定于云的问题。与其他公司一样,我们已经看到多个理论上配置相同的虚拟机性能不相似的案例(这仍然是真实的)。同样,我们也看到过在本地运行良好的工作负载在云上引发问题的情况。更糟糕的是,云意味着对底层基础设施细节的可见性降低。
如果没有最近完成的Crane 项目,修复所有这些问题几乎是不可能的——我们可以在无需人工参与的情况下测量、修复和推广对数以万计的机器的更改。现在,发现的所有问题都可自动检测和修复。
这些修复的一个明显好处是它们适用于每个工作负载,无论它如何处理或发起其工作(Kafka、Cadence、RPC、计时器、批处理作业等)。除了负载不平衡改进之外,它们还为我们提供了有效的免费容量——一些 CPU 一夜之间“变得更快”。
可观察性可观察性是问题中一个有趣的部分。在项目开始之前,我们知道由于 1 分钟的窗口大小,样本收集存在局限性,但我们发现了更多问题。
从技术上讲,问题是由cgroups、 cexporter、我们的内部Prometheus指标抓取工具和m3之间的交互引起的。具体来说,由于指标以不断增加的量表形式发出,管道中任何地方的统计数据收集延迟都会导致百分位数计算出现(大量)人为峰值。我们投入了大量工作来保存样本的时间戳,以及妥善处理目标和收集器服务的重启。一个示例问题是,对于任何足够大的服务,数据收集都会被破坏。
可观察性问题的一个有趣方面与人际互动有关,或者说,人是不可信任的。在项目早期,我们询问服务所有者,容器利用率达到何种程度会对用户产生影响(延迟增加)。有趣的是,几个月后,在我们推出修复方案后,当我们再次询问时,我们得到了相同的答案。这两种说法都不成立,因为我们知道旧数据是错误的。最终,人类的非理性带来了净效率的提升:服务所有者最终以更高的温度运行他们的服务,而认为什么都没有改变。
负载均衡正如《更好的负载平衡:实时动态子集 | Uber 博客》中所述,我们的服务网格在两个层面上工作。最初,控制平面发送分配,决定应向每个目标集群发送多少流量。集群之间的不平衡在这里决定。之后,数据平面遵循此分配,但随后它负责选择正确的主机——第二级集群内负载平衡在这里发生。虽然我们考虑改变这个模型,但我们保持不变,并为每个级别推出了两种解决方案。
集群间不平衡在 Uber,服务在多个地区的多个区域中运行。由于每个区域的启动时间不同,因此无法保证每个区域中的主机相同——通常,区域越新,其硬件就越新。区域性能的差异会导致 CPU 不平衡。
我们最初的方法是为每个区域设置一个静态权重;然后该权重将用于负载平衡,以便具有更快硬件的区域可以处理更多请求。每个区域的权重计算为部署在该区域中的每个主机的标准化计算单元 (NCU) 因子的平均值。NCU 因子根据基准分数衡量主机 CPU/核心性能,其中分数取决于核心每周期指令数(核心每时钟周期完成的工作量)和核心频率(每秒可用的时钟周期数)的乘积。
然后,我们可以使用静态区域权重作为乘数,将更多的流量发送到更强大/更快的区域。
具有更高乘数的更快区域将按比例路由更多流量,以增加 CPU 利用率,从而缓解 CPU 不平衡。
例如,如果某个服务在区域 A(权重 = 1)和区域 B(权重 = 1.2)中部署了 10 个实例,则负载平衡将按照 B 具有 12(10 * 1.2)个实例进行,因此 B 将比 A 接收更多的请求。
图 8:区域权重
这种方法效果出奇地好——我们能够以相对较少的努力缓解大部分不平衡。然而,也存在一些问题:
区域权重是区域中所有主机的估计值(平均 NCU 因子)。但是,服务可能会非常幸运/不幸地被部署在区域中速度最快/最慢的主机上。虽然不常见,但我们运营的区域会因启动或关闭而发生变化。此外,在启动期间,我们通常会逐渐引入硬件,这可能需要多次更新。有时,我们会将新硬件引入旧区域以调整其大小或更换损坏的硬件。这些硬件可能属于不同类型,因此需要调整权重。动态主机感知集群负载平衡因此,我们重新审视了这个问题,并投资了一个先进的解决方案:主机感知流量负载平衡。
这种方法通过查看服务实例部署到的确切主机、收集其服务器类型,然后更新每个服务集群之间的负载平衡来解决缺点。这是通过让我们的发现系统了解主机(按 IP)、其主机类型和权重的映射来实现的,这样对于部署在集群中的给定服务,发现系统可以向我们的流量控制系统提供额外的权重信息。下图显示了一个示例:
图 9:动态主机感知集群负载平衡
对于服务 Foo,如果我们平等对待每个实例,负载平衡率应为37.5 %/ 62.5 %,而不是示例中显示的36 %/ 64 %。如果主机跨多代(我们的队列中不同主机之间的权重差异高达 2 倍),差异可能会更加明显。
与静态权重方法相比,主机感知负载平衡会动态调整每个服务的权重,以减少集群间不平衡。由于很少引入新的主机类型,因此维护起来也更容易。
集群内不平衡如前所述,集群内不平衡是主机代理(称为 Muttley)的责任。每个代理都可以完全控制为每个请求选择正确的对等体。所有服务使用的 Muttley 的原始负载平衡算法都是最少待处理,它会将请求发送到已知未完成请求数量最少的对等体。虽然这导致以 1 分钟为间隔测量时 RPS 几乎完美平衡,但由于硬件类型不同,它仍然导致 CPU 利用率不平衡。
辅助负载平衡 (ALB)图 10:辅助负载平衡简介。
我们构建了一个系统,其中每个后端都协助负载均衡器选择下一个对等端。应用程序中间件层将负载元数据作为标头附加到每个响应。我们有效地实现了一个没有中央协调的协调系统。以前,每个 Muttley 只知道它造成的负载(以及它可以从延迟中推断出的一些信息),现在,它可以动态了解每个后端的总体状态。此状态不仅受后端本身的影响(例如,在较慢的硬件上运行),还受其他 Muttley 做出的决策的影响。例如,如果后端(随机)被选入太多子集,系统会动态调整。这让我们以后可以减少 ALB 上服务的子集大小。
虽然Google SRE 书中的简短提及部分启发了这种方法,但我们还是做出了一些不同的选择。这两项变化相互关联,旨在简化方法。我们打算稍后开始、评估并转向更复杂的解决方案——幸运的是,我们不必这样做。在实施后期,我们发现了一篇Netflix 博客文章,我们各自得出了类似的结论。
首先,作为负载元数据,我们使用正在处理的并发请求数,以整数形式报告(q=1、q=2、..、q=100 等)。我们也考虑过报告利用率,但这并不明显(报告的利用率应该基于getrusage还是cgroups)。cgroups更自然,因为服务所有者用它来跟踪他们的目标。然而,它们带来了更多挑战——我们的基础团队担心每个 docker 容器独立抓取 cgroups 的成本,以及如果 cgroups 布局发生变化(包括在 cgroupsv2 迁移期间)可能出现的紧密耦合。我们可以通过与收集统计数据的主机守护程序集成来解决这个问题,但我们希望避免添加新的运行时依赖项。最后,只使用逻辑整数就足够了(经过一些调整,如下所述)。此外,它允许每个服务覆盖而无需更改负载平衡器代码——虽然绝大多数应用程序使用标准负载指示器,但一些(异步)应用程序会覆盖它以更好地反映其负载。
第二个不同之处是使用两个随机选择的力量而不是加权循环。由于我们只有一个整数作为负载指示器,因此 pick-2 实现似乎更直接、更安全。与上述方法类似,这种方法效果很好,我们不需要改变它。这种方法对我们所有应用程序的故障都非常宽容。除了典型的崩溃循环或 OOMing 应用程序外,我们还遇到过中间件的不良/错误实现不会导致事故的情况。我们推测,由于加权循环更精确和“严格”,它在某些情况下可能会表现得“更好”,但可能会导致类似惊群效应的情况。
在实现方面,每个 Muttley 都使用修改后的移动平均值来保存每个对等体在 25 个之前请求中的得分——这个值在我们的测试中效果最好。为了在较低的 RPS 情况下获得有意义的数字,我们将每个报告的负载按一千倍放大。
pick-2 负载均衡器的一个有趣问题是“负载最重”的对等点永远不会被选中。而且由于我们被动地发现对等点负载,我们也会刷新其状态,从而使其实际上处于未使用状态,直到另一个对等点变得更慢。我们最初通过实施“失败者惩罚”来缓解这一问题,即每次对等点失去选择时,其“负载值”都会在内部降低 - 因此,如果损失足够多,对等点将再次被选中。这对于大调用者实例数低 RPS 场景效果不佳,有时重新选择一个对等点需要几分钟。最终,我们将其更改为时间衰减,即根据上次选择时间降低对等点的分数。我们目前使用 5 秒的半衰期进行分数衰减。
我们还实现了一个内部称为“吞吐量奖励”的功能。这源于经验观察,即较新的硬件可以更好地处理并发请求。我们注意到,当在不同硬件上的两个对等点之间进行负载平衡并且两个对等点都报告相同的“负载值”时,我们会像预期的那样向更快的对等点发送更多请求。但是,更快对等点的 CPU 利用率(处理=15、CPU=10%、Q=5)将保持低于较慢对等点(处理=10、CPU=12%、Q=5)。为了弥补这一点,每次对等点“完成”请求时,我们都会稍微降低其负载以向其推送更多请求。对等点相对于子集中的其他对等点越快,它获得的“吞吐量奖励”就越多。此功能将 P99 CPU 利用率降低了 2%。
ALB 设计文档的很大一部分(大部分)致力于可能的替代方案。我们认真考虑过,不是将负载元数据附加到每个响应,而是使用中央组件来收集和分发数据。担心的是元数据可能会消耗大量可用带宽。我们内部有两个表面上看似相关的系统。第一个是集中式健康检查系统,它几乎实时地从容器群中的每个容器收集健康状态。第二个是上一篇博文中描述的实时聚合系统。
重复使用其中任何一个都是不可行的:健康检查系统可以轻松收集所有容器的负载状态,但收集后,该系统被设计为不频繁地分发健康变化——绝大多数时间,容器保持健康。然而,负载平衡指标不断变化,这是设计使然。由于我们运行的是平面网格(每个容器都可以与每个容器通信),我们需要不断将数百万个容器的数据分发到数十万台机器,或者构建一个新的聚合和缓存层。同样,负载报告聚合系统也不匹配——它以低几个数量级的基数对每个集群的聚合值进行操作。
最终,我们对所选的(基于响应头的)方法感到满意。它易于实施,并且使成本归因变得容易——推动更多 RPS 的服务的带宽成本更高。从绝对数字来看,与附加到每个请求的其他跟踪/身份验证元数据相比,额外元数据(每个请求约 8 字节)的成本几乎是看不见的。
延迟是“分布式”与“集中式”负载数据收集的一个有趣方面。从理论上讲,响应标头方法接近实时,因为负载附加到每个响应。但是,由于每个 Muttley 都需要独立发现这一点,然后将响应与之前的响应平均,因此对于基于低 RPS 的场景,发现可能需要一些时间。基于健康检查的方法需要完整的往返(通常约 5 秒),但会立即分发给所有调用者实例。
但是,如果我们实施了它,我们可能会将推送频率降低到 1 分钟左右,因为上一段中列出了带宽问题。这可能足以修复硬件引起的偏差,但可能不足以解决其他问题,例如流量高峰、应用程序启动缓慢或故障转移。这两种方法在不同情况下可能会略有不同。不过,最终,我们对分布式方法感到满意——它很容易推理,并且没有可能会失败的集中式组件。
所选方法的一个缺点是它需要目标服务的配合。虽然需要的工作很少,但将其应用于数千个微服务将是艰巨的。幸运的是,Uber 过去几年构建的大多数应用程序都使用了通用框架,使我们能够快速插入所需的中间件。几个大型服务没有使用这些框架,但多年来并发的努力已经迁移了几乎所有服务。我们发现选择框架的决定是有益的,因为它具有复合效应——服务所有者又多了一个投资迁移的理由。在我们撰写这篇文章时,几乎所有服务都使用了通用框架。
静态组件 – ALB v1.1最初的推出并未达到我们减少硬件引起的不平衡的目标。主要原因是我们的硬件大部分时间都严重未得到充分利用——我们有用于区域故障转移和每周峰值的缓冲区。事实证明,在容器利用率相对较低的情况下,旧硬件可能会爆发到足以使延迟差异不可见的程度,同时消耗更多的 CPU 时间。虽然这意味着负载平衡在压力下(当我们需要它时)工作得更好,但它让产品工程师对我们的目标利用率感到不舒服——非高峰期的不平衡看起来太高了。
我们在负载平衡中添加了第二个静态组件来解决这个问题。我们利用了这样一个事实:在我们的设置中,主机的 IP 地址永远不会改变。由于代理自然知道目标的 IP 地址,我们只需要提供 IP 地址到相对主机性能的映射。由于数据的静态性质,我们开始将此信息作为构建时配置的一部分添加。这个权重本身并不完美:不同的应用程序在同一硬件类型上的表现不同。然而,结合 ALB 的动态部分,这效果很好——我们不需要添加特定于应用程序的权重。
测试开发过程中的一个大问题是测试。虽然我们的临时环境有限,但新解决方案需要处理许多参数:一些调用者或被调用者有三个实例,一些有三千个实例。一些后端服务于 <1,而一些则 > 1,000 RPS。一些服务服务于单个同质过程,而另一些服务于数百个,延迟从几毫秒到几十秒不等。最终,我们在生产中使用了一个虚拟服务,并配置了一组假负载生成器来表示异构负载。我们运行了 300 多次模拟,然后才找到正确的参数并尝试推广到生产服务。
结果我们对最终结果感到满意——确切数字取决于每个集群内的服务和硬件组合。不过,平均而言,我们将 P99 CPU 利用率降低了 12%,有些服务的好处超过 30%。目标服务在每个后端的规模越大,结果就越好——幸运的是,我们最关心的最大服务通常都得到了足够的优化。同样的运气也适用于入职——虽然 Uber 有超过 4,000 个微服务,但入职前 100 个微服务让我们获得了绝大多数的潜在覆盖范围。
推出和未来的变化部署进展顺利——我们尚未发现重大错误。事实证明,pick-2 负载平衡和安全回退具有弹性。我们按层级、按地区引入服务,试图找到具有代表性的服务类型。
ALB 已推广到我们最大的数百项服务,且几乎没有出现任何问题或变化:
长寿命 RPC 流。一小部分服务将少量长寿命 RPC 流与许多非常短暂的请求混合在一起。我们回滚了那里的入职培训。慢启动运行时。推出大约两年后,我们调整了解决方案以更好地处理慢启动(Java)服务。由于 JIT,这些服务在启动后无法提供相同的请求率,但使用记录的静态请求进行预热效果不够好;我们需要以较低的速率使用实际请求来预热服务。在这里,我们决定用池平均权重的百分比作为每个对等体的初始“权重”的种子,同时保持算法的核心不变。我们发现这在一系列服务中运行良好,我们很高兴这不需要任何静态窗口设置,与 Envoy 的慢启动模式不同- 算法会自动调整到一系列 RPS。启动时预取数据。另一个非常小的服务类别是在启动时预加载静态数据几分钟。由于我们的服务发布机制的特殊性,这些服务的实例在我们的服务发现中显示为“不健康”。旧算法强烈偏向健康实例。我们在 ALB 中更改了这一点,以避免在服务在临时过载后无法启动时出现类似惊群效应的情况(因为每个实例在依次恢复健康时都会立即过载)。新算法明显偏向健康实例,但在某些情况下,请求可能会发送到“不健康”的节点。这对这些服务不起作用——虽然报告的错误为 <0.01% 和 0.002%,但我们正在探索类似于恐慌阈值的变化,以使其完全消失。IP 地址映射。IP 地址到服务器类型的静态映射已经运行了 2 年多,但随着我们将工作负载转移到云端,它可能需要进行调整。有趣的是,两项服务覆盖了默认负载提供程序,以根据后台作业处理发出自定义负载指标。这证明默认设置对大多数服务都适用,但该解决方案足够灵活,可以支持其他用例。
概括图 11:区域权重展开。
该项目带来了非常显著的效率提升。我们可以在更高的利用率水平上运行容器,并且无状态工作负载的负载不平衡不再是问题。硬件配置的改进带来了双重好处,即减少不平衡和提高纯计算能力。
更有趣的是,从工程博客的角度来看,该项目还带来了一些学习。
最主要的是数据的重要性。问题是真实存在的,但我们在错误的假设下开始了这个项目。我们不知道如何衡量它;一旦我们达成一致,我们就缺乏有效衡量它的工具,尤其是长期衡量。即使在那之后,我们也意识到从底层基础设施收集样本的底层方式是有缺陷的。与此同时,数据赢得了争论,帮助我们解决问题,并优先考虑与其他团队的工作。另一个数据教训是建立正确的长期数据基础设施——它在项目期间和之前都很有帮助。我们能够使用现有的数据仓库作为基础,现在我们定期收到有关负载不平衡的问题。仪表板的链接通常可以回答所有问题。
第二个教训是在堆栈的正确位置添加解决方法以获取我们所需的数据。构建适当的实时可观察性需要数月或数个季度的时间。尽管如此,我们还是很快通过有针对性的黑客攻击并有选择地根据服务样本进行观察得出了正确的结论。与此相关的是愿意做大量的手动繁重工作:为了建立理解,我们在开始编码之前花了数周时间盯着仪表板并验证假设。后来,在实施 ALB 和区域/集群权重时,我们从相对较小的更改开始,验证假设,然后迭代到下一个版本。
第三个教训可能不那么具有普遍性,那就是信任平台。我们打赌我们的微服务将迁移到通用框架。同样,在实施时,我们以多年来对平台的现有投资为基础——已有工具(仪表板、调试工具、操作知识、推出政策),我们可以相当快速和安全地推出重大变更。我们根据平台的需要进行构建,避免了可能导致项目脱轨的重大重写。
致谢该项目有许多人参与。我们感谢 Avinash Palayadi、Prashant Varanasi、Zheng Shao、Hiren Panchasara 和 Ankit Srivastava 的总体贡献。Jeff Bean、Sahil Rihan、Vikrant Soman、Jon Nathan 和 Vaidas Zlotkus 提供硬件帮助,Vytenis Darulis 提供可观察性修复,Jia Zhan 和 Eric Chung 提供 ALB 审查,Nisha Khater 提供每个实例统计项目,Allen Lu 在全球范围内推出 yarpc。
Logo 归属:“正义天平 – 法律 – 律师和辩护律师”由 weiss_paarz_photos 拍摄,根据 CC BY-SA 2.0许可。
作者:
Pawel Krolikowski
Pawel Krolikowski is a Staff Software Engineer on the Software Networking team. Before working on load balancing, he spent most of his time on the Software Networking management plane and integrations with stateful and stateless orchestration systems.
Chien-Chih Liao
Chien-Chih Liao is a Senior Staff Software Engineer on Uber’s Software Networking team. His contributions include traffic control, traffic load balancing, data center failover, and resilience features for Uber’s service mesh.
Ying Jiang
Ying Jiang is the Manager of our Network Lifecycle Team. Before she shifted her path to become a manager, she worked on various projects, including imagebuilder of Crane infra stack, making Uber portable, Canary, and traffic load balancing.
出处:https://www.uber.com/en-JP/blog/load-balancing-handling-heterogeneous-hardware/