前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Netflix 微服务异步迁移:从同步的“请求响应”模式转换为异步事件

Netflix 微服务异步迁移:从同步的“请求响应”模式转换为异步事件

作者头像
深度学习与Python
发布2022-11-28 16:47:53
7360
发布2022-11-28 16:47:53
举报

作者 | Sharma Podila

译者 | 张卫滨

策划 | 丁晓昀

假设我们正在运行一个基于 Web 的服务。请求处理变慢最终将会导致服务不可用。实际上,并不是所有的请求都需要立即处理。有些请求只要确认已收到即可。你有没有问过自己这样的问题:“我是否能够从异步请求处理中获益?如果确实如此的话,我该如何在一个实时的、大规模的关键任务系统中做出这种转变?”

接下来,我将要讨论我们是如何将一个面向用户的系统从基于请求 - 响应的同步系统迁移为一个异步系统的。我将会介绍是什么原因促使我们开始了这样的旅程,我们做出了哪些系统设计变更,这一过程中的挑战是什么,以及我们做出了哪些设计选择和权衡。最后,我会介绍在上线新系统时我们所使用的验证过程。

原始架构

Netflix 是一个流视频服务,全球有超过两亿的会员在使用该服务。会员可以在众多支持的设备上观看电视节目、纪录片和电影。在访问 Netflix 的时候,借助我们的个性化推荐系统,他们能够看到各种选择。用户只需要点击播放按钮,就可以坐下来享受观看电影所带来的乐趣了。

我们会在这个电影放映过程中收集很多数据,用于运维和用例分析。其中有些数据会用来支撑我们的产品特性,比如继续观看,该特性允许会员中途停止观看电影,稍后等他回来时,在任何其他设备上都能从这个暂停点继续观看。这些数据还用于个性化和推荐引擎,以及核心业务分析。

我将会介绍迁移某个产品特性的经验,也就是浏览历史,该特性允许会员看到他们过去的浏览活动历史并且可以选择隐藏它。我们先看一下迁移之前的现有系统。在较高的层次上来讲,我们有支持不同设备的 Netflix 客户端,比如手机、台式机、笔记本电脑以及电视,它们在放映过程中会向 Netflix 云发送消息。

首先,数据会到达 Gateway 服务。在这里,它会被发送至 Playback API,该 API 会管理放映会话的生命周期。此外,它还会将放映数据发送至 Request Processor 层。在 Request Processor 中,除了其他的任务之外,它会将长期和短期的观看数据存储到持久层中,对我们来说,也就是 Apache Cassandra 以及用作缓存层的 EVCache,后者能够让我们实现快速查找。

回 压

大多数时候,该系统都能运行得非常好。在极少数情况下,有可能因为网络突发事件,或者某个 Cassandra 节点暂时运行缓慢,导致正在处理的某个请求被延迟。当这种情况发生时,鉴于此处是同步处理,请求处理线程不得不等待。这反过来又会使上游的 Playback API 服务变慢,进而使得 Gateway 网关服务变慢。

除了云中的重试策略之外,这种速度变慢还会响应到运行在用户设备上的 Netflix 客户端。有时候,这种现象被称为回压。回压可以表现为系统中的不可用,并且会建立一个客户端必须要进行重试的队列。在这里面,有些数据对我们的工作至关重要,所以我们希望避免数据丢失,例如,如果客户端填充其本地队列时,该队列的容量是有限的,那么就可能会造成数据丢失。

我们对该问题的解决方案是在系统中引入异步处理。在 Playback API 服务和 Request Processor 之间,我们引入了一个可持久化的队列。现在,当请求传入的时候,它会被放到一个持久化队列中,并且立即确认。这样,就没有必要等待该请求处理完毕了。

事实证明,Apache Kafka 非常适合这样的场景。Kafka 提供了一个日志抽象,像 Playback API 这样的生产者可以追加日志,然后多个消费者都可以基于偏移量按照自己的节奏从 Kafka 日志中读取数据。

这听上去很简单。但是,如果只是在两个处理层之间引入 Apache Kafka,我们就能宣告大功告成吗?并非如此。Netflix 的运营规模达到了每秒钟 100 万个事件。在这种规模下,我们遇到了异步处理的一些挑战,包括数据丢失、处理延迟、乱序和重复记录,以及偶然性的处理失败。另外,还有涉及 Kafka 消费者平台的选择以及跨区域方面的设计决策。

挑战:数据丢失

