写点什么

大规模任务调度在 AfterShip 的高可用实践

作者:AfterShip
  • 2021 年 11 月 19 日
  • 本文字数:2696 字

    阅读完需:约 9 分钟

文/Hulk Lin


本文将为大家介绍:

  1. AfterShip 是如何将系统成本下降为原成本 20%

  2. 系统是如何以轻松满足未来 10x 倍的容量增长

  3. 系统的可见性和稳定性是如何做到分钟级别的告警和问题定位等。


AfterShip 产品矩阵,已从单一产品 AfterShip (Tracking)拓展到整个电商 SaaS 产品闭环,致力于连接全球电商基础设施。目前已与 20 +电商平台、50+ 应用程序、900 +家物流商达成合作。



全球化电商基础设施带来了高延时和大量任务调度问题。另外,针对不同的服务 SLA 以及性能优先级,也需要有不同的调度策略。


以订单更新为例,需要定时轮询各个平台,而不同的平台优先级不一样;”包裹跟踪”则需要定时轮询合作的快递物流商,但同时要保证用户查询任务的优先级更高。 


为应对众多挑战,建立一个能够运行“千万级别”优先级任务的调度系统至关重要。


1. 任务队列选型

在老版本的架构设计中,存在系统脆弱、问题定位困难、系统可见性差,且缺少关键指标等问题。



老版本架构设计


这个设计看起来很复杂,但实际上左边框这部分其实就是实现一个优先级任务的功能。


1. 请求从进来,然后经过 Google 的 PubSub;( Google PubSub 可以理解跟 Kafka 一样,都是先进先出的队列)

2. 再流转到 Redis 实现优先级的功能;(由 Valve 这个组件实现)

3. 右边两个组件实现延时功能, Deadletter 用来存储重试到达上限任务, Kicker 组件负责将 Deadletter 里面的任务重新拿出来消费。

4. Sheduler 组件基于 Google PubSub 实现了延时的功能,基于先进先出队列去实现会有很多限制,比如时间粒度上没办法做得很细。


以整个系统里面的几个组件实际就是实现了一个延时队列的功能。


我们按照时间维度来划分 topic, 类似于图中每 15 分钟一个 topic,每天 96 个。任务进来之后我们会根据时间偏移和延时时间放到对应的 topic 里面。比如 0 点的时候一个任务需要延时 15 十分钟,那么就放到编号 15 的 topic。再由 Schduler 组件每 15 分钟去拉取对应 topic 里面的任务。相当于实现一个 15 分钟粒度的延时队列。


当然,我们也可以把粒度划分得更细一些,比如秒级别,但这又会引入 Topic 管理之类的问题。



因为调度间隔是 15 分钟,所以可以看到每 15 分钟就会有一个比较大的波峰,带来的问题就是处理这些峰值需要更多的资源。同时, 由于间隔不够长,这些资源很难在低峰期重新被调度利用,从而造成资源浪费。


整个系统的核心问题在于:

1. 错误抽象设计,将一个基础服务功能(延时队列)拆分为很多个服务来实现,同时夹杂着业务逻辑而导致实现复杂和边界模糊!所以在做设计的时候,抽象和边界是及其重要的。

2. 基于先进先出队列服务之上去实现了一个不是特别优雅的延时队列功能,这种设计在延时粒度上会有比较大的限制,同时管理上也极其麻烦。


如何解决这些问题?

引入任务队列来解决延时和优先级的功能,同时解耦业务逻辑。

在队列的选型上,秒级延时调度任务优先级支持队列 Rate Limit容量可扩展数据可靠均为必需的关键特性,同时也需要考虑功能实现、可用性和数据可靠性、服务性能等问题。



在综合了 AfterShip 的核心需求和各任务队列的表现后,我们最终确认选型 LMSTFY。

在功能和性能满足的前提下,可选择系统设计简单、可靠且符合团队技术栈的选型。


队列选型总结


2. 任务队列的设计与实现



任务队列实现的本质就是管理任务状态的流转。


