Meta Description: 本文介绍某海外科技公司如何重构异步计算系统 Async,通过延迟容忍度、多队列调度、时间转移、批处理、配额和速率限制等机制,在大规模场景下提升请求处理效率、容量利用率和开发者生产力。

每天,数十亿用户都会使用我们的应用和服务,完成各种各样的操作:分享照片,关注新闻和体育动态,记录人生中的重要时刻,例如孩子出生;也会在危机事件中通过安全确认功能向亲友报平安。如今,人们比以往任何时候都更加依赖线上服务保持联系,弥合因全球性社交隔离带来的距离感。
全球数十亿用户在大型社交应用、图片社交应用和即时通信服务中产生的这些行为,都会形成用户交互,并需要由系统进行处理。这些交互会带来数百万乃至数十亿个并发请求。其中很多请求可以在后台处理,不需要用户等待结果。我们将这类请求称为异步请求。
在大规模互联网系统中,异步计算的核心挑战并不只是“把任务放到后台执行”,而是如何在海量请求中进行优先级排序、任务调度、容量优化、资源隔离和稳定性保障。Async 正是为了解决这些问题而构建的异步请求调度系统。
这些异步请求中,有些可以在稍后的某个时间执行,例如新消息通知、好友动态通知,或者直播开始通知。不同请求的时效性也不相同:有些请求必须尽快完成,有些则可以容忍一定延迟。
为了管理不同类型异步请求的紧急程度,我们构建了一个名为 Async 的系统。最初的 Async 系统非常简单,但随着应用和服务持续发展,我们需要扩展它,以处理更大的请求量。在一次技术大会上,我们分享了如何重新设计 Async 系统,使其能够高效处理数十亿次请求。
早期版本:简单队列如何处理异步请求
在 Async 的最初版本中,所有异步请求都会被处理并存储到一个集中式数据库中。调度器随后查询这些数据库中的请求,并对其进行排序。之后,当调度器从数据库中取出任务并发送给 worker 服务器时,排序逻辑会根据请求的预期执行时间,提供一些基础的优先级能力。

当时,我们设置了三个优先级。每个优先级队列内部都采用先到先得原则。那时系统处理的流量远低于现在,因此一切运行良好。当出现新的使用场景时,我们只需要增加更多 worker 即可。

