生产中的荒谬

Armin Ronacher 工具

摘要

Armin Ronacher(pocoo)分享了他在Absurd(一个完全基于Postgres构建的持久执行系统)上的生产经验,重点介绍了诸如分解步骤、任务结果以及名为absurdctl的命令行工具等改进。

<p>大约五个月前,我写了一篇关于 <a href="/2025/11/3/absurd-workflows/">Absurd</a> 的文章,这是我们在 Earendil 为自己构建的一个持久执行系统,完全运行在 Postgres 之上,只依赖 Postgres。理念很简单:你不需要 <a href="https://hatchet.run/">独立的</a> <a href="https://www.inngest.com/">服务</a>、<a href="https://useworkflow.dev/">编译器插件</a> 或 <a href="https://temporal.io/">整个运行时</a> 来实现持久化工作流。你只需要一个 SQL 文件和一个轻量 SDK。</p> <p>从那时起,我们一直在生产环境中使用它,我觉得值得分享一下实际体验。简而言之:设计经受住了考验,系统用起来很愉快,其他人似乎也认同。</p> <h2>快速回顾</h2> <p>Absurd 是一个完全运行在 Postgres 内部的持久执行系统。核心是一个单独的 SQL 文件(<a href="https://github.com/earendil-works/absurd/blob/main/sql/absurd.sql">absurd.sql</a>),定义了用于任务管理、检查点存储、事件处理和基于声明的调度的存储过程。在此基础上,是轻量化的 SDK(目前有 <a href="https://www.npmjs.com/package/absurd-sdk">TypeScript</a>、<a href="https://pypi.org/project/absurd-sdk/">Python</a> 和一个实验性的 <a href="https://github.com/earendil-works/absurd/tree/main/sdks/go/absurd">Go</a> 版本),让系统在你选择的语言中更易用。</p> <p>模型很直观:注册任务,将其分解为步骤,每个步骤作为一个检查点。如果任何一步失败,任务会从上一次完成的步骤开始重试。任务可以休眠、等待外部事件、暂停数天或数周。所有状态都存放在 Postgres 中。</p> <p>如果想了解完整介绍,<a href="/2025/11/3/absurd-workflows/">原博客文章</a> 涵盖了基础知识。下面是自那以后我们学到的经验。</p> <h2>变化</h2> <p>在过去的五个月里,项目发布了多个版本。大部分变化是那种当人们真正开始依赖一个系统时你会预料到的:强化了声明处理、终止故障工作者的看门狗、死锁预防、恰当的租约管理、事件竞态条件,以及所有只在真实工作负载下才会出现的边缘情况。</p> <p>有几个事情值得特别提一下。</p> <p><strong>分解步骤。</strong> 最初的设计只有 <code>ctx.step()</code>,你传入一个函数,返回其检查点后的结果。这对许多场景有效,但并非全部。有时你需要在决定下一步做什么之前,知道某个步骤是否已经运行过。所以我们添加了 <code>beginStep()</code> / <code>completeStep()</code>,它们给你一个句柄,可以在提交结果之前检查它。这对于建模故意失败和条件逻辑非常有用。特别是在处理“调用前”和“调用后”类型的钩子 API 时,这是必需的。</p> <p><strong>任务结果。</strong> 你现在可以启动一个任务,去做其他事情,稍后回来获取或等待它的结果。回头看来这似乎理所当然,但最初的系统纯粹是即发即忘的。有了适当的结果检查,就可以在 Absurd 中实现诸如在父工作流内部生成子任务并等待它们完成的功能。这对调试代理来说尤其有用。</p> <p><strong><a href="https://earendil-works.github.io/absurd/tools/absurdctl/">absurdctl</a>。</strong> 我们将其构建为一个完整的 CLI 工具。你可以从命令行初始化 schema、运行迁移、创建队列、生成任务、发送事件、重试失败。可以通过 <code>uvx</code> 或作为独立二进制安装。这对于调试生产问题非常宝贵。当某个任务卡住时,能够直接运行 <code>absurdctl dump-task --task-id=&lt;id&gt;</code> 并准确看到它停在哪里,这种体验与翻阅日志截然不同。</p> <p><strong><a href="https://earendil-works.github.io/absurd/tools/habitat/">Habitat</a>。</strong> 一个小的 Go 应用程序,提供用于监控任务、运行、检查点和事件的 Web 面板。它直接连接到 Postgres,让你实时查看正在发生的事情。它很简单,但正是那种让系统对更有人情味的东西。</p> <p><strong>代理集成。</strong> 由于 Absurd 最初是为代理工作负载构建的,我们添加了一个预置的技能,编码代理可以发现并使用它通过 <code>absurdctl</code> 调试工作流状态。还有一个记录在案的模式,用于使 <a href="https://pi.dev/">pi</a> 代理的每次交互变得持久,通过将每条消息记录为检查点。</p> <h2>哪些经受住了考验</h2> <p>我最满意的是核心设计并没有太大改变。任务、步骤、检查点、事件和挂起的基本模型仍然与最初完全相同。我们在其周围添加了功能,但没有什么是迫使我们重新思考基本抽象的。</p> <p>将复杂性放在 SQL 中并保持 SDK 轻量,被证明是一个真正的好决定。TypeScript SDK 大约 1400 行。Python SDK 大约 1900 行,但这主要来自支持染色函数的复杂性。相比之下,Temporal 的 Python SDK 约有 170,000 行。这意味着 SDK 易于理解、调试和移植。当出现问题时,你可以在一个下午内通读整个 SDK 并理解它的功能。</p> <p>基于检查点的重放模型也经受住了时间的考验。与需要确定性重放整个工作流函数的系统不同,Absurd 只是加载缓存的步骤结果并跳过已完成的工作。这意味着你的代码在步骤之外不需要是确定性的。你可以在步骤之间调用 <code>Math.random()</code> 或 <code>datetime.now()</code>,一切仍然正常工作,因为只有步骤边界才重要。在实践中,这使得推理什么安全、什么不安全变得容易得多。</p> <p>基于拉取的调度也是正确的选择。工作节点在其能力范围内从 Postgres 拉取任务。没有协调器,没有推送机制,没有 HTTP 回调。这使得它非常容易自托管,并且意味着你不必在基础设施层面考虑负载管理。</p> <h2>可能不是最优的</h2> <p>我与一些人讨论过,是否正确的抽象应该是 <a href="https://www.distributed-async-await.io/specification/programming-model/durable-promise-specification">持久化 Promise</a>。这是一个非常有吸引力的想法,但在实践中实现起来要复杂得多。不过理论上它也更强大。我确实尝试过看看如果 Absurd 基于持久化 Promise 会是什么样子,但到目前为止没有取得进展。不过,这是一个我觉得尝试起来会很有趣的实验!</p> <h2>我们的用途</h2> <p>主要用例仍然是代理工作流。一个代理本质上是一个循环:调用 LLM、处理工具结果、重复直到它认为完成。每次迭代成为一个步骤,每个步骤的结果都会被检查点化。如果进程在第 7 次迭代时崩溃,它会重启并从存储中重放第 1 到第 6 次迭代,然后从第 7 次继续。</p> <p>但我们也发现它对许多其他事情很有用。我们所有的 cron 作业都会分发分布式工作流,并附带一个从调用生成的预生成去重键。我们可以运行两个 cron 进程,它们只会触发一次 Absurd 任务调用。我们还在需要部署后仍然存活的后台处理中使用它。基本上,任何你本来需要自己构建重试和恢复逻辑的场景,现在都可以交给 Absurd。
查看原文
查看缓存全文

