去年,Uber 工程团队发表了一篇关于为其微服务架构设计的新减载机制的文章。
这篇文章从各个角度来看都很有趣。因此,我在阅读时做了一些笔记,以记录我的理解,并写下如果我最后没有得到答案时我想更深入研究的内容。我多次发现这是我学习新事物的最佳方式。
从一开始就吸引我的是对用于构建此解决方案的百年原则的参考。这就是我喜欢的事情——借鉴不同领域的概念/想法,并调整它们来解决不同领域的问题。
如果您对系统弹性和稳定性感兴趣,我建议您还阅读优秀的书籍“Release It!”作者:迈克尔·T·尼加德。
这是一本老书,但却是一本好书——一本深入探讨构建有弹性和稳定的软件系统的策略、模式和实践指南的书,强调如何有效地处理故障。
Uber 实施了一种名为 Cinnamon 的新负载卸载解决方案,该解决方案利用 PID 控制器(已有数百年历史的机制),根据服务的当前负载和请求优先级来决定服务应处理或丢弃哪些请求。
它不涉及服务级别的任何调整(尽管我对此有疑问),可以自动适应,并且比他们之前的解决方案 QALM 更高效。还要记住,Uber 的微服务架构不适合胆小的人……
PID 控制器是工业控制应用中用于调节温度、流量、压力、速度和其他过程变量的仪器。 PID(比例积分微分)控制器使用控制环反馈机制来控制过程变量,是最精确和稳定的控制器。
如果您想了解有关这个具有数百年历史的概念的更多信息,请访问维基百科。
现在,回到文章。 PID 代表比例、积分和微分。在他们的例子中,他们使用称为 PID 控制器的组件来监控基于三个组件(或度量)的服务(输入请求)的运行状况。
术语“成比例”表示所采取的行动与当前误差成比例。简单来说,这意味着所应用的校正与期望状态和实际状态之间的差异成正比。如果误差较大,则纠正措施也相应较大。
当端点过载时,后台 goroutine 开始监视优先级队列中请求的流入和流出。
因此,负载卸载器中的比例 (P) 组件根据当前队列大小与目标或所需队列大小的差距来调整卸载速率。如果队列比期望的大,就会发生更多的脱落;如果较小,脱落就会减少。
这就是我的理解。
PID 控制器的工作是最小化排队请求的数量,而自动调节器的工作是最大化服务的吞吐量,而不牺牲响应延迟(太多)。
虽然文本没有在队列大小的上下文中明确提及“积分 (I)”,但它表明 PID 控制器的作用是最大限度地减少排队请求的数量。排队请求的最小化与 Integral 组件解决随时间累积的错误的目标一致。
为了确定端点是否过载,我们会跟踪请求队列上次为空的时间,如果在最后 10 秒内尚未清空,我们就认为端点过载(受到 Facebook 的启发)。
在负载卸载器中,它可能与与请求队列的历史行为相关的决策相关联,例如自上次为空以来的时间。
老实说,我并不完全清楚。我必须说,这有点令人沮丧。虽然他们提到利用一个有数百年历史的机制,但如果他们明确说明哪个部分对应于它的运作方式或运作方式,那将会很有帮助。我不想贬低他们这篇精彩文章的价值。这只是我的咆哮……毕竟,我是法国人……;)
我认为这个更容易识别。
在经典 PID(比例积分微分)控制器中,当您希望控制器根据当前误差变化率预测系统的未来行为时,“微分 (D)”操作特别有用。它有助于抑制振荡并提高系统的稳定性。
在本文提到的负载切断器和 PID 控制器的上下文中,微分组件可能用于评估请求队列填充的速度。通过这样做,它有助于做出旨在维持稳定系统并防止突然或不可预测的变化的决策。
拒绝器组件有两个职责:a)确定端点是否过载,b)如果端点过载,则丢弃一定比例的请求以确保请求队列尽可能小。当端点过载时,后台 goroutine 开始监视优先级队列中请求的流入和流出。根据这些数字,它使用 PID 控制器来确定卸载请求的比率。 PID 控制器非常快地找到正确的级别(因为需要很少的迭代),并且一旦请求队列被耗尽,PID 就会确保我们只会缓慢地减小比率。
在上述上下文中,PID 控制器用于确定端点过载时要丢弃的请求的比率,并监视请求的流入和流出。 PID 控制器的微分组件响应变化率,隐含地参与评估请求队列填充或耗尽的速度。这有助于做出动态决策以维持系统稳定性。
在确定过载的上下文中,积分组件可能与跟踪请求队列处于非空状态的时间有关。这与随着时间的推移累积误差信号的积分的想法是一致的。
“积分——基于请求在队列中的等待时间……”
另一方面,导数部分与变化率有关。它响应请求队列状态变化的速度。
“衍生——基于队列填满速度的拒绝……”
Integral 组件强调非空状态的持续时间,而 Derivative 组件则考虑队列更改的速率。
在游戏结束时,他们利用这三种措施来确定请求的行动方案。
我的问题是他们如何将这三个组件结合起来(如果有的话)。我也很想了解他们如何监控它们。
尽管如此,我想我已经有了这个想法……
边缘中的端点用请求的优先级进行注释,并且通过Jaeger从边缘传播到所有下游依赖项。通过传播这些信息,请求链中的所有服务都会知道请求的重要性以及它对我们的用户有多重要。
我首先想到的是它将无缝集成到服务网格架构中。
我很欣赏使用分布式服务跟踪和标头来传播请求优先级的概念。沿着这些思路,为什么选择将这种依赖项添加到每个微服务的共享库,而不是将其放置在服务之外,也许作为 Istio 插件?考虑到它提供的好处:独立的发布/部署周期、多语言支持等。
以下是一些额外的想法:
好吧,我有偏见,因为我不是共享库的忠实粉丝,只是因为我认为它们使发布/部署过程变得复杂。但是,我不确定是否需要考虑特定于服务的配置方面。也许他们配置服务应该等待多长时间才能开始处理查询并完成它?
也许值得测试的一个方面是喷射器的决策过程。
据我了解,它根据本地化到服务的PID控制器来确定是否拒绝请求。是否有更全球化的方法可供选择?例如,如果已知管道中的下游服务之一过载(由于其自己的 PID 控制器),则任何上游服务是否可以在到达此过载服务之前决定拒绝该请求(这可能是进一步向下 n 步)小路)?
该决定可以基于 PID 控制器或下游服务的自动调节器返回的值。
现在,我正在思考他们在总结文章时提到的各个方面,并提供一些数字来展示他们的系统的效率,这令人印象深刻
他们在某个时候提到“每个请求都有 1 秒的超时时间”。
我们运行 5 分钟的测试,其中发送固定数量的 RPS(例如 1,000),其中 50% 的流量为第 1 层,50% 为第 5 层。每个请求的超时时间为 1 秒。
在分布式系统中,将请求与特定的到期时间或截止时间相关联是很常见的,处理路径上的每个服务负责强制执行该时间限制。如果在请求完成之前达到过期时间,链中的任何服务都可以选择中止或拒绝该请求。
我假设这个 1 秒超时附加到请求中,并且每个服务都可以决定中止请求,具体取决于我们在这个截止日期内的位置。这是一项全球性的衡量标准,因为它是通过服务聚合的。我认为这与我之前提出的观点相呼应,即对整个系统的运行状况或依赖关系有一个全局的了解,如果由于其中一项服务关闭而导致请求没有机会完成,则决定尽快中止请求。小路。
下游服务的“健康状况”(包括来自本地 PID 控制器的数据)是否可以作为附加到响应的标头返回,并用于构建更先进的断路器/早期抢占式脱落机制?
最后,我很好奇更多地了解以前的方法,因为根据本文给出的描述,它似乎是合理的。
当您检查吞吐量和延迟的衡量标准时,毫无疑问 QALM 或 Cinnamon 哪一个表现最好。请注意,他们在文章中提到了 QALM 方法的链接。可能应该从那里开始;)
一如既往,这些方法并不适合所有人。 Uber 的架构和负载是有自己的。我实际上迫不及待地想阅读本系列的下一篇文章,特别是想了解有关 PID 控制器和自动调节器的更多信息。
通过 Cinnamon,我们构建了一个高效的负载卸载器,它使用百年历史的技术来动态设置拒绝和估计服务容量的阈值。它解决了我们在 QALM(以及任何基于 CoDel 的负载卸载器)中注意到的问题,即 Cinnamon 能够:
- 快速找到稳定的废品率
- 自动调整服务容量
- 无需设置任何配置参数即可使用
- 开销非常低
这种方法的有趣之处在于,它们考虑要处理的所有请求来决定对每个新输入请求做什么,因为它们使用(优先级)队列。如前所述,我很好奇该机制是否还可以考虑基于相同 PID 测量的所有依赖服务的运行状况......
本文还有其他有趣的方面,例如他们如何衡量策略的效果以及与之前的方法的比较。然而,它不需要我比已经提供的内容更详细的注释。因此,我强烈建议您阅读原文。
觉得这篇文章有用吗?在Linkedin 、 Hackeroon和Medium上关注我!请👏这篇文章分享吧!
也发布在这里。