Postgresで耐久性ワークフローを構築する
(dbos.dev)- 耐久性(Durable)ワークフローは、実行状態をデータベースにチェックポイントし、クラッシュ後に最後に完了したステップから復旧できるようにする
- Temporal、Airflow、AWS Step Functionsのような外部オーケストレーションは、中央オーケストレーターとストレージを追加するため複雑さが増す
- Postgresベースの構成では、アプリケーションサーバーがワークフローテーブルをポーリングし、ステップの出力を直接保存して復旧可能にする
- 複数サーバーはロックと整合性制約によって重複実行を防ぎ、スケーラビリティ・可用性・可観測性の問題をPostgresの解決策で扱う
- 単一のPostgresでも毎秒数万件のワークフローまで垂直スケール可能で、レプリケーション・マルチAZ・SQL分析をそのまま活用できる
耐久性ワークフローの基本モデル
- 耐久性ワークフローは、プログラム実行中の進行状態をデータベースに定期的にチェックポイントとして保存し、クラッシュや障害の後に最後に完了したステップから復旧できるようにする方式
- ビデオゲームのセーブとロードのように、プログラムの進行状況を定期的に「保存」し、クラッシュ後に最後のチェックポイントから「読み込む」モデルと考えられる
- 一般的な実装は、Temporal、Airflow、AWS Step Functionsのような外部オーケストレーションシステム
- 外部オーケストレーションでは、耐久性のあるプログラムを複数ステップから成るワークフローとして記述し、中央オーケストレーターが実行を調整する
外部オーケストレーションの動作方式
- クライアントがワークフローを送信すると、オーケストレーターがデータストアにレコードを作成し、実行のためにワーカーへディスパッチする
- ワーカーが各ステップを完了するたびに結果をオーケストレーターへ送り、オーケストレーターはその出力をデータストアにチェックポイントしてから次のステップをディスパッチする
- ワーカーがクラッシュまたは失敗すると、オーケストレーターはそのワークフローを別のワーカーに再ディスパッチし、最後のチェックポイントされたステップから開始させる
- この構造は、ワークフロー状態をデータベースに保存するという中核的なアイデアに別個のオーケストレーターサーバーを加えることで、複雑さを増している
Postgresをオーケストレーターとして使う構成
- 耐久性ワークフローの本質が、データベースにプログラム状態をチェックポイントすることにあるなら、別個のオーケストレーターサーバーなしでデータベース自体をオーケストレーターとして使う方が、よりシンプルで効率的
- Postgresは、その人気、スケーラビリティ、豊富なエコシステムにより、耐久性ワークフローを構築するのに適した選択肢
- Postgresベースの耐久性ワークフローシステムでは、アプリケーションサーバーが中央オーケストレーターを介さずにPostgresと直接通信してワークフローを実行する
- クライアントはPostgresのワークフローテーブルに項目を作成して実行を送信し、アプリケーションサーバーはそのテーブルをポーリングしてワークフローをデキューし実行する
- サーバーはワークフローを実行する間、各ステップの出力をPostgresにチェックポイントし、実行中のサーバーがクラッシュまたは失敗した場合は、別のサーバーがチェックポイントからワークフローを復旧する
中央オーケストレーターが不要になる理由
- アプリケーションサーバーはPostgresを通じて調整できるため、中央オーケストレーターがワークフローをワーカーにディスパッチする必要がない
- サーバーはPostgresテーブルから協調的にワークフローをデキューし、ロック句のようなメカニズムによって各ワークフローが正確に1つのワーカーによってのみデキューされることを保証できる
- ステップ出力のチェックポイントも、オーケストレーターではなくワーカーが直接Postgresに書き込む
- 複数のワーカーが同じワークフローを同時に実行しようとしても、Postgresの整合性制約がチェックポイント時点で重複作業を検出して退避させられる
- 中央オーケストレーターをPostgresや他のデータベースに置き換えることで、スケーラビリティ、可用性、可観測性、セキュリティといった問題を、よく知られたPostgresネイティブの解決策で扱える
スケーラビリティと可用性
- データベースベースの耐久性ワークフローシステムのスケーラビリティと可用性は、根本的には基盤となるデータベースによって決まる
- ワーカーサーバーを追加して水平スケールできるため、最大処理能力はデータベースがどれだけ高速にワークフローを処理できるかに依存する
- ワーカーは相互に代替可能で、互いの状態を復旧できるため、システムは基盤データベースが利用可能である限り可用性を保てる
- Postgresを使う場合、スケーラビリティと可用性が長年研究されてきた問題であり、堅牢な解決策が存在する点が利点
- 単一のPostgresサーバーは、毎秒数万件のワークフローを処理できるように垂直スケールできる
- さらなる拡張は、CockroachDBのような分散Postgresや、シャーディングされたPostgresを使うことで実現できる
- Postgresは自動フェイルオーバー付きのストリーミングレプリケーションをサポートしており、マネージドサービスではマルチAZ配置と高可用性SLAが標準で提供される
- Postgresを大規模運用するために数十年かけて蓄積されたエンジニアリングと研究は、耐久性ワークフローの運用にも直接活用できる
可観測性
- Postgresベースの耐久性実行では、ワークフローとステップがPostgresテーブルにチェックポイントされるため、そのチェックポイントをスキャンしてワークフローをリアルタイムに監視し、実行を可視化できる
- ほぼすべてのワークフロー可観測性クエリをSQLで表現できる点で、Postgresは強みを持つ
- 過去1か月にエラーが発生したすべてのワークフローを見つけるクエリのように、複雑なフィルタリングや分析作業をSQLで宣言的に表現できる
- これは、Postgresのリレーショナルモデルと数十年にわたるクエリ最適化研究を活用しているからこそ可能
- 人気のある外部オーケストレーターが使うキーバリューストアのように、より単純なデータモデルを持つ多くのシステムでは、同じレベルのSQLベース分析支援を提供できない
- ワークフローとステップデータをPostgresテーブルに保存し、高速な分析クエリのための補助インデックスを追加すれば、耐久性実行における効率的な可観測性を得られる
信頼性とセキュリティ
- 外部オーケストレーターを使う耐久性実行では、オーケストレーターとそのデータストアの両方が単一障害点になる
- オーケストレーターとデータストアがワークフロー実行を直接調整するため、どちらか一方でも停止するとアプリケーション全体が利用不能になる
- これらはワークフローやステップのチェックポイントを処理・保存するため、機密性の高いアプリケーションデータにアクセスしうる存在であり、機密インフラとしてのハードニング、アクセス制御、監査が必要になる
- Postgresベースの耐久性実行では、唯一の障害点はPostgres自体であり、すべてのワークフローデータはPostgresに直接保存され、他のシステムを経由しない
- アプリケーションがすでにPostgresに依存しているなら、耐久性実行を導入しても新たな障害点を追加せず、保護すべき新しい攻撃面も生み出さない
- データベースはすでに中核インフラであるため、オーケストレーションのために新たな中核インフラを追加するより、既存のデータベースを再利用する方が理にかなっている
1件のコメント
Hacker News のコメント
Armin Ronacher の
absurdは Postgres 向けの耐久性のあるワークフロー実装https://lucumr.pocoo.org/2025/11/3/absurd-workflows/
https://github.com/earendil-works/absurd
https://earendil-works.github.io/absurd/
実際には使ったことはないが、他の選択肢と比較してみる価値はある
absurdとその Rust 派生実装であるdurableは、クライアント側を非常にシンプルに保てる良い選択肢だと思う軽量なのでコーディングエージェントが全体構造を簡単に頭に入れられ、必要ならクエリで状態を確認すればよい
dbos.dev、restate.dev、cf workflows を使っていて、うちの Agents.md にはこう書いてある
Restate.dev は northflank の決済連携に使っている。cf workflows より速く、Cloudflare とその障害から独立しており、セルフホスト可能なのでベンダーロックインがない
Cloudflare workflows は CSV/PDF レポート生成のような重要度の低い作業に使っている。非常に安いからだ
DBOS.dev は、Postgres トランザクションに結び付いたアトミックメッセージングが必要で、100% の信頼性/耐久性が求められるワークフローに使っている。たとえば materialized row の投入や、店舗運営者にとって重要なメール/プッシュ送信など
DBOS と Restate は見た目には似ているが、Restate は中央の “orchestrator” を必要とするため一長一短があり、cf/vercel のサーバーレスワーカーと組み合わせて構築しやすい
また VirtualObject があるので、Cloudflare のシングルスレッド DurableObject に対するベンダーロックインのないオープンソース代替としても悪くない
DBOS が特に光る点は 2 つある。1)
dbos.enqueue_workflowにより、ビジネスロジックと同じ DB トランザクション内でアトミックメッセージングができること。この部分はどんな解決策でも最も弱くなりがちな箇所なので、ビジネスロジックを実行した同じトランザクションでアトミックかつ耐久的に処理できれば、複雑さを大きく減らせる2) DBOS はワークフロー状態を DB に保存するので、metabase/looker で可観測性ダッシュボードを作りやすそうだ。Restate も rocksdb インスタンスを公開して metabase に接続できるとよいのだが
DBOSとTemporalを使ったことがある人の実感を知りたい。
以前 Temporal を使ったことがあり、かなりうまく動いていたが、リクエストペイロードやイベントサイズの制限のせいで、解決策を作る際に不便だったことがある。
良いエンジニアリングプラクティスを強制してくれる利点もあるが、CSVファイルが2MBを超えるからといって、S3に上げてリンクを渡し、その後ワークフローで再度ダウンロードする特別なロジックを毎回使いたいわけではない。
DBOSの体験がどうなのか、運用の複雑さや機能の同等性などの面でTemporalとどう比較されるのかが気になる。
自宅でも時間にそれほど厳しくないホームオートメーションの処理に使っている。ワークフローの遅延はそこまでひどくないが、家の動作検知イベントのように即時反応が必要なトリガーには使わないと思うし、非アクティブ後に何かをオフにするタイムアウト程度なら問題ない。
VPCやKubernetesクラスターの中で、Temporalの前段に薄い REST API を置くやり方がかなり気に入っている。イベントベースのトリガーがTemporalの認証やワークフロー状態の確認を気にしなくてよくなるし、イベントを可能な限りロジックレスに保つ助けにもなるからだ。
たとえばDBトリガーが直接動作するかイベントをキューに入れ、ハンドラーが必要なイベント詳細を持って薄いREST APIを呼び出す。REST APIは、それがワークフローを開始すべきか、既存のワークフローにsignalを送るべきか、無視すべきかを判断できる。パターンは状況によって異なるが、自分の場合は
SignalWithStartをよく使うし、開始する価値がなく既存ワークフローもなければそのまま捨てる。また、単一オブジェクトのライフサイクルの中で互いに独立した動作をオーケストレーションしなければならないとき、親/子ワークフロー機能は非常に有用で、外部要因によってオブジェクトの進行経路が変わるときにキャンセルできる点も良い。
長く曖昧に言うなら、とても強力で扱いやすく、ライフサイクルロジックをAPIの外に出すのに大いに役立つ。APIの中に入れてしまうと技術的負債がたまりやすく、保守が危うくなる。見えやすい場所にロジックを放り込んで後で隠れた罠になるより、模範的なプラクティスに従わせてくれるという点には同意する。
ただ、Cloud製品を試して価格に愕然とした。本番に載せる前に無料クレジット1,000ドルを使い切ってしまった。ローカルTemporalを自前運用したいとも思わなかった。
個人的には、最善なのはアーキテクチャからアイデアだけを取り入れて、Postgresで自分で実装することだと思う。
100%気に入っているわけではない。本質的な一部というより後付け感があり、まだ初期リリースだ。それでも、今では事実上解決したと見なしてよい。
1年以上本番で運用してきた立場から言うと、Temporalは設計が良くなく、遅く、インフラ面でばかげて重い。
たいして小さくない作業、たとえばワークフローごとにイベントが200件を超え、同時に数百件を一日中回すだけでも、インフラコストに数百万ドルかかりうるし、それでもなお良くない。
自前のベンチマークを回してみると数字はひどい。
営業チームも本当にひどく、切羽詰まっているように見える。
開発者視点ではSDKはかなり良い。
nexus に閉じ込められないようにし、営業から電話が来たら必ず法務を同席させたほうがいい。
Celeryからどう移行するか理解するのに時間はかかったが、私たちのケースではその価値があった。
Conductor OSSもこれをかなりうまくやっている https://docs.conductor-oss.org/devguide/ai/index.html
https://github.com/agentspan-ai/agentspan は本質的にConductor向けの エージェントSDKレイヤー で、langgraph、OpenAI、vercel、ADKエージェントをコード変更なしで永続化し、オーケストレーションを追加できる。
データストア、状態機械、有効状態制約、有効状態の間を遷移するロジックを分離する代わりに、これらをアプリ状態の何らかのカーネルに統合できるとよいと思う。
正直、Postgresにはすでにこうした能力がかなりあるが、アプリや製品レベルで、アプリが遷移可能な証明可能な状態集合を提供し、それをクライアントにとって有益な形で自動公開する明確な絵はまだ見えていない。たとえば、このユーザーはこの投稿にいいねはできるが編集はできない、といった具合だ。
私には カラー・ペトリネット の形に見えるが、データベースが成功しているような明確な境界を持つ単純なアプリ状態パラダイムはまだ見えていない。
ただし、完全な統合かどうかは確信がない。
DBOSがRustをサポートしていないので、これに似た非常に最小限のRust版を https://github.com/tensorzero/durable に実装した。
かなり安定してスケーラブルだったが、当然ながら SQL実装 には非常に注意が必要だ。ここを読んでいる人たちにとって興味深いといいのだが。
https://flawless.dev/
概念は完全に理解できるし、賛成でもある。こういう種類の耐久性をワークフローシステムに持ち込む優れた方法だと思う
ただ、ゲーマー脳としてはこれを「大規模なセーブスカミング」と呼びたくなる。すでに多くの人がこのアプローチが機能することは知っていても、抽象的なコンピュータサイエンスの概念と結び付けられていなかったのかもしれない
堅牢性を高めるもう一つの戦略は、ワークフローを冪等操作で構成することだ。ワークフローの状態が大きすぎてバックアップしにくい状況で役立つかもしれない。その代わり最初から処理を再実行すれば、再び進捗が生まれる地点まではすべて no-op になる
Postgresがツールボックスに入っているだけで、少ない道具でできることの多さにはいつも驚かされる
最近分散キューを実装したが、本当にうまく動いていて、ベンチマーク結果も良く、競合状態や衝突もない。ワーカーが安全に競合できるように
SKIP LOCKEDを使った複数ノードにまたがるワーカー同士の衝突を避けるには、セッションスコープの mutex、つまり
pg advisory lockも使えるSELECT FOR UPDATEを大量に保持するとスケールしにくいからだ修正: もう一度確認したところ、今ではアドバイスは逆に変わっているようだ
Railsにはデータベースベースのジョブバックエンドがいくつもあるが、慣習としてジョブは常に一つのことだけを行い、可能な限り非常に短時間で終わるべきとされている
このせいでワークフローを作るのがやや不自然になる。最初のジョブの最後の行で二つ目のジョブをキューに入れ、二つ目のジョブの最後の行で三つ目のジョブをキューに入れる、といった形になる
ジョブバックエンドはこれらをつながったワークフローとしては見せず、独立したジョブとして扱うため、ワークフローを高いレベルで理解しようとしても複数のジョブクラスを読まなければならない
Railsは最近、ジョブ内で段階的にチェックポイントを取り、再開できるcontinuableという概念を導入したが、それでもジョブを単一責任に保つ慣習が強く、本物のワークフローに使うにはまだしっくりこない
他の人もこういうことを経験したのか、そして解決策を見つけたのか気になる
これは素晴らしいパターンだ。できるだけ多くのことをデータベース内で行うのがよい
外部のSpannerは change streams を提供している。内部のSpannerは異なっていて、主に一部のケースで極端なスケーリング要件があるためであり、「すでにうまく動いているから」という理由や、「任意の change stream は怖い」という理由も混ざっている
内部Spannerでは、どのトランザクションでもキューエントリを書き込める。ここでキューは、概ね特別な時間認識を持つテーブルだ。配信を予約でき、エントリはキューからハンドラへプッシュされ、ハンドラも dequeue トランザクションの中でDB書き込みを行える。そして同じスケーラビリティもすべて維持される