本文目录导读:

追踪定时任务状态通常需要结合任务本身的特性(是否有返回值、是否独立进程)以及监控系统,以下是几种主流且实用的脚本追踪方案:
核心方法:日志 + 标记位(最通用)
这是最基础、最可靠的方案,适用于任何语言编写的脚本。
原理: 在脚本的关键节点(开始、成功、失败、异常)写入带有时间戳的日志,并更新一个状态文件或数据库记录。
脚本示例 (Shell/Bash):
#!/bin/bash
TASK_NAME="daily_report"
STATUS_DIR="/var/log/task_status/"
PID_FILE="/var/run/${TASK_NAME}.pid"
LOG_FILE="/var/log/${TASK_NAME}.log"
# 1. 记录开始状态
echo "[$(date '+%Y-%m-%d %H:%M:%S')] TASK_STARTED" >> "$LOG_FILE"
echo "RUNNING" > "${STATUS_DIR}${TASK_NAME}.status"
echo $$ > "$PID_FILE" # 记录进程ID,方便 kill
# 2. 执行实际业务逻辑
python3 /app/scripts/generate_report.py
# 假设 $? 获取退出码
EXIT_CODE=$?
# 3. 更新最终状态
if [ $EXIT_CODE -eq 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] TASK_SUCCESS" >> "$LOG_FILE"
echo "SUCCESS" > "${STATUS_DIR}${TASK_NAME}.status"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] TASK_FAILED (code: $EXIT_CODE)" >> "$LOG_FILE"
echo "FAILED" > "${STATUS_DIR}${TASK_NAME}.status"
fi
# 清理PID文件
rm -f "$PID_FILE"
追踪方式:
- 手动查看:
cat /var/log/task_status/daily_report.status - 监控脚本: 另一个守护进程定期读取
.status文件,如果超过预期执行时间且状态为RUNNING,则报警。 - 日志聚合: 将
LOG_FILE发送到 ELK (Elasticsearch, Logstash, Kibana)、Splunk 等。
利用流程控制工具(推荐生产环境)
对于复杂的任务依赖、重试、超时管理,不要自己写脚本追踪,而是用成熟的工具。
A. 使用 Systemd Timer (Linux)
Systemd 自带状态追踪,非常适合系统级定时任务。
示例: report.service (任务单元)
[Unit] Description=Daily Report Generator [Service] Type=oneshot ExecStart=/usr/local/bin/generate-report.sh # 标准输出/错误记录到 journal StandardOutput=journal StandardError=journal
示例: report.timer (定时触发器)
[Unit] Description=Run daily report at 2am [Timer] OnCalendar=daily Persistent=true # 如果错过执行时间,立即补跑 [Install] WantedBy=timers.target
追踪命令:
# 查看所有 timer 状态及最近运行时间 systemctl list-timers --all # 查看特定服务的最后几次运行日志 journalctl -u report.service --since "1 hour ago"
优点: 无需额外代码,systemctl 直接提供 Active、Last trigger、Next trigger 状态。
B. 使用工作流引擎 (Airflow, Prefect, Dagster)
适用于几十上百个任务,需要依赖关系和重试逻辑的场景。
追踪方式: 这些工具自带 Web UI,显示任务的 运行中、成功、失败、重试中、已跳过 等状态,并提供详细的日志链路,脚本本身只需返回 0/非0 退出码即可。
分布式/跨机器场景:心跳 + 外部存储
当脚本运行在不同的服务器上,需要统一监控时,需要将状态写入共享存储。
方案: 脚本周期性地向 Redis 或 数据库 写入“心跳”记录。
示例 (Python 脚本):
import redis, os, time
r = redis.Redis(host='monitor_host', db=0)
task_id = "data_cleanup"
def update_status(status, msg=""):
# 使用 Hash 存储结构化状态
r.hset(f"task:{task_id}", mapping={
"status": status, # running, success, failed
"message": msg,
"pid": os.getpid(),
"timestamp": time.time(),
"host": os.uname().nodename
})
# 设置过期时间,防止僵尸记录
r.expire(f"task:{task_id}", 3600)
try:
update_status("running", "开始处理数据...")
# 模拟业务逻辑
time.sleep(30)
# 可以在循环中更新进度:r.hset(f"task:{task_id}", "progress", "50%")
update_status("success", "任务完成")
except Exception as e:
update_status("failed", str(e))
外部监控脚本 (使用另一个脚本定期检查):
import redis
r = redis.Redis(...)
task_info = r.hgetall("task:data_cleanup")
if task_info.get(b'status') == b'running' and (time.time() - float(task_info[b'timestamp'])) > 600:
alert("任务 data_cleanup 已运行超过10分钟,可能卡死!")
特殊情况处理
情况1:任务被直接 Kill 或系统重启
问题: 脚本来不及更新状态文件。 解决方案:
- 使用 Systemd:它会自动追踪服务状态,被 Kill 后会显示
failed。 - 使用 PID 文件 + 计数器:如果监控脚本发现 PID 文件存在但进程不存在,则认为任务异常中断。
情况2:脚本是后台长期运行的 Daemon(而非一次性的 Cron Job)
方案: 上述的心跳机制最适合,单独写一个健康检查端点(如 HTTP API),返回 {"status": "healthy", "last_heartbeat": "...}"。
我应该选哪种?
| 你的场景 | 推荐方案 |
|---|---|
| 1-2个简单脚本,在单个 Linux 服务器上 | 日志 + 状态文件 (Shell 脚本) |
| 多个系统级任务,希望零代码管理 | Systemd Timer |
| 任务有依赖链、失败重试、UI 监控 | Airflow / Prefect |
| 脚本运行在多个服务器,需要统一监控 | Redis 心跳 (Python/Go) |
| 只需知道“有没有跑完” | 退出码 $? + 外部监控定时检查最后一次执行时间 |
最后的重要提醒:
- 永远不要只靠脚本定时任务,一定要在任务内部埋点。
- 幂等性:确保你的脚本被重复执行不会导致数据错误(即使状态追踪失败,重跑也要安全)。
- 报警要即时:建议在脚本异常时直接发邮件/飞书/钉钉/Datadog,不要只写日志等别人看。