如何设计稳定性横跨全球的Cron服务

621 查看

这篇文章主要来描述下Google是如何实现一套可靠的 分布式Cron服务,服务于内部那些需要绝大多数计算作业定时调度的团队。 在这个系统的实践过程中,我们收获了很多,包括如何设计、如何实现 使得他看上去像一个靠谱的基础服务。 在这里,我们来讨论下分布式Cron可能会遇到哪些问题,以及 如何解决他。

Cron是UNIX中一个常见的工具,用来定期执行一些用户指定的随意Jobs。我们先来分析下Cron的基本原则和他最常见的实现,然后我们来回顾下像Cron这样的服务应该如何运行在一个大型的、分布式的环境中,这样即使单机故障也不会对系统可用性造成影响。 我们将会介绍了一个建立在少量机器上的Cron系统,然后结合数据中心的调度服务,从而可以在整个数据中心中运行Cron任务作业。

在我们在描述 如何运行一个靠谱的分布式Cron服务之前,让我们先来从一个SRE的角度来回顾下Cron。

Cron是一个通用的工具,无论是admin还是普通用户都可以用它来在系统上运行指定的命令,以及指定何时运行命令,这些指定运行的命令可以是定期垃圾回收,也可以是定期数据分析。 最常见的时间指定格式被称为“crontab”,他不仅支持简单的时间周期(如 每天中午一次,每个小时一次),也支持较复杂的时间周期,如每个周六、每个月的第30天等等。

Cron通常只包含一个组件,被称为“crond”,他是一个deamon程序,加载所有需要运行的cron定时任务,根据他们接下来的运行时间来进行排序,然后这个守护进程将会等待直到第一个任务开始执行。在这个时刻,crond将会加载执行这个任务,之后将它放入队列等待下一次运行。

可靠性Reliability

从可靠性的角度来看一个服务,需要有很多注意的地方。
第一,比如crond,他的故障域本质上来说只是一台机器,如果这个机器没有运行,不论是cron调度还是加载的任务都是不可运行的。因此,考虑一个非常简单的分布式的例子 ——— 我们使用两台机器,然后cron调度在其中一台机器上运行job任务(比如通过ssh)。然后产生了一个故障域了:调度任务和目标服务器都可能失败。

另外一个需要注意的地方是,即使是crond重启(包括服务器重启),上面部署的crontab配置也不应该丢失。crond执行一个job然后就‘忘记’这个job的状态,他并不会尝试去跟踪这个job的执行状态,包括是否该执行是否已经执行。

anacron是一个例外,他是crontab的一个补充,它尝试运行哪些因为服务器down而应该执行却没执行的任务。这仅限于每日或者更小执行频率的job,但对于在工作站和笔记本电脑上运行维护工作非常有用。通过维护一个包括最后执行时间的配置文件,使得运行这些特殊的任务更加方便。

Cron的jobs和幂等性

Cron的jobs用来执行定期任务,但是除此之外,却很难在进一步知道他们的功能。让我们先把要讨论的主题抛开一边,现在先来就Cron Jobs本身来做下探讨,因为 只有理解了Cron Jobs的各种各样的需求,才能知道他是如何影响我们需要的可靠性要求,而这一方面的探讨也将贯穿接下来的文章。

有一些Cron任务是幂等性的,这样在某些系统故障的情况下,可以很安全的执行他们多次,比如,垃圾回收。然而有些Cron任务却不应该被执行多次,比如某个发送邮件的任务。

还有更复杂的情况,有些Cron任务允许因为某些情况而“忘了”运行,而某些Cron任务却不能容忍这些,比如,垃圾回收的Cron任务没5分钟调度一次,即使某一次没有执行也不会有太大的问题,然而,一个月一次的支付薪水的任务,却绝对不允许有失误。

Cron Jobs的大量不同的类型使得不可能有一个通用的解决方案,使得它可以应对各种各样的Fail。所以,在本文中上面说的那些情况,我们更倾向于错过某一次的运行,而不是运行它们二次或者更多。Cron 任务的所有者应该(也必须)监控着他们的任务,比如返回任务的调用结果,或者单独发送运行的日志给所属者等等,这样,即使跳过了任务的某次执行,也能够很方便的采取对应的补救动作。当任务失败时,我们更倾向于将任务状态置为“fail closed”来避免产生系统性的不良状态。

