Dropbox中的异步任务调度

2020-11-12 03:49:02

我在计算机科学硕士毕业后不久就加入了Dropbox。除了实习,这是我在大联盟的第一份工程工作。我的团队已经开始设计一个关键的内部服务,我们的大多数软件都会使用它:它将在幕后处理异步计算请求,为从将文件拖入Dropbox文件夹到安排营销活动的一切提供动力。

这个异步任务框架(ATF)将取代不同工程团队使用的多个定制的异步系统。它将减少重复开发、不兼容性和对遗留软件的依赖。没有适合我们用例和规模的开源项目或购买而不是构建的解决方案,所以我们必须创建自己的解决方案。ATF既是一个重要的挑战,也是一个有趣的挑战,所以我们很高兴设计、构建和部署我们自己的内部服务。

ATF不仅必须运转良好,而且必须在规模上运转良好:它将成为Dropbox基础设施的基础构件。它从一开始就需要每秒处理10,000个异步任务,并为未来的增长量身定做。它需要从一开始就支持近100种独特的异步任务类型,而且还有增长的空间。至少有24个工程团队希望将其用于我们代码库的完全不同的部分,用于许多产品和服务。

就像任何工程师都会做的那样,我们在谷歌上搜索,看看其他拥有大规模服务的公司在处理异步任务方面做了什么。我们失望地发现,建造超大型异步服务的工程师几乎没有发表什么材料。

现在ATF已经部署,目前每秒为9000个计划的异步任务提供服务,并由28个工程团队在内部使用,我们很高兴能填补这一信息空白。我们详细记录了Dropbox ATF,作为寻求自己的异步解决方案的工程界的参考和指南。

按需调度异步任务是一项关键功能,它为Dropbox的许多功能和内部平台提供了支持。异步任务框架(ATF)是通过基于回调的架构在Dropbox支持此功能的基础设施系统。ATF使开发人员能够定义回调,并调度针对这些预定义回调执行的任务。

自一年多前引入以来,ATF已经成为Dropbox基础设施中的重要构建块,被我们代码库中的近30个内部团队使用。它目前支持100多个需要立即或延迟任务调度的用例。

在这篇文章中反复使用的一些基本术语,定义为在本讨论的上下文中使用。

任务:执行波长的单位。使用ATF调度的每个异步作业都是一个任务。

集合:属于lambda的带标签的任务子集。如果将Send Email实现为lambda,则密码重置电子邮件和营销电子邮件将是集合。

任务调度客户端可以调度任务在指定时间执行。任务可以计划为立即执行,也可以根据用例推迟执行。

基于优先级的执行任务应该与优先级相关联。一旦优先级较高的任务准备好执行,就应该在优先级较低的任务之前执行。

任务选通ATF允许基于lambda的任务选通,或者基于集合的lambda上的任务子集的选通。可以选择完全删除或暂停任务,直到合适的执行时间。

至少一次任务执行,ATF系统保证任务在被调度后至少执行一次。一旦用户定义的回调向ATF系统发出任务完成的信号,就可以说执行完成了。

无并发任务执行ATF系统保证在任何给定时间点最多有一个任务实例在活动执行。这有助于用户编写回调,而无需设计从不同位置并发执行同一任务。

给定lambda中的隔离任务与其他lambda中的任务隔离。这种隔离跨越多个维度,包括任务执行的工作人员能力和任务调度的资源使用。相同lambda但不同优先级上的任务在用于任务调度的资源使用上也是孤立的。

任务调度的高可用性ATF服务99.9%可用于接受来自任何客户端的任务调度请求。

幂等在ATF系统中,一个lambda上的单个任务可以多次执行。开发人员应该确保他们的lambda逻辑和客户端任务执行的正确性不受此影响。

执行任务的弹性工作进程可能在任务执行过程中的任何时刻死亡。ATF重试突然中断的任务,也可以在不同的主机上重试。Lambda所有者必须设计他们的lambda,以便在不同主机上重试不会影响lambda的正确性。

终端状态处理ATF重试任务,直到从lambda逻辑发信号通知它们完成。客户端代码可以将任务标记为成功完成、致命终止或可重试。重要的是,lambda所有者将客户端设计为适当地发出任务完成的信号,以避免无限次重试等错误行为。

在这一部分中,我们描述了ATF的高层体系结构,并简要描述了它的不同组件。(见上图1)。在这一节中,我们描述ATF的高层体系结构,并对其不同组件进行简要描述。(见上文图1。)。Dropbox使用GRPC进行远程调用,并使用我们内部的Edsterore来存储任务。