关于数据丢失,有两个潜在的诱因。首先,如果 Kafka 集群本身不可用,毫无疑问,我们将会丢失数据。解决这个问题的简单方法就是添加一个额外的备用集群。如果主集群由于难以预见的原因而导致不可用的话,发布者(本例中,也就是 Playback API)可以将请求发布至备用集群。消费者的请求处理器可以连接至两个 Kafka 集群,因此不会丢失任何数据。

显然,这里付出的代价就是额外的成本。对于特定类型的数据来讲,这种行为是可行的。但是,是否所有的数据都需要这样做呢?幸运的是,答案是否定的。我们有两种类型的放映数据。关键数据将会按照这种方式来处理,这证明备用集群的额外成本是值得的。其他不太重要的数据则会只使用一个普通的 Kafka 集群。由于 Kafka 本身采用了很多的策略来提升高可用性,所以这足以满足我们的要求。

数据丢失的另外一个诱因是发布时间。Kafka 具有多个分区来增加可扩展性。每个分区都是由一组叫做 broker 的服务器来提供服务。其中,有一台服务器会被选为首领(leader)。当发布消息到某个分区时,会将数据发送至首领 broker。我们可以仅等待首领确认条目已经持久化到它的存储中,也可以等待跟随者(follower)broker 都确认它们也已写入到了持久化存储中。如果我们处理的是关键数据的话,等待分区所有 broker 的确认是合理的做法。在大规模的场景中,这不仅仅会涉及到多次写入的成本问题。

如果丢失了对首领 broker 的连接,会发生什么呢?在部署新架构仅仅几个月后,我们就遇到了这样的情况。如果在等待某个 broker 的确认信息时,该 broker 突然不可用,这显然会降低处理速度。这种速度减慢会导致回压和不可用,这正是我们所要避免的。

如果我们只是从首领 broker 中获取确认信息,将会遇到一个非常有意思的故障场景。如果我们在成功发布后又丢失了对首领 broker 的连接,会发生什么呢?首领选举会产生一个不同的首领。但是,如果原来首领已确认的条目并没有完全复制到其他 broker 上,那么这样的首领选举就会造成数据丢失,而这正是我们想要避免的。这叫做不完整的(unclean)broker 首领选举。

我们是如何处理这种情况的呢?同样,这里需要进行权衡。我们有一个生产者库,它是基于 Kafka 生产者客户端的一个包装器。这里有两个相关的优化。首先,因为我们使用了 non-keyed 分区,所以库能够选择它写入的分区。如果某个分区因为首领 broker 不可用而无法使用它的话,我们的库会写入到不同的分区中。同时,因为分区是在一个 under-replicated 的 broker 集合上,也就是首领 broker 具有的条目比跟随者 broker 更多,复制还没有全部完成,那么我们的库会选择一个复制情况更好的分区。

通过这些策略,我们最终选择了以异步模式写入,即发布者将消息写入内存队列,并异步发送至 Kafka。这有助于扩展性能,但是我们希望在同时遇到多个错误的时候,能够有一个最坏情况的数据损失上限。基于内存队列的大小和避免 under-replicated 分区策略,我们配置了令自己满意的上限。

我们监控了数据的持久性,从中可以持续得到 4 到 5 个 9 的可靠性,对于我们来讲,这是可以接受的。如果你的应用不允许丢失任何数据,那么可以选择在接受到所有 broker 的确认之后,再将该条目视为已处理。

挑战:处理延迟和自动扩展

在我们的系统中引入 Kafka 会有一个无法避免的副作用,那就是请求处理会有额外的延迟,这包括 Playback API 发布消息到 Kafka 以及 Request Processor 消费消息所需的时间。

还有就是数据在 Kafka 队列中等待的时间。这被称为滞后(lag),它是由消费者工作者节点和流量组成的一个函数。对于给定数量的节点,随着流量的增加,滞后也会随之增加。

如果你对峰值流量有一个明确的预估,那么就可以计算出系统中所需的处理节点的数量,以达到可接受的滞后。这样,我们就可以简单配置系统来管理峰值流量,只需“简单地设置即可,无需过多关注”。

对于我们来讲,流量在每天的不同时段和每周的不同日子都会有所变化。我们发现峰值的流量是低谷期的 5 倍。鉴于如此巨大的流量变化,我们希望能够更有效地利用资源,所以选择了自动伸缩。具体来讲,我们会基于流量添加或移除特定数量的消费者处理节点。