大规模部署Cron

当从单机到集群部署Cron时,需要重新思考如何使Cron在这种环境下良好的运行。在对google的Cron进行解说之前,让我们先来讨论下单机以及多机之间的区别,以及针对这变化如何设计。

扩展基础架构

常规的Cron仅限于单个机器,而大规模部署的Cron解决方案不能仅仅绑定到一个单独的机器。
假设我们拥有一个1000台服务器的数据中心,如果即使是1/1000的几率造成服务器不可用都能摧毁我们整个Cron服务,这明显不是我们所希望的。

所以,为了解决这个问题,我们必须将服务与机器解耦。这样如果想运行一个服务,那么仅仅需要指定它运行在哪个数据中心即可,剩下的事情就依赖于数据中心的调度系统(当然前提是调度系统也应该是可靠的),调度系统会负责在哪台或者哪些机器上运行服务,以及能够良好的处理机器挂掉这种情况。 那么,如果我们要在数据中心中运行一个Job,也仅仅是发送一条或多条RPC给数据中心的调度系统。

然而,这一过程显然并不是瞬时完成的。比如,要检查哪些机器挂掉了(机器健康检查程序挂了怎么办),以及在另外一些机器上重新运行任务(服务依赖重新部署重新调用Job)都是需要花费一定时间的。

将程序转移到另外一个机器上可能意味着损失一些存储在老机器上的一些状态信息(除非也采用动态迁移),重新调度运行的时间间隔也可能超过最小定义的一分钟,所以,我们也必须考虑到上述这两种情况。一个很直接的做法,将状态文件放入分布式文件系统,如GFS,在任务运行的整个过程中以及重新部署运行任务时,都是用他来记录使用相关状态。 然而,这个解决方案却不能满足我们预期的时效性这个需求,比如,你要运行一个每五分钟跑一次的Cron任务,重新部署运行消耗的1-2分钟对这个任务来说也是相当大的延迟了。

及时性的需求可能会促使各种热备份技术的使用,这样就能够快速记录状态以及从原有状态快速恢复。

需求扩展

将服务部署在数据中心和单服务器的另一个实质性的区别是,如何规划任务所需要的计算资源,如CPU或MEM等。

单机服务通常是通过进程来进行资源隔离,虽然现在Docker变得越来越普遍,但是使用他来隔离一切目前也不太是很通用的做法,包括限制crond以及它所要运行的jobs

大规模部署在数据中心经常使用容器来进行资源隔离。隔离是必要的,因为我们肯定希望数据中心中运行的某个程序不会对其他程序产生不良影响。为了隔离的有效性,在运行前肯定得先预知运行的时候需要哪些资源——包括Cron系统本身和要运行的任务。这又会产生一个问题,即 如果数据中心暂时没有足够的资源,那么这个任务可能会延迟运行。这就要求我们不仅要监控Cron任务加载的情况,也要监控Cron任务的全部状态,包括开始加载到终止运行。

现在,我们希望的Cron系统已经从单机运行的情况下解耦,如之前描述的那样,我们可能会遇到部分任务运行或加载失败。这时候辛亏Job配置的通用性,在数据中心中运行一个新的Cron任务就可以简单的通过RPC调用的方式来进行,不过不幸的是,这样我们只能知道RPC调用成功,却无法具体知道任务失败的具体地方,比如,任务在运行的过程中失败,那么恢复程序还必须将这些中间过程处理好。

在故障方面,数据中心远比一台单一的服务器复杂。Cron从原来仅仅的一个单机二进制程序,到整个数据中心运行,其期间增加了很多明显或不明显的依赖关系。作为像Cron这样的一个基础服务,我们希望得到保证的是,即使在数据中心中运行发生了一些“Fail”(如 部分机器停电或存储挂掉),服务依然能够保证功能性正常运行。为了提高可靠性,我们应该将数据中心的调度系统部署在不同的物理位置,这样,即使一个或一部分电源挂掉,也能保证至少Cron服务不会全部不可用。

Google的Cron是如何建设的

现在让我们来解决这些问题,这样才能在一个大规模的分布式集群中部署可靠的Cron服务,然后在着重介绍下Google在分布式Cron方面的一些经验。

跟踪Cron任务的状态