缓存时间: 2026/05/16 03:29

# 生产环境中的 Absurd 来源:https://lucumr.pocoo.org/2026/4/4/absurd-in-production/ 写于 2026 年 4 月 4 日 大约五个月前,我写了一篇关于 [Absurd](https://lucumr.pocoo.org/2025/11/3/absurd-workflows/) 的文章——这是我们在 Earendil 内部自用的持久化执行系统,完全构建在 Postgres 之上,仅依赖 Postgres。当时的核心理念很简单:你不需要一个[独立的](https://hatchet.run/)[服务](https://www.inngest.com/)、一个[编译器插件](https://useworkflow.dev/)或一个[完整运行时](https://temporal.io/)就能实现持久化工作流。你只需要一个 SQL 文件和一个轻量 SDK。 从那以后,我们一直在生产环境中运行它,我觉得值得分享一下实际体验。简而言之:设计经住了考验,系统使用起来很愉悦,而且其他人似乎也认同这一点。 ## 快速回顾 Absurd 是一个完全运行在 Postgres 内部的持久化执行系统。核心是一个单独的 SQL 文件([absurd.sql](https://github.com/earendil-works/absurd/blob/main/sql/absurd.sql)),其中定义了用于任务管理、检查点存储、事件处理和基于声明的调度等存储过程。在此基础上,有轻量 SDK(目前有 [TypeScript](https://www.npmjs.com/package/absurd-sdk)、[Python](https://pypi.org/project/absurd-sdk/) 和一个实验性的 [Go](https://github.com/earendil-works/absurd/tree/main/sdks/go/absurd) 版本)让你在偏好的语言中也能舒适使用。 模型很直接:注册任务,将其分解为步骤,每个步骤充当一个检查点。如果发生任何故障,任务会从最后完成的步骤重试。任务可以休眠、等待外部事件,以及暂停数天或数周。所有状态都存储在 Postgres 中。 如果你想了解完整介绍,[原始博文](https://lucumr.pocoo.org/2025/11/3/absurd-workflows/)涵盖了基本原理。接下来分享的是我们自那以后学到的东西。 ## 发生的变化 在过去的五个月里,该项目发布了多个版本。大部分变化都是你期望在一个已有人依赖的系统上会看到的:加固了声明处理、添加了终止故障工作者的看门狗、预防死锁、完善的租约管理、事件竞态条件处理,以及只有在运行真实负载时才会暴露的各种边界情况。 有几件值得特别提一下的事。 **分解步骤。** 最初的设计只有 `ctx.step()`,传入一个函数并返回其检查点后的结果。这对很多情况都适用,但并非全部。有时你需要知道某个步骤是否已运行,然后才能决定下一步做什么。因此我们添加了 `beginStep()` / `completeStep()`,它们返回一个句柄,你可以在提交结果之前检查它。这对于建模有意为之的失败和条件逻辑非常有用。这一点在需要处理“调用前”和“调用后”类型的钩子 API 时尤其必要。 **任务结果。** 现在你可以生成一个任务,去做其他事,稍后再回来获取或等待它的结果。事后看来这很明显,但最初的系统纯粹是“发后即忘”。有了正确的结果检查,就可以在父工作流中生成子任务并等待它们完成。这在调试 agent 时也特别有用。 **absurdctl**。 我们将其构建为一个完整的 CLI 工具。你可以初始化 schema、运行迁移、创建队列、生成任务、发出事件、从命令行重试失败任务。可通过 `uvx` 或独立二进制安装。这对于调试生产问题非常宝贵。当某件事卡住时,只需运行 `absurdctl dump-task --task-id=` 就能看到它确切停在哪里,这与翻日志的体验完全不同。 **Habitat**。 一个用 Go 编写的小型应用,提供用于监控任务、运行、检查点和事件的 Web 仪表板。它直接连接到 Postgres,让你看到实时的运行情况。很简单,但正是那种让系统对人类更友好的东西。 **Agent 集成。** 由于 Absurd 最初是为 agent 工作负载构建的,我们添加了一个内置技能,编码 agent 可以发现并使用它通过 `absurdctl` 调试工作流状态。还有一个记录好的模式,用于通过将每条消息记录为检查点来使 [pi](https://pi.dev/) agent 的回合持久化。 ## 哪些部分经住了考验 最让我满意的是,核心设计不需要太多改动。任务、步骤、检查点、事件和暂停这一基本模型仍然和最初完全一样。我们围绕它添加了功能,但没有什么迫使我们重新思考基本抽象。 将复杂性放在 SQL 中并保持 SDK 轻量被证明是一个非常正确的决定。TypeScript SDK 大约 1400 行。Python SDK 大约 1900 行,但大部分来自于支持彩色函数(colored functions)的复杂性。相比之下,Temporal 的 Python SDK 大约有 170,000 行。这意味着 SDK 易于理解、易于调试、易于移植。当出现问题时,你可以在一个下午读完整个 SDK 并理解它的作用。 基于检查点的重放模型也经受住了时间考验。与需要确定性地重放整个工作流函数的系统不同,Absurd 只是加载缓存的步骤结果并跳过已完成的工作。这意味着你的代码在步骤之外不需要是确定性的。你可以在步骤之间调用 `Math.random()` 或 `datetime.now()` 而一切正常,因为只有步骤边界才重要。在实践中,这使得推理哪些是安全的、哪些不是变得容易得多。 基于拉取的调度也是正确的选择。工作者根据自身容量从 Postgres 拉取任务。没有协调器、没有推送机制、没有 HTTP 回调。这使得自托管变得非常简单,并且意味着你不需要在基础设施层面考虑负载管理。 ## 可能不是最优的地方 我与一些人讨论过,正确的抽象是否应该是[持久化 promise](https://www.distributed-async-await.io/specification/programming-model/durable-promise-specification)。这是一个非常吸引人的想法,但实际实现起来要复杂得多。不过从理论上讲它也更强大。我尝试过看看如果 Absurd 基于持久化 promise 会是什么样子,但到目前为止没有取得什么进展。不过我认为这是一个很有趣的实验,值得一试! ## 我们用它来做什么 主要用例仍然是 agent 工作流。一个 agent 本质上是这样一个循环:调用 LLM、处理工具结果、重复直到决定完成。每次迭代成为一个步骤,每个步骤的结果被检查点化。如果进程在第 7 次迭代时崩溃,它会重启,从存储中重放第 1 到第 6 次迭代,然后从第 7 次继续。 但我们发现它对许多其他事情也很有用。我们所有的 cron 任务都通过一个预先生成的去重键来分发分布式工作流。即使有两个 cron 进程在运行,它们也只会触发一次 absurd 任务调用。我们还用它来处理需要在部署后存活的后台处理。基本上,任何你原本需要在队列之上构建自己的重试和恢复逻辑的场景。 ## 仍然缺少的东西 Absurd 有意保持最小化,但仍有一些我希望看到的东西。没有内置调度器。如果你想要类似 cron 的行为,需要自己运行一个调度循环并使用幂等键去重。这可行,而且我们有[记录好的模式](https://earendil-works.github.io/absurd/patterns/cron/),但如果有更集成的方案会更好。 没有推送模型。一切都是拉取。如果你需要一个 HTTP 端点来接收 webhook 并唤醒任务,你需要自己构建。我认为这是正确的默认选择,因为推送系统更难以运维且更容易被压垮,但有些情况下它会很方便。特别是在一些 agentic 系统中,如果能原生集成 webhooks(通过传入的 POST 请求唤醒)会非常好。我绝对不希望把这个放到核心中,但这听起来像是一个很好的外围库,可以构建在 absurd 之上。 最大的缺失是目前不支持分区。这很不幸,因为它使得清理数据比本应更昂贵。从理论上讲,支持分区相对简单。你可以按周分区,然后在它们过期时分离并删除。唯一阻碍这一点的是 Postgres 没有一个方便的方法来做这件事。难点不在于分区本身,而在于真实工作负载下的分区生命周期管理。如果某个工作线程插入一行,其 `expires_at` 落在了一个没有分区的月份里,插入会失败,工作流会崩溃。所以你需要一个单独的后台维护循环,始终提前创建足够远的未来分区以支持休眠/重试,并且对每个队列都这样做。在删除方面,安全的方式是 `DETACH PARTITION CONCURRENTLY`,但要从 `pg_cron` 运行它却不行,因为它不能在事务内运行,而 `pg_cron` 一切都在一个事务中运行。我不认为这是一个无法解决的问题,但我还没有找到好的解决办法,非常希望能[得到建议](https://github.com/earendil-works/absurd/issues/4)。 ## 开源还有意义吗? 这让我想到一个元问题:在 agentic 工程时代,开源库的意义是什么。持久化执行现在有很多初创公司在卖。另一方面,agent 也能为你构建它,人们可能甚至不会再去找解决方案了。这有点……奇怪?我不认为一个持久化执行库能够支撑一家公司,真的不认为。但另一方面,我认为这个问题足够复杂,可以成为一个没有商业利益的开源项目。你需要一个围绕它的小型生态系统,特别是用于 UI 和良好的调试体验,而这很难从一个抛弃式实现中获得。 我不认为我们已经解决了这个问题,但它已经比几个月前好用多了。如果你正在使用 Absurd、正在考虑用它,或者正在构建相关想法,我非常希望听到你的反馈。错误报告、粗糙边缘、设计批评和贡献都非常欢迎——这个项目每次有人从不同角度审视它时都会变得更好。 本文标记为 [ai](https://lucumr.pocoo.org/tags/ai/) 和 [announcements](https://lucumr.pocoo.org/tags/announcements/)。 [复制为 markdown](https://lucumr.pocoo.org/2026/4/4/absurd-in-production.md) / [查看 markdown](https://lucumr.pocoo.org/2026/4/4/absurd-in-production.md)

相似文章

持久化执行:硬核方式

Hacker News Top

一本教程指南,教你如何受Kubernetes the hard way启发,从零开始使用Go和Postgres构建持久化执行引擎。

@ycombinator: Ardent (@ArdentAI) 让你在 TB 级规模下 <6秒 克隆任何 Postgres 数据库,让编码代理可以测试代码,工程团队可以快速上线而不用担心影响生产…

X AI KOLs Following

Ardent 是一款 Y Combinator 支持的工具,能在 TB 级规模下于 6 秒内克隆任何 PostgreSQL 数据库,让编码代理和开发者可以在接近生产环境的克隆副本上测试代码,而不会造成停机风险。该工具已被 Supermemory 和 Surface Labs 等公司采用。

使用 Postgres 作为作业队列的潜在后果

Lobsters Hottest

文章分析了使用 PostgreSQL 作为作业队列的可扩展性限制,特别强调了高并发下 MultiXact SLRU 争用导致的性能瓶颈。文章解释了为什么这种架构在开发环境中表现良好,但在生产环境中却会失败,并建议考虑替代方案。