每当我们改变 Kafka 主题的消费者数量时,该主题的所有分区会在新的消费者集合之间重新平衡。这里的权衡是资源使用效率与重新平衡的成本。重新平衡会以不同的方式影响到我们。

如果你的处理是有状态的,那么必须要做一些复杂的事情。比如,消费者必须要暂停处理,然后获取内存状态,并根据 Kafka 中的偏移量进行已处理到何处的检查点判断。在分区重平衡之后,消费者会重新加载检查点之后的数据,然后基于检查点的偏移重新开始处理。

如果你的处理稍微简单一些,或者以外部方式存储状态,那么你可能会在重平衡进行的时候,继续正常进行处理。这样的话,当重平衡开始时,某些条目正在处理中,它们还没有发送确认消息到 Kafka,这些条目将会出现在另外一个处理节点中,因为该节点在重平衡后对应到了这个分区。在最糟糕的场景中,我们会重复处理某些条目。如果你的处理是幂等的或者有其他方式处理重复的项目,那么这并不是什么问题。

下一个问题就是,何时以及按照多大的幅度来进行自动扩展?有人可能认为滞后是触发自动扩展的一个好指标。但问题在于,我们无法很容易地通过这个指标进行收缩。当滞后是零的时候,我们如何得知该收缩 1 个、10 个还是 50 个处理节点呢?一次性移除太多的节点会导致“摇摆(flapping)”,也就是在短时间内一次又一次地移除和重复添加节点。

在实践中,很多开发人员会使用某个代理指标,比如 CPU 利用率。对于我们来讲,事实证明,每秒记录数(records-per-second,RPS)是一个很好的触发器,能够用来实现自动扩展。当系统处于稳定状态时,我们会测量 RPS 并建立一个基线。然后,当吞吐量相对于该基线发生变化时,我们就可以添加或移除节点了。

我们也有不同的模式实现扩展和收缩。我们希望避免在扩展过程中进行重平衡,因为此时已经有大量输入的数据,重平衡会暂时减缓消费者的速度,所以我们希望快速扩展。而收缩可以逐渐完成,因为此时支持的吞吐量要高于实际的值,我们可以接受再平衡带来的减速。

挑战:乱序和重复记录

在分布式系统中,会出现乱序和重复记录的情况。如何解决这个问题取决于应用的具体情况。在我们的场景中,我们使用了会话机制(sessionization),它会收集一个视频放映会话内的事件,该会话有一个明确的开始和结束事件。因此,我们会收集这个边界内该会话的所有事件。

对于给定会话内的多个事件,基于数据内的特定属性,我们会对其进行排序并去重。例如,每个事件会有一个递增的 ID 或来自客户端的时间戳。对于写入操作来说,当事件抵达服务器时,我们能够通过时间戳进行去重。

挑战:偶然性的处理失败

在消费者端,我们还要解决偶尔处理失败的问题。通常情况下,我们不想因为一个失败的条目而中断对整个队列的处理,有时候这种做法叫做队头(head-of-line)阻塞。相反,我们想要把失败的条目放到一边,继续处理队列中的其他内容,稍后再来单独处理它。

我们希望系统具备这样的特点,那就是再次尝试之前应该有一个有限的等待时间,没有必要立即进行尝试。我们可以为这些失败的条目使用一个单独的队列,也叫做延迟队列(delay queue)。有多种方式可以实现这一点。我们可以将其写入到另外一个 Kafka 队列中,然后构建一个延迟执行的处理器。

对于我们来讲,使用 Amazon Simple Queue Service(SQS)很容易实现这一点,因为我们已经在 Amazon Elastic Compute Cloud(EC2)上运行了。我们会将失败的条目提交到 SQS 队列中,该队列有一项特殊的功能,里面的条目在消费之前,可以指定一个间隔时间。

消费者平台

我们可以使用多种平台来消费和处理来自 Kafka 的条目。在 Netflix,我们使用了三种不同的平台。Apache Flink 是一个流行的流处理系统。Mantis 是 Netflix 几年前开源的一个流处理系统。最后,Kafka 有一个嵌入式的消费者客户端,借助它我们可以编写微服务,直接处理 Kafka 中的条目。

我们首先面临的问题是,哪种平台最好用?最终,我们意识到这个问题本身就是错误的。相反,问题应该是,哪种处理平台在哪种使用场景下最为有利?这三种方式各有优缺点,在不同的场景下,这三种平台我们都用到了。

