一、时间条件执行
时间条件是 Cron 任务中最常见的条件判断方式。通过合理配置时间表达式和结合外部条件检查,可以让任务只在指定的时间窗口内执行,避免在非预期的时间段运行。
仅工作日执行
在 Cron 表达式中,第五个字段(星期)可以控制任务仅在工作日运行。例如 0 9 * * 1-5 表示每周一到周五的早上 9 点执行,周末自动跳过。这是运维任务的常见模式,用于避开非工作时段的人员值守空窗期。
# 每个工作日早9点执行
0 9 * * 1-5 /path/to/task.sh
# 每个工作日下午6点执行
0 18 * * 1-5 /path/to/task.sh
仅工作时间执行(8:00-18:00)
通过小时字段的范围限定,可以让任务仅在工作时间区间内触发。例如 */30 8-17 * * * 表示每天 8:00 到 17:59 之间每 30 分钟执行一次。如果希望任务在 18:00 之前完成,需要将上限设置为 17,因为 Cron 的最小时间粒度是分钟,小时字段指定的是任务启动的小时范围。
# 工作时间内每15分钟执行一次
*/15 8-17 * * * /path/to/check.sh
# 仅中午12点到下午2点执行(避开早晚高峰)
* * 12-13 * * /path/to/midday_task.sh
跳过节假日和特殊日期
Cron 本身的表达式不足以表达复杂的节假日逻辑(如"跳过春节"、"跳过法定假日")。通常的解决方案是在脚本入口处添加日期判断逻辑:
#!/bin/bash
# 在任务脚本开头判断是否为节假日
if grep -q "$(date +%Y-%m-%d)" /etc/holidays.list; then
echo "今天是节假日,跳过执行"
exit 0
fi
# 后续是正常的业务逻辑
echo "执行任务..."
更高级的方案是维护一个节假日 API 接口,在脚本中远程查询当天是否为工作日,这样无需手动维护节假日列表,但需要确保网络连通性。
跨时区的时间条件调整
服务器默认使用系统时区(通常是 UTC),如果 Cron 任务需要按特定时区的时间执行,可以通过设置 CRON_TZ 环境变量来调整。在 crontab 文件中,CRON_TZ=Asia/Shanghai 可以让后续的 Cron 表达式以北京时间解释。需要注意的是,时区变化(如夏令时切换)可能导致任务跳过或重复执行,需要额外处理。
# 设置 Cron 使用北京时间
CRON_TZ=Asia/Shanghai
0 9 * * * /path/to/task.sh
二、状态条件执行
状态条件执行是指任务在执行前检查当前系统的某种状态,只有状态满足预期时才继续执行。这种模式在 Cron 中通常通过在脚本开头加入条件判断来实现,而不是由 Cron 本身直接提供。
前置条件检查:仅在特定状态下执行
任务执行前检查某个标志文件或服务状态,确保只有在前置条件满足时才运行。例如,数据库备份任务可以先检查数据库服务是否正在运行:
#!/bin/bash
# 检查 MySQL 服务是否运行
if ! systemctl is-active --quiet mysql; then
echo "MySQL 未运行,跳过备份"
exit 1
fi
# 执行备份逻辑
mysqldump --all-databases > /backup/db.sql
文件存在/不存在作为触发条件
通过判断某个锁定文件、触发文件或状态文件是否存在,可以控制任务的行为。这是 Unix 系统中经典的条件控制手段:
#!/bin/bash
LOCKFILE="/var/run/my_task.lock"
# 锁文件存在表示任务正在运行,跳过本次
if [ -f "$LOCKFILE" ]; then
echo "上次任务尚未完成,跳过"
exit 0
fi
# 创建锁文件
touch "$LOCKFILE"
# ... 任务逻辑 ...
# 清理锁文件
rm -f "$LOCKFILE"
除了锁文件,还可以用标志文件控制任务的启停。例如在共享存储中放置一个 stop_signal 文件,所有节点上的 Cron 检测到该文件后自动暂停任务执行。
进程运行状态判断
某些任务要求只有在前驱进程结束后才能执行,或者需要确保某个守护进程正在运行。通过 pgrep 或 pidof 可以检查目标进程是否存在:
#!/bin/bash
# 仅当 Nginx 运行时才执行日志轮转
if ! pgrep -x nginx > /dev/null; then
echo "Nginx 未运行,跳过日志轮转"
exit 1
fi
# 执行日志轮转
logrotate /etc/logrotate.d/nginx
命令的退出码作为条件判断
在 Shell 脚本中,上一条命令的退出码(exit code)是天然的条件判断依据。通过 $? 可以获取最近一条命令的退出码,0 表示成功,非 0 表示失败:
#!/bin/bash
# 先执行健康检查,根据结果决定是否继续
curl -f http://localhost:8080/health
if [ $? -ne 0 ]; then
echo "服务健康检查未通过,跳过部署任务"
exit 1
fi
echo "服务状态正常,开始部署..."
# 部署逻辑
利用退出码可以实现级联条件判断,只有前一步成功才执行后一步,形成条件链。
三、资源条件执行
资源条件执行是指任务在执行前检查系统资源(CPU、内存、磁盘、网络)的使用情况,只在资源充裕或条件合适时才运行。这对于资源密集型任务尤其重要,可以避免任务间互相干扰或耗尽系统资源。
系统 CPU 负载低于阈值时执行
通过 /proc/loadavg 或 uptime 命令获取系统平均负载,只有在负载低于设定阈值时才执行任务。一般建议检查 5 分钟或 15 分钟平均负载,避免短期峰值导致误判:
#!/bin/bash
# 获取 15 分钟平均负载
LOAD=$(uptime | awk -F'load average:' '{print $2}' | cut -d, -f3 | tr -d ' ')
THRESHOLD=2.0
# 使用 awk 进行浮点数比较
if awk "BEGIN {exit !($LOAD > $THRESHOLD)}"; then
echo "系统负载过高 ($LOAD),跳过编译任务"
exit 1
fi
echo "系统负载正常 ($LOAD),开始编译..."
内存充足时才运行资源密集型任务
对于需要大量内存的任务(如大型数据处理、视频转码),在执行前检查可用内存是必要的保护措施。可以检查可用物理内存而非总量,避免因 Swap 占用过高导致性能急剧下降:
#!/bin/bash
# 获取可用内存(单位 MB)
AVAILABLE_MEM=$(free -m | awk '/^Mem:/ {print $7}')
MIN_MEM=1024
if [ "$AVAILABLE_MEM" -lt "$MIN_MEM" ]; then
echo "可用内存不足 (${AVAILABLE_MEM}MB),跳过内存密集型任务"
exit 1
fi
echo "内存充足,开始数据处理..."
磁盘空间足够时才执行备份任务
备份任务是最典型的磁盘空间敏感型任务。如果磁盘空间不足,备份本身可能失败,甚至导致系统异常。应该检查目标磁盘的剩余空间,并估算备份所需的空间:
#!/bin/bash
# 检查备份目录磁盘空间(单位 KB)
AVAILABLE_SPACE=$(df /backup | awk 'NR==2 {print $4}')
ESTIMATED_NEED=$((10 * 1024 * 1024)) # 预估需要 10GB
if [ "$AVAILABLE_SPACE" -lt "$ESTIMATED_NEED" ]; then
echo "磁盘空间不足,跳过备份任务"
exit 1
fi
echo "磁盘空间充足,开始备份..."
更好的做法是结合日志清理策略,在备份前先清理过期的旧备份,释放空间后再执行新的备份。
网络连通性确认后才执行网络任务
对于需要联网的任务(如远程同步、API 调用、数据采集),在执行前先确认网络连通性可以避免超时等待和误报。常见的检查方式包括 ping 目标主机或尝试建立 TCP 连接:
#!/bin/bash
# 检查远程服务器是否可达
REMOTE_HOST="backup.example.com"
if ! ping -c 3 -W 5 "$REMOTE_HOST" > /dev/null 2>&1; then
echo "远程主机 $REMOTE_HOST 不可达,跳过同步任务"
exit 1
fi
echo "网络连通正常,开始远程同步..."
rsync -avz /local/data/ "$REMOTE_HOST":/remote/backup/
四、智能跳过策略
智能跳过是比简单条件判断更高级的执行策略。它让任务具备"自我感知"能力,能够根据工作状态的变化决定是否继续执行,避免在无工作可做的情况下空转浪费资源。
/loop 中的智能判断:任务已完成自动停止
在 Cron 的 /loop 模式下,任务会按照设定间隔重复执行。通过内部计数器或状态检测机制,任务可以判断所有工作是否已完成,一旦完成则自动退出循环。这避免了传统 Cron 任务"执行了就完成任务了"的简单模式:
#!/bin/bash
# 带自动停止的循环任务
MAX_RUNS=10
COUNTER=0
while [ $COUNTER -lt $MAX_RUNS ]; do
# 检查是否有待处理的工作项
WORK_ITEMS=$(ls /queue/pending/ 2>/dev/null | wc -l)
if [ "$WORK_ITEMS" -eq 0 ]; then
echo "所有工作已完成,自动停止循环"
break
fi
# 处理一个工作项
echo "处理第 $((COUNTER+1)) 个工作项..."
# process_one_item()
COUNTER=$((COUNTER+1))
done
状态未变化跳过本次执行
如果任务检测到系统的状态与上次执行时相比没有变化,可以跳过本次执行。这种方法适用于监控类任务或定期检查任务,避免在无变化的情况下重复执行相同的逻辑:
#!/bin/bash
STATE_FILE="/var/run/last_state.hash"
# 计算当前状态的校验值
CURRENT_HASH=$(find /watched/directory -type f -exec md5sum {} + | md5sum | cut -d' ' -f1)
# 与上次状态比较
if [ -f "$STATE_FILE" ]; then
LAST_HASH=$(cat "$STATE_FILE")
if [ "$CURRENT_HASH" = "$LAST_HASH" ]; then
echo "状态未变化,跳过本次执行"
exit 0
fi
fi
# 保存当前状态并执行
echo "$CURRENT_HASH" > "$STATE_FILE"
echo "状态已变化,执行任务..."
# 实际的业务逻辑
连续 N 次无变化自动终止循环
在循环任务中,仅跳过单次执行还不够高效。如果连续多次都检测到无变化,应该认为当前已经进入稳定状态,主动终止整个循环以避免持续的空轮询。这种做法在数据采集、日志监控等场景中尤为实用:
#!/bin/bash
STALE_COUNT_FILE="/var/run/stale_count"
MAX_STALE=5
# 读取历史无变化计数
STALE_COUNT=0
[ -f "$STALE_COUNT_FILE" ] && STALE_COUNT=$(cat "$STALE_COUNT_FILE")
# 检查是否有新数据
if ! has_new_data; then
STALE_COUNT=$((STALE_COUNT+1))
echo "连续 $STALE_COUNT 次无变化"
if [ "$STALE_COUNT" -ge "$MAX_STALE" ]; then
echo "连续 $MAX_STALE 次无变化,自动终止循环"
rm -f "$STALE_COUNT_FILE"
exit 0
fi
echo "$STALE_COUNT" > "$STALE_COUNT_FILE"
exit 0
fi
# 有新数据,重置计数器并处理
rm -f "$STALE_COUNT_FILE"
echo "检测到新数据,开始处理..."
# 处理逻辑
与动态模式配合实现智能调度
动态模式(Dynamic Mode)允许 Cron 任务在运行时动态调整其执行计划。与智能跳过策略配合时,任务可以根据当前负载、队列长度或业务指标,自适应地调整执行频率或跳过不必要的执行。例如,一个数据同步任务可以监控增量数据量:数据量大时缩短执行间隔,数据量小时增加跳过次数,无数据时完全暂停:
#!/bin/bash
# 动态调整跳过策略
QUEUE_SIZE=$(get_queue_size)
if [ "$QUEUE_SIZE" -eq 0 ]; then
# 队列为空,休眠更长时间
echo "队列为空,延长休眠周期"
exit 75 # 特殊退出码,通知 Cron 降低执行频率
elif [ "$QUEUE_SIZE" -lt 10 ]; then
echo "队列数据较少,跳过本次"
exit 0
fi
echo "队列中有 $QUEUE_SIZE 项待处理,开始执行..."
process_queue
五、条件执行最佳实践
条件执行和智能跳过虽然功能强大,但如果设计不当,反而可能引入新的问题。以下实践可以帮助你在使用中避免常见的陷阱。
条件检查的幂等性设计
条件判断本身应该是幂等的——无论执行多少次,只要系统状态不变,判断结果就应该一致。避免在条件检查中修改系统状态(如创建临时文件、改变全局变量)。如果条件判断本身有副作用,那么下次判断时"系统状态"已经被人为改变了,这将导致不可预测的行为。一个好的实践是将条件检查和业务逻辑严格分离:条件检查只读不写,业务逻辑才进行写操作。
条件不满足时的日志记录
当条件不满足导致任务跳过时,应该记录清晰的日志信息,包括:跳过的原因、当前系统状态的关键指标(如 CPU 负载值、磁盘剩余空间)、跳过的次数统计。这些日志对于排查问题和调优阈值至关重要。建议使用结构化的日志格式,便于后续分析和告警:
#!/bin/bash
# 带结构化日志的跳过记录
log_skip() {
local reason="$1"
local detail="$2"
local timestamp=$(date -Iseconds)
echo "{\"time\":\"$timestamp\",\"action\":\"skip\",\"reason\":\"$reason\",\"detail\":\"$detail\"}"
}
# 在条件判断不满足时调用
log_skip "CPU_OVERLOAD" "Current load: $LOAD, Threshold: $THRESHOLD"
避免条件判断本身消耗过多资源
条件判断本身也消耗系统资源。如果条件检查的开销接近甚至超过了任务本身,那就本末倒置了。例如,对一个文件目录进行全量递归校验和(checksum)来判断是否有变化,可能比直接同步文件还慢。应该优先使用轻量级的判断方式:用文件修改时间(mtime)替代 checksum、用系统 API 替代命令解析、用增量检查替代全量扫描。对于高频执行的任务,条件检查应该控制在毫秒级完成。
条件判断的超时保护
任何条件判断都应该有超时保护。如果条件检查本身卡住了(如 DNS 解析超时、远程 API 无响应),整个任务也会被阻塞。使用 timeout 命令可以为条件检查设置最大执行时间,超时后按"条件不满足"处理,确保任务不会无限期等待:
#!/bin/bash
# 带超时保护的条件检查
TIMEOUT=10 # 10秒超时
# 如果网络检查超过10秒,视为不满足条件
if ! timeout $TIMEOUT ping -c 1 remote-host.example.com > /dev/null 2>&1; then
echo "网络检查超时或失败,跳过任务"
exit 1
fi
echo "条件检查通过,执行任务..."
核心要点总结:条件执行与智能跳过是 Cron 任务从"机械执行"迈向"智能调度"的关键能力。时间条件控制"何时执行",状态条件控制"是否该执行",资源条件控制"能否执行",智能跳过则让任务具备自主决策的生命周期管理能力。在实际应用中,通常需要将这几种策略组合使用,形成一个多层次的条件判断体系,才能在复杂的生产环境中实现稳健可靠的任务调度。