遗憾的是,这种方案无法真正扩展。
简单的优先级排序,终究还是过于简单。采用先到先得策略时,大型用例会占据大量服务资源,而小型用例只能排队等待执行机会。如果某个用例因为错误或配置失误而停滞,它后面的用例也可能被无限期阻塞,无法离开队列。
增加 worker 节点并不能解决根本问题,因为这并不是一种可扩展方案。在高峰时段,每个作业都必须争夺资源,导致机器负载过高;而在非高峰时段,许多机器又会闲置。服务器利用率不足,一些用例也无法在预期时间内运行。这个系统原本是为小规模场景设计的,已经无法满足快速增长的需求。
走向规模化:异步系统面临的新挑战
随着使用场景不断扩展,工程师数量也随之增加,早期系统逐渐无法满足需求。系统开始不堪重负,工程师们也开始自行构建解决方案。
这不仅造成重复劳动,还增加了代码量和潜在问题,最终导致告警数量激增。为了确保网站和应用能够快速、稳定地运行,即使我们持续增加新的使用场景并面对新的流量高峰,也必须重新思考异步处理策略。
管理如此大规模的请求会带来许多挑战。其中最重要的问题之一,是如何设计系统架构来应对海量流量。我们需要解决的核心挑战包括以下几类。
优先级:如何判断哪个异步请求更重要
首先要回答的问题是:应该优先处理哪个请求?
是自然灾害期间通过安全确认功能发出的求助通知,还是某人点赞了一张宠物照片后产生的通知?系统如何判断哪个操作更重要?如果是直播开始通知,或者账户被未知设备登录的安全通知,又该如何排序?
为了帮助系统理解优先级,我们引入了“延迟容忍度”的概念,也就是每个请求最多可以延迟多长时间。
容量优化:如何提升机器利用率
服务需要能够处理高流量,同时保持较高的容量利用率。例如,日常流量存在高峰期和低谷期。如果按照高峰期分配足够多的机器,那么在低谷期,许多机器就会处于闲置状态。
对于危机响应这类场景,流量很难提前预测,也很难预先分配容量。为了管理不同流量级别,我们将流量大致分为三类:日常流量、重大事件和突发事件响应。随后,我们使用队列、时间转移和批处理等技术,更好地优化机器容量。
日常流量
日常流量的一个例子,是用户每天浏览信息流、评论并与帖子互动。另一个例子,是某些机构每周在大致固定时间进行直播。无论是每日流量还是每周流量,这类流量都相对可预测。
重大事件
例如新年夜、世界杯,以及公众人物直播等活动,通常意味着更多用户会在应用上观看和互动。这类流量具有一定可预测性,因为我们提前知道这些事件,并能提前预估和应对流量高峰。
突发事件响应
这类事件与重大事件类似,但无法提前规划。突发事件响应期间的流量通常持续时间较短,会在短时间内急剧上升,并在几分钟或几小时内恢复到此前水平。
容量调节:如何避免单个用例占用过多资源
如何确保每个用例都能获得公平的资源分配?如果某个用例在新功能上线后流量暴增 10 倍怎么办?如果某个作业因为 bug 突然消耗了大量 CPU 和内存,又该怎么办?
为了控制每个用例的资源消耗,我们引入了相应策略,防止任何单个作业占用过多资源。
今天的 Async:多租户异步计算系统
系统的可靠性取决于它所有依赖项中最不可靠的那一个。多个产品和内部系统都依赖 Async。重新思考 Async 时,我们决定采用一种通用方案:多租户架构,以尽可能让服务器保持繁忙状态。
如今,Async 的多租户系统由一个共享服务器池构成,可供具有不同流量模式的多种用例共同使用。这有利于共享工作负载,但也要求我们找到方法,对每天数千亿次请求进行优先级排序,并优化和调节容量。
此外,随着新产品不断推出,我们会持续引入新的用例和极端情况。管理具有不同行为和不同预期的流量,只会进一步增加复杂性,并直接影响系统性能和可靠性。
构建这些解决方案,需要在多个因素之间做权衡,以公平方式满足所有用例。为此,我们构建了以下几类能力。
延迟容忍度:更细粒度的任务优先级
当我们开始重新思考异步处理时,最大的痛点在于:即便任务处于同一优先级,它们的重要程度也并不相同。引入的用例越多,我们就越需要根据业务需求进行更细粒度的优先级排序。
有些任务可以容忍几分钟延迟,而有些任务必须尽快执行。例如,某人点赞了你昨天发布的照片,这类通知可以延迟几秒钟。但对于限时事件通知,例如你关注的人正在直播,通知就必须立即发送,否则用户可能在直播结束后才收到。
我们可以通过了解每个异步请求的延迟容忍度,推断出更细致的优先级。为此,我们引入了延迟容忍度的概念:对于每个等待执行的任务,我们会尽量让它在接近其延迟容忍度的时间点被执行。
优化容量:队列、时间转移与批处理
容量方面主要有两个痛点。
第一,当服务过载时,系统可能无法及时处理紧急任务。第二,在流量低谷时,大量机器会处于闲置状态。
为了改善这些问题,我们新增了三项能力:队列、时间转移和批处理。
队列:从单队列到多队列调度
引入延迟容忍度概念后,我们就获得了优化系统的空间。当服务过载时,我们会延迟执行容忍度较高的任务,从而将工作负载分散到更长的时间窗口中。
队列在选择最紧急任务并优先执行方面发挥着重要作用。
早期的异步服务使用 MySQL 中的一张表表示队列。调度器从这张表中拉取任务,根据当前工作负载动态选择一定数量的任务,并将剩余任务推回 MySQL。我们将这种来回操作称为“推回”。
在高峰时段,调度器会把更多时间花在推回任务上,而不是将任务发送给 worker。为了解决这个问题,我们引入了多队列系统:
- 每个用例都有自己的队列,并按照作业执行截止时间排序;
- 调度器会比较每个队列的队首任务,并根据优先级和截止时间选择最紧急的任务;
- 当某个用例队列的队首任务被选中时,后台线程会从存储中为该队列补充更多作业;没有作业被消费的用例不会产生额外活动。
时间转移:平滑高峰期计算压力
时间转移是优化异步计算容量的一个有趣场景。时间转移主要包括两类计算。
预测计算 会收集用户过去使用的数据。根据用户可能需要的数据,系统会在高峰期到来前进行预计算,并将结果存入缓存。当高峰期用户请求到来时,系统直接读取缓存数据。这样,计算任务就从高峰期转移到了非高峰期,并以少量缓存未命中为代价,换取更高效率。
延迟计算 则相反。它会在用户请求处理过程中安排一个任务,但该任务会在稍后的时间运行。例如,某些推荐列表会在非高峰时段处理,然后在用户在线时,也就是通常的高峰时段展示。
这两种时间转移方式都能减少高峰时段的按需计算容量,并在非高峰时段充分利用计算资源。

