既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
将程序转移到另外一个机器上可能意味着损失一些存储在老机器上的一些状态信息(除非也采用动态迁移),重新调度运行的时间间隔也可能超过最小定义的一分钟,所以,我们也必须考虑到上述这两种情况。一个很直接的做法,将状态文件放入分布式文件系统,如 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 分来运行这个任务。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新