交付级定时数据管道:改 JSON 声明调度,84 个单测全绿

本文代码来自一个真实的外包交付项目:一个面向「客户提需求、我改配置」的定时数据管道框架。写这篇文章时它刚通过 84 个单元测试、demo 实测跑通。不是玩具 Demo,是能直接交付买家的那种。

一、先说我为什么这么设计

给客户做定时任务,最常见的是这两种形态:

  1. 手搓 crontab + 一堆脚本。客户要「每天早上 9 点拉一次数据、归档、发个汇总」。脚本越写越多:拉数一个、清洗一个、汇总一个,每个都有自己的参数和日志,时间一长自己都记不清哪个是哪个。
  2. 上一套重型调度系统。Airflow、DolphinScheduler 这类,功能是强,但买家大多只是「定时跑几条数据流」,为这个搭集群、学 DAG,交付成本直接翻几倍。

我这次的客户需求更直白:「我要能自己改的定时任务」------今天加一个数据源、明天改个频率,不想每次找你改代码。

所以这版的架构目标就三条:

  • 配置驱动:什么时间、跑什么任务、失败怎么办,全部 JSON 声明,买家改配置不改代码
  • 编排能力:一个任务 = 有序 step 链,前一步产物自动喂给后一步
  • 交付友好 :换新环境 pip install 就能跑、运行留痕可查、失败自动告警,买家 5 分钟能上手

二、整体架构

ruby 复制代码
t3_pipeline/
├── main.py                # CLI 入口(--demo / --config / --serve / --history)
├── pipeline/              # 框架核心(7 个模块,各司其职)
│   ├── config.py          # 配置加载与语义校验(JSON + .env 覆盖)
│   ├── registry.py        # 任务注册表(内置 4 类 + @register 扩展点)
│   ├── builtin.py         # 内置任务:http_fetch / file_archive / sql_export / shell_run
│   ├── runner.py          # 执行引擎:step 链编排 + ctx 传参 + 重试 + 产物追踪
│   ├── scheduler.py       # APScheduler 封装:cron / interval / date 三种触发
│   ├── ledger.py          # 运行留痕:SQLite,--history 查询
│   ├── alerts.py          # 告警:log / SMTP / webhook 渠道抽象
│   └── logconf.py         # 日志按天轮转,默认保留 14 天
├── demo_pipe/             # 演示管道(demo 任务 + demo 配置)
├── config/                # 买家自定义管道配置放这里
├── tests/                 # 84 个单测(全部离线)
├── requirements.txt       # 依赖就两个:apscheduler + requests,其余全标准库
└── .env.example           # 凭据模板(URL/Token/SMTP 全走 .env)

模块边界一句话:config 管"声明什么",registry 管"有哪些任务能做",runner 管"怎么按顺序跑",scheduler 管"什么时候触发",ledger 管"跑了没、跑成没",alerts 管"失败了找谁"

依赖是刻意控制的------apscheduler(调度)+ requests(HTTP 拉取)就两个,配置加载、SQLite、CSV、SMTP、日志全是标准库。买家环境越干净,交付越少踩坑

三、核心设计逐块拆

1. 配置层:一个 JSON 声明整个管道

买家每天打交道的就是这个文件(截自真实 demo 配置):

jsonc 复制代码
{
  "timezone": "Asia/Shanghai",
  "output_dir": "output",
  "jobs": [
    {
      "name": "demo_daily_snapshot",          // job 名(全仓不重复)
      "description": "生成订单快照 → 统计行数 → 归档",
      "schedule": { "type": "interval", "hours": 24 },   // 每 24 小时
      "steps": [
        { "task": "demo_gen_orders", "as": "orders_csv", "params": { "rows": 120 } },
        { "task": "demo_tally_rows", "as": "meta_file", "params": { "source": "$orders_csv" } },
        { "task": "file_archive", "params": { "source": "$orders_csv", "target_dir": "archive", "layout": "date-dirs" } }
      ],
      "alert_on_failure": ["log"],
      "alert_on_success": ["log"],
      "retry_times": 0
    },
    {
      "name": "demo_sql_export",              // 每天凌晨 3 点跑 SQL 汇总
      "description": "SQLite 查询导出 CSV(演示 sql_export 内置任务)",
      "schedule": { "type": "cron", "expr": "0 3 * * *" },
      "steps": [
        { "task": "demo_gen_orders", "as": "orders_csv", "params": { "rows": 60 } },
        { "task": "demo_sql_from_csv", "as": "db_path", "params": { "source": "$orders_csv" } },
        { "task": "sql_export", "params": {
            "db": "$db_path",
            "query": "SELECT city, COUNT(*) AS cnt, ROUND(SUM(amount),2) AS total FROM orders GROUP BY city ORDER BY total DESC",
            "out": "sql/city_summary.csv" } }
      ],
      "alert_on_failure": ["log"],
      "alert_on_success": ["log"]
    }
  ]
}