批处理:降低调度器与队列系统负载
一个任务的生命周期包括:被提交到队列、被调度器领取、发送给 worker,以及从队列中删除。无论任务运行时间是几百毫秒还是几分钟,它都会占用队列中的一个槽位,并且需要在调度器和 worker 之间完成一次往返。除了任务真正执行的部分之外,异步服务的其他负载基本相同。
对于可能导致某些用例流量激增 10 倍的重大事件,即使我们有足够的 worker,仍然会对异步服务中的其他组件造成巨大压力。
批处理是一种降低负载的方法。在服务端,它会将多个任务累积成一个大型任务,并将其存储在队列中。这样,调度器和 worker 之间仍然只需要一次往返。虽然 worker 仍需在同一个请求中运行多个任务,但其他异步组件的负载会显著降低。
我们还发现,在某些情况下,由于缓存复用率更高、代码已经预热,worker 端的执行效率反而更高。
容量策略:配额、速率限制与资源隔离
随着异步请求数量持续增长,错误和配置失误开始影响 Async 的可靠性。为了防止这些问题影响更多用例,我们引入了配额和速率限制机制。
由于 Async 是一个多租户系统,它需要跟踪每个请求使用的资源,以便准确执行资源限制。为此,系统引入了基于 CPU 指令使用量和内存使用量的配额机制。系统会根据每个用例的计算需求分配配额。如果需要更多资源,工程师可以申请向服务器池中增加更多服务器。
当某个用例的资源使用量超过设定配额时,它的执行速度将受到限制,系统也会向相关工程师发送告警。这种配额机制可以防止某个用例干扰 Async 的正常运行。
然而,异步处理并不仅仅包括任务执行。配置错误也可能导致提交器,也就是接收异步请求的组件过载,甚至偶尔停止服务。因此,我们引入了请求速率限制,防止意外流量高峰干扰系统的正常接收能力。
实施过程中的一大挑战,是确保事件响应用例的流量不会被错误限制。这类用例流量波动较大,而且很难预测。为此,我们增加了配额灵活性,允许用例在短时间内超过配额限制。
最新 Async 架构:任务提交器、队列服务与调度器
在最新方案中,客户端会将任务提交给任务提交器。任务提交器负责批处理,并将任务存储在队列服务中。另一方面,调度器从队列中取出任务,比较各个用例中的任务,并将最重要的任务发送给 worker。

新方案带来的挑战:复杂性、可观测性与资源归因
系统演进弥补了最初架构中的缺陷,但也带来了新的挑战。如今,系统更加复杂,处理的数据流量显著增加,边界情况也更多。这使得系统更难理解,新成员需要更长时间才能掌握。
更高的复杂性意味着学习曲线更加陡峭,也会直接影响团队成员解决问题的效率。更完善的故障排查工具,是系统继续发展的必要条件。作为一种人机交互工具,它必须直观、易懂,才能确保用户在将新用例接入系统时不会遇到阻碍。
在这些新挑战中,一项重要优先级是为系统用户和 Async 维护者提供更完善的资源归因和管理流程。更好的资源归因也有助于预测未来容量需求,并提高不同用例之间资源分配的公平性,使我们能够更灵活地应对流量激增或峰值流量。
对于这类复杂基础设施的长期演进而言,技术架构之外的研发协同同样重要。团队需要把目标拆解、需求评审、开发排期、测试验证、发布上线、故障复盘和知识沉淀连接起来,才能降低复杂系统带来的维护成本。企业可以借助 PingCode 这类智能化研发管理工具管理研发全生命周期,并通过 Wiki 沉淀系统设计和故障经验;如果团队更关注跨部门任务协作、文档、日历、甘特图、工时和审批等通用协同场景,也可以结合 Worktile 提升推进效率。
异步管理带来的挑战,也为大规模系统创新提供了令人兴奋的机会。无论你已经具备这种规模的运营能力,还是刚刚开始了解如何构建大规模系统,我们都希望这些经验能够帮助你构建更可靠的服务。
文章包含AI辅助创作,作者:guo,如若转载,请注明出处:https://docs.pingcode.com/baike/5245505