Netflix按优先级减载保障可用性(二)

July 3, 2024

背景

已经在之前的文章中介绍了Netflix在API网关Zuul中实施的按优先级减载策略来保障服务的可用性。当前这一策略已经扩展到了单个服务级别。重点关注在视频控制层面(即AWS中的后台服务)和数据平面。

相对于之前在API网关Zuul中的实现,在服务级别中使用优先减载策略的优势在于:

  1. 服务团队可以拥有自己的优先级逻辑,并可以应用更细粒度的优先级
  2. 可用于后端到后端的通信,即不位于我们的边缘 API 网关后面的服务。
  3. 服务可以通过将不同类型的请求组合到一个集群中,并在必要时删除低优先级的请求,而不是维护单独的集群来进行故障隔离,从而更有效地利用云容量。

方案引入

总体思路是先在PlayAPI服务中引入优先减载策略,验证后再全面推广。PlayAPI是视频流控制平面上的一个关键后端服务,用于播放前的manifest拉取。

根据请求的重要性将其分为两类:

  1. 用户发起的请求(关键):

    这些请求是在用户点击播放时发出的,直接影响用户开始观看节目或电影的能力。

  2. 预取请求(非关键):

    为减少终端等待耗时所做的预加载类请求,失败也不会影响播放,仅增加少量的加载耗时。

问题

PlayAPI之前采用并发控制器来限流,但是没有区分流量类型,这会导致两类流量之间的相互影响

可行的解决方案之一是隔离两类流量,将关键请求和非关键请求分片到单独的集群。但是带来的问题是成本的增加,包括开发(系统复杂度)、维护(机器、流水线、监控)成本。

💡
隔离在某些场景下也是可行的。比如在我司的业务中将数据上报通道与信令通道相隔离,避免相互影响。另外在早期也曾经实施过单独部署重试set,但是效果一般。

解决方案

不进行物理隔离,而是在 PlayAPI 中实现了一个并发限制器,限制器优先处理用户发起的请求,而非预取请求。

此机制使用开源Netflix/concurrency-limits Java 库的分区功能。我们在限制器中创建了两个分区:

  • 用户启动的分区(User-Initiated Partition):

    保证 100% 的吞吐量。

  • 预取分区(Pre-fetch Partition):

    仅利用多余的容量。

每个实例都可处理两种类型的请求,并且具有两个分区,分区的大小可动态调整,以确保预取请求仅获得过剩容量。这允许用户发起的请求在必要时“窃取”预取容量。

 

分区限制器配置为预处理Servlet 过滤器,它使用设备发送的 HTTP 标头来确定请求的关键性,从而避免了读取和解析被拒绝请求的请求主体的需要。这确保了限制器本身不是瓶颈,并且可以在使用最少 CPU 的情况下有效地拒绝请求。例如,过滤器可以初始化为

Loading...

请注意,在稳定状态下,不会出现任何限制,优先级对预取请求的处理也没有影响。优先级机制仅在服务器达到并发限制并需要拒绝请求时才会启动。

💡
Servlet 过滤器可以动态地拦截请求和响应,以变换或使用包含在请求或响应中的信息。

可以将一个或多个 Servlet 过滤器附加到一个 Servlet 或一组 Servlet。Servlet 过滤器也可以附加到 JavaServer Pages (JSP) 文件和 HTML 页面。调用 Servlet 前调用所有附加的 Servlet 过滤器。

Servlet 过滤器是可用于 Servlet 编程的 Java 类,可以实现以下目的:

  • 在客户端的请求访问后端资源之前,拦截这些请求。
  • 在服务器的响应发送回客户端之前,处理这些响应。

实际应用效果

在部署优先负载削减几个月后,Netflix 的基础设施出现一次故障,影响了许多用户的流媒体播放。故障修复后,Android 设备每秒的预取请求量激增 12 倍,可能是因为排队请求积压。

虽然预取请求的可用性下降到 20%,但由于优先减载,用户发起请求的可用性达到 99.4% 以上。

