本文介绍了 Google 如何设计分布式 Cron 服务,为绝大多数需要定期安排计算任务的内部团队提供服务。在实践的过程中,我们已经遇到了很多关于如何设计和实现类似于这类基本服务的经验教训。在这里,我们将讨论在设计和实现分布式 Cron 时面临的问题,并提出一些我们的解决方案。
Cron 是一种常见的 Unix 实用程序,帮助用户在定义的时间或以固定的间隔周期性地启动任意作业。首先,我们将分析一下 Cron 的基本原理及其最常见的实现,然后再查看 Cron 等应用程序如何在大型分布式环境中工作,从而提高系统对单机故障的可靠性。我们在这里描述的是部署在少量机器上的分布式 Cron 系统,但能够与数据中心调度系统结合,在整个数据中心的机器上启动 Cron 作业。
在我们跳到如何为整个数据中心运行可靠的 Cron 服务的主题之前,我们先来看看 Cron - 从一个 SRE 的角度介绍其属性并查看它。
Cron 的设计使得系统管理员和系统的普通用户都可以指定要运行的命令以及何时运行它们,执行各种作业,包括垃圾收集和定期数据分析。最常见的时间说明格式我们称之为 “crontab”。它支持简单的间隔(例如,”每天中午一次” 或 “每小时一次”),也可以配置成 “每个星期六和每月30日” 这类稍复杂的间隔。
Cron 通常使用单个组件(通常称为 crond )实现。这是一个守护进程,加载要运行的 Cron 作业列表,并根据其下一个执行时间对它们进行排序,守护程序然后等待直到第一个项目被调度执行,这个时候,crond 就应该启动特定的作业或一堆作业,重新计算下次启动它们的时间,并等待到下一个执行时间。
可靠性
从可靠性的角度来看待服务,需要注意几件事情。首先,故障域本质上只是一台机器,如果机器未运行,则Cron 调度程序及其启动的作业也不能运行(例如由于无法访问网络或尝试访问坏掉的硬盘驱动器导致的个别作业失败超出了本文分析的范围)。我们就先考虑使用两台机器的非常简单的分布式案例,其中 Cron 调度程序在单独的机器上运行作业(例如使用SSH)。我们就已经有了不同的可能会影响我们运行作业可靠性的故障域:调度程序 或 目标机器可能会失败。
Cron 的另一个重要方面是在 crond 重新启动(包括机器重新启动)时,需要持续的唯一状态是 crontab配置 本身。Cron 执行者向来都是睡醒就忘的,而且 crond 也不要试图去跟踪它们的状态。
有一个比较独特的例外就是 anacron,它在系统关闭时会运行那些预设的任务。这局限于每天运行一次或不太频繁的工作,但对于在工作站和笔记本电脑来说,运行维护工作却非常有用。通过保存包含所有注册的 Cron 任务的上次启动的时间戳的文件从而运行这些作业。
Cron 作业和幂等
Cron 工作常常被用于周期任务,但事实上我们很难预料它们可能执行的真正功能。让我们跑题一下,说说Cron 工作本身的行为,因为了解 Cron 工作的各种要求将成为文章其余部分中的一个主题,显然会影响我们的可靠性要求。
一些 Cron 作业是幂等的,并且在系统故障的情况下,可以安全地多次运行它们,例如垃圾收集,但是其他 Cron 任务则不然,例如,一个发送电子邮件的任务显然就不应该如此。
为了让事情变得更加复杂,一些 Cron 任务不能运行是可以的,但是对于一些任务来说却又是不行的。例如,计划每五分钟运行一次的 垃圾收集 Cron 任务 可能会跳过一次启动,但计划每月运行一次的工资单结算任务却不行。
这种各种各样的 Cron 工作让我们难以定义故障模式:对于像 Cron 这样的服务,没有一个适合每种情况的单一答案。在本文中,我们倾向于采用 跳过执行 这种方法,而不是像其他基础架构那样采用有风险的 再次运行。Cron 管理员可以(并且应该)监视他们的 Cron 任务,例如通过使 Cron 服务为其管理的任务显示状态,或者设置对 Cron 任务的影响的独立监控。如果跳过运行,Cron 管理员可以采取与任务性质相符的行动。相比之下,撤销再次运行可能就很困难了,在某些情况下(考虑发邮件的例子)甚至是不可能的。所以,我们更倾向于 “fail closed”,避免系统自动地创建出不良状态。
大规模的 Cron
从单机迁移到大规模部署需要对如何使 Cron 在这样的环境中良好运行做一些根本性的反思,在介绍 Google Cron 解决方案的详细内容之前,让我们先讨论一下这些差异及其在设计上需要做的改变。
扩展基础架构
普通 Cron 限于单机,然而,对于大规模的系统部署,我们的 Cron 解决方案却不能与单机绑定。假设如果我们一个单独的数据中心有 1000 台机器,那么只要有 1/1000 的机器发生故障都将会引起整个 Cron 服务不可用,显而易见,这肯定是不可接受的。
为了解决这个常见的问题,我们将 处理 与 机器 分离。如果你想要运行服务,只需指定在哪个数据中心运行以及要运行的服务,然后 数据中心调度系统(本身应该是可靠的)就可以确定要哪些机器去运行它。接着在数据中心中,就可以将执行作业有效地转换成一个或多个RPC发送到数据中心调度程序。
但是,这个过程并不是瞬间发生的,有可能在 发现机器宕机(健康检查超时)和 将作业重新安排到不同机器(软件安装,过程启动时间)之间存在一些延迟。
由于将进程移动到不同的机器,那就可能意味着可能存在丢失存储在旧机器上的任何本地状态(除非采用实时迁移),并且重新调度时延可能会超过一分钟,所以我们应该清楚如何应对这个情况。最明显的选择之一是简单地将状态保存在分布式文件系统(如GFS)上,并在启动期间使用它来识别由于重新安排而无法启动的作业,这种方案可以让作业快速失败,但是,如果是每五分钟运行一次 Cron 作业,那么由于重新调度导致的延迟时间是一分钟或两分钟,那么这个时间间隔其实就长调度周期的很大一部分了。
这种方法可能会激发持续热备件,但是却可以让任务快速得切入并恢复运行。
扩展要求
在数据中心和单台机器上部署的另一个显着差异在于有更多的资源(例如 CPU 或 RAM 之类的)需要计划。单机系统通常只需将所有正在运行的进程进行独立限制。容器(如Docker)在现在很常见,但是使用它们可以一部分独立隔离在单个机器上部署的软件的内容,包括 crond 和所有任务。但是,数据中心规模的部署通常意味着部署到强制隔离的容器中。
隔离是很有必要的,因为我们希望数据中心中运行的进程不应该对任何其他进程产生负面影响。为了实现这种操作,您需要知道要运行的任何给定进程的前期资源,包括 Cron 系统及其运行的任务。作为一个预期的结果,如果数据中心没有符合该任务要求的可用资源,那么可能会延迟 Cron 作业。这一点以及监控 Cron 工作发布的愿望意味着我们需要跟踪 Cron 工作的完整状态,从定时启动到终止。
因为 Cron 系统现在已经将进程运行与特定机器分离,如上所述,我们可能会遇到部分运行失败。由于工作配置的多功能性,在数据中心启动新的 Cron 任务可能需要多个RPC。不幸的是,这意味着我们可以遇到部分 RPC 是成功的,但是还有一些其他的缺没有成功,可能因为发送它们的过程在中间就跪了。因此,在处理这些情况下如何恢复程序也是个问题。
在故障模式方面,数据中心比单机更复杂。在单个机器上作为一个相对简单的二进制文件的 Cron 服务在数据中心案例中有许多或明显或不太明显的依赖。对于与 Cron 基本相同的服务,我们希望确保即使数据中心遭受部分故障(例如部分断电或存储服务存在问题),服务仍然起作用。为了提高可靠性,我们确保数据中心调度程序可以在数据中心内的不同位置找到副本,以避免例如从单个配电单元取出所有 Cron 进程。
我们可能可以在全球各地部署一个个单独的 Cron 服务,但在数据中心内部署 Cron 服务的优势更多,包括与数据中心调度程序(这是核心依赖关系)的共享生存状态以及低延迟。
Cron在谷歌和如何建立一个
让我们解决一些必须解决的问题,以便在大规模分布式部署中提供可靠的 Cron,并重点介绍我们对实现 Google Cron 做出一些重要决定。
跟踪 Cron 工作状态
如上所述,我们应该持有一些关于 Cron 工作的状态,并能够在失败的情况下快速恢复。此外,状态的一致性是至关重要的:一段时间内不要启动 Cron 任务比错误地重新启动相同的 Cron 任务十次相比,不要启动更为可接受。回想一下,许多 Cron 工作不是幂等的,例如,工资单计算或发送电子邮件通讯之内的。
因此,我们有两个选择:将数据存储在外部通用高可用的分布式存储器中,或将少量状态存储为 Cron 服务本身的一部分。在设计分布式 Cron 时,我们选择了第二个选项。这有几个原因:
- 分布式文件系统(如 GFS 和 HDFS)通常针对非常大的文件(例如,Web 爬虫程序的输出)的使用情况设计,而我们需要存储的有关 Cron 作业的信息非常小。在这种分布式文件系统上的小写是非常昂贵的,并且由于文件系统未针对它们进行优化,因此具有高延迟。
- 具有广泛影响的基础服务(如 Cron )应具有非常少的依赖关系。即使部分数据中心离开,Cron 服务应该能够运行至少一段时间。这并不意味着存储必须直接作为 Cron 进程的一部分(实质上是一个实现细节)。但是,它应该能够独立于满足大量内部用户的下游系统运行。
使用 Paxos
我们部署 Cron 服务的多个副本,并使用 Paxos 算法来确保它们之间的一致状态。
Paxos 算法及其后继(例如 Zab 或 Raft)在如今的分布式系统(Chubby,Spanner 等)中是较为常见的。详细描述 Paxos 超出了本文的范围,但基本思想是在多个不可靠的副本之间达成对状态变化的共识。只要大多数 Paxos 集群成员可用,分布式系统作为一个整体,可以成功地处理新的状态变化,尽管基础设施的有限子集失败了。
分布式 Cron 使用单个主任务,如图 1 所示,它是唯一可以修改共享状态的副本,也可以是启动 Cron 作业的唯一副本。我们利用 Paxos 的变体(称为 Fast Paxos)在内部使用主副本作为优化 - Fast Paxos 主副本也充当Cron服务主机。
如果主副本死亡,Paxos 团队的健康检查机制可以快速发现(几秒钟内); 其他 Cron 进程已经启动并可用,我们可以选择一个新的主控。新任 master 当选后,我们将通过一个专门针对 Cron 服务的选择协议,该协议负责接管前任 master 未完成的所有工作。Cron 的专长与 Paxos master 相同,但 Cron master 需要在晋升时采取额外的行动。选择新主机的快速反应时间使我们能够保持在一般可容忍的一分钟故障切换时间内。
与 Paxos 保持最重要的状态是有关 Cron 工作的启动信息。对于每个 Cron 工作,我们同步地通知每个计划启动的开始和结束的复制数量。
master 和 slave 的角色
如上所述,我们使用 Paxos 及其在 Cron 服务中的部署具有两个分配的角色:主服务器 和 从服务器。让我们来看看每个角色的运作。
master
主复制品是唯一的主动启动 Cron 作业的副本。master 有一个内部的调度程序,很像开始描述的简单的crond,它保留了 Cron 作业按预定发布时间排序的列表。主副本等待直到列表的第一个元素的预定启动时间。
当我们达到预定的发布时间时,主副本宣布即将推出这个特定的 Cron 作业,并计算新的预定启动时间,就像一个常规的 crond 实现一样。当然,与普通 crond 一样,Cron 作业启动规范自上次执行以来可能已经发生变化,并且此启动规范也必须与从站保持同步。简单地识别 Cron 工作是不够的:我们还应该使用开始时间来唯一标识特定的启动,以避免 Cron 工作启动跟踪中的歧义(特别是高频 Cron 作业,例如每分钟运行的工作)。这种沟通是通过 Paxos 完成的。图2从 master 的角度说明了 Cron 作业启动的进度。
保持此 Paxos 通信同步并且不进行实际的 Cron 作业启动,直到确认 Paxos 仲裁已收到启动通知很重要。Cron 服务需要知道每个 Cron 作业是否启动,以决定主故障切换时的下一个操作步骤。不执行此同步可能意味着整个 Cron 作业启动发生在主服务器上,但从属副本不知道。在故障切换的情况下,他们可能会尝试再次执行相同的启动,因为它们没有被通知已经发生了。
通过 Paxos 向其他副本同步发布了 Cron 工作发布的完成。请注意,由于外部原因(例如,如果数据中心调度程序不可用),启动是成功还是失败无关紧要。在这里,我们正在跟踪 Cron 服务在预定时间尝试启动的事实。我们还需要在本操作中解决 Cron 系统的故障,我们在下面讨论。
master 的另一个重要特征是,一旦由于任何原因失去掌握,它必须立即停止与数据中心调度程序的交互。掌握掌握权应保证互相访问数据中心调度程序。如果没有,旧的和新的主人可能对数据中心调度程序执行冲突的动作。
slave
从复制机跟踪 master 所提供的世界状态,以便在需要时立即接管。所有状态的变化,slave 复制机的轨迹都是通过 Paxos 从 master 复制来的。很像 master,他们还保留了系统中所有 Cron 作业的列表。这个列表必须保持一致的复制品(通过使用Paxos)。
在收到有关启动的通知后,从副本将更新其本地下一个计划的启动时间,用于给定的 Cron 作业。这个重要的状态改变(回想一下它是同步完成的)确保系统内的 Cron 作业计划是一致的。我们跟踪所有的公开发行,也就是说,我们已经被通知了他们的开始,而不是他们的结束。
如果主副本死亡或其他故障(例如,从网络上的其他副本分离),则应选择从站作为新的主控。这种选举过程必须在不到一分钟的时间内发生,以避免 Cron 工作发动失踪(或无理拖延)的风险。一旦掌握了当选,所有的开放式(即部分失败)都必须结束。这可能是一个复杂的过程,对 Cron 系统和数据中心基础架构施加了额外的要求,它应该得到更详细的解释。
解决部分失败
如上所述,主副本和数据中心调度程序之间的交互在发送描述单个逻辑 Cron 作业启动的多个 RPC 之间可能会失败,我们也应该能够处理此条件。
回想一下,每个 Cron 工作发布都有两个同步点:当我们即将执行发布时,以及完成它们。这允许我们限定发射。即使启动由单个 RPC 组成,我们如何知道 RPC 是否实际发送?我们知道预定的发射开始了,但是在主副本死亡之前,我们没有通知其完成。
为了达到这个条件,我们可能需要继续连任的外部系统的所有操作都必须是等号的(即我们可以再次安全地执行),或者我们需要能够查看其状态和看看他们是否完成,明确地说。
这些条件施加了重大的限制,可能难以实现,但它们对于可能遭受部分故障的分布式环境中 Cron 服务的准确运行至关重要。如果没有适当处理,可能会导致错误的启动或双重启动相同的 Cron 工作。
在数据中心(例如 Mesos)中启动逻辑作业的大多数基础架构为这些作业提供命名,使其可以查看其状态,停止或执行其他维护。对于幂等问题的合理解决方案是提前构建这些名称,而不会对数据中心调度程序造成任何突变操作,并将其分发到 Cron 服务的所有副本。如果 Cron 服务主机在启动过程中死机,则新主机只需查看所有预先计算的名称的状态并启动缺少的作业。
回想一下,在保持副本之间的内部状态时,我们会跟踪计划的启动时间。同样,我们也需要消除我们与数据中心调度器的交互,也可以通过使用预定的启动时间来消除歧义。例如,考虑一个短暂但经常运行的 Cron 作业。Cron 工作已经启动,但在此之前被传送给所有副本,主机崩溃,异常长时间的故障切换 - 足够长,Cron 作业成功完成。新的主人查找工作的状态,观察到它完成了,并尝试启动它。如果时间已经被包括在内,那么主人就会知道数据中心调度程序上的工作是这个特殊的Cron工作启动的结果,而且双重启动不会发生。
由底层基础设施的实施细节驱动,实际实现具有更复杂的状态查找系统。然而,上述描述涵盖了任何此类系统的实现独立要求。根据可用的基础架构,您可能还需要考虑在双重启动和冒用启动发布的风险之间的权衡。
存储状态
使用 Paxos 达成共识只是处理状态问题的一部分。Paxos 本质上是状态变化的连续日志,与状态更改同步。这有两个含义:首先,日志需要压缩,以防止日志无限增长; 第二,日志本身必须存储在某个地方。
为了防止无限增长,我们只是简单地获取当前状态的快照,所以我们可以重构状态,而不需要重播所有导致它的状态变化日志条目。例如,如果我们在日志中存储的状态改变是 “将计数器递增1”,那么经过一千次迭代,我们有一千个日志条目,可以轻松地更改为 “设置计数器为1000” 的快照。
在日志丢失的情况下,我们只会丢失自上次快照以来的状态。快照实际上是我们最关键的状态 - 如果我们失去了快照,我们基本上从零开始,因为我们失去了内部状态。另一方面,丢失日志只会导致有局限的状态丢失,并将 Cron 系统及时发回到最新快照的时间。
我们有两个存储我们的数据的主要选择:外部在一般可用的分布式存储器中,或者存储在作为 Cron 服务本身的一小部分状态的系统中。在设计系统时,我们选择了两者。
我们将 Paxos 日志存储在计划 Cron 服务副本的机器的本地磁盘上。在默认操作中有三个副本意味着我们有三个日志副本。我们也将快照存储在本地磁盘上。然而,由于它们至关重要,因此我们还将其备份到分布式文件系统上,从而防止影响所有三台机器的故障。
我们没有在我们的分布式文件系统上存储日志,因为我们有意识地认为丢失日志(代表最近的一小部分状态更改)是一个可以接受的风险。在分布式文件系统上存储日志可能会导致频繁的小写入造成的实质性能损失。所有这三台机器的同时丢失是不太可能的,但是如果发生这种情况,我们将自动从快照中恢复,并且从上次的快照中丢失少量的日志,我们以可配置的时间间隔执行。当然,这些权衡可能会因基础设施的细节以及给定的 Cron 系统的要求而有所不同。
除了存储在本地磁盘上的日志和快照以及分布式文件系统上的快照备份外,新创建的副本还可以通过网络从已运行的副本获取状态快照和所有日志。这使得复制启动与本地计算机上的任何状态无关,并且在重新启动(或机器死机)时将副本重新安排到不同的计算机,基本上是服务可靠性的非问题。
运行大型 Cron
运行大型 Cron 部署还有其他更小但更小的意义。传统的 Cron 很小:它可能包含最多数十个 Cron 工作。但是,如果您在数据中心中为数千台机器运行 Cron 服务,那么您的使用情况差不多这样,那么自然而然你也会碰到这些问题。
第一个问题就是分布式系统的一个经典问题 —— 闪电兽群,Cron 服务(基于用户配置)可能会导致数据中心使用量的大幅上升。当大多数人想到每天的 Cron 工作时,他们会立刻想到在午夜运行它,这就是他们如何配置他们的 Cron 工作。如果 Cron 工作在同一台机器上启动,那么这个工作很好,但是如果你的工作能够与数千个 worker 一起生成 MapReduce 呢?而如果三十个不同的团队决定在同一个数据中心中每天运行这样的 Cron 工作呢?为了解决这个问题,我们引入了一个 crontab 格式的扩展。
在普通 crontab 中,用户指定 分钟,小时,月份(或周)的日期,以及 Cron 作业应启动的月份,或星号表示每个值。然后每天午夜运行,然后 crontab 规定为 “0 0 *”(即,零分钟,第零小时,每月的每一天,每个月,每周的每一天)。我们引入了问号的使用,这意味着任何值都是可以接受的,而Cron系统被赋予了选择价值的自由。该值通过在给定时间范围内(例如,0 … 23小时)上的Cron作业配置进行散列选择,从而更均匀地分布这些启动。
尽管如此,Cron 工作所造成的负担仍然非常棘手。图3中的图表显示了 Google 在全球范围内启动 Cron 任务的总数。这突出了频繁的高峰,这通常是由于特定时间需要启动的工作造成的,例如由于外部事件的时间依赖性。
小结
Cron 服务几十年来一直是 UNIX 系统的基础功能。行业转向大型分布式系统,其中数据中心可能是最小的硬件单元,需要在堆栈的大部分进行更改,而 Cron 也不例外。仔细查看 Cron 服务所需的属性以及 Cron 作业的要求驱动我们的新设计。
基于 Google 解决方案,我们已经在分布式系统环境中讨论了新的限制条件和可能的 Cron 服务设计。该解决方案在分布式环境中需要强大的一致性保证 因此,分布式 Cron 实现的核心是 Paxos,这是在不可靠环境中达成共识的常用算法。在大规模,分布式环境中使用 Paxos 并正确分析 Cron 工作的新故障模式,使我们能够构建一个在 Google 大量使用的强大的 Cron 服务。
Reference
Burrows, M. 2006. The Chubby lock service for loosely-coupled distributed systems. Proceedings of the 7th Symposium on Operating Systems Design and Implementation: 335-350. http://research.google.com/archive/chubby-osdi06.pdf
Corbett, J. C., et al. 2012. Spanner: Google’s globally-distributed database, Proceedings of OSDI’12. Tenth Symposium on Operating System Design and Implementation. http://research.google.com/archive/spanner-osdi2012.pdf
Docker. https://www.docker.com/
Junqueira, F. P., Reed, B. C., Serafini, M. 2011. Zab: High-performance broadcast for primary-backup systems. Dependable Systems & Networks (DSN), 2011 IEEE/IFIP 41st International Conference: 245-256. http://ieeexplore.ieee.org/xpls/abs_all.jsp?arnumber=5958223&tag=1
Lamport, L. 2001. Paxos made simple. ACM SIGACT News 32 (4): 18-25, http://research.microsoft.com/en-us/um/people/lamport/pubs/pubs.html#paxos-simple
Lamport, L. 2006. Fast Paxos. Distributed Computing 19 (2): 79-103, http://research.microsoft.com/pubs/64624/tr-2005-112.pdf
Ongaro, D., Ousterhout, J. 2014. In search of an understandable consensus algorithm (extended version). https://ramcloud.stanford.edu/raft.pdf