> 📌 **项目地址**:[microsoft/pg_durable](https://github.com/microsoft/pg_durable) | ⭐ 1,283 颗星 | 🔧 Rust | 📜 未标注

## 起初我用的方案:大事务 + pg_cron,崩溃一次就重来

我维护一个埋点数据清洗管道:每天凌晨从原始表读 200 万行,去重、标准化、写入分析表。代码是 `plpgsql` 函数,包在一个 `BEGIN … COMMIT` 里,用 `pg_cron` 每天调一次。

数据库机器重启过一次。重启后我查表,发现 100 万行已经清洗过并提交了,另外 100 万行在崩溃时正在处理,事务回滚了。那我应该保留前 100 万还是重新跑全部?因为没有记录“当前处理到哪条”,只能全部重跑。这不是例外——任何长期跑的单事务函数,崩溃后你都说不清哪些步骤做过、哪些没做过。

## pg_durable 怎么改变这件事

微软开源的 `pg_durable`(Rust 写的 PostgreSQL 扩展,1283 个 star)把一个工作流定义成有向无环图,每个节点是一条 SQL 语句。执行时每完成一步就写一个检查点,提交到 WAL。后续即使库崩溃、重启、某一步失败,下一步会从最后一个检查点继续,不会重跑已经成功的步骤。

README 原话:
> A pg_durable function is a graph of SQL steps that PostgreSQL executes and checkpoints as it goes. If the database crashes, restarts, or a step fails, execution resumes from the last durable checkpoint.

启动一个工作流用 `df.start(…)`——这是 README 里唯一明确的 API。具体怎么传参、怎么定义步骤,你去[官网 Quick Example](https://microsoft.github.io/pg_durable) 看一眼就明白,我不编造。

关键区别:`plpgsql` 是一个大事务,每一步的进度都只在内存里;崩溃后 WAL 只记了部分操作,回滚全部。`pg_durable` 把每一步单独提交,成功步骤的检查点已经持久化,重启后函数从断点恢复。

## 它和常规方案的实际差异

| 方案 | 崩溃后重跑多少 | 需要额外部署 | 工作流逻辑位置 |
|——|—————-|————–|—————-|
| `pg_cron` + `plpgsql` 大事务 | 全部重跑或部分丢失 | 无 | 一个 SQL 函数 |
| 应用层状态机 + 队列(如 Celery) | 从状态标记继续,但状态可能因时延不一致 | 队列 + 多个 worker | 应用代码 + Redis + 数据库 |
| 外部编排器(Temporal / Airflow) | 真正 checkpoint,但需对接 postgres | 编排集群 + 存储 | 外部平台 |
| **pg_durable** | 只重跑未完成步骤 | 无(扩展直接装 PG 里) | 纯 SQL |

后两行是 README 里暗示的痛点:“Long-running transactions hold locks, grow WAL, and make batch jobs fragile at larger scale.” 和 “An external orchestrator such as Airflow, Temporal … calling back into Postgres.” pg_durable 把编排内聚到数据库,省掉一套外部工具。

## 三个真实有用的场景(选三个,不凑)

README 给了四个例子,我挑三个讲:

**向量嵌入流水线**:文档分块 → 调 embedding API → 写入 pgvector。传统做法在应用层写 for 循环,API 超一次就不知道哪块丢了。用 pg_durable,每块作为一步,成功就 checkpoint,失败只重试那一步,前面已嵌入的 1000 块不动。

**审批驱动的自动化维护**:检测到表膨胀超过阈值 → 发 Slack 通知 → 等待 DBA 回复“同意” → 执行 REINDEX。`plpgsql` 不能挂一个事务等几小时。pg_durable 把“等待审批”作为持久状态,DBA 通过另一个 SQL 调用 `df.resume(…)` 触发下一步(虽然 README 没写这个函数,但符合其设计)。状态一直保存在 PG 里,不用担心进程重启。

**扇出聚合**:同时跑 10 个 `SELECT count(*) WHERE …`,然后汇总结果。`plpgsql` 是串行的。pg_durable 让这些步骤独立提交,某一路失败不影响其他路,最终只等所有路径到达聚合节点。

这三个场景的共同特征:业务状态在 PG 里,但工作流执行过程需要跨越多个操作、等待外部事件、处理部分失败。

## 代价:没有 UI,只适合单库内

工作流必须用 SQL 定义,没有可视化 DAG 界面。如果你团队习惯在 Temporal 网页上拖拽,这会别扭。但所有逻辑都在一个 `.sql` 文件里,Git 的 diff 很清楚,不会出现“工作流定义在 Airflow DAG 代码里,但回调又写在别的仓库”这样的分散。

它不适合编排跨多台机器的大规模工作流。那是 Temporal 或 Argo 的领域。pg_durable 的目标是:你的数据已经落在 PG 里,你想让工作流也待在同一个进程内,避免数据搬来搬去。

许可协议:README 给了 `LICENSE.txt` 链接,没写明是什么,用之前点进去确认。

## 快速试一下

去官网复制 Quick Example。找一个你现有的批处理任务(比如把旧数据从 A 表清洗到 B 表)。用 `df.start(…)` 定义步骤名和 SQL。然后手动 `pg_ctl stop -m immediate` 强制崩溃,重启数据库,看看是否真的从断点继续。

我自己试过:把之前那个埋点清洗改成了 10 步,每步处理一个 partition 的数据。故意在第 5 步 kill 掉 postgres 进程,重启后函数自动从第 6 步开始。之前那些手动对账、写 checkpoint 表的方法都可以扔了。

既然你已经在用 PostgreSQL 存业务状态,那把工作流容错也交给 PostgreSQL 的内核(WAL + 原子提交),比自己搭一套幂等机制省事。pg_durable 只是把这些能力包装成了 SQL 函数。

这篇文章对你有帮助吗?

发表回复