向上面描述过的那样,我们应该跟踪Cron任务的实时状态,这样,即使失败了,我们也更加容易恢复它。而且,这种状态的一致性是至关重要的:相比错误的多运行10遍相同的Cron任务,我们更能接受的是不去运行它。回想下,很多Cron任务,他并不是幂等性的,比如发送通知邮件。

我们有两个选项,将cron任务的数据通通存储在一个靠谱的分布式存储中,或者 仅仅保存任务的状态。当我们设计分布式Cron服务时,我们采取的是第二种,有如下几个原因:

分布式存储,如GFS或HDFS,往往用来存储大文件(如 网页爬虫程序的输出等),然后我们需要存储的Cron状态却非常非常小。将如此小的文件存储在这种大型的分布式文件系统上是非常昂贵的,而且考虑到分布式文件系统的延迟,也不是很适合。

像Cron服务这种基础服务,它需要的依赖应该是越少越好。这样,即使部分数据中心挂掉,Cron服务至少也能保证其功能性并持续一段时间。这并不意味着存储应该直接是Cron程序的一部分(这本质上是一个实现细节).Cron应该是一个能够独立运作的下游系统,以便供用户操作使用。

使用Paxos

我们部署多个实例的Cron服务,然后通过Paxos算法来同步这些实例间的状态。

Paxos算法和它其他的替代算法(如Zab,Raft等)在分布式系统中是十分常见的。具体描述Paxos不在本文范围内,他的基本作用就是使多个不可靠节点间的状态保持一致,只要大部分Paxos组成员可用,那么整个分布式系统,就能作为一个整体处理状态的变化。

分布式Cron使用一个独立的master job,见图Figure 1,只有它才能更改共享的状态,也只有它才能加载Cron任务。我们这里使用了Paxos的一个变体——Fast Paxos,这里Fast Paxos的主节点也是Cron服务的主节点。

如果主节点挂掉,Paxos的健康检查机制会在秒级内快速发现,并选举出一个新的master。一旦选举出新的主节点,Cron服务也就随着选举出了一个新的主节点,这个新的主节点将会接手前一个主节点留下的所有的未完成的工作。在这里Cron的主节点和Paxos的主节点是一样的,但是Cron的主节点需要处理一下额外的工作而已。快速选举新的主节点的机制可以让我们大致可以容忍一分钟的故障时间。

我们使用Paxos算法保持的最重要的一个状态是,哪些Cron任务在运行。对于每一个运行的Cron任务,我们会将其加载运行的开始以及结束 同步给一定数量的节点。

Master和Slave角色

如上面描述的那样,我们在Cron服务中使用Paxos并部署,其拥有两个不同的角色,master 以及 slave。让我们来就每个角色来做具体的描述。

The Master

主节点用来加载Cron任务,它有个内部的调度系统,类似于单机的crond,维护一个任务加载列表,在指定的时间加载任务。

当任务加载的时刻到来,主节点将会 “宣告” 他将会加载这个指定的任务,并且计算这个任务下次的加载时间,就像crond的做法一样。当然,就像crond那样,一个任务加载后,下一次的加载时间可能人为的改变,这个变化也要同步给slave节点。简单的标示Cron任务还不够,我们还应该将这个任务与开始执行时间相关联绑定,以避免Cron任务在加载时发生歧义(特别是那些高频的任务,如一分钟一次的那些)。这个“通告”通过Paxos来进行。图2 展示了这一过程。

保持Paxos通讯同步非常重要,只有Paxos法定数收到了加载通知,这个指定的任务才能被加载执行。Cron服务需要知道每个任务是否已经启动,这样即使master挂掉,也能决定接下来的动作。如果不进行同步,意味着整个Cron任务运行在master节点,而slave无法感知到这一切。如果发生了故障,很有可能这个任务就被再次执行,因为没有节点知道这个任务已经被执行过了。

Cron任务的完成状态通过Paxos通知给其他节点,从而保持同步,这里要注意一点,这里的 “完成” 状态并不是表示任务是成功或者失败。我们跟踪cron任务在指定调用时间被执行的情况,我们同样需要处理一点情况是,如果Cron服务在加载任务进行执行的过程中失败后怎么办,这点我们在接下来会进行讨论。

master节点另一个重要的特性是,不管是出于什么原因master节点失去了其主控权,它都必须立马停止同数据中心调度系统的交互。主控权的保持对于访问数据中心应该是互斥了。如果不这样,新旧两个master节点可能会对数据中心的调度系统发起互相矛盾的操作请求。

