Cron 是 UNIX 中一个常见的工具,用来按期执行一些用户指定的任意任务。咱们先来分析下 Cron 的基本原则和它最多见的实现,而后咱们来回顾下像 Cron 这样的服务应该如何运行在一个大型的、分布式的环境中,这样即便单机故障也不会对系统可用性形成影响。 咱们将会介绍了一个创建在少许机器上的 Cron 系统,而后结合数据中心的调度服务,从而能够在整个数据中心中运行 Cron 任务。html
在咱们在描述如何运行一个靠谱的分布式 Cron 服务以前,让咱们先来从一个 SRE 的角度来回顾下 Cron。linux
Cron 是一个通用的工具,不管是管理员仍是普通用户均可以用它来在系统上运行指定的命令,以及指定什么时候运行命令,这些指定运行的命令能够是按期垃圾回收,也能够是按期数据分析。 最多见的时间指定格式被称为 crontab,它不只支持简单的时间周期(如,天天中午一次,每一个小时一次),也支持较复杂的时间周期,如每一个周6、每月的第 30 天等等。算法
Cron 一般只包含一个组件,被称为 crond,它是一个后台守护程序,加载全部须要运行的 cron 定时任务,根据它们接下来的运行时间来进行排序,而后这个守护进程将会等待直到第一个任务开始执行。在这个时刻,crond 将会加载执行这个任务,以后将它放入队列等待下一次运行。网页爬虫
可靠性安全
从可靠性的角度来看一个服务,须要有不少注意的地方。服务器
第一,好比 crond,它的故障域本质上来讲只是一台机器,若是这个机器没有运行,不管是 cron 调度仍是加载的任务都是不可运行的。所以,考虑一个很是简单的分布式的例子 ——— 咱们使用两台机器,而后 cron 调度在其中一台机器上运行任务(好比经过 ssh)。而后产生了一个故障域了:调度任务和目标服务器均可能失败。网络
另一个须要注意的地方是,即便是 crond 重启(包括服务器重启),上面部署的 crontab 配置也不该该丢失。crond 执行一个任务而后就‘忘记’了这个任务的状态,它并不会尝试去跟踪这个任务的执行状态,包括是否该执行是否已经执行。架构
anacron是一个例外,它是crontab的一个补充,它尝试运行哪些由于服务器宕机而应该执行却没执行的任务。这仅限于每日或者更小执行频率的任务,但对于在工做站和笔记本电脑上运行维护工做很是有用。经过维护一个包括最后执行时间的配置文件,使得运行这些特殊的任务更加方便。ssh
Cron 的任务和幂等性分布式
Cron 的任务用来执行按期任务,可是除此以外,却很难在进一步知道它们的功能。让咱们先把要讨论的主题抛开一边,如今先来就 Cron 任务自己来作下探讨,由于只有理解了 Cron 任务的各类各样的需求,才能知道它是如何影响咱们须要的可靠性要求,而这一方面的探讨也将贯穿接下来的文章。
有一些 Cron 任务是幂等性的,这样在某些系统故障的状况下,能够很安全的执行它们屡次,好比,垃圾回收。然而有些 Cron 任务却不该该被执行屡次,好比某个发送邮件的任务。
还有更复杂的状况,有些 Cron 任务容许由于某些状况而“忘了”运行,而某些 Cron 任务却不能容忍这些,好比,垃圾回收的 Cron 任务每 5 分钟调度一次,即便某一次没有执行也不会有太大的问题,然而,一个月一次的支付薪水的任务,却绝对不容许有失误。
Cron 任务的各类不一样的类型使得不可能有一个通用的解决方案,使得它能够应对各类各样的失败。因此,在本文中上面说的那些状况,咱们更倾向于错过某一次的运行,而不是运行它们两次或者更多。Cron 任务的全部者应该(也必须)监控着它们的任务,好比返回任务的调用结果,或者单独发送运行的日志给所属者等等,这样,即便跳过了任务的某次执行,也可以很方便的采起对应的补救动做。当任务失败时,咱们更倾向于将任务状态置为 “fail closed” 来避免产生系统性的不良状态。
大规模部署 Cron
当从单机到集群部署 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 服务不会所有不可用。
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 使用一个独立的主任务,见下图,只有它才能更改共享的状态,也只有它才能加载 Cron 任务。咱们这里使用了 Paxos 的一个变体—— Fast Paxos,这里 Fast Paxos 的主节点也是 Cron 服务的主节点。
若是主节点挂掉,Paxos 的健康检查机制会在秒级内快速发现,并选举出一个新的主节点。一旦选举出新的主节点,Cron 服务也就随着选举出了一个新的 Cron 主节点,这个新的 Cron 主节点将会接手前一个主节点留下的全部的未完成的工做。在这里 Cron 的主节点和 Paxos 的主节点是同样的,可是 Cron 的主节点须要处理一下额外的工做而已。快速选举新的主节点的机制可让咱们大体能够容忍一分钟的故障时间。
咱们使用 Paxos 算法保持的最重要的一个状态是,哪些 Cron 任务在运行。对于每个运行的 Cron 任务,咱们会将其加载运行的开始以及结束同步给必定数量的节点。
主节点和从节点角色
如上面描述的那样,咱们在 Cron 服务中使用 Paxos 并部署,其拥有两个不一样的角色,主节点以及从节点。让咱们来就每一个角色来作具体的描述。
主节点
主节点用来加载 Cron 任务,它有个内部的调度系统,相似于单机的crond,维护一个任务加载列表,在指定的时间加载任务。
当任务加载的时刻到来,主节点将会 “宣告” 它将会加载这个指定的任务,而且计算这个任务下次的加载时间,就像 crond 的作法同样。固然,就像 crond 那样,一个任务加载后,下一次的加载时间可能人为的改变,这个变化也要同步给从节点。简单的标识 Cron 任务还不够,咱们还应该将这个任务与开始执行时间相关联绑定,以免 Cron 任务在加载时发生歧义(特别是那些高频的任务,如一分钟一次的那些)。这个“通告”经过 Paxos 来进行。下图展现了这一过程。
保持 Paxos 通信同步很是重要,只有 Paxos 法定数收到了加载通知,这个指定的任务才能被加载执行。Cron 服务须要知道每一个任务是否已经启动,这样即便主节点挂掉,也能决定接下来的动做。若是不进行同步,意味着整个 Cron 任务运行在主节点,而从节点没法感知到这一切。若是发生了故障,颇有可能这个任务就被再次执行,由于没有节点知道这个任务已经被执行过了。
Cron 任务的完成状态经过 Paxos 通知给其它节点,从而保持同步,这里要注意一点,这里的“完成” 状态并非表示任务是成功或者失败。咱们跟踪 Cron 任务在指定调用时间被执行的状况,咱们一样须要处理一点状况是,若是 Cron 服务在加载任务进行执行的过程当中失败后怎么办,这点咱们在接下来会进行讨论。
主节点另外一个重要的特性是,无论是出于什么缘由主节点失去了其主控权,它都必须立马中止同数据中心调度系统的交互。主控权的保持对于访问数据中心应该是互斥了。若是不这样,新旧两个主节点可能会对数据中心的调度系统发起互相矛盾的操做请求。
从节点
从节点实时监控从主节点传来的状态信息,以便在须要的时刻作出积极响应。全部主节点的状态变更信息,都经过 Paxos 传到各个从节点。和主节点相似的是,从节点一样维持一个列表,保存着全部的 Cron 任务。这个列表必须在全部的节点保持一致(固然仍是经过 Paxos)。
当接到加载任务的通知后,从节点会将此任务的下次加载时间放入本地任务列表中。这个重要的状态信息变化(这是同步完成的)保证了系统内部 Cron 做业的时间表是一致的。咱们跟踪全部有效的加载任务,也就是说,咱们跟踪任务什么时候启动,而不是结束。
若是一个主节点挂掉或者由于某些缘由失联(好比,网络异常等),一个从节点有可能被选举成为一个新的主节点。这个选举的过程必须在一分钟内运行,以免 Cron 任务丢失的状况。一旦被选举为主节点,全部运行的加载任务(或部分失败的),必须被从新验证其有效性。这个多是一个复杂的过程,在 Cron 服务系统和数据中心的调度系统上都须要执行这样的验证操做,这个过程有必要详细说明。
故障恢复
如上所述,主节点和数据中心的调度系统之间会经过 RPC 来加载一个逻辑 Cron 任务,可是,这一系列的 RPC 调用过程是有可能失败的,因此,咱们必须考虑到这种状况,而且处理好。
回想下,每一个加载的 Cron 任务会有两个同步点:开始加载以及执行完成。这可以让咱们区分开不一样的加载任务。即便任务加载只须要调用一次 RPC,可是咱们怎么知道 RPC 调用实际真实成功呢?咱们知道任务什么时候开始,可是若是主节点挂了咱们就不会知道它什么时候结束。
为了解决这个问题,全部在外部系统进行的操做,要么其操做是幂等性的(也就是说,咱们能够放心的执行它们屡次),要么咱们必须实时监控它们的状态,以便能清楚的知道什么时候完成。
这些条件明显增长了限制,实现起来也有必定的难度,可是在分布式环境中这些限制倒是保证 Cron 服务准确运行的根本,可以良好的处理可能出现的 “fail”。若是不能妥善处理这些,将会致使 Cron 任务的加载丢失,或者加载屡次重复的 Cron 任务。
大多数基础服务在数据中心(好比 Mesos)加载逻辑任务时都会为这些任务命名,这样方便了查看任务的状态,终止任务,或者执行其它的维护操做。解决幂等性的一个合理的解决方案是将执行时间放在名字中 ——这样不会在数据中心的调度系统里形成任务异变操做 —— 而后在将它们分发给 Cron 服务全部的节点。若是 Cron 服务的主节点挂掉,那么新的主节点只须要简单的经过预处理任务名字来查看其对应的状态,而后加载遗漏的任务便可。
注意下,咱们在节点间保持内部状态一致的时候,实时监控调度加载任务的时间。一样,咱们也须要消除同数据中心调度交互时可能发生的不一致状况,因此这里咱们以调度的加载时间为准。好比,有一个短暂可是频繁执行的 Cron 任务,它已经被执行了,可是在准备把状况通告给其它节点时,主节点挂了,而且故障时间持续的特别长——长到这个 Cron 任务都已经成功执行完了。而后新的主节点要查看这个任务的状态,发现它已经被执行完成了,而后尝试加载它。若是包含了这个时间,那么主节点就会知道,这个任务已经被执行过了,就不会重复执行第二次。
在实际实施的过程当中,状态监督是一个更加复杂的工做,它的实现过程和细节依赖与其它一些底层的基础服务,然而,上面并无包括相关系统的实现描述。根据你当前可用的基础设施,你可能须要在冒险重复执行任务和跳过执行任务 之间作出折中选择。
状态保存
使用 Paxos 来同步只是处理状态中遇到的其中一个问题。Paxos 本质上只是经过一个日志来持续记录状态改变,而且随着状态的改变而进行将日志同步。这会产生两个影响:第一,这个日志须要被压缩,防止其无限增加;第二,这个日志自己须要保存在一个地方。
为了不其无限增加,咱们仅仅取状态当前的快照,这样,咱们可以快速的重建状态,而不用在根据以前全部状态日志来进行重演。好比,在日志中咱们记录一条状态 “计数器加 1”,而后通过了 1000 次迭代后,咱们就记录了 1000 条状态日志,可是咱们也能够简单的记录一条记录 “将计数器设置为 1000”来作替代。
若是日志丢失,咱们也仅仅丢失当前状态的一个快照而已。快照实际上是最临界的状态 —— 若是丢失了快照,咱们基本上就得从头开始了,由于咱们丢失了上一次快照与丢失快照期间全部的内部状态。从另外一方面说,丢失日志,也意味着,将 Cron 服务拉回到有记录的上一次快照所标示的地方。
咱们有两个主要选择来保存数据: 存储在外部的一个可用的分布式存储服务中,或者,在内部一个系统来存储 Cron 服务的状态。当咱们设计系统时,这两点都须要考虑。
咱们将 Paxos 日志存储在 Cron 服务节点所在服务器本地的磁盘中。默认的三个节点意味着,咱们有三份日志的副本。咱们一样也将快照存储在服务器自己,然而,由于其自己是很是重要的,咱们也将它在分布式存储服务中作了备份,这样,即便小几率的三个节点机器都故障了,也可以服务恢复。
咱们并无将日志自己存储在分布式存储中,由于咱们以为,丢失日志也仅仅表明最近的一些状态丢失,这个咱们实际上是能够接受的。而将其存储在分布式存储中会带来必定的性能损失,由于它自己在不断的小字节写入不适用与分布式存储的使用场景。同时三台服务器全故障的几率过小,可是一旦这种状况发生了,咱们也能自动的从快照中恢复,也仅仅损失从上次快照到故障点的这部分而已。固然,就像设计 Cron 服务自己同样,如何权衡,也要根据本身的基础设施状况来决定。
将日志和快照存本地,以及快照在分布式存储备份,这样,即便一个新的节点启动,也可以经过网络从其它已经运行的节点处获取这些信息。这意味着,启动节点与服务器自己并无任何关系,从新安排一个新的服务器(好比重启)来担当某个节点的角色 其本质上也是影响服务的可靠性的问题之一。
运行一个大型的 Cron
还有一些其它的、小型的,可是一样有趣的一些状况或能影响部署一个大型的 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 值仍然有明显的尖峰,下图表示了 Google 中 cron 任务加载的数量。尖峰值每每表示那些须要固定频率在指定时间运行的任务。
总结
Cron 服务做为 UNIX 的基础服务已经有接近 10 年。当前整个行业都朝着大型分布式系统演化,那时,表示硬件的最小单位将会是数据中心,那么大量的技术栈须要对应改变,Cron 也不会是例外。仔细审视下 Cron 服务所须要的服务特性,以及 Cron 任务的需求,都会推进咱们来进行新的设计。
基于 Google 的解决方案,咱们已经讨论了 Cron 服务在一个分布式系统中对应的约束和可能的设计。这个解决方案须要在分布式环境中的强一致性保证,它的实现核心是经过 Paxos 这样一种通用的算法,在一个不可靠的环境中达成最终一致。使用 Paxos,正确对大规模环境下 Cron 任务失败状况的分析,以及分布式的环境的使用,共同造就了在 Google 内部使用的健壮的 Cron 服务。