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 重试与工作流可靠性关系图 重试与工作流可靠性的关系 工作流可靠性 幂等性设计 状态持久化 监控与告警 重试次数 延迟策略 条件过滤 重试是「事后补救」,幂等性是「事前保障」 两者结合,才能构建真正可靠的工作流

从这张图你可以看到,重试机制不是孤立的。它需要跟其他可靠性手段配合:

  • 幂等性设计:重试的前提是「重复执行不会出问题」。如果你的任务不是幂等的,重试反而会制造脏数据。
  • 状态持久化:Prefect 会把每次重试的状态记录到数据库。这样即使调度器挂了,重启后也能知道「这个任务已经重试过 2 次了」。
  • 监控与告警:重试次数多了,说明系统有隐患。我习惯在重试超过 3 次时触发告警——不是等任务彻底失败才处理。

一句话总结:重试是工作流可靠性的「安」,但不是「救生艇」。它能兜住瞬时故障,但解决不了设计缺陷。我见过太多团队把重试次数设成 10 次,结果任务挂了 10 次才放弃——这其实是在掩盖问题。

1.4 什么时候不该用重试?

嗯,这里要泼点冷水。重试不是银弹。下面这几种情况,我建议你直接让任务失败:

  1. 业务逻辑错误:比如参数校验失败、数据格式不对。重试一万次结果也一样。
  2. 非幂等操作:比如发送邮件、扣减库存。除非你做了去重处理。
  3. 下游已经明确拒绝:比如 HTTP 返回 403/404。重试只会加重对方负担。
  4. 重试成本太高:比如每次重试要重新下载 10GB 数据。这时候应该考虑断点续传,而不是简单重试。

避坑指南:我曾经在一个数据同步任务上设了 5 次重试,每次间隔 30 秒。结果下游 API 因为限流返回 429,我这边还在拼命重试——等于在给人家 DDoS。后来我加了 retry_condition,只对 5xx 重试,4xx 一律跳过。

1.5 本章小结