预取和用户发起请求的可用性

我们一度限制了超过 50% 的所有请求,但用户发起的请求的可用性仍然保持在 99.4% 以上

通用策略

基于这种方法的成功,我们创建了一个内部库,使服务能够根据可插入利用率措施执行优先负载削减,具有多个优先级别。

与需要处理大量具有不同优先级的请求的 API 网关不同,大多数微服务通常只接收具有几个不同优先级的请求。为了保持不同服务之间的一致性,我们引入了四个预定义的优先级存储桶,灵感来自Linux tc-prio 级别

  • CRITICAL:影响核心功能 — 如果我们没有彻底失败,这些功能就永远不会消失。比如播放鉴权。
  • DEGRADED:影响用户体验 — 随着负载的增加,这些功能将逐渐消失。比如推荐。
  • BEST_EFFORT:不会影响用户 — 我们将尽最大努力来响应这些影响,并且可能会在正常运行中逐步消除。比如观看历史同步。
  • BULK:APP的后台类工作,比如异步数据上报。

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

Loading...

基于通用 CPU 的负载削减

优先级减载仅在达到目标 CPU 利用率后才开始减载,并且随着系统负载的增加,将逐步减载更关键的流量以保持用户体验

例如,如果集群的自动扩展目标是 CPU 利用率达到 60%,则可以将其配置为在 CPU 利用率超过此阈值时开始减少请求。当流量高峰导致集群的 CPU 利用率大大超过此阈值时,它将逐渐减少低优先级流量以节省资源用于高优先级流量。这种方法还为自动扩展(即服务扩容)提供了更多时间,以便向集群添加更多实例。一旦添加了更多实例,CPU 利用率就会降低,低优先级流量将恢复正常服务。

根据不同优先级存储桶的 CPU 利用率(X 轴)计算的负载削减请求百分比(Y 轴)

💡
思路总结: 1. 根据流量自带优先级标记或者上游服务优先级来确定流量的优先级。 2. 根据当前负载,实时调整优先级等级。比如 [60% - 65%]开始丢弃BULK流量,[65%-75%]丢弃BEST_EFFORT流量,[75%-85%]丢弃DEGRADE流量。

优先级减载策略的目标:

  1. 按流量优先级减载
  2. 进入减载模式之后,系统CRITICAL流量的成功率以及耗时不应该有明显的变化

原文中还有相关实验来验证减载策略的有效性。

基于通用 I/O 的负载削减

有些服务不是 CPU 密集型的,而是 IO 密集型的,受支持服务或数据存储的影响,当它们在计算或存储容量方面超载时,它们可以通过增加延迟来施加反压。对于这些服务,我们重新使用优先负载削减技术,但我们引入了新的利用率指标来输入削减逻辑。除了标准自适应并发限制器(本身是平均延迟的度量)之外,我们的初始实现还支持两种基于延迟的削减形式:

  1. 该服务可以指定每个端点的目标和最大延迟,当服务异常缓慢时,无论后端如何,这都允许服务脱落。
  2. 数据网关上运行的 Netflix 存储服务返回观察到的存储目标和最大延迟 SLO 利用率,从而允许服务在分配的存储容量超载时进行卸载。

结语

容灾策略的整体思路应该是越接近故障点实施容灾策略效果越好,所以Netflix在之前API网关实施减载策略的基础上进一步推广至微服务级。但是实施的思路是相同的:

  1. 做好流量分类,区分核心流量和非核心流量。可以根据业务场景做更加精细化的拆分。精准的流量分类是一切的前提。
  2. 仅在当系统负载到阈值时启用按优先级降载策略,按当前负载+优先级策略,丢弃(或者降级)低优先级流量,保障高优先级流量的服务可用性。
  3. 优先级确认:终端携带表示或者主动根据上游识别。
  4. 有效验证:流量模拟压测,验证各优先级流量的成功率和耗时。理论上高优先级流量的成功率、耗时影响不大。

参考

See all postsSee all posts