三个关键设计:

  • step 链 + as 命名 :前一步用 "as": "orders_csv" 把产物(这里是文件路径)挂进共享上下文,后一步 "source": "$orders_csv" 直接引用。管道逻辑 = 配置里的 step 顺序,不用写一行胶水代码。
  • $ 引用两级查找 :先找前序 step 产物,找不到再找 .env 环境变量(URL/Token 这种敏感值走这里)。整串匹配才替换,普通字符串里的 $ 不受影响,没有歧义。
  • 三种调度interval(每 N 小时/分钟)、cron 表达式(0 3 * * *)、date(指定时刻跑一次),APScheduler 驱动,够覆盖 99% 买家需求。

配置加载时做语义校验 (不是 JSON 能解析就放行):job 名重复、task 未注册、schedule 类型不合法、必填参数缺失,全部在启动时报明确错误,绝不带病运行。启动器顺手把 .env 加载进环境(轻量自实现,连 python-dotenv 依赖都省了)。

2. 任务层:内置 4 类 + 一行代码自定义

买家 80% 的需求落在四个内置任务上:

task 干什么 要点
http_fetch HTTP GET 拉取存文件 headers/auth 走 .env;超时、状态码校验可配
file_archive 文件按日期归档 date-dirs(2026/09/)或日期前缀两种布局
sql_export SQLite 查询 → CSV 只读护栏:仅放行 SELECT / WITH
shell_run 执行 shell 命令 cwd / 超时可配

剩下的需求,写个函数 + 一个装饰器就接入(这是交付时给买家演示最多的一步):

python 复制代码
from pipeline.registry import register

@register("my_task")                 # 任务名,config 里 "task": "my_task" 引用
def my_task(params: dict, ctx: dict):
    # params: 本 step 配置($ENV 已解析、$前步产物已替换)
    # ctx:    共享上下文,__output_dir__ = 输出根目录
    out = ctx["__output_dir__"] + "/result.txt"
    open(out, "w").write("done")
    return out                        # 返回文件路径 → 自动记入运行记录产物

注册表模式的好处:框架本身永远不用改 。新需求 = 新函数 + 配置里加一个 job,框架代码零改动,测试不回归。demo 里就是靠这个机制注册了 demo_gen_orders(生成确定性订单 CSV)、demo_tally_rows(统计行数)、demo_sql_from_csv(CSV 灌 SQLite)三个演示任务。

3. 执行引擎:ctx 传值 + 双层重试

runner 按 steps 顺序执行:每个 step 从注册表取任务函数,(params, ctx) 调一次,返回值写入 ctx[step.as],下一个 step 就能 $ 引用。

失败处理做了两层,粒度分开:

  • step 级重试:单步失败重试 N 次(如 HTTP 拉取抖动,隔 5 秒重试 2 次)
  • job 级重试:整条管道失败后整体重跑(适用于"某一步把中间产物写坏了,重跑整条更稳")

重试耗尽仍失败 → job 记 failed → 触发告警。执行结果不靠猜:每个 job 的成败、耗时、重试次数、产物列表全部落 SQLite。

4. 调度层:--serve 进调度模式

配置校验通过后一条命令进调度:

bash 复制代码
python3 main.py --config config/my_pipe.json --serve   # 按配置 schedule 触发,Ctrl+C 退出

CLI 全家桶(交付文档直接抄):

bash 复制代码
python3 main.py --demo                                  # 内置演示管道立即跑一遍
python3 main.py --config x.json                         # 立即跑一遍(--run-once 语义)
python3 main.py --config x.json --only job_a            # 只跑指定 job
python3 main.py --config x.json --serve                 # 调度模式
python3 main.py --history                               # 查看最近运行记录
python3 main.py --history --status failed --limit 10    # 只看失败最近 10 条
python3 main.py --list-tasks                            # 列出已注册任务

5. 留痕:每次运行都进账本

每次执行写一条 SQLite 记录,--history 直接查(真实输出):

ini 复制代码
[  12] OK   2026-09-08 15:03:11 job=demo_daily_snapshot      dur=   312ms retries=0 | artifacts=2
[  11] OK   2026-09-08 15:03:10 job=demo_sql_export          dur=   128ms retries=0 | artifacts=1

统计: total=12 success=12 failed=0 avg_dur=211ms

产物的文件路径也记在账本里(JSON 列)。买家问"昨天那个文件生成到哪了",一条命令查出来,不用翻目录。日志按天轮转、保留 14 天,不会跑半年把磁盘撑爆------这些细节都是交付后少接一个"报障工单"的关键。

