背景

某些业务需要周期性的定时任务,如:

  • 周期性的数据分析服务;

  • 周期性的资源回收服务;

  • 周期性的 push 服务;

等,任务类型种类多种多样。

在物理机部署的场景下,此类任务通过单机 crond 加载 crontab 的方式执行

在容器化平台部署场景下,此类定时任务面临着不同的挑战。

容器化场景下定时任务要考虑的几个问题

在容器化场景下,我们对定时任务要考虑以下几个问题

1. 定时任务是否具备幂等性(idempotent)?

有些任务具备幂等性,重复执行并不会有很严重的副作用,比如周期性的资源回收任务;但有些任务不具备幂等性,重复执行将有可能导致严重的后果,比如周期性的 push 服务,重复执行 push 动作将对终端用户带来很不好的影响;

2. 定时任务是否要求高实时性 ?

有些任务对触发时间点有着很高敏感性,比如一个每 5 分钟就运行 1 次的定时任务,期间 1~2 分钟的延迟都有可能导致任务时机错误,从而造成影响;

3. 定时任务是否需要保存状态 ?

有些有状态的任务需要将运行过程中产生的数据进行存储,下个周期任务的执行将强依赖于这部分存储数据。如果将数据存储在执行单元上,将会有单点故障且不易于迁移任务到其他节点执行,所以将状态存储于专门可靠的外部存储服务中应该更为稳妥;

4. 定时任务是否需要并发多实例执行 ?

大部分场景下的定时任务都是单实例运行,所以对应到容器化部署下,一个定时任务通常就是一个 Pod(问题 6 针对可靠性会有多实例的情况)。 有可能在某些场景,比如周期性的数据分析任务,采用并发的多实例(如 Map-Reduce 模型)也未尝不可;

5. 系统是否需要预分配运行资源 ?

在物理机部署的场景下,定期任务的执行是采用 crontab 的方式定期在某个物理机上运行,所以机器资源是已经就分配好的,不存在定期任务被触发时会因没有节点运行而失败。

在容器化部署的场景下,同样可参考物理机部署时候的做法:预分配运行资源,即预先建立 Pod 来执行定时任务。在触发时段之外的时间内,这个 Pod 处于空闲状态;

一旦触发运行时段,Pod 开始执行任务。如果不预先分配运行资源,而是等到时间触发后启动容器,有可能会遇到以下这些情况:

  • 对应 Node 上启动容器的开销(比如拉取镜像);

  • 对应 Node 上出现故障导致无法新建 Pod;

  • 当前集群资源不足,无法新建 Pod;

所以,不预先分配运行资源有可能在触发时间点时发生无法准时运行或无法运行的情况;

6. 系统是否对定时任务的执行有极高的可靠性 ?

在单物理机运行 crond 的场景下,定时任务的执行其实是一个单点。如果物理机发生故障,那么当前物理机上的定期任务都将无法执行,直到物理机故障解除或者迁移定期任务(比如在另外一台物理机上执行一样的 crontab)。

在容器化部署的场景下,可采用如下几种不同的方式来保障任务的可靠:

  • 监控定时任务的执行的状态,当发生异常的时候,重启任务

  • 每个任务都预备一定数量的热备任务,并由某个调度模块进行调度。当其中一个任务执行失败时,可快速切换到热备任务上进行执行;

7. 系统是否可以追踪定时任务执行的状态 ?

在单物理机运行 crond 的场景下,crond 并不了解当前任务执行的状态(比如上一个周期任务是否执行成功),它仅依赖 crontab 来去获取当前需要执行任务的所有信息。为了获取任务执行的状态,用户可以以分析日志的方式来获取。 在容器化场景下,系统可以对某个容器的启停有一定的状态监控,理论上可以比物理机部署场景下更能掌握任务运行的状态;

8. 系统是否可以对定时任务的元数据保持强一致 ?

在单物理机场景下,定时任务所产生的元数据(有可能没有)将无需考虑一致性的问题。结合问题 6 和问题 7,我们需要:拥有强一致的数据来获取任务的状态并可以此部分数据恢复任务执行的上下文

如果数据在同一时间内存在多个版本,我们有可能看到:一个任务明明执行了,但是状态显示并未执行,从而导致决策的错误;

Kubernetes 的 CronJob 使用

CronJob 建立在 Job 的基础上,增加了 crontab 格式的定期任务,其运行模式与 crond 一致。CronJob 有几个比较重要的配置参数:

最后执行期限的控制

由参数 .spec.startingDeadlineSeconds 配置。

如果 Job 因各种原因错过了 schedule time,该参数为执行该任务的最后期限(秒)。错过了 deadline,系统将不执行 Job,并将此次执行标记为失败;

任务执行的并发策略

由参数 .spec.concurrencyPolicy 配置。

控制 Job 并发运行的策略,一共有 3 种策略:

  • Allow:允许并发运行(默认配置);

  • Forbid:不允许并发执行,如果新的任务到时间运行而上一个任务还未结束,则新任务会被忽略而不执行;

  • Replace:如果新的任务到时间运行而上一个任务还未结束,则新任务将会替代上一个任务执行;

局限性

CronJob 有以下如下局限CronJob 有可能执行超过 1 次或者不执行,比如

  • 如果 startingDeadlineSeconds 设置过大并且允许并发任务,则有任务有可能允许超过 1 次;

  • 如果 Job 在超过 开始时间+startingDeadlineSeconds 还未调度(比如 CronJob Controller 失效),且不允许并发执行,则任务将不会被创建;

可能的方案

方案 1:预先启动单个 Pod 实例来运行 crontab

优点

  • 操作简单,方便部署;

