导读 | 这篇文章主要来描述下?Google?是如何实现一套可靠的分布式 Cron 服务,服务于内部那些需要绝大多数计算作业定时调度的团队。 在这个系统的实践过程中,我们收获了很多,包括如何设计、如何实现使得它看上去像一个靠谱的基础服务。 在这里,我们来讨论下分布式 Cron 可能会遇到哪些问题,以及如何解决它。 |
Cron 是?UNIX?中一个常见的工具,用来定期执行一些用户指定的任意任务。我们先来分析下?Cron?的基本原则和它最常见的实现,然后我们来回顾下像?Cron?这样的服务应该如何运行在一个大型的、分布式的环境中,这样即使单机故障也不会对系统可用性造成影响。 我们将会介绍了一个建立在少量机器上的?Cron?系统,然后结合数据中心的调度服务,从而可以在整个数据中心中运行?Cron?任务。
在我们在描述如何运行一个靠谱的分布式 Cron 服务之前,让我们先来从一个 SRE 的角度来回顾下 Cron。
Cron 是一个通用的工具,无论是管理员还是普通用户都可以用它来在系统上运行指定的命令,以及指定何时运行命令,这些指定运行的命令可以是定期垃圾回收,也可以是定期数据分析。 最常见的时间指定格式被称为 crontab,它不仅支持简单的时间周期(如,每天中午一次,每个小时一次),也支持较复杂的时间周期,如每个周六、每个月的第 30 天等等。
Cron 通常只包含一个组件,被称为?crond,它是一个后台守护程序,加载所有需要运行的 cron 定时任务,根据它们接下来的运行时间来进行排序,然后这个守护进程将会等待直到第一个任务开始执行。在这个时刻,crond?将会加载执行这个任务,之后将它放入队列等待下一次运行。
从可靠性的角度来看一个服务,需要有很多注意的地方。
第一,比如 crond,它的故障域本质上来说只是一台机器,如果这个机器没有运行,不论是 cron 调度还是加载的任务都是不可运行的。因此,考虑一个非常简单的分布式的例子 ——— 我们使用两台机器,然后 cron 调度在其中一台机器上运行任务(比如通过 ssh)。然后产生了一个故障域了:调度任务和目标服务器都可能失败。
另外一个需要注意的地方是,即使是?crond?重启(包括服务器重启),上面部署的 crontab 配置也不应该丢失。crond 执行一个任务然后就‘忘记’了这个任务的状态,它并不会尝试去跟踪这个任务的执行状态,包括是否该执行是否已经执行。
anacron是一个例外,它是crontab的一个补充,它尝试运行哪些因为服务器宕机而应该执行却没执行的任务。这仅限于每日或者更小执行频率的任务,但对于在工作站和笔记本电脑上运行维护工作非常有用。通过维护一个包括最后执行时间的配置文件,使得运行这些特殊的任务更加方便。
Cron 的任务用来执行定期任务,但是除此之外,却很难在进一步知道它们的功能。让我们先把要讨论的主题抛开一边,现在先来就 Cron 任务本身来做下探讨,因为只有理解了 Cron 任务的各种各样的需求,才能知道它是如何影响我们需要的可靠性要求,而这一方面的探讨也将贯穿接下来的文章。
有一些 Cron 任务是幂等性的,这样在某些系统故障的情况下,可以很安全的执行它们多次,比如,垃圾回收。然而有些 Cron 任务却不应该被执行多次,比如某个发送邮件的任务。
还有更复杂的情况,有些 Cron 任务允许因为某些情况而“忘了”运行,而某些 Cron 任务却不能容忍这些,比如,垃圾回收的 Cron 任务每 5 分钟调度一次,即使某一次没有执行也不会有太大的问题,然而,一个月一次的支付薪水的任务,却绝对不允许有失误。
Cron 任务的各种不同的类型使得不可能有一个通用的解决方案,使得它可以应对各种各样的失败。所以,在本文中上面说的那些情况,我们更倾向于错过某一次的运行,而不是运行它们两次或者更多。Cron 任务的所有者应该(也必须)监控着它们的任务,比如返回任务的调用结果,或者单独发送运行的日志给所属者等等,这样,即使跳过了任务的某次执行,也能够很方便的采取对应的补救动作。当任务失败时,我们更倾向于将任务状态置为 “fail closed” 来避免产生系统性的不良状态。
当从单机到集群部署 Cron 时,需要重新思考如何使 Cron 在这种环境下良好的运行。在对 Google 的 Cron 进行解说之前,让我们先来讨论下单机以及多机之间的区别,以及针对这变化如何设计。
常规的 Cron 仅限于单个机器,而大规模部署的 Cron 解决方案不能仅仅绑定到一个单独的机器。假设我们拥有一个 1000 台服务器的数据中心,如果即使是 1/1000 的几率造成服务器不可用都能摧毁我们整个 Cron 服务,这明显不是我们所希望的。
所以,为了解决这个问题,我们必须将服务与机器解耦。这样如果想运行一个服务,那么仅仅需要指定它运行在哪个数据中心即可,剩下的事情就依赖于数据中心的调度系统(当然前提是调度系统也应该是可靠的),调度系统会负责在哪台或者哪些机器上运行服务,以及能够良好的处理机器挂掉这种情况。 那么,如果我们要在数据中心中运行一个任务,也仅仅是发送一条或多条 RPC 给数据中心的调度系统。
然而,这一过程显然并不是瞬时完成的。比如,要检查哪些机器挂掉了(机器健康检查程序挂了怎么办),以及在另外一些机器上重新运行任务(服务依赖重新部署重新调用任务)都是需要花费一定时间的。
将程序转移到另外一个机器上可能意味着损失一些存储在老机器上的一些状态信息(除非也采用动态迁移),重新调度运行的时间间隔也可能超过最小定义的一分钟,所以,我们也必须考虑到上述这两种情况。一个很直接的做法,将状态文件放入分布式文件系统,如 GFS,在任务运行的整个过程中以及重新部署运行任务时,都是用它来记录使用相关状态。 然而,这个解决方案却不能满足我们预期的时效性这个需求,比如,你要运行一个每五分钟跑一次的 Cron 任务,重新部署运行消耗的 1-2 分钟对这个任务来说也是相当大的延迟了。
及时性的需求可能会促使各种热备份技术的使用,这样就能够快速记录状态以及从原有状态快速恢复。
将服务部署在数据中心和单服务器的另一个实质性的区别是,如何规划任务所需要的计算资源,如 CPU 或内存等。
单机服务通常是通过进程来进行资源隔离,虽然现在 Docker 变得越来越普遍,但是使用它来隔离一切目前也不太是很通用的做法,包括限制crond以及它所要运行的任务。
大规模部署在数据中心经常使用容器来进行资源隔离。隔离是必要的,因为我们肯定希望数据中心中运行的某个程序不会对其它程序产生不良影响。为了隔离的有效性,在运行前肯定得先预知运行的时候需要哪些资源——包括 Cron 系统本身和要运行的任务。这又会产生一个问题,即如果数据中心暂时没有足够的资源,那么这个任务可能会延迟运行。这就要求我们不仅要监控 Cron 任务加载的情况,也要监控 Cron 任务的全部状态,包括开始加载到终止运行。
现在,我们希望的 Cron 系统已经从单机运行的情况下解耦,如之前描述的那样,我们可能会遇到部分任务运行或加载失败。这时候幸亏任务配置的通用性,在数据中心中运行一个新的 Cron 任务就可以简单的通过 RPC 调用的方式来进行,不过不幸的是,这样我们只能知道 RPC 调用是否成功,却无法具体知道任务失败的具体地方,比如,任务在运行的过程中失败,那么恢复程序还必须将这些中间过程处理好。
在故障方面,数据中心远比一台单一的服务器复杂。Cron 从原来仅仅的一个单机二进制程序,到整个数据中心运行,其期间增加了很多明显或不明显的依赖关系。作为像 Cron 这样的一个基础服务,我们希望得到保证的是,即使在数据中心中运行发生了一些 “Fail”(如,部分机器停电或存储挂掉),服务依然能够保证功能性正常运行。为了提高可靠性,我们应该将数据中心的调度系统部署在不同的物理位置,这样,即使一个或一部分电源挂掉,也能保证至少 Cron 服务不会全部不可用。
现在让我们来解决这些问题,这样才能在一个大规模的分布式集群中部署可靠的 Cron 服务,然后在着重介绍下 Google 在分布式 Cron 方面的一些经验。
向上面描述过的那样,我们应该跟踪 Cron 任务的实时状态,这样,即使失败了,我们也更加容易恢复它。而且,这种状态的一致性是至关重要的:相比错误的多运行 10 遍相同的 Cron 任务,我们更能接受的是不去运行它。回想下,很多 Cron 任务,它并不是幂等性的,比如发送通知邮件。
我们有两个选项,将 Cron 任务的数据通通存储在一个靠谱的分布式存储中,或者仅仅保存任务的状态。当我们设计分布式 Cron 服务时,我们采取的是第二种,有如下几个原因: