还有更复杂的情况,有些 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 服务拉回到有记录的上一次快照所标示的地方。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)*
[外链图片转存中…(img-z2FdarYU-1713025519594)]
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!