2.4 定时任务:从 cron 到云函数

场景:三个脚本都要按时跑

前面三节的脚本各就各位之后,新的问题来了:它们不能总靠我手动 python xx.py 触发。需求摆在那——资讯采集每晚 23:00 跑、周报合并每周一 9:00 跑、日志备份每天凌晨 3:00 跑。最早我在自己的开发机上挂定时任务,三个月后悲剧发生:有一天我休假关了电脑,老板问昨天的行业日报呢?

这事的核心矛盾是:本机定时任务依赖一台永不开机的机器。从那时起我把脚本逐步往云上迁,这也是我们AI 工具箱项目从脚本走向服务的关键一步。

方案演进:三个阶段

阶段一:本机定时。 Linux 用 crontab,Windows 用任务计划程序,写法简单直接,适合个人工具:

# 每晚 23:05 跑采集
5 23 * * * /usr/bin/python3 /opt/tools/daily_spider.py >> /var/log/spider.log 2>&1
# 每周一 9:10 跑周报
10 9 * * 1 /usr/bin/python3 /opt/tools/weekly_report.py >> /var/log/report.log 2>&1

cron 表达式五位从左到右分别是分、时、日、月、周,新手最常记错的是周日是 0 还是 7(两个都行),以及周和日不能同时有效——写了 * * 15 * 1 想表达每月 15 号且周一,实际是 15 号或周一都会触发,这个坑开会时被点名过两次。

本机阶段还有个隐蔽问题:任务跑没跑、跑成什么样,全靠你记得去翻日志。我给本机脚本也加了一个心跳文件,每次执行完写一行时间戳进 status.txt,一周扫一眼就知道哪天漏跑了。这个习惯后来直接搬到了云端,演变成函数里的健康检查端点——本地工程化做过的功课,上云后一行没浪费。

阶段二:服务器常驻。 买了一台轻量应用服务器后,脚本搬上云,顺带解决了家里宽带抓不到海外站的问题。但纯 cron 的问题依旧在:进程崩了没人知道、日志散成渣、重试机制全靠手写。后来我给每个任务套了一层壳:

import logging, sys, time, subprocess

logging.basicConfig(
    filename="/var/log/tools/run.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

def run_with_retry(cmd, retries=3, wait=60):
    for i in range(retries):
        code = subprocess.call(cmd)
        if code == 0:
            logging.info(f"成功: {cmd}")
            return True
        logging.warning(f"第 {i+1} 次失败 退出码 {code}")
        time.sleep(wait)
    logging.error(f"彻底失败: {cmd}")
    return False

三层捕获、日志落盘、失败重试,勉强够用,但部署和监控仍然是自己造的轮子。

阶段三:云函数。 最终把这些任务迁到了云函数 SCF,用 Timer 触发器替代 cron,日志、重试、告警都是平台现成的。函数入口极简:

# -*- coding: utf-8 -*-
import json

def main_handler(event, context):
    print("触发器事件:", json.dumps(event, ensure_ascii=False))
    from jobs import daily_spider
    ok = daily_spider.run()
    return {"ok": ok, "tasks": len(ok)}

三种方案对比

迁还是不迁,看这张对比表就清楚:

维度 本机 cron 服务器常驻 云函数 SCF
在线率 依赖个人电脑 高,需运维 平台保障
日志 散落本地 需自建收集 平台自动收集
失败重试 无 手写代码 平台重试加告警
成本 零 固定月费 按调用量计费
冷启动 无 无 有,秒级波动

表格的结论很直白:个人工具本机挂着最省事;团队共享、要求在线率的服务上云函数;中间态过渡用轻量服务器。我们团队的采集和周报最终都落在云函数上,日报产出稳定后再没人半夜打电话找我要数据了,这部分部署细节在Serverless 部署入门里有完整记录。

迁移本身的顺序也有讲究,不是一锅端。我先选了最独立的采集任务试水,因为它不依赖内部网络、失败了影响也小;跑稳两周后才动周报这类有上下游依赖的任务,每迁一个都保留本机版本观察一周,确认云端输出一致再下线本机的定时。灰度迁移这套打法是从惨痛教训里学来的——当年一锅端迁移导致周一例会没有周报,全组盯着我和我的笔记本。

踩坑与排查

坑一:时区。 云函数默认多为 UTC,写 cron 表达式 0 23 * * * 本意是北京时间 23 点,实际半夜才跑。所有调度配置先确认时区,或者统一在表达式上加 8 小时,这事我在腾讯云实践的案例里也见过类似的坑。

坑二:超时。 采集脚本数据量增长后跑着跑着被掐断,云函数默认超时几十秒,改大超时阈值或者把任务拆成多次调用。函数式编程风格的要点是幂等——同一任务跑两次结果不变,这样被重试也不会出双倍的数据。

坑三:冷启动。 Timer 触发的函数长时间不跑会冷启动,首个请求慢几秒。时延敏感的功能得配合保持活跃的策略,或者老老实实用轻量服务器常驻。

坑四:依赖打包。 云函数环境里 requests、pandas 都不是自带的,依赖要一起打 zip。打包脚本固定下来后,每次升级依赖跑一遍即可,别在线上手工调整环境,环境漂移会让所有踩坑经验一夜归零。

坑五:告警最后一公里。 任务失败本身不可怕,可怕的是失败了没人知道。我在每个任务的关键路径加了企业微信 webhook 通知,几行代码的事:

import requests

def alert(title, detail):
    requests.post(
        "https://qyapi.weixin.qq.com/cgi-bin/webhook/send",
        json={
            "msgtype": "text",
            "text": {"content": f"【定时任务告警】{title}\n{detail}"},
        },
        timeout=10,
    )

告警的阈值也要克制:刚开始我每个小错都发,群里很快就把机器人免打扰了,真正的大故障反而被淹没。后来定了规矩,只有重试耗尽、影响产出时才报警,小错误进日志周报。

小结

定时任务的演进路线,其实是一个脚本走向工程化的缩影:从能跑,到稳定跑,到可观测地跑。本机阶段培养的是把需求写成代码的能力,云函数阶段考验的是把代码变成服务的能力。回头总结,最容易忽略的是任务之间的依赖关系:采集失败会让周报缺数据、备份失败会让回滚无据可依。后期我们在调度层加了依赖排序,上游失败下游自动跳过并标记原因,这套机制一直沿用到后来给大模型应用做 Agent 编排——Agent 编排那篇讲的任务规划,底层就是自己跑过无数遍的老问题。第二篇到这里,数据采集、清洗、定时的基本盘就齐了,接下来第三篇要给这些脚本加大脑:接大模型 API、建 RAG 知识库、用 Function Calling 调工具,让这套数据流水线变成真正的智能应用。如果你对最终形态感兴趣,可以先看全书目录有个全貌。

延伸阅读