1、Prefect重试机制概述:为什么需要重试、核心概念与可靠性
1.1 为什么需要重试?—— 我踩过的那些坑
先讲个真实故事。几年前我在做一个金融数据管道,每天凌晨跑批处理。有一回上游数据库连接池满了,任务直接抛了个 ConnectionTimeout。整个 DAG 挂了,第二天老板拍桌子问为什么报表没出来。
其实问题很简单——网络抖动、资源竞争、服务重启,这些在分布式系统里就跟吃饭喝水一样常见。你想想看,一个跑 3 小时的 ETL 流程,中间任何一步被临时故障打断,难道都要人工介入重启?
这就是重试机制存在的理由。它不是为了掩盖代码 bug,而是为了应对那些「过一会儿自己就好了」的瞬时故障。
核心观点:重试不是万能药,但它是工作流可靠性的第一道防线。我个人的习惯是:所有涉及外部 I/O 的任务,默认都配上重试——除非你能证明它永远不会失败。
常见的需要重试的场景包括:
- 网络超时:API 调用、数据库连接、消息队列发送
- 资源竞争:锁等待、连接池耗尽、限流被拒
- 临时服务不可用:容器重启、负载均衡切换、DNS 缓存过期
- 幂等性操作:写入操作本身是幂等的,重试不会产生副作用
注意:非幂等操作(如扣款、发消息)不能简单重试。Prefect 本身不帮你判断幂等性,你得自己保证——要么用事务,要么用去重 ID。
1.2 Prefect 重试的核心概念
Prefect 的重试机制,说白了就是三个参数:最多重几次、每次等多久、什么情况下重试。嗯,就这么简单。但背后有几个概念你得先搞清楚。
| 概念 | 说明 | 我常用的值 |
|---|---|---|
retries |
最大重试次数(不包括首次执行) | 2~3 次,再多就说明有问题了 |
retry_delay_seconds |
每次重试前的等待时间(秒) | 10~60,看下游恢复速度 |
retry_delay |
更灵活的延迟策略(支持列表或函数) | 指数退避时用 |
retry_condition |
自定义重试条件(比如只对特定异常重试) | 只重试 TimeoutError |
举个例子,一个最简单的重试配置长这样:
from prefect import task, Flow
@task(
retries=3,
retry_delay_seconds=10,
retry_condition=lambda exc: isinstance(exc, ConnectionError)
)
def fetch_data(url: str) -> dict:
# 如果抛出 ConnectionError,最多重试 3 次,每次间隔 10 秒
# 其他异常直接失败,不重试
response = http_client.get(url)
return response.json()
我的习惯:永远给 retry_condition 加一个白名单。我曾经犯过错误——没加条件,结果一个 ValueError(代码 bug)被重试了 5 次,白白浪费了 5 分钟。从那以后,我只对网络类、资源类异常做重试。
1.3 重试与工作流可靠性的关系
重试不是孤立的。它跟工作流的整体可靠性设计是绑在一起的。我画了一张图,帮你理清它们之间的关系。
从这张图你可以看到,重试机制不是孤立的。它需要跟其他可靠性手段配合:
- 幂等性设计:重试的前提是「重复执行不会出问题」。如果你的任务不是幂等的,重试反而会制造脏数据。
- 状态持久化:Prefect 会把每次重试的状态记录到数据库。这样即使调度器挂了,重启后也能知道「这个任务已经重试过 2 次了」。
- 监控与告警:重试次数多了,说明系统有隐患。我习惯在重试超过 3 次时触发告警——不是等任务彻底失败才处理。
一句话总结:重试是工作流可靠性的「安」,但不是「救生艇」。它能兜住瞬时故障,但解决不了设计缺陷。我见过太多团队把重试次数设成 10 次,结果任务挂了 10 次才放弃——这其实是在掩盖问题。
1.4 什么时候不该用重试?
嗯,这里要泼点冷水。重试不是银弹。下面这几种情况,我建议你直接让任务失败:
- 业务逻辑错误:比如参数校验失败、数据格式不对。重试一万次结果也一样。
- 非幂等操作:比如发送邮件、扣减库存。除非你做了去重处理。
- 下游已经明确拒绝:比如 HTTP 返回 403/404。重试只会加重对方负担。
- 重试成本太高:比如每次重试要重新下载 10GB 数据。这时候应该考虑断点续传,而不是简单重试。
避坑指南:我曾经在一个数据同步任务上设了 5 次重试,每次间隔 30 秒。结果下游 API 因为限流返回 429,我这边还在拼命重试——等于在给人家 DDoS。后来我加了 retry_condition,只对 5xx 重试,4xx 一律跳过。