Jupiter:Facebook 的高性能 job-matching 服务

  • Sergey Doroshenko
  • 运和凭

2017 年 6 月 7 日

话题:Facebook语言 & 开发架构

在代码的编写、编辑、测试与发布等工程流程当中,我们需要依靠后端服务及设备以执行各类任务,具体包括构建新软件包、安装依赖关系并运行测试任务。考虑到 Facebook 极为可观的业务规模,执行上述工作的员工数量及职务种类每一天都可能不断增加,这显然会成为系统性能的瓶颈所在。我们的目标在于尽可能减少工程师在设备上的等待时长,确保其能够快速获取反馈,并将更多时间与精力投入到生产任务身上。

每项任务实际上都运行在一个被称为“工作程序”的隔离环境当中,而这些环境则分布在不同数据中心的不同主机之上。各工作程序会通过预配置以针对某些特定任务类型进行优化——例如输入的 Android 任务会被交付至拥有正确代码库且完成了依赖性更新的工作程序处。

此类分布式系统需要一些保障性机制以可靠且高效地为正确的工作程序交付对应的任务内容。作为一类常见解决方案,人们通常会利用分布式队列以及调度器 / 调度程序系统等追踪当前全书状态,并尝试以最佳方式进行工作负载分配。然而,这些解决方案亦各有利弊 ; 举例来说,调度器往往会对系统的可扩展性造成负面影响。在今天的文章中,我们将介绍的 Jupiter 项目正是专门为此而生——一项负责任务匹配的服务。

对于各工作程序,其职能的本质在于对其特征或者所能处理之任务的声明。其中的具体特征可能表现为内存大小、工作程序所处之数据中心、设备的当前负载因子、工作程序接入的智能手机型号以及预加载至设备之上的存储库或数据部件等等。每个工作程序都拥有多项功能与特征,且各功能特性在实际上属于一个或者一组值(例如,内存大小即为一个值,而多套预加载源代码库则为一组值)。同样的,不同任务亦会对其所需功能特性提出要求。举例来说,资源密集型构建任务需要强大的设备作为支持 ; 特定代码库中的 CI 任务更适合由已经预加载对应代码的工作程序来完成 ; 需要特定内核版本的任务只能匹配运行在该内核之上的工作程序 ; 而一组分布式构建任务则最好能够在集群环境下执行。

在由更新与并发请求构成的持续流当中,我们往往很难准确对具有多维功能要求的任务进行快速匹配。而作为一项服务,Jupiter 非常擅长高效响应工作程序的任务要求,同时追踪当前可用的工作负载,且确切符合工作程序与负载需求所作出的多层级功能限定条件。Jupiter 服务能够为工作匹配提供基础性保障——即使同时存在多项请求,每项任务也只会被分配给一个工作程序 ; 而一旦工作程序确认收到该任务,那么此任务将不再进行分配。

与众多其它 Facebook 后端服务一样,Jupiter 服务同样由 C++ 编写而成,且可通过 Thrift 进行访问。Jupiter 拥有横向可扩展性——意味着其能够进行轻松拆分,并确保其中各个分区根据不同任务集要求作出独立决策,且不存在任何负责全部判断的单一领导节点。Jupiter 强调任务队列顺序,各分区可采取最为简单的先进先出型排序策略,但亦支持其它一些更为复杂的概念——例如作业优先级机制。在性能方面,Jupiter 服务的每个分区每秒能够处理数十万项请求。

工作程序向 Jupiter 发送请求以描述其具备的全部功能特性,而 Jupiter 则查询内部数据结构以找到最佳匹配选项。在实际生产工作负载场景下,Jupiter 的任务获取请求延迟通常在数十微秒左右。收取到合适的任务后,工作程序将向 Jupiter 发送新的请求以确认接受或者加以拒绝。如果加以拒绝,则该任务会被放置在请求所指定的暂存区域内 ; 在此段时间中该任务无法被分配给其它工作程序。Jupiter 亦可对接各类不同类型的 API 使用方式。举例来说,工作程序可以向 Jupiter 发送租约请求,通知自身已经开始处理相关任务,但只有在真正成功处理完成后方可进行确认。从高层级角度来看,全部 Jupiter 客户皆遵循获取任务、确认工作、处理工作并再次循环这一基本模式。

由于 Jupiter 掌握着队列中全部任务的相关信息,因此其亦能够执行高级调度决策——例如仅在某些高优先级任务完成后才允许向工作程序提供其它任务。在大多数情况下,Jupiter 的主要优势在于能够通过一组(异构)工作程序高效消费处理能力,同时实现理想的可靠性与原子性。在 Facebook 公司,Jupiter 被用于支持各类不同内部客户——包括我们的资源管理系统 One World 以及我们的持续集成系统 Sandcastle。由于数据与匹配模块之间的 API 边界非常明确,因此与新系统之间的集成也就非常简单。

同样值得一提的是,Jupiter 在某些场景下并不适用。其并不属于数据持久层。因此如果需要对任务数据进行记录,则应由数据产生方负责进行——而 Jupiter 则仅不断从持久层处获取增量更新以保持上下文同步。之所以选择这样的处理方式,主要出于两大考量。第一,我们认为服务应该专注于特定任务,而非面向一切需求。第二,通过将任务存储与任务匹配加以拆分,我们能够确保 Jupiter 不致成为整体系统中的单点故障来源:一旦 Jupiter 发生故障,任务生产方仍然能够以队列方式正常运作 ; 而 Jupiter 顺利恢复后,全部队列内工作则可继续由其交付至工作程序。Jupiter 将数据供应接口背后的所有状态操作抽象出来,这意味着其能够随意使用选定的任意存储引擎——包括关系型数据库、分布式文件系统或者是在无需对数据加以持久保留的情况下使用 Jupiter 的内存内存储。

Jupiter 亦不具备对工作程序状态的全局性了解(例如特定时间点上哪些工作程序正处于忙碌状态,或者哪些工作程序有能力处理某一任务),亦无法将任务的具体分配趋向作出任何中央式决策。相反,Jupiter 的惟一原则就是以高效方式进行任务分配,这是因为工作程序只负责提出其适合处理的任务类型、而任务本身则提出工作程序所需要具备的具体特性——在其之中,Jupiter 将作为有效的仲裁者,单纯为最适合处理对应任务的工作程序提供接入任务负载的途径。

总而言之,Jupiter 是一项高效的任务匹配服务,非常适用于生产者 / 消费者类型的分布式系统。通过将全局任务保留在这项专用服务当中,Jupiter 的功能完全可供各类其它系统引入并使用。最后,希望我们的这一实践经验与发现能够为软件工程领域带来更为广泛的助益。

Facebook语言 & 开发架构