前端这是通过RPC接口调度请求的服务。前端接受来自客户端的RPC请求,并通过与ATF的任务存储交互来调度任务,如下所述。

任务存储区ATF任务存储在任务存储区中,并从任务存储区触发。任务存储可以是具有索引查询能力的任何通用数据存储。在ATF的例子中,我们使用我们内部的元数据存储Edsterore来支持任务存储。更多细节可以在下面的Data模型部分找到。

商店消费者商店消费者是一项服务,它定期轮询任务存储以查找准备执行的任务,并将它们推入正确的队列,如下面的队列部分所述。这些任务可能是新准备执行的任务,也可能是因为在执行时以可回收的方式失败,或者在ATF系统中的其他位置被丢弃而准备再次执行的较旧任务。

每秒钟重复一次: 1.从任务存储中轮询准备执行的任务 2.将任务推送到正确的队列中 3.更新任务状态。

Store Consumer轮询在较早的执行尝试中失败的任务。这有助于ATF系统提供至少一次的保证。有关Store Consumer如何轮询新的和以前失败的任务的更多详细信息,请参见下面的任务生命周期部分。

队列ATF使用AWS简单队列服务(SQS)在内部对任务进行排队。这些队列充当Store Consumer和控制器(如下所述)之间的缓冲区。每个<;lambda、Priority>;对都有一个专用的SQS队列。ATF使用的SQS队列总数是#lambdas x#Priority。

控制器工作主机是专用于任务执行的物理主机。每个工作主机都有一个控制器进程,负责从后台线程中的SQS队列轮询任务,然后将它们推送到进程本地缓冲队列中。控制器只知道它正在服务的lambdas,因此只轮询所需队列的有限集合。

控制器提供来自其进程本地队列的任务,作为对NExtWORK RPC的响应。这是执行级别任务优先级排序发生的层。控制器对不同优先级的任务有不同的进程级别队列,因此可以响应NExtWORK RPC来确定任务的优先级。

执行器执行器是一个具有多个线程的进程,负责实际任务的执行。Executor进程中的每个线程都遵循这个简单的循环:

每个工作主机都有一个控制器进程和多个执行器进程。控制器和执行器都在“拉”模式下工作,在这种模式下,活动循环不断地长时间轮询要完成的新工作。

心跳和状态控制器(HSC)HSC为RPC提供服务,用于声明要执行的任务(ClaimTask)、设置执行后的任务状态(SetResults)以及任务执行期间的心跳(HeartBeats)。ClaimTask请求来自控制器,以响应NExtWORK请求。心跳和SetResults请求源自任务执行过程中和之后的Executor进程。HSC与任务存储交互,以更新其接收到的请求类型的任务状态。

ATF使用我们内部的元数据存储库Edsterore作为任务存储库。Edsterore对象可以是实体或关联(ASSOC),每个实体或关联都可以有用户定义的属性。关联用于表示实体之间的关系。Edsterore仅支持对关联属性进行索引。

基于这个设计,我们在Edsterore中有两种与ATF相关的对象。ATF关联存储调度信息,例如商店消费者应轮询给定任务的下一个调度时间戳(第一次轮询或重试轮询)。ATF实体存储用于跟踪任务执行的任务状态和有效载荷的所有任务相关信息。我们在Pull模型中查询来自Store Consumer的关联,以挑选准备执行的任务。

到了处理任务的时候,Store Consumer从Edsterore拉出任务并将其推送到相关的SQS队列。

Executor向控制器发出NExtWORK RPC调用,后者从SQS队列中提取任务,向HSC发出ClaimTask RPC,然后将任务返回给Executor。

Executor调用任务的回调。在处理过程中,Executor对心跳和状态控制器(HSC)执行心跳RPC调用。处理完成后,Executor执行对HSC的TaskStatus RPC调用。

任务生命周期中的每个状态更新都伴随着对ASSOC中下一个触发器时间戳的更新。这确保了如果任务的状态在下一个触发时间戳内没有变化,Store Consumer将再次拉回该任务。这有助于ATF通过确保不丢弃任何任务来实现其至少一次交付保证。

以下是ATF中的任务实体和关联状态及其相应的时间戳更新:

如果任务处于入队状态太久,请重新入队。如果队列丢失数据或控制器在轮询队列之后、任务被认领之前关闭,则可能发生这种情况。

如果任务被认领但从未转移到处理中,请重新排队。如果控制器在声明任务后关闭,则可能会发生这种情况。任务状态在重新入队后更改为已入队。

如果任务太长时间没有发送心跳信号,请重新排队。如果Executor关闭,可能会发生这种情况。任务状态在重新入队后更改为已入队。

