4. 作业计划与调度策略

SQL Server Agent 作业的“计划”(Schedule)决定了作业在什么时间、以什么频率被触发。理解计划类型与调度语义,是作业治理中最容易踩坑、也最值得先打牢的一环。很多生产事故并非作业逻辑写错,而是计划配置导致同一作业被重复触发、多个作业在同一时间窗口争抢资源,或者本该错峰执行的作业全部挤在凌晨 2:00。

一、计划类型与调度语义

SQL Server Agent 支持四类计划:一次性计划(One time)、重复执行计划(Recurring,按频率分为每日、每周、每月)、CPU 空闲计划(Start automatically when CPU becomes idle)以及由这些计划组合而成的多计划作业。一个作业可以绑定多个计划,任一计划触发都会启动该作业。

每日计划的核心参数是“每 N 天执行一次”,配合一个具体时间点。例如“每 1 天,02:00:00 执行”,表示每天凌晨 2 点触发。每周计划需要指定星期几(可多选)与每 N 周执行一次,例如“每 1 周的周一、周三、周五 03:00 执行”。每月计划有两种表达方式:按“第 N 天”或按“第 N 个星期几”,例如“每月 1 日 01:00”或“每月最后一个工作日 23:00”。后者在月末对账、月度归档场景中非常实用,因为它能自动适配不同月份的天数差异。

需要特别注意的是,Agent 计划使用的是服务器本地时间,而不是 UTC。如果服务器时区、夏令时(DST)发生切换,计划触发时间会随之偏移。跨时区部署或使用云主机时,应确认实例所在时区,必要时通过调整计划时间或改用外部调度工具来规避。

二、CPU 空闲计划的适用边界

CPU 空闲计划的设计初衷是:当 SQL Server Agent 检测到 CPU 持续空闲(默认空闲定义与持续时间可在 Agent 属性中配置)时,才启动作业。它适合那些“不紧急、可延后、资源消耗大”的维护类任务,例如索引重建、统计信息更新、历史数据清理。

但 CPU 空闲计划在生产环境中往往不可靠:如果服务器长期处于中等以上负载,空闲条件可能永远不满足,作业就一直不执行;反之,如果空闲判定过于宽松,又可能在业务低峰期之外被触发。因此,对于有明确 SLA 要求的作业,不建议依赖 CPU 空闲计划,而应使用固定时间窗口 + 资源调控(如 Resource Governor)来约束影响。

三、计划冲突与并发控制

计划冲突主要有三种形态:

  • 同一作业被多个计划同时触发:例如一个作业既有每日 02:00 计划,又有每周一 02:00 计划,周一凌晨就会产生两次触发请求。
  • 不同作业在同一时间窗口竞争资源:多个大作业同时启动,导致 CPU、I/O、tempdb 争抢,整体耗时反而拉长。
  • 作业执行时间超过计划间隔:例如每 5 分钟执行一次的作业,单次运行需要 8 分钟,就会出现重叠执行。

SQL Server Agent 对同一作业的并发控制,取决于作业的“已启用”状态与历史记录判断。默认情况下,如果上一次执行尚未结束,新的触发请求会被跳过(不排队、不等待),并在作业历史中记录一条“作业已在运行”的信息。这意味着:Agent 本身不会让同一作业并行执行两次,但也不会补跑被跳过的这一次。对于必须“每次都要执行”的场景,这种跳过行为会造成数据缺口,需要额外设计补偿机制。

不要依赖“作业不会重叠”这一默认行为来保证业务正确性。如果作业逻辑本身不具备幂等性,或者依赖上一次执行完成后的状态,仍应在作业内部加入显式的并发控制。

四、避免作业重叠执行的工程做法

最直接的做法是在作业步骤开头加入“互斥锁”判断。常见实现是使用 sp_getapplock 获取应用锁,获取失败则直接退出本次执行:

DECLARE @rc INT;
EXEC @rc = sp_getapplock
     @Resource = 'Job_ETL_DailyLoad',
     @LockMode = 'Exclusive',
     @LockOwner = 'Session',
     @LockTimeout = 0;

IF @rc < 0
BEGIN
    PRINT '上一次执行尚未结束,本次跳过。';
    RETURN;  -- 直接结束作业步骤
END

-- 正常业务逻辑
-- ...
EXEC sp_releaseapplock @Resource = 'Job_ETL_DailyLoad', @LockOwner = 'Session';

另一种做法是使用一张“作业运行状态表”,在作业开始时插入运行标记,结束时更新为完成;若发现存在未完成的运行记录,则跳过或等待。这种方式便于审计和人工干预,但需要额外维护表结构与清理逻辑。

此外,还应从计划设计层面减少冲突:

  • 为同一作业只保留一个主计划,避免多计划在同一时间点重叠触发。
  • 将大作业错峰排列,例如索引维护 01:00、统计信息更新 03:00、数据归档 05:00,而不是全部放在 02:00。
  • 对执行时间波动大的作业,使用“每 N 分钟”计划时预留足够余量,或改为由上游作业完成后触发。
  • 在作业属性中设置合理的“执行超时”与“重试次数”,避免失败后无限重试造成堆积。

五、计划配置检查清单

下面这份清单可直接用于日常巡检或上线前评审,字段均围绕 Agent 计划本身:

检查项判断标准常见问题
计划类型与作业时效要求匹配用 CPU 空闲计划承载 SLA 作业
触发时间避开业务高峰与其它大作业多个作业集中在同一分钟
频率间隔大于作业平均执行时长每 5 分钟执行但单次需 8 分钟
多计划重叠同一作业无重复触发点每日与每周计划在周一重叠
并发保护关键作业有互斥或幂等设计依赖默认跳过行为
时区与 DST确认服务器本地时间跨时区部署未调整
作业计划类型 → 触发 → 并发控制 → 执行结果 计划类型 一次性计划 每日 / 每周 / 每月 CPU 空闲计划 多计划组合 触发与冲突 同一作业多计划重叠触发 多作业同窗口争抢资源 执行时长 > 计划间隔 时区 / DST 偏移 并发控制手段 sp_getapplock 互斥锁 运行状态表标记 错峰排布计划时间 超时与重试上限 执行结果判读 正常执行:历史记录成功 耗时在预期窗口内 被跳过:作业已在运行 需评估是否补跑 失败:步骤返回错误 检查重试与告警配置 未触发:空闲条件不满足 改用固定时间计划

把计划配置当作作业治理的第一道防线:先确认“什么时候该跑”,再确认“跑的时候会不会撞车”,最后确认“撞车了怎么发现、怎么补”。这三步做完,绝大多数因调度引发的生产问题都能提前拦截。