the slave

slave节点实时监控从master节点传来的状态信息,以便在需要的时刻做出积极响应。所有master节点的状态变动信息,都通过Paxos传到各个slave节点。和master节点类似的是,slave节点同样维持一个列表,保存着所有的Cron任务。这个列表必须在所有的节点保持一致(当然还是通过Paxos)。

当接到加载任务的通知后,slave节点会将此任务的下次加载时间放入本地任务列表中。这个重要的状态信息变化(这是同步完成的)保证了系统内部Cron作业的时间表是一致的。我们跟踪所有有效的加载任务,也就是说,我们跟踪任务何时启动,而不是结束。

如果一个master节点挂掉或者因为某些原因失联(比如,网络异常等),一个slave节点有可能被选举成为一个新的master节点。这个选举的过程必须在一分钟内运行,以避免Cron任务丢失的情况。一旦被选举为master节点,所有运行的加载任务(或 部分失败的),必须被重新验证其有效性。这个可能是一个复杂的过程,在Cron服务系统和数据中心的调度系统上都需要执行这样的验证操作,这个过程有必要详细说明。

故障恢复

如上所述,master节点和数据中心的调度系统之间会通过RPC来加载一个逻辑Cron任务,但是,这一系列的RPC调用过程是有可能失败的,所以,我们必须考虑到这种情况,并且处理好。

回想下,每个加载的Cron任务会有两个同步点:开始加载以及执行完成。这能够让我们区分开不同的加载任务。即使任务加载只需要调用一次RPC,但是我们怎么知道RPC调用实际真实成功呢?我们知道任务何时开始,但是如果master节点挂了我们就不会知道它何时结束。

为了解决这个问题,所有在外部系统进行的操作,要么其操作是幂等性的(也就是说,我们可以放心的执行他们多次),要么我们必须实时监控他们的状态,以便能清楚的知道何时完成。

这些条件明显增加了限制,实现起来也有一定的难度,但是在分布式环境中这些限制却是保证Cron服务准确运行的根本,能够良好的处理可能出现的“fail”。如果不能妥善处理这些,将会导致Cron任务的加载丢失,或者加载多次重复的Cron任务。

大多数基础服务在数据中心(比如Mesos)加载逻辑任务时都会为这些任务命名,这样方便了查看任务的状态,终止任务,或者执行其他的维护操作。解决幂等性的一个合理的解决方案是将执行时间放在名字中 ——这样不会在数据中心的调度系统里造成任务异变操作 —— 然后在将他们分发给Cron服务所有的节点。如果Cron服务的master节点挂掉,那么新的master节点只需要简单的通过预处理任务名字来查看其对应的状态,然后加载遗漏的任务即可。

注意下,我们在节点间保持内部状态一致的时候,实时监控调度加载任务的时间。同样,我们也需要消除同数据中心调度交互时可能发生的不一致情况,所以这里我们以调度的加载时间为准。比如,有一个短暂但是频繁执行的Cron任务,它已经被执行了,但是在准备把情况通告给其他节点时,master节点挂了,并且故障时间持续的特别长——长到这个cron任务都已经成功执行完了。然后新的master节点要查看这个任务的状态,发现它已经被执行完成了,然后尝试加载他。如果包含了这个时间,那么master节点就会知道,这个任务已经被执行过了,就不会重复执行第二次。

在实际实施的过程中,状态监督是一个更加复杂的工作,他的实现过程和细节依赖与其他一些底层的基础服务,然而,上面并没有包括相关系统的实现描述。根据你当前可用的基础设施,你可能需要在冒险重复执行任务跳过执行任务 之间做出折中选择。

状态保存

使用Paxos来同步只是处理状态中遇到的其中一个问题。Paxos本质上只是通过一个日志来持续记录状态改变,并且随着状态的改变而进行将日志同步。这会产生两个影响:第一,这个日志需要被压缩,防止其无限增长;第二,这个日志本身需要保存在一个地方。

为了避免其无限增长,我们仅仅取状态当前的快照,这样,我们能够快速的重建状态,而不用在根据之前所有状态日志来进行重演。比如,在日志中我们记录一条状态 “计数器加1”,然后经过了1000次迭代后,我们就记录了1000条状态日志,但是我们也可以简单的记录一条记录 “将计数器设置为1000”来做替代。