在ATF中,通过重试任务直到其完成执行(由Success或FatalFailure状态表示),可以保证任务至少执行一次。所有ATF系统错误都被隐含地认为是可恢复的故障,并且lambda所有者可以选择将任务标记为RetriableFailure状态。任务可能会从系统不同部分的ATF执行流水线中通过暂时性的RPC故障和依赖项(如Edsterore或SQS)中的故障丢弃。不过,由于Store Consumer的超时和重新轮询系统,系统不同部分的这些暂时性故障不会影响至少一次的保证。

没有并发任务执行通过ATF中两种方法的组合来避免并发任务执行。首先,在开始执行之前,通过独占任务状态(声明)显式声明任务。任务执行完成后,任务状态将更新为Success、FatalFailure或RetriableFailure之一。只有当任务的现有任务状态为已排队时,才能声明该任务(重试任务在被重新推送到SQS后也将进入已排队状态)。

但是,可能会出现这样的情况:一旦长时间运行的任务开始执行,其检测信号可能会反复失败,但任务仍在继续执行。ATF将通过从商店消费者轮询来重试该任务,因为心跳超时已经到期。然后,该任务可以由另一个Worker认领并导致并发执行。

为了避免这种情况,在Executor进程中有一个终止逻辑,一旦连续三次心跳调用失败,Executor进程就会自行终止。每个心跳超时都足够大,足以使连续三次心跳故障黯然失色。这确保了Store Consumer无法在终止逻辑结束任务之前拉出这些任务--这是帮助实现这一保证的第二种方法。

通过专用的工作者集群、专用的队列和专用的每波长调度配额实现了对波长的隔离。此外,同样通过专用队列和调度带宽实现同一波长内不同优先级的隔离。

交付延迟ATF使用案例不需要超低的任务交付延迟。任务交付延迟在几秒左右是可以接受的。准备好执行的任务由Store Consumer定期轮询,这段轮询在很大程度上控制了任务交付延迟。ATF将此作为调谐杠杆,可以根据需要实现不同的交付延迟。提高轮询频率可以减少任务交付延迟,反之亦然。目前,我们已将ATF校准为每两秒轮询一次就绪任务。

ATF是为Dropbox的开发者设计的自助式框架。这种设计非常有意于推动一种所有权模式,在这种模式下,lambda所有者拥有其lambdas运营的所有方面。为了促进这一点,所有的lambda工人集群都归lambda所有者所有。他们可以完全控制这些群集上的操作,包括代码部署和容量管理。每个执行器进程都绑定到一个lambda。所有者可以选择在其工作集群上部署多个lambda,只需在其主机上生成新的执行器进程即可。

如上所述,ATF提供了用于调度异步任务的基础结构构建块。在此基础上,ATF可以进行扩展以支持更通用的用例,并提供更多作为框架的功能。以下是可以作为ATF扩展的一些示例。

周期性任务执行目前,ATF是一种一次性任务调度系统。构建对定期任务执行的支持,将其作为此框架的扩展,将有助于为我们的客户释放新功能。

更好地支持任务链目前,通过将一个任务调度到ATF上,然后在其执行期间将其他任务调度到ATF上,可以在ATF上链接任务。尽管在当前的ATF设置中可以做到这一点,但在框架级别上缺乏对此链接的可见性和控制。这里的另一个自然扩展是通过框架级别的可见性和控制更好地支持任务链,使此用例成为ATF模型中的第一类概念。

行为不端任务的死信队列我们在ATF上观察到的维护开销的一个常见来源是,由于lambda逻辑中偶尔出现的错误,一些任务会陷入无限重试循环。这需要ATF框架所有者的手动干预,在某些情况下,有大量任务滞留在这样的循环中,占用了系统中的大量调度带宽。响应这种情况的典型手动操作包括暂停执行行为不佳的lambdas,或者直接丢弃它们。

减少这种操作开销并为lambda所有者提供从此类事件中恢复的简单界面的一种方法是创建充满此类不良行为任务的死信队列。在任务被推入死信队列之前,ATF框架可以施加最大重试次数。一旦修复了相关的lambda错误,我们可以创建和公开工具,使将任务从死信队列重新调度回ATF系统变得容易。

我们希望这篇文章能帮助其他地方的工程师开发他们自己的更好的异步任务框架。非常感谢参与这个项目的每个人:Anirudh Jayakumar、Deepak Gupta、Dmitry Kopytkov、Koundinya Muppalla、彭康、Rajiv Desai、Ryan Armstrong、Steve Rodrigues、Thomissa Comellas、张晓楠和杜玉环。