如果要进行复杂的流处理,那么 Mantis 和 Apache Flink 非常合适。Apache Flink 还内置了对有状态流处理的支持,其中每个节点都可以存储本地状态,例如可以用于会话机制。微服务是很有吸引力的方案,至少对我们来讲是这样的,因为 Netflix 工程师对微服务生态有良好的支持,涵盖生成或初始化整洁的代码库,到 CI/CD 流水线和监控。

跨区域的问题

跨区域(cross-region)问题是很重要的,因为 Netflix 要在多个区域运营。由于我们正在运维的是一个大型的分布式系统,某个区域可能偶尔会出现不可用的情况。我们每年都会进行多次演练,关闭某个区域,确保能够实现跨区域的流量转发。

乍看上去,这没有什么问题,如果某个条目本来要发往另外一个区域,我们可以借助跨区域的隧道,将其远程发布到 Kafka 主题中。通常情况下,这是可行的,但是当我们遇到该区域真正无法使用时,远程发布方式就无法奏效了。

我们做了一个简单但微妙的修改,那就是始终希望在本地发布。我们发布到另外一个 Kafka 主题,并使用区域路由器异步将其发送至另外一个区域中。通过这种方式,某个放映会话的所有事件就可以一起处理了。

测试、验证和上线

现在我们已经解决了挑战,并做了权衡,那么该如何测试和上线的呢?在这个过程中,可以使用影子测试(Shadow testing)技术。在你的环境中,可能已经使用过类似的策略了。对我们来讲,这包括让 Playback API 双重写入到现有的同步系统和 Apache Kafka 中,异步请求处理器会基于 Apache Kafka 进行消费。然后,我们会有一个验证程序,它能校验正在处理中的请求是一致的。

下一步就是确保存储的制品是相同的。为此,我们创建了一个影子 Cassandra 集群。在这里,我们使用成本来换取足够的信心。如果你能有一个环境,可以相对容易地在短时间内获得额外的资源,那么在上线之前,它可以给你带来额外的信心,对于我们这样的云环境来讲,这一点是很容易实现的。

最终,我们上线了这个方案,按 userId 分割流量,也就是说,给定 userId 的所有流量被持续写入新系统或旧系统。我们从 1% 的用户数据写入新系统开始,然后逐步增加比例,一直到 100% 的用户。这使我们的迁移非常顺利,没有对上游或下游系统造成影响。

下图显示了我们现在的情况和下一步的发展方向。其中,蓝色的内容,即 Playback API、Viewing History Processor 和 Bookmark Processor 以及 Kafka,现在已经投入生产。我们还有处理额外数据的其他组成部分,包括 Attributes processor 和 Session Logs 服务,它们将会很有趣,因为数据量非常大,比你通常写进 Kafka 的数据要大得多。我们将有一些新的挑战需要解决。

结 论

我们已经看到,异步处理是如何为我们提高可用性和数据质量的,以及在我们的环境中,是如何进行设计选择和权衡的。在实施之后,影子测试和增量上线确保我们能够进行自信和顺利地部署。有了这些信息,请思考如何将这些经验应用到你的环境中,以及在类似的旅程中可能会做出哪些其他的权衡。

作者简介

Sharma Podila 是一位软件工程领导、系统建设者、合作者和导师。他在云资源管理、分布式系统、数据基础设施方面有很深的造诣,并在交付跨功能的大规模分布式系统方面有着丰富的成功经历。

原文链接:

Microservices to Async Processing Migration at Scale(https://www.infoq.com/articles/microservices-async-migration/)

相关阅读:

Netflix 基于 Redis、Kafka 和 Elasticsearch 构建高吞吐优先队列 Timesone(https://www.infoq.cn/article/JC8GL4g4OHL1WFISs4g1)

破天荒!Netflix 为软件工程师引入细分职级,已有不少老员工因对新职级不满离职(https://www.infoq.cn/article/MXfmbyJkSmDkIHNRJb5Q)

Netflix 工作 10 年,我收获的一些关键经验(https://www.infoq.cn/article/wMuuGUleIPIIDhWGibu5)

声明:本文为InfoQ翻译,未经许可禁止转载。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-11-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 InfoQ 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
API 网关
腾讯云 API 网关(API Gateway)是腾讯云推出的一种 API 托管服务,能提供 API 的完整生命周期管理,包括创建、维护、发布、运行、下线等。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档