如果日志丢失,我们也仅仅丢失当前状态的一个快照而已。快照其实是最临界的状态 —— 如果丢失了快照,我们基本上就得从头开始了,因为我们丢失了上一次快照与丢失快照 期间所有的内部状态。从另一方面说,丢失日志,也意味着,将Cron服务拉回到有记录的上一次快照所标示的地方。

我们有两个主要选择来保存数据: 存储在外部的一个可用的分布式存储服务中,或者,在内部一个系统来存储Cron服务的状态。当我们设计系统时,这两点都需要考虑。

我们将Paxos日志存储在Cron服务节点所在服务器本地的磁盘中。默认的三个节点意味着,我们有三份日志的副本。我们同样也将快照存储在服务器本身,然而,因为其本身是非常重要的,我们也将它在分布式存储服务中做了备份,这样,即使小概率的三个节点机器都故障了,也能够服务恢复。

我们并没有将日志本身存储在分布式存储中,因为我们觉得,丢失日志也仅仅代表最近的一些状态丢失,这个我们其实是可以接受的。而将其存储在分布式存储中会带来一定的性能损失,因为它本身在不断的小字节写入不适用与分布式存储的使用场景。同时三台服务器全故障的概率太小,但是一旦这种情况发生了,我们也能自动的从快照中恢复,也仅仅损失从上次快照到故障点的这部分而已。当然,就像设计Cron服务本身一样,如何权衡,也要根据自己的基础设施情况来决定。

将日志和快照存本地,以及快照在分布式存储备份,这样,即使一个新的节点启动,也能够通过网络从其他已经运行的节点处获取这些信息。这意味着,启动节点与服务器本身并没有任何关系,重新安排一个新的服务器(比如重启)来担当某个节点的角色 其本质上也是影响服务的可靠性的问题之一。

运行一个大型的Cron

还有一些其他的、小型的,但是同样有趣的一些case或能影响部署一个大型的Cron服务。传统的Cron规模很小:最多包含数十个Cron任务。然而,如果在一个数据中心的超过千台服务器来运行Cron服务,那么你就会遇到各种各样的问题。

一个比较大的问题是,分布式系统常常要面临的一个经典问题:惊群问题,在Cron服务的使用中会造成大量的尖峰情况。当要配置一个每天执行的Cron任务,大多数人第一时间想到的是在半夜执行,然后他们就这么配置了。如果一个Cron任务在一台机器上执行,那没有问题,但是如果你的任务是执行一个涉及数千worker的mapreduce任务,或者,有30个不同的团队在数据中心中要配置这样的一个每天运行的任务,那么我们就必须要扩展下Crontab的格式了。

传统的crontab,用户通过定义“分钟”,“小时”,“每月(或每周)第几天”,“月数”来指定cron任务运行的时间,或者通过星号(*)来代表每个对应的值。如,每天凌晨运行,它的crontab格式为“0 0 * * *”,代表每天的0点0分运行。我们在此基础之上还推出了问号(?)这个符号,它标示,在这个对应的时间轴上,任何时间都可以,Cron服务就会自由选择合适的值,在指定的时间段内随机选择对应的值,这样使任务运行更均衡。如“0 ? * * *”,表示每天0-23点钟,随机一个小时的0分来运行这个任务。

尽管加了这项变化,由cron任务所造成的load值仍然有明显的尖峰,图3表示了google中cron任务加载的数量。尖峰值往往表示那些需要固定频率在指定时间运行的任务。

总结

Cron服务作为UNIX的基础服务已经有接近10年。当前整个行业都朝着大型分布式系统演化,那时,表示硬件的最小单位将会是数据中心,那么大量的技术栈需要对应改变,Cron也不会是例外。仔细审视下Cron服务所需要的服务特性,以及Cron任务的需求,都会推动我们来进行新的设计。

基于google的解决方案,我们已经讨论了Cron服务在一个分布式系统中对应的约束和可能的设计。这个解决方案需要在分布式环境中的强一致性保证,它的实现核心是通过Paxos这样一种通用的算法,在一个不可靠的环境中达成最终一致。使用Paxos,正确对大规模环境下Cron任务失败情况的分析,以及分布式的环境的使用,共同造就了在google内部使用的健壮的Cron服务。


原文:https://queue.acm.org/detail....