点击下方“JavaEdge”,选择“设为星标”

第一时间关注技术干货!

免责声明~ 任何文章不要过度深思! 万事万物都经不起审视,因为世上没有同样的成长环境,也没有同样的认知水平,更「没有适用于所有人的解决方案」; 不要急着评判文章列出的观点,只需代入其中,适度审视一番自己即可,能「跳脱出来从外人的角度看看现在的自己处在什么样的阶段」才不为俗人。 怎么想、怎么做,全在乎自己「不断实践中寻找适合自己的大道」

0 引言

在 2020 年 11 月,我们在博客文章 通过优先级负载丢弃保持 Netflix 的可靠性 中引入了在 API 网关层进行优先级负载丢弃的概念。本文探讨如何将这一策略扩展到单个服务层,特别是在视频流控制平面和数据平面中,以进一步提升用户体验和系统弹性。

1 Netflix 负载丢弃的演进

最初的优先级负载丢弃方法是在 Zuul API 网关层实现的。该系统能够有效管理不同类型的网络流量,确保关键的播放请求优先于不太关键的遥测流量

在此基础上,我们认识到需要在架构的更深层次——具体到服务层——应用类似的优先级逻辑,在同一服务中对不同类型的请求赋予不同的优先级。在服务层以及边缘 API 网关同时应用这些技术的优势包括:

  1. 服务团队可以自主掌控其优先级逻辑,并应用更细粒度的优先级控制。

  2. 可用于后台对后台的通信,例如不通过边缘 API 网关的服务之间的通信。

  3. 服务可以通过将不同请求类型整合到一个集群中并在必要时丢弃低优先级请求,而不是为失败隔离维护单独的集群,从而更高效地使用云资源。

2 服务级优先级负载丢弃的引入

PlayAPI 是视频流控制平面上的一个关键后台服务,负责处理设备发起的播放清单和许可证请求,这些请求是启动播放所必需的。我们根据关键程度将这些请求分为两类:

  1. 用户发起请求(关键):这些请求在用户点击播放时发出,直接影响用户开始观看节目或电影的能力。

  2. 预取请求(非关键):这些请求是在用户浏览内容时为了优化潜在延迟而乐观地发出的。如果用户选择观看特定标题,预取失败不会导致播放失败,但会略微增加从点击播放到视频显示在屏幕上的延迟。

用户浏览内容时 Netflix 在 Chrome 上向 PlayAPI 发出的预取请求

2.1 问题

为了应对流量高峰、高后端延迟或后端服务扩展不足的情况,PlayAPI 过去使用并发限制器来限制请求,这会同时减少用户发起请求和预取请求的可用性。这种方法存在以下问题:

  1. 预取流量高峰降低了用户发起请求的可用性。

  2. 当系统有足够的能力处理所有用户发起请求时,后端延迟增加会同时降低用户发起请求和预取请求的可用性。

将关键请求和非关键请求分片到单独的集群是一个选项,这可以解决问题 1,并在两种请求类型之间提供故障隔离,但其计算成本更高。分片的另一个缺点是增加了一些操作开销——工程师需要确保 CI/CD、自动扩展、指标和警报针对新集群正确配置。

打开网易新闻 查看精彩图片

选项 1— 无隔离

打开网易新闻 查看精彩图片

选项 2— 隔离但计算成本更高

2.2 我们的解决方案

我们在 PlayAPI 中实现了一个并发限制器,该限制器在不物理分片两个请求处理程序的情况下优先处理用户发起请求。这种机制使用了开源 Netflix/concurrency-limits Java 库的分区功能。我们在限制器中创建了两个分区:

  • 用户发起分区:保证 100% 的吞吐量。

  • 预取分区:仅使用多余的容量。

打开网易新闻 查看精彩图片
img

选项 3— 单集群优先级负载丢弃提供应用级隔离且计算成本更低。每个实例处理两种请求类型,并具有一个动态调整大小的分区,确保预取请求仅使用多余容量。必要时,用户发起请求可以“借用”预取容量。