目前 LMSTFY 主要有三种状态,第一种是 Ready 状态,表示任务是可消费。任务被消费后但未 ACK 之前会迁移到 Delay 状态,假设在 TTR 时间内没有 ACK,那么会重新变成 Ready 状态。如果任务到达最大重试,那么会进入 DeadLetter 里面,需要手动去 Respawn 才能被重新消费。


LMSTFY 内部的数据流转情况:

从任务发布侧来看,任务可以分为两种类型:带延时和不带延时。

  • 对于 delay = 0 表示任务是直接可消费,那么会放到 Ready Queue, 消息直接对消费者可见;

  • 如果需要延时,需要进入 Timer Set,接着等待后台线程来轮询,到期之后再迁移到 Ready Queue;

  • 我们会将 job 内容以 KV 的方式单独存储。


后台轮询线程主要作用是将到期的任务从 timer_set 迁移到其他 ready queue,如果已经到达最大重试上限,那么会放进死信队列中。


扩容就很简单,类似上图 cluster-2,只需要加入新的存储配置,生成新 Token, 数据就会流向新集群。


缩容稍微麻烦一些,需要迁移老数据,在这里我们提供了一个 migration 的 engine, 通过配置可以自动来做集群之间的数据迁移。


简化后架构设计


在引入延时队列之后,整个系统变得十分简单,之前架构图里面的优先级和延时功能都 LMSTFY 里面实现了。整个请求流程变成:


1. Core API 接收用户请求

2. 通过 PubSub 投递给 Ready Processor

3. Ready Processor 根据优先级写到 Lmstfy

4. Worker 根据根据优先级消费 Lmstfy

5. 接着请求外部资源

6. 回写到 result processor,result processor 重新调度到 lmstfy

7. patch 回 Core API

优化后调度对比


通过优化后的调度对比,我们也不难发现在引入延时队列后,整个调度系统的调度很平滑,几乎没有波峰。同时,优化效果明显,特别是在消费资源上直接降低了 80%,同时简化系统设计之后,问题定位也更快,由于组件数减少,延时也降低了 50%。


3. 高可用保障


在高可用保障的组成部分中,包含整体系设计的高可用、系统的可见性、容量监控以及在线扩容。

系统的可见性会决定稳定性,需要定义和组织监控指标,重点需要注意:


1. 要明确最核心指标,太多约等于没有;

2. 符合问题定位逻辑,先统一入口再从整体到细化;

3. 组织要有逻辑,比如按照系统数据流来组织。


“有指标但组织的不好就约等于没有。”

系统可见性包含几个部分,Metrics 是聚合类型的指标,可以看到系统整体的情况以及是否有毛刺点,但无法定位具体是哪个请求有问题,同时无法观察一个请求的生命周期。而 Tracing 可以作为 Metrics 的互补,通过 Trace ID 将整个任务的生命周期串起来。而日志是离散的事件记录,但日志中会包含更加细致的问题描述。



在 Metrics 方面,整个服务有一个监控大盘,大盘上抽象了几个核心组件以及外部依赖监控指标。作为整个系统的监控入口,出现问题会第一时间到这个大盘报表来看哪个组件有问题,然后再由这里作为入口跳转到其他页面。我们的大盘报表实际上和整个系统的数据流也是一致的,第一行是 ready processor 的关键指标, 然后第二行是 worker, 第三个行是 result processor,然后下面是系统容量以及外部依赖的一些关键指标。

总结


1. 在大规模任务调度中,正确的抽象能够大大简化整个系统的复杂度;

2. 选型不是选择功能最全或者性能最好,而是选择最适合自身现状和需求;

3. 系统的可见性会决定稳定性,需要定义和组织监控指标;

4. 预期跟实际结果会有很大差异,一定要做好演练。

发布于: 1 小时前阅读数: 5
用户头像

AfterShip

关注

拥抱开源,极客文化 2017.10.17 加入

如何构建成熟的国际化 SaaS 产品,AfterShip 在过去将近 10 年里积累了不少实战经验,期望通过 InfoQ 写作平台让更多人了解 AfterShip,并与同行们有更多深度沟通交流。 传送门:https://engineering.aftership.com/

评论

发布
暂无评论
大规模任务调度在 AfterShip 的高可用实践