声明
本文是对Netflix 博客的翻译
前言
在先前关于Zuul 2开源的文章中,我们简要概述了近期在负载均衡方面的一些工作。在这篇文章中,我们将更详细地介绍这项工作的原因、方法和结果。
因此,我们开始从Zuul和其他团队那里学习,并改进我们的负载均衡实现,以进一步减少由服务器过载引起的错误。
背景
在Zuul的实现中,我们持续采用Ribbon负载均衡器,它运用轮询算法和过滤机制,以识别并隔离连接失败率高的服务器。
多年来,我们经历了一些改进和定制,旨在向最近启动的服务器发送更少的流量,以避免它们过载。这些方法已经取得了显著的改进,但是对于一些特别麻烦的源集群,我们仍然会看到与负载相关的错误率比期望的要高得多。
如果集群中的所有服务器都过载,那么我们在选择一个服务器而不是另一个服务器时几乎没有什么改进,但是我们经常看到只有一部分服务器过载的情况。例如:
- 启动后的冷服务器(在红黑部署和自动伸缩事件期间)
- 由于交错的动态属性/脚本/数据更新或大型GC事件,服务器暂时变慢/阻塞。
- 服务器硬件不好。我们经常会看到一些服务器的运行速度永远比其他服务器慢,这可能是由于邻居的噪音或不同的硬件。
指导原则
项目启动之初,确立一些核心原则至关重要,它们将引导我们在软件开发过程中做出明智的决策。
在现有负载均衡器框架的约束下工作
我们将之前的负载均衡定义与Zuul代码库相结合,这使得我们无法与Netflix的其他团队共享这些功能。所以这次我们决定接受约束和额外的投资,并从一开始就考虑重用。这使得在其他系统中的采用更加直接,并减少了重新发明轮子的机会。
运用从别人那里学到的东西
尝试在别人的想法和实现的基础上进行构建。例如,之前在Netflix在其他IPC堆栈中试用的choice-of-2和试用算法。
避免分布式状态
更倾向于局部决策,以避免弹性问题、复杂性和跨集群协调状态的滞后。
避免客户端配置和手动调优
我们多年来使用Zuul的操作经验表明,将服务配置的一部分置于不属于同一团队的客户端服务中……会导致问题。
一个问题是,这些客户端配置往往与服务器端不断变化的现实不同步,或者在不同团队拥有的服务之间引入变更管理的耦合
例如,升级了用于Service X的EC2实例类型,从而减少了该集群所需的节点。因此,现在Service Y中的“每台主机的最大连接数”客户端配置应该增加,以反映新增加的容量。应该先进行客户端更改,还是先进行服务器端更改,还是同时进行两者的更改?更有可能的是,设置被完全遗忘,导致更多的问题。
在可能的情况下,不要配置静态阈值,而是使用根据当前流量、性能和环境变化的自适应机制。
负载均衡方法
一个核心理念是,尽管客户端视角是获取服务器延迟信息的最佳途径,但服务器本身则是提供服务器利用率信息的最佳来源。结合这两个数据源应该会给我们带来最有效的负载均衡。
我们使用了相互补充的机制组合,其中大多数已经被其他人开发和使用过,尽管以前可能没有以这种方式组合。
- 在服务器之间进行选择的二选一(choice-of-2 algorithm)算法
- 主要根据负载均衡器对服务器利用率的视图进行均衡
- 其次,根据服务器的利用率视图进行均衡
- 基于缓慢释放流量和服务器生命周期的机制,以避免新启动的服务器过载 - 避免刚启动的服务突然接受流量洪峰顶不住
- 收集到的服务器指标随时间衰减为零 - 服务指标衰减是为了避免指标差的服务永远得不到请求的机会
将最短队列加入与服务器报告的利用率相结合
我们决定采用广泛使用的最短队列连接(JSQ)算法,并结合基于服务器自报利用率的算法,以期达到两者优势的融合。
加入最短队列对于单个负载均衡器非常有效,但如果是在集群中的负载均衡,则会出现严重问题。问题是,负载均衡器将倾向于惊群(Herd),并同时选择相同的低利用率服务器,从而使它们过载,然后转移到下一个利用率最低的服务器并使其过载,然后继续……
这可以通过结合使用JSQ和choice-of-2算法来解决。这在很大程度上消除了“**惊群”**问题,除了每个负载均衡器没有完整的服务器利用率之外,它工作得很好。
JSQ通常通过仅从本地负载均衡器计算到服务器的正在使用的连接数来实现,但是当有10到100个负载均衡器节点时,本地视图可能会产生误导。
例如在上图中,负载均衡器A有一个流量请求服务X和Z,但是没有请求到Y。当有一个新的请求到来时他应该请求哪个服务,从数据上看应该选择Y。但这是不正确的,实际上其他两个负载均衡器正在请求他,他已经利用率最大了,但是负载均衡器A没法知道。
这说明了单个负载均衡器的视角如何与实际情况完全不同
我们在只依赖客户端视图时遇到的另一个问题是,对于大型集群(特别是在低流量的情况下),负载均衡器通常只有几个正在使用的连接,连接到数百个池中的服务器子集。因此,当它选择哪个服务器负载最少时,它通常只能在零和零之间进行选择。它没有任何关于它所选择的服务器的利用率的数据,因此只能随机猜测。
这个问题的一个解决方案可能是与所有其他负载均衡器共享每个负载均衡器的进行中的请求数量,但这样你就有一个分布式状态问题需要解决。
我们通常将分布式可变状态作为最后的手段,因为所获得的价值需要超过所涉及的实质性成本:
- 操作开销和复杂性会增加,比如部署和金丝雀发布
- 与数据损坏的爆炸半径相关的弹性风险(即 1% 的负载均衡器上的坏数据会令人烦恼,100% 的负载均衡器上的坏数据会导致中断
- 在负载均衡器之间实现P2P分布式状态系统的成本,或者操作具有处理大量读写流量所需的性能和弹性凭证的单独数据库的成本。
另一种更简单的解决方案——也是我们选择的一种——是依赖服务器向每个负载均衡器报告它们的使用情况……
利用率
采用每台服务器对其利用率的自我评估的优势在于,它汇总了所有负载均衡器对该服务器的使用情况,从而解决了JSQ算法可能存在的信息不全面问题。
我们可以通过两种方式实现这一点:
- 使用运行状况检查端点主动轮询每个服务器的当前利用率。
- 被动地跟踪带有当前利用率数据注释的服务器的响应
我们选择了第二个选项,因为它很简单,允许频繁更新这些数据,并且避免了让N个负载均衡器每隔几秒钟轮询M个服务器而给服务器带来的额外负载。
这种被动策略的一个影响是,负载均衡器向一台服务器发送请求的频率越高,它对该服务器利用率的看法就越最新。所以RPS越高,负载均衡的效率就越高。但反过来说,RPS越低,负载均衡的效果就越差。
这对我们来说不是问题,但对于通过一个特定负载均衡器接收低RPS的服务(同时通过另一个单独的负载均衡器接收高RPS),主动轮询运行状况检查可能更有效。临界点是负载均衡器向每个服务器发送的RPS低于用于运行状况检查的轮询频率。
服务器实现
我们在服务器端实现了这一点,通过简单地跟踪进行中的计数,将其转换为该服务器配置的最大值的百分比,并将其作为HTTP响应头写出来:
X-Netflix.server.utilization: <current-utilization>[, target=<target-utilization>]
可选的目标利用率可以由服务器指定,以指示它们在正常条件下打算使用的利用率百分比。然后,负载均衡器将使用它进行一些粗粒度的过滤,如后面所述。
我们尝试使用进行中请求以外的指标,如操作系统报告的cpu利用率和平均负载,但发现它们似乎会引起振荡,这是由于它们基于滚动平均值而引起的延迟。因此,我们决定现在只使用计算飞行请求的相对简单的实现。
Choice-of-2 算法代替轮循
由于我们希望能够通过比较服务器的统计信息来选择服务器,因此必须放弃现有的简单循环实现。
在Ribbon中,我们尝试了将JSQ与ServerListSubsetFilter结合的另一种方案,目的是减轻分布式JSQ可能导致的惊群问题。这给出了合理的结果,但是最终的跨目标服务器的请求分布仍然太广。
因此,我们转而应用了Netflix另一个团队的一些早期经验,并实现了“二选一”算法。这样做的优点是易于实现,使负载均衡器上的cpu成本保持在较低水平,并提供良好的请求分发。
根据综合因素选择
在选择服务器时,我们依据三个独立因素对它们进行评估:
- 客户端运行状况:该服务器的连接错误率的滚动百分比。
- 服务器利用率:该服务器提供的最新分数。
- 客户端利用率:从此负载均衡器发送到该服务器的当前请求数。
这3个因素被用来为每个服务器分配分数,然后比较总分数来选择获胜者。
使用像这样的多个因素确实会使实现变得更加复杂,但它可以避免仅依赖一个因素可能出现的边缘情况问题。
例如,如果一台服务器开始出现故障并拒绝所有请求,那么报告的利用率将会低得多——因为拒绝请求比接受请求快——如果仅有一个评判因子,那么所有负载均衡器将开始向该故障服务器发送更多请求。客户机综合选择因素缓解了这种情况。
过滤
在随机挑选两台服务器进行比较的过程中,我们将排除那些利用率和运行状况超出我们设定的保守阈值的服务器。
此过滤针对每个请求执行,以避免仅定期过滤的过时问题。为了避免在负载均衡器上造成高cpu负载,我们只尽最大努力进行N次尝试,以找到一个随机选择的可行服务器,然后在必要时退回到未过滤的服务器。
当大部分服务器池存在持续的问题时,这样的过滤非常有用。在这种情况下,随机选择2个服务器经常会导致选择2个坏服务器进行比较,即使有许多好的服务器可用。
缓慢释放
对于负载均衡器尚未收到响应的任何服务器,我们一次只允许一个进行中请求。我们过滤掉这些试用中的服务器,直到从它们那里收到响应。
这有助于避免在新启动的服务器有机会表明它们的利用情况之前,因为大量的请求而使服务器过载。
基于服务器生命周期的预热
我们依据服务器的生命周期,在新服务器启动后的最初90秒内,逐步增加其接收的流量。
统计衰变
为避免服务器被永久性地列入黑名单,我们对所有用于负载均衡决策的统计数据实施了衰减机制(目前设定为超过30秒的线性衰减)。例如,如果服务器的错误率上升到80%,我们停止向它发送流量,那么我们使用的值将在30秒内衰减到零(即。15秒后会变成40%)。
操作的影响
放弃使用轮询来实现负载均衡的一个负面影响是,以前我们在集群中的服务器之间有一个非常紧密的请求分布,现在我们在服务器之间得到了更大的增量。
使用choice-of-2算法有助于减轻这种情况(与跨集群中所有服务器或服务器子集的JSQ相比),但不可能完全避免这种情况。
因此,在操作方面确实需要考虑到这一点,特别是在金丝雀分析中,我们通常会比较请求计数、错误率、cpu等的绝对值。
较慢的服务器接收较少的流量
显然,这是预期的效果,但对于使用 round-robin 的团队来说,流量是平均分配的,这对操作方面有一些连锁反应。
由于源服务器之间的流量分布现在取决于它们的利用率,如果一些服务器正在运行效率更高或更低的不同版本,那么它们将接收或多或少的流量。所以:
- 当集群进行红黑部署时,如果新的服务器组有性能退化,那么该组的流量比例将小于50%
- 同样的效果也可以在金丝雀上看到——基线可能接收到与金丝雀集群不同的流量。所以在查看参数时,最好将RPS和CPU结合在一起(游戏邦注:例如RPS在金丝雀中较低,而CPU则相同)
- 不太有效的异常检测——我们通常有自动化来监视集群内的异常服务器(通常是由于某些硬件问题而从启动开始就变慢的VM)并终止它们。当这些异常值由于负载均衡而接收到较少的流量时,这种检测就更加困难
滚动动态数据更新
从 round-robin 到这个新的负载均衡器的有用效果是,它可以很好地与动态数据和属性的分阶段推出一起工作。
我们的最佳实践是一次部署一个区域(数据中心)的数据更新,以限制意外问题的影响范围。
即使没有数据更新本身引起的任何问题,服务器应用更新的行为也可能导致短暂的负载峰值(通常与GC相关)。如果此峰值同时发生在集群中的所有服务器上,则可能导致负载减少和错误向上游传播的大峰值。在这种情况下,负载均衡器几乎无能为力,因为所有服务器都在承受高负载。
综合负载测试结果
在开发、测试和调优这个负载均衡器的不同方面时,我们广泛地使用了综合负载测试场景。这些对于验证真实集群和网络的有效性非常有用,作为单元测试之上的可重复步骤,但尚未使用真实的用户流量。
关于这个测试的更多细节将在后面的附录中列出,但总结一下要点:
- 与轮循实现相比,启用了所有特性的新负载均衡器在负载减少和连接错误方面有了数量级的减少。
- 在平均和长尾延迟方面有了实质性的改进(与轮询实现相比减少了3倍)。
- 服务器利用率特性本身的添加增加了显著的价值,提供了一个数量级的错误减少和大部分延迟减少。
对实际生产流量的影响
我们发现新的负载均衡器在将尽可能多的流量分配到每个源服务器上时非常有效。这在不需要任何人工干预的情况下,可以绕过间歇性和持续降级的服务器进行路由,从而避免了导致工程师在半夜被唤醒的重大生产问题。
在正常运行期间,很难说明这种影响,但在生产事件期间,甚至在某些服务的正常稳态运行期间,都可以看到这种影响。
进行中事件
最近的一个事件涉及到服务中的一个bug,该bug导致越来越多的服务器线程随着时间的推移而阻塞。从服务器启动开始,每小时会有几个线程阻塞,直到它最终达到最大值并释放负载。
在下面的每台服务器的RPS图表中,您可以看到,在凌晨3点之前,服务器之间存在广泛的分布。这是由于阻塞线程数量较多的服务器通过负载均衡器发送的流量较少。然后在凌晨3点25分之后,自动伸缩开始启动更多的服务器,每个服务器的RPS大约是现有服务器的两倍——因为它们还没有任何阻塞的线程,因此可以成功处理更多的流量。
现在,如果我们看一下同一时间范围内每台服务器的错误率图表,您可以看到,在整个事件过程中,所有服务器的错误分布是相当均匀的,尽管我们知道有些服务器的容量比其他服务器小得多。这表明负载均衡器在有效地工作,并且由于集群中的总体可用容量太少,所有服务器的负载都略微超过了它们的有效容量。
然后,当自动扩展启动新服务器时,它们会被发送尽可能多的流量,直到它们与集群的其他服务器在相同的低水平上出错。
总之,负载均衡在将流量分配给服务器方面非常有效,但在本例中,没有足够多的新服务器启动,因此无法将总体错误级别降至零。
稳态
我们还看到,在一些服务器的服务中,由于GC事件而出现几秒钟的负载下降,仅在稳态噪声方面就有了显著的减少。在启用新的负载均衡器后,可以看到错误大大减少:
警告间隙
一个意想不到的影响是突出了我们自动警报中的一些漏洞。一些现有的基于服务错误率的警报,以前会在渐进问题只影响集群的一小部分时触发,现在会延迟触发,或者根本不会触发,因为错误率保持在较低水平。这意味着团队有时没有收到影响其集群的大问题的通知。解决方案是通过在利用率指标中添加额外的偏差警报(而不仅仅是错误指标)来填补这些差距。
结论
本文的主旨不在于推广Zuul——尽管它无疑是一个卓越的系统——而是在于与代理、服务网格、负载均衡社区分享经验,为其中那些引人入胜的方法增添新的视角。Zuul是测试、实现和改进这些类型的负载均衡方案的好系统;根据Netflix的需求和规模来运行它们,使我们有能力证明和改进这些方法。
与任何软件系统一样,您应该根据自己组织的约束条件和目标做出决策,并尽量避免追求完美。
如果你对这类工作感兴趣,请随时联系我或我们Netflix的云网关团队。
Reference
- 原文链接