分区限制器被配置为一个预处理 Servlet Filter,它通过设备发送的 HTTP 头确定请求的关键性,从而避免了读取和解析被拒绝请求的请求体的需要。这确保了限制器本身不会成为瓶颈,并且可以有效拒绝请求,同时使用最少的 CPU。例如,该过滤器可以初始化如下:

Filter filter = new ConcurrencyLimitServletFilter(
new ServletLimiterBuilder()
.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网关不同,大多数微服务通常只接收具有少数几个不同优先级的请求。为了在不同服务之间保持一致性,我们引入了四个预定义的优先级桶,受到Linux tc-prio级别的启发:

  • CRITICAL:影响核心功能 — 如果我们没有完全失败,这些永远不会被削减。

  • DEGRADED:影响用户体验 — 随着负载的增加,这些将逐步被削减。

  • BEST_EFFORT:不影响用户 — 这些将以最大努力的方式响应,并可能在正常操作中逐步被削减。

  • BULK:后台工作,预计这些将定期被削减。

服务可以选择上游客户端的优先级或通过检查各种请求属性(如HTTP头或请求体)将传入请求映射到这些优先级桶之一,以实现更精确的控制。以下是服务如何将请求映射到优先级桶的一个示例:

ResourceLimiterRequestPriorityProvider requestPriorityProvider() {
return contextProvider -> {
if (contextProvider.getRequest().isCritical()) {
return PriorityBucket.CRITICAL;
} else if (contextProvider.getRequest().isHighPriority()) {
return PriorityBucket.DEGRADED;
} else if (contextProvider.getRequest().isMediumPriority()) {
return PriorityBucket.BEST_EFFORT;
} else {
return PriorityBucket.BULK;
}
};
}
3.1 通用基于CPU的负载削减

Netflix的大多数服务都在CPU利用率上自动扩展,因此它是系统负载的自然度量,可以与优先级负载削减框架结合使用。一旦请求被映射到优先级桶,服务可以根据CPU利用率决定何时从特定桶中削减流量。为了维持自动扩展所需的信号,优先级削减只有在达到目标CPU利用率后才开始削减负载,并且随着系统负载的增加,更多关键流量将逐步被削减,以维持用户体验。

例如,如果一个集群针对自动扩展的目标是60%的CPU利用率,它可以被配置为在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之后,服务开始拒绝大多数请求,并且无法维持在负载削减启动之前成功的240 RPS。这可以在固定并发限制器中看到,或者当负载削减消耗太多CPU阻止其他工作被完成时

我们可以看到,在上述的基于CPU的负载削减实验部分,我们的负载削减实现避免了这两种反模式,通过保持低延迟并在负载削减期间维持与之前一样多的成功RPS。

4 通用基于IO的负载削减

一些服务不是CPU限制的,而是由于后端服务或数据存储在超载时通过增加延迟施加反向压力,它们是IO限制的。对于这些服务,我们重用了优先级负载削减技术,但我们引入了新的利用率度量来输入到削减逻辑中。我们最初的实现支持两种基于延迟的削减形式,除了标准的自适应并发限制器(本身是平均延迟的度量):

  1. 服务可以指定每个端点的目标和最大延迟,允许服务在服务异常缓慢时削减,无论后端如何。

  2. 在Data Gateway上运行的Netflix存储服务返回观察到的存储目标和最大延迟SLO利用率,允许服务在它们超载分配的存储容量时削减。

这些利用率度量提供了早期警告迹象,表明服务正在向后端生成过多的负载,并允许它在压倒后端之前削减低优先级工作。这些技术与仅并发限制相比的主要优势是它们需要的调整更少,因为我们的服务已经必须维持严格的延迟服务水平目标(SLOs),例如p50 < 10ms和p100 < 500ms。因此,将这些现有的SLOs重新表述为利用率使我们能够及早削减低优先级工作,以防止对高优先级工作产生进一步的延迟影响。同时,系统将接受尽可能多的工作,同时维持SLO。

为了创建这些利用率度量,我们计算有多少请求处理慢于我们的目标和最大延迟目标,并发出未能满足这些延迟目标的请求的百分比。例如,我们的KeyValue存储服务为每个命名空间提供了10ms的目标和500ms的最大延迟,所有客户端都接收到每个数据命名空间的利用率度量,以输入到它们的优先级负载削减中。这些度量看起来像:

utilization(namespace) = {
overall = 12
latency = {
slo_target = 12,
slo_max = 0
}
system = {
storage = 17,
compute = 10,
}
}

在这种情况下,12%的请求慢于10ms目标,0%慢于500ms最大延迟(超时),17%的分配存储被利用。不同的用例在它们的优先级削减中咨询不同的利用率,例如,每天写入数据的批次可能在系统存储利用率接近容量时被削减,因为写入更多数据会造成进一步的不稳定。

一个延迟利用率有用的示例是我们的一个关键文件源服务,它接受在AWS云中新文件的写入,并作为这些文件的源(为Open Connect CDN基础设施提供读取服务)。写入是最关键的,服务永远不应该削减,但当后端数据存储超载时,逐步削减对CDN较不关键的文件的读取是合理的,因为它可以重试这些读取,它们不影响产品体验。

为了实现这个目标,源服务配置了一个基于KeyValue延迟的限制器,当数据存储报告的目标延迟利用率超过40%时,开始削减对CDN较不关键的文件的读取。然后我们通过生成超过50Gbps的读取流量来压力测试系统,其中一些是针对高优先级文件的,一些是针对低优先级文件的:

打开网易新闻 查看精彩图片
打开网易新闻 查看精彩图片

在这个测试中,有一定数量的关键写入和大量对低优先级和高优先级文件的读取。在左上角的图表中,我们增加到每秒2000次读取的~4MiB文件,直到我们可以在右上角的图表中超过50Gbps触发后端存储的超载。当这种情况发生时,右上角的图表显示,即使在显著负载下,源只削减低优先级读取工作以保留高优先级写入和读取。在此之前,当我们达到断裂点时,关键写入和读取会与低优先级读取一起失败。在这个测试期间,文件服务的CPU负载是名义上的(<10%),所以在这种情况下,只有基于IO的限制器能够保护系统。还需要注意的是,只要后端数据存储继续以低延迟接受它,源将服务更多的流量,防止我们过去与并发限制遇到的问题,它们要么在实际上没有问题时过早削减,要么在我们已经进入充血性失败时太晚削减。

5 总结

服务级别的优先级负载削减的实施已被证明是在保持高可用性和为Netflix客户提供卓越用户体验方面迈出的重要一步,即使在意外的系统压力下也是如此。

关注我,紧跟本系列专栏文章,咱们下篇再续!

★ 作者简介:魔都架构师,多家大厂后端一线研发经验,在分布式系统设计、数据平台架构和AI应用开发等领域都有丰富实践经验。 各大技术社区头部专家博主。具有丰富的引领团队经验,深厚业务架构和解决方案的积累。 负责: 中央/分销预订系统性能优化 活动&券等营销中台建设 交易平台及数据中台等架构和开发设计 车联网核心平台-物联网连接平台、大数据平台架构设计及优化 LLM Agent应用开发 区块链应用开发 大数据开发挖掘经验 推荐系统项目 目前主攻市级软件项目设计、构建服务全社会的应用系统。 ”

参考:

  • 编程严选网

编程严选网:http://www.javaedge.cn/ 专注分享软件开发全生态相关技术文章、视频教程资源、热点资讯等,全站资源免费学习,快来看看吧~ 【编程严选】星球

欢迎长按图片加好友,我会第一时间和你分享软件行业趋势面试资源学习方法等等。

添加好友备注【技术群交流】拉你进技术交流群

关注公众号后,在后台私信:

  • 更多教程资源应有尽有,欢迎关注并加技术交流群,慢慢获取

  • 为避免大量资源被收藏白嫖而浪费各自精力,以上资源领取分别需要收取1元门槛费!