异步任务处理系统,如何解决业务长耗时、高并发难题?
发送电子邮件/即时消息 检查垃圾邮件 文档处理(转换格式,导出,……) 音视频,图片处理(生成缩略图,加水印,鉴黄,转码,……) 调用外部的三方服务 重建搜索索引 导入/导出大量数据 网页爬虫 数据清洗 ……
更快的系统响应时间。将长耗时的,重资源消耗的逻辑从请求处理流程中剥离,在别的地方异步执行,能有效的降低请求响应延时,带来更好的用户体验。
更好的处理大量突发性请求。在电商等很多场景下,常常有大量突发性请求对系统造成冲击。同样的,如果将重资源消耗逻辑从请求处理流程中剥离,在别的地方异步执行,那么相同资源容量的系统能响应更大峰值的请求流量。
更低的成本。异步任务的执行时长通常在数百毫秒到数小时之间,根据不同的任务类型,合理的选择任务执行时间和更弹性的使用资源,就能实现更低的成本。
更完善的重试策略和错误处理能力。任务保证被可靠的执行(at-least-once),并且按照配置的重试策略进行重试,从而实现更好的容错能力。例如调用第三方的下游服务,如果能变成异步任务,设置合理的重试策略,即使下游服务偶尔不稳定,也不影响任务的成功率。
更快的完成任务处理。多个任务的执行是高度并行化的。通过伸缩异步任务处理系统的资源,海量的任务能够在合理的成本内更快的完成。
更好的任务优先级管理和流控。任务根据类型,通常按照不同的优先级处理。异步任务管理系统能帮助用户更好的隔离不同优先级的任务,既让高优先级任务能更快的被处理,又让低优先级任务不至于被饿死。
更多样化的任务触发方式。任务的触发方式是多种多样的,例如通过 API 直接提交任务,或是通过事件触发,或是定时执行等等。
更好的可观测性。异步任务处理系统通常会提供任务日志,指标,状态查询,链路追踪等能力,让异步任务更好的被观测、更容易诊断问题。
更高的研发效率。用户专注于任务处理逻辑的实现,任务调度,资源扩缩容,高可用,流控,任务优先级等功能都由任务处理系统完成,研发效率大幅提高。
一 任务处理系统架构
1 任务 API/Dashboard
日志:能够收集和展示任务日志,用户能够快速查询指定任务的日志。
指标:系统需要提供排队任务数等关键指标,帮助用户快速判断任务的执行情况。
链路追踪:任务从提交到执行过程中,各个环节的耗时。比如在队列中排队的时间,实际执行的时间等等。下图展示了 Netflix Cosmos 平台的 tracing 能力。
2 任务分发
任务的可靠分发:任务一旦提交成功后,无论遇到任何情况,系统都应当保证该任务被调度执行。
任务的定时/延时分发:很多类型的任务,希望在指定的时间执行,例如定时发送邮件/消息,或者定时生成数据报表。另一种情况是任务可以延时较长一段时间执行也没问题,例如下班前提交的数据分析任务在第二天上班前完成即可,这类任务可以放到凌晨资源消耗低峰的时候执行,通过错峰执行降低成本。
任务去重:我们总是不希望任务被重复执行。除了造成资源浪费,任务重复执行可能造成更严重的后果。比如一个计量任务因为重复执行算错了账单。要做到任务只执行一次(exactly-once),需要在任务提交,分发,执行全链路上的每个环节都做到,包括用户在实现任务处理代码时也要在执行成功,执行失败等各种情况下,做到 exactly-once。如何实现完整的 exactly-once 比较复杂,超出了本文的讨论范围。很多时候,系统提供一个简化的语义也很有价值,即任务只成功执行一次。任务去重需要用户在提交任务时指定任务 ID,系统通过 ID来判断该任务是否已经被提交和成功执行过。
任务错误重试:合理的任务重试策略对高效、可靠的完成任务非常关键。任务的重试要考虑几个因素:1)要匹配下游任务执行系统的处理能力。比如收到下游任务执行系统的流控错误,或者感知到任务执行成为瓶颈,需要指数退避重试。不能因为重试反而加大了下游系统的压力,压垮下游;2)重试的策略要简单清晰,易于用户理解和配置。首先要对错误进行分类,区分不可重试错误,可重试错误,流控错误。不可重试错误是指确定性失败的错误,重试没有意义,比如参数错误,权限问题等等。可重试错误是指导致任务失败的因素具有偶然性,通过重试任务最终会成功,比如网络超时等系统内部错误。流控错误是一种比较特殊的可重试错误,通常意味着下游已经满负荷,重试需要采用退避模式,控制发送给下游的请求量。
任务的负载均衡:任务的执行时间变化很大,短的几百毫秒,长的数十小时。简单的 round-robin 方式分发任务,会导致执行节点负载不均。实践中常见的模式是将任务放置到队列中,执行节点根据自身任务执行情况主动拉取任务。使用队列保存任务,让根据节点的负载把任务分发到合适的节点上,让节点的负载均衡。任务负载均衡通常需要分发系统和执行子系统配合实现。
任务按优先级分发:任务处理系统通常对接很多的业务场景,他们的任务类型和优先级各不相同。位于业务核心体验相关的任务执行优先级要高于边缘任务。即使同样是消息通知,淘宝上买家收到一个商品评论通知的重要性肯定低于新冠疫情中的核酸检测通知。但另一方面,系统也要保持一定程度的公平,不要让高优先级任务总是抢占资源,而饿死低优先级任务。
任务流控:任务流控典型的使用场景是削峰填谷,比如用户一次性提交数十万的任务,期望在几个小时内慢慢处理。因此系统需要限制任务的分发速率,匹配下游任务执行的能力。任务流控也是保证系统可靠性的重要手段,某类任务提交量突然爆发式增长,系统要通过流控限制其对系统的冲击,减小对其他任务的影响。
批量暂停和删除任务:在实际生产环境,提供任务批量暂停和删除非常重要。用户总是会出现各种状况,比如任务的执行出现了某些问题,最好能暂停后续任务的执行,人工检查没有问题后,再恢复执行;或者临时暂停低优先级任务,释放计算资源用于执行更高优先级的任务。另一种情况是提交的任务有问题,执行没有意义。因此系统要能让用户非常方便的删除正在执行和排队中的任务。任务的暂停和删除需要分发和执行子系统配合实现。
资源自动伸缩和负载均衡复杂。任务执行实例和任务队列建立连接,拉取任务。当任务执行实例规模较大时,对任务队列的连接资源会造成很大的压力。因此需要一层映射和分配,任务实例只和对应的任务队列连接。下图是 Slack 公司的异步任务处理系统架构。Worker 节点只和部分 Redis 实例相连。这解决了 worker 节点大规模扩展的能力,但是增加了调度和负载均衡的复杂度。
从支持任务优先级,隔离和流控等需求的角度考虑,最好能使用不同的队列。但队列过多,又增加了管理和连接资源消耗,如何平衡很有挑战。
任务去重,任务批量暂停或者删除等功能依赖消息队列功能,但很少有消息类产品能满足所有需求,常常需要自行开发。例如从可扩展性的角度,通常做不到每一类任务都对应单独的任务队列。当任务队列中包含多种类型的任务时,要批量暂停或者删除其中某一类的任务,是比较复杂的。
任务队列的任务类型和任务处理逻辑耦合。如果任务队列中包含多种类型的任务,要求任务处理逻辑也要实现相应的处理逻辑,对用户不友好。在实践中,A 用户的任务处理逻辑不会预期接收到别的用户任务,因此任务队列通常由用户自行管理,进一步增加了用户的负担。
3 任务执行
任务的可靠执行。任务一旦提交成功,无论任何情况,系统应当保证任务被执行。例如执行任务的节点宕机,任务应当调度到其他的节点执行。任务的可靠执行通常是任务分发和任务执行子系统共同配合实现。
共享资源池。不同类型的任务处理资源共享统一的资源池,这样才能削峰填谷,提高资源利用效率,降低成本。例如把计算密集,io密集等不同类型的任务调度到同一台 worker 节点上,就能更充分的利用节点上的CPU,内存,网络等多个维度的资源。共享资源池对容量管理,任务资源配额管理,任务优先级管理,资源隔离提出了更高的要求。
资源弹性伸缩。系统能根据负载的执行情况伸缩执行节点资源,降低成本。伸缩的时机和数量非常关键。常见的根据任务执行节点的 CPU,内存等资源水位情况伸缩,时间较长,不能满足实时性要求高的场景。很多系统也使用排队任务数等指标进行伸缩。另一个值得关注的点是执行节点的扩容需要匹配上下游系统的能力。例如当任务分发子系统使用队列来分发任务时,worker 节点的扩容要匹配队列的连接能力。
任务资源隔离。在 worker 节点上执行多个不同的任务时,资源是相互隔离的。通常使用容器的隔离机制实现。
任务资源配额。用户的使用场景多样,常常包含多种任务类型和优先级。系统要支持用户为不同优先级的任务或者处理函数设置资源配额,为高优先级任务预留资源,或者限制低优先级任务能使用的资源。
简化任务处理逻辑的编码。好的任务处理系统,能够让用户专注于实现单个任务处理逻辑,系统自动并行、弹性、可靠的执行任务。
平滑升级。底层系统的升级不要中断长时任务的执行。
执行结果通知。实时通知任务执行状态和结果。对于执行失败的任务,任务的输入被保存到死信队列中,方便用户随时手动重试。
K8s 的 HPA 一般难以满足任务场景下的自动伸缩。Keda 等开源项目提供了按排队任务数等指标伸缩的模式。AWS 也结合 CloudWatch 提供了类似的解决方案。
K8s 一般需要配合队列来实现异步任务,队列资源的管理需要用户自行负责。
K8s 原生的作业调度和启动时间比较慢,而且提交作业的 tps 一般小于 200,所以不适合高 tps,短延时的任务。
二 大规模多租户异步任务处理系统实践
1 动态队列资源伸缩和流量路由
如果应用的流量持续保持高位,导致队列积压,系统将为他们自动创建单独的队列,并将流量分流到新的队列上。
将一些延时敏感,或者优先级高的应用流量迁移到其他队列上,避免被高流量应用产生的队列积压影响。
允许用户设置任务的过期时间,对于有实时性要求的任务,在发生积压时快速丢弃过期任务,确保新任务能更快的处理。
2 负载随机分片
3 自适应下游处理能力的任务分发
4 向上游的任务生产方发送背压(back pressure)
公平。尽可能流控A而不是B应用。流控本质是一个概率问题,为每一个对象计算流控概率,概率越准确,流控越公平。
及时。背压要传递到系统最外层,例如在任务提交时就对A应用流控,这样对系统的冲击最小。
三 异步任务处理系统的能力分层
Level 1:一般需 1-5 人研发团队,系统是通过整合 K8s 和消息队列等开源软件/云服务的能力搭建的。系统的能力受限于依赖的开源软件/云服务,难以根据业务需求进行定制。资源的使用偏静态,不具备资源伸缩,负载均衡的能力。能够承载的业务规模有限,随着业务规模和复杂度增长,系统开发和维护的代价会迅速增加。
Level 2:一般需 5-10人研发团队,在开源软件/云服务的基础之上,具备一定的自主研发能力,满足常见的业务需求。不具备完整的任务优先级、隔离、流控的能力,通常是为不同的业务方配置不同的队列和计算资源。资源的管理比较粗放,缺少实时资源伸缩和容量管理能力。系统缺乏可扩展性,资源精细化管理能力,难以支撑大规模复杂业务场景。
Level 3:一般需 10+ 人研发团队,能够打造平台级的系统。具备支撑大规模,复杂业务场景的能力。采用共享资源池,在任务调度,隔离流控,负载均衡,资源伸缩等方面能力完备。平台和用户界限清晰,业务方只需要专注于任务处理逻辑的开发。具备完整的可观测能力。
四 结论
附录
函数计算异步任务和 K8S Jobs 的能力对比。
网易云音乐 Serverless Jobs 实践,音频处理算法业务落地速度10x提升 其他异步任务案例
[1] slack engineering:https://slack.engineering/scaling-slacks-job-queue/
[2] Facebook:https://engineering.fb.com/2020/08/17/production-engineering/async/
[3] Dropbox 统计:https://dropbox.tech/infrastructure/asynchronous-task-scheduling-at-dropbox
[4] Netflix Cosmos 平台:https://netflixtechblog.com/the-netflix-cosmos-platform-35c14d9351ad
[5] keda:https://keda.sh/
[6] Autoscaling Asynchronous Job Queues :https://d1.awsstatic.com/architecture-diagrams/ArchitectureDiagrams/autoscaling-asynchronous-job-queues.pdf
[7] 异步任务:https://help.aliyun.com/document_detail/372531.html
[8] Sample and Hold 算法:https://dl.acm.org/doi/10.1145/633025.633056
[9] 网易云音乐音视频算法的 Serverless 探索之路: https://developer.aliyun.com/article/801501
[10] 其它异步任务案例:https://developer.aliyun.com/article/815182
快速体验,使用函数计算快速搭建掌上游戏机
点击阅读原文查看详情
微信扫码关注该文公众号作者