缺点

  • 可能会造成资源的浪费(因为触发时间之外 Pod 都处于空闲状态);

  • 如果发生节点调度或代码更新,有可能在该时间窗口中导致定时任务丢失;

方案 2:物理机以 docker run 的方式运行 crontab

优点

  • 不存在节点调度问题,任务将一直在物理机上运行;

  • 代码更新方便,直接替换镜像重新运行;

缺点

  • 不方便管理:

    • 每次操作都必须直接作用物理机,有潜在的运维风险;

    • 当物理机上的 docker 挂了或者机器宕机,将以运维方式介入做定时任务的迁移;

    • 无法全局获知定期任务的状态,缺少统一的管理单元;

  • 与方案 1 类似,有潜在的资源浪费现象;

方案 3:使用 Kubernetes 的 CronJob

优点

  • 直接利用 k8s 原生功能,方便管理和状态监控;

  • 使用方便简单;

  • 按需分配运行资源,Pod 的调度和容灾全交给 k8s;

缺点

  • 无法保证每个任务执行的可靠性,可能重复执行或者不执行;

方案 4:crontab 任务与业务 Pod 混合部署

优点

  • 充分利用资源,直接将 crontab 任务嵌入到业务 Pod 中;

缺点

  • 无法控制任务的并发运行,有可能只能将 crontab 任务部署在单例运行的业务中(一般不太可能),所以这类 crontab 的应用有限,某些可重复执行的定时任务可以将其部署于多个 Pod 中;

  • 可管理性极差;

实际执行方案可能遇到的问题

综合上面描述的问题和实际的业务需求,实际执行方案需要着重考虑的问题:

  1. 可以确定定时任务的执行状态,比如:

    • 上一次执行的时间点;
    • 执行的成功/失败次数;
    • 某个周期内任务执行的次数;
  2. 任务执行失败后,要有告警信息(可通过任务日志或者 CronJob 的状态);

  3. 任务执行失败后,要采取什么策略:

    以上几种方案都没有内置的重试机制,为确保安全,这里可做:

    • 如果任务执行失败,发出告警,并以运维方式介入修复,简化逻辑,但提高了运维成本;
    • 提供某种机制,可进行有限次数的重试,有可能会复杂整个逻辑,大多数情况下,重试比不做副作用要大
  4. 任务未按时运行,可能触发什么后果,比如提前运行(很少)或者滞后运行(常见),如果对实时性要求很高且容忍一定次数的任务执行失败的话,可采用 CronJob 并设置 .spec.startingDeadlineSeconds 来控制这类情况,超过 deadline 任务将不执行;

  5. 在采用 CronJob 的时候,如果发生新老任务同时存在(即允许并发),此时若任务满足幂等性,则可容忍此类情况发生;

  6. 如果采用 CronJob ,有可能调度执行成功,但是因为其他原因,实际任务并未执行成功,这时候必须对定时任务是否执行成功有一定的健康检查方式:

    • 任务日志
    • 开发类似于探针的探测方式;

结论

综上,方案 2 和 方案 3 是相对可行的方案。因为业务定时任务的多样性,所以很难找到一个满足所有需求的方案,所以可以对业务定时任务的类型进行分类来进行不同的方案。

1. 确定定时任务的类型

如以下表格中罗列的几个关键特性的标准评判(如上文描述):

实时性 幂等性 可靠性
定时任务 A 一般,可容忍一定的误差(即允许提前或者滞后执行) 不满足,任务不可重复执行 一般,可容忍任务执行失败

2. 对不同类型的业务执行不同的方案

对实时性和可靠性要求不高的任务

可直接使用 CronJob,即方案 3,如:

实时性 幂等性 可靠性
定时任务 A 一般,可容忍一定的误差 可满足也可不满足 一般,可容忍任务执行失败

对于不满足幂等性的任务可将 CronJob 设置为 .spec.concurrencyPolicy,即禁止并发

此时,需要云平台对 CronJob 的部署和监控提供一定的支持:

  • 部署 CronJob 服务,业务可自行部署和删除定时任务,并根据自身业务特点设置配置参数(比如执行的 deadline 和任务并发执行的策略);

  • 查看/监控 CronJob 的执行状态,当某个周期任务执行失败,要及时告警;

  • 根据定时任务的内部业务日志

对实时性和可靠性要求很高的任务

可采用方案 2 ,但是必须为方案 2 的执行开发一系列的运维辅助工具以提高可管理性,此类任务特点有:

实时性 幂等性 可靠性
定时任务 B 高,只允许极小的误差范围 可满足也可不满足 高,基本不允许任务执行失败
  1. 运维部署加载了 crontab Docker 镜像到物理机上;

  2. 设置日志输入目录并设置监控脚本,任务执行失败就通知用户,并收集任务执行一些指标数据;

3. 定时任务的异常控制

场景 1:任务执行失败

策略:告警并运维处理;

场景 2:任务提前或者滞后执行

策略:实时性要求不高的任务可记录下这个情况,累计一定程度后告警;实时性要求高的立刻告警并运维处理;

场景 3:新老任务并发执行

策略:对不满足幂等操作的任务可设置方案 3 CronJob 的相关参数来避免这个情况;反正,可忽略。对于方案 2,此类情况不会发生;

4. 对现有定时任务的改造

一般由如下几项改造项:

  • 原本适应于物理机部署场景的代码必须改成适应容器化平台(比如获取运行实例的 IP);

  • 定时任务必须要有一定的执行日志输出(比如每成功运行一次就输出一条日志)以确保外部可探测任务执行的状态;

参考文档

  1. Kubernetes CronJob

  2. Kubernetes Job

  3. Reliable Cron across the Planet