点击下方“JavaEdge”,选择“设为星标”
0引言
1Netflix负载丢弃的演进
最初的优先级负载丢弃方法是在ZuulAPI网关层实现的。该系统能够有效管理不同类型的网络流量,确保关键的播放请求优先于不太关键的遥测流量。
在此基础上,我们认识到需要在架构的更深层次——具体到服务层——应用类似的优先级逻辑,在同一服务中对不同类型的请求赋予不同的优先级。在服务层以及边缘API网关同时应用这些技术的优势包括:
2服务级优先级负载丢弃的引入
PlayAPI是视频流控制平面上的一个关键后台服务,负责处理设备发起的播放清单和许可证请求,这些请求是启动播放所必需的。我们根据关键程度将这些请求分为两类:
用户浏览内容时Netflix在Chrome上向PlayAPI发出的预取请求
2.1问题
为了应对流量高峰、高后端延迟或后端服务扩展不足的情况,PlayAPI过去使用并发限制器来限制请求,这会同时减少用户发起请求和预取请求的可用性。这种方法存在以下问题:
将关键请求和非关键请求分片到单独的集群是一个选项,这可以解决问题1,并在两种请求类型之间提供故障隔离,但其计算成本更高。分片的另一个缺点是增加了一些操作开销——工程师需要确保CI/CD、自动扩展、指标和警报针对新集群正确配置。
选项1—无隔离
选项2—隔离但计算成本更高
2.2我们的解决方案
我们在PlayAPI中实现了一个并发限制器,该限制器在不物理分片两个请求处理程序的情况下优先处理用户发起请求。这种机制使用了开源Netflix/concurrency-limitsJava库的分区功能。我们在限制器中创建了两个分区:
选项3—单集群优先级负载丢弃提供应用级隔离且计算成本更低。每个实例处理两种请求类型,并具有一个动态调整大小的分区,确保预取请求仅使用多余容量。必要时,用户发起请求可以“借用”预取容量。
分区限制器被配置为一个预处理ServletFilter,它通过设备发送的HTTP头确定请求的关键性,从而避免了读取和解析被拒绝请求的请求体的需要。这确保了限制器本身不会成为瓶颈,并且可以有效拒绝请求,同时使用最少的CPU。例如,该过滤器可以初始化如下:
Filterfilter=newConcurrencyLimitServletFilter(newServletLimiterBuilder().named("playapi").partitionByHeader("X-Netflix.Request-Name").partition("user-initiated",1.0).partition("pre-fetch",0.0).build());
需要注意的是,在稳定状态下,没有限流,优先级对预取请求的处理没有任何影响。优先级机制仅在服务器达到并发限制并需要拒绝请求时启动。
2.3测试
为了验证我们的负载削减是否按预期工作,我们使用了故障注入测试,在预取调用中注入了2秒的延迟,这些调用的典型p99延迟小于200毫秒。故障被注入到一个基线实例中,该实例有常规的负载削减,还有一个金丝雀实例中,有优先级的负载削减。PlayAPI调用的一些内部服务使用单独的集群来处理用户发起的和预取请求,并使预取集群运行得更热。这个测试案例模拟了一个预取集群对于下游服务正在经历高延迟的场景。
基线—没有优先级负载削减。预取和用户发起的都看到了可用性的同等下降
金丝雀—有优先级负载削减。只有预取可用性下降,而用户发起的可用性保持在100%
没有优先级负载削减的情况下,当注入延迟时,用户发起的和预取的可用性都会下降。然而,在添加了优先级负载削减之后,用户发起的请求保持了100%的可用性,只有预取请求被节流。
我们已经准备好将这个功能推广到生产环境,并看看它在实际中的表现如何!
2.4现实世界的应用和结果
Netflix的工程师努力保持我们的系统可用,在我们部署优先级负载削减几个月后,Netflix发生了一次基础设施故障,影响了我们许多用户的流媒体播放。一旦故障被修复,我们从Android设备上看到了每秒预取请求的12倍激增,这可能是因为积累了大量的排队请求。
Android预取RPS的激增
这可能会导致第二次故障,因为我们的系统没有扩展到能够处理这种流量激增。PlayAPI中的优先级负载削减在这里有帮助吗?
是的!虽然预取请求的可用性下降到了20%,但由于优先级负载削减,用户发起的请求的可用性保持在99.4%以上。
预取和用户发起的请求的可用性
在某个时刻,我们节流了超过50%的所有请求,但用户发起的请求的可用性继续保持在99.4%以上。
3通用服务工作优先级
基于这种方法的成功,我们创建了一个内部库,使服务能够根据可插拔的利用率度量执行优先级负载削减,具有多个优先级级别。
与需要处理大量具有不同优先级的请求的API网关不同,大多数微服务通常只接收具有少数几个不同优先级的请求。为了在不同服务之间保持一致性,我们引入了四个预定义的优先级桶,受到Linuxtc-prio级别的启发:
服务可以选择上游客户端的优先级或通过检查各种请求属性(如HTTP头或请求体)将传入请求映射到这些优先级桶之一,以实现更精确的控制。以下是服务如何将请求映射到优先级桶的一个示例:
ResourceLimiterRequestPriorityProviderrequestPriorityProvider(){returncontextProvider->{if(contextProvider.getRequest().isCritical()){returnPriorityBucket.CRITICAL;}elseif(contextProvider.getRequest().isHighPriority()){returnPriorityBucket.DEGRADED;}elseif(contextProvider.getRequest().isMediumPriority()){returnPriorityBucket.BEST_EFFORT;}else{returnPriorityBucket.BULK;}};}3.1通用基于CPU的负载削减
Netflix的大多数服务都在CPU利用率上自动扩展,因此它是系统负载的自然度量,可以与优先级负载削减框架结合使用。一旦请求被映射到优先级桶,服务可以根据CPU利用率决定何时从特定桶中削减流量。为了维持自动扩展所需的信号,优先级削减只有在达到目标CPU利用率后才开始削减负载,并且随着系统负载的增加,更多关键流量将逐步被削减,以维持用户体验。
基于CPU利用率的不同优先级桶的请求被负载削减的百分比
3.2基于CPU的负载削减实验
我们进行了一系列实验,向一个服务发送大量请求,该服务通常以45%的CPU为目标进行自动扩展,但为了防止其扩展,以便在极端负载条件下监控CPU负载削减。实例被配置为在60%的CPU后削减非关键流量,在80%的CPU后削减关键流量。
随着RPS超过自动扩展量的6倍,服务能够首先削减非关键请求,然后削减关键请求。在整个过程中,延迟保持在合理的限制内,成功的RPS吞吐量保持稳定。
使用合成流量的基于CPU的负载削减的实验行为.
即使RPS超过了自动扩展目标的6倍,P99延迟在整个实验中也保持在合理的范围内.
3.3负载削减的反模式反模式1—不削减
在上述图表中,限制器很好地保持了成功请求的低延迟。如果没有在这里削减,我们将看到所有请求的延迟增加,而不是一些可以重试的请求的快速失败。此外,这可能导致死亡螺旋,其中一个实例变得不健康,导致其他实例负载增加,导致所有实例在自动扩展启动之前变得不健康。
没有负载削减:在没有负载削减的情况下,增加的延迟可能会降低所有请求的质量,而不是拒绝一些可以重试的请求,并且可能使实例不健康
反模式2—充血性失败
另一个需要注意的反模式是充血性失败或过于激进的削减。如果负载削减是由于流量增加,成功的RPS在负载削减后不应该下降。以下是充血性失败的一个例子:
充血性失败:在16:57之后,服务开始拒绝大多数请求,并且无法维持在负载削减启动之前成功的240RPS。这可以在固定并发限制器中看到,或者当负载削减消耗太多CPU阻止其他工作被完成时
我们可以看到,在上述的基于CPU的负载削减实验部分,我们的负载削减实现避免了这两种反模式,通过保持低延迟并在负载削减期间维持与之前一样多的成功RPS。
4通用基于IO的负载削减
一些服务不是CPU限制的,而是由于后端服务或数据存储在超载时通过增加延迟施加反向压力,它们是IO限制的。对于这些服务,我们重用了优先级负载削减技术,但我们引入了新的利用率度量来输入到削减逻辑中。我们最初的实现支持两种基于延迟的削减形式,除了标准的自适应并发限制器(本身是平均延迟的度量):
这些利用率度量提供了早期警告迹象,表明服务正在向后端生成过多的负载,并允许它在压倒后端之前削减低优先级工作。这些技术与仅并发限制相比的主要优势是它们需要的调整更少,因为我们的服务已经必须维持严格的延迟服务水平目标(SLOs),例如p50<10ms和p100<500ms。因此,将这些现有的SLOs重新表述为利用率使我们能够及早削减低优先级工作,以防止对高优先级工作产生进一步的延迟影响。同时,系统将接受尽可能多的工作,同时维持SLO。
为了创建这些利用率度量,我们计算有多少请求处理慢于我们的目标和最大延迟目标,并发出未能满足这些延迟目标的请求的百分比。例如,我们的KeyValue存储服务为每个命名空间提供了10ms的目标和500ms的最大延迟,所有客户端都接收到每个数据命名空间的利用率度量,以输入到它们的优先级负载削减中。这些度量看起来像:
utilization(namespace)={overall=12latency={slo_target=12,slo_max=0}system={storage=17,compute=10,}}
在这种情况下,12%的请求慢于10ms目标,0%慢于500ms最大延迟(超时),17%的分配存储被利用。不同的用例在它们的优先级削减中咨询不同的利用率,例如,每天写入数据的批次可能在系统存储利用率接近容量时被削减,因为写入更多数据会造成进一步的不稳定。
一个延迟利用率有用的示例是我们的一个关键文件源服务,它接受在AWS云中新文件的写入,并作为这些文件的源(为OpenConnectCDN基础设施提供读取服务)。写入是最关键的,服务永远不应该削减,但当后端数据存储超载时,逐步削减对CDN较不关键的文件的读取是合理的,因为它可以重试这些读取,它们不影响产品体验。
为了实现这个目标,源服务配置了一个基于KeyValue延迟的限制器,当数据存储报告的目标延迟利用率超过40%时,开始削减对CDN较不关键的文件的读取。然后我们通过生成超过50Gbps的读取流量来压力测试系统,其中一些是针对高优先级文件的,一些是针对低优先级文件的:
在这个测试中,有一定数量的关键写入和大量对低优先级和高优先级文件的读取。在左上角的图表中,我们增加到每秒2000次读取的~4MiB文件,直到我们可以在右上角的图表中超过50Gbps触发后端存储的超载。当这种情况发生时,右上角的图表显示,即使在显著负载下,源只削减低优先级读取工作以保留高优先级写入和读取。在此之前,当我们达到断裂点时,关键写入和读取会与低优先级读取一起失败。在这个测试期间,文件服务的CPU负载是名义上的(<10%),所以在这种情况下,只有基于IO的限制器能够保护系统。还需要注意的是,只要后端数据存储继续以低延迟接受它,源将服务更多的流量,防止我们过去与并发限制遇到的问题,它们要么在实际上没有问题时过早削减,要么在我们已经进入充血性失败时太晚削减。
5总结
服务级别的优先级负载削减的实施已被证明是在保持高可用性和为Netflix客户提供卓越用户体验方面迈出的重要一步,即使在意外的系统压力下也是如此。
★作者简介:魔都架构师,多家大厂后端一线研发经验,在分布式系统设计、数据平台架构和AI应用开发等领域都有丰富实践经验。各大技术社区头部专家博主。具有丰富的引领团队经验,深厚业务架构和解决方案的积累。负责:中央/分销预订系统性能优化活动&券等营销中台建设交易平台及数据中台等架构和开发设计车联网核心平台-物联网连接平台、大数据平台架构设计及优化LLMAgent应用开发区块链应用开发大数据开发挖掘经验推荐系统项目目前主攻市级软件项目设计、构建服务全社会的应用系统。”