6. 告警:log / SMTP / Webhook

告警渠道做成抽象,job 配置里声明要哪些:"alert_on_failure": ["log", "smtp", "webhook"]。SMTP 凭据、Webhook URL 全走 .env,配置里只写渠道名。默认只开 log,外发渠道买家要用了才开------避免一装上就满世界发信。

四、测试:84 个单测,全部离线可跑

定时任务项目最容易栽的坑:测试里等真实调度------cron 触发动不动等几十分钟,CI 根本没法跑。

这版测试策略:把"调度触发"mock 掉,直接测执行逻辑 ;APScheduler 相关的用例用 interval=0.2s + 轮询断言,禁用真实 cron 等待。84 个用例分 4 个文件,全离线、毫秒级跑完:

bash 复制代码
python3 -m unittest discover -s tests -v
# Ran 84 tests --- OK ✅

覆盖点:配置加载与语义校验(含 .env 合并、$引用解析、非法配置报错)、内置 4 任务(含 SQL 只读护栏:DELETE/DROP 直接被拒)、runner step 链与 ctx 传值、重试耗尽转失败、产物追踪、调度注册、ledger 记录与查询、告警渠道(SMTP/Webhook 用 mock 断言触发,禁网)。

五、合规与安全(交付级必守)

  • sql_export 服务端护栏:只放行只读 SELECT / WITH ,买家把 DELETE FROM orders 写进配置也执行不了
  • 密钥/Token/SMTP 凭据严禁写进 JSON 配置 ,一律 .env.env 已 gitignore,交付源码干净)
  • 不含绕过反爬 / 破解验证码 / 未授权采集逻辑(与同系列 T1/T2 同一铁律)
  • shell_run 参数白名单化,危险命令需买家自行确认

六、已知边界(交付前说清楚,比藏着强)

  • 单机任务编排,不支持分布式调度------买家数据量到多机/海量场景,明说需要升级方案
  • 管道是线性 step 链:不分叉、不合并、无条件分支,真有 DAG 需求走定制
  • sql_export 内置只支持 SQLite(demo 零外部依赖);接 MySQL/PostgreSQL 需买家环境装驱动
  • SMTP 告警仅 SSL(465) 模式,特殊端口需求改 alerts.py 一行

边界写进 README,是为了买家预期对齐------最怕的是交付时说"都能做",上线后才发现场景不符,退款纠纷往往就这么来的。

七、复用价值

对做外包/接私活的朋友,这套设计的价值不在代码量,在决策可复制

  1. 配置驱动 → 买家能自己改需求,报价敢说"后续小改动免费/极低价",复购率上来了
  2. 依赖最小化 → 买家服务器上 pip install 两条就完事,少一个依赖少一个事故
  3. 测试先行 + mock 调度 → 定时类代码改起来有安全网,不怕改崩线上
  4. 留痕内置 → 出问题先查 ledger,不用 SSH 上去翻半天日志

代码量不大,模块职责单一,可按需裁剪。评论区聊聊你的定时任务场景,人多了我下一篇写「如何把任意"每日要跑的数据流"十分钟配置成管道」的完整实战。

⚠️ 本文代码为外包项目交付物节选,仅作技术分享;实际使用请人工审核代码并在本地完整测试。

相关推荐
mldong1 小时前
AI Agent 不能自己签字:用 Python 工作流引擎给 AI 加一道人类审批闸门
后端·python·agent
青少儿编程课堂1 小时前
贪心算法进阶:区间调度与最少资源整合解析
c++·python·算法·贪心·信息学竞赛·区间调度
Yanjun2i1 小时前
Agent学习记录六:Tool 类 + Tool Registry
开发语言·python·学习
2601_962381861 小时前
Python异步编程async/await核心用法
python·async/await·事件循环·异步编程·并发调度
量化吞吐机1 小时前
会写代码学量化,先把数据、规则和执行连清楚
人工智能·python
泡干脆面就番茄2 小时前
02_摄像头实时人脸检测与微笑识别
python·opencv
Generalzy2 小时前
像 gofmt 一样格式化 Python:Black、Ruff、YAPF、autopep8 谁才是 2026 年的首选?
开发语言·python
计算机源码社2 小时前
【大数据项目实战】基于Python数据挖掘的新能源车充电行为关联风险分析研究-基于Hadoop+Spark的电动汽车故障多维数据可视化
大数据·hadoop·python·数据挖掘·spark·毕业设计·课程设计
LlmCraft|大模型工程实践2 小时前
NLP预处理Python内置函数_02_分词辅助与过滤筛选
python·自然语言处理·easyui