世界を動かす技術を、日本語で。

Elixirのジョブ処理フレームワーク「Oban」がPythonに登場

概要

  • Oban はElixir・Python両方に実装があり、ジョブ管理の中核的存在
  • Oban-py はPostgreSQLベースでジョブを管理し、通知や並列処理が特徴
  • OSS版とPro版で 並列処理・機能差 が存在
  • ジョブ投入から実行・完了までの 内部フロー を詳細解説
  • リーダー選出・孤立ジョブ救済・古いジョブの自動削除 などのバックグラウンド処理も充実

Oban-pyの概要とElixir版との比較

  • Oban はElixirで長く使われてきたジョブ管理ツール、Python実装も登場
  • データベースのみ でジョブ投入・実行を完結できる設計
  • ユーザー作成などと同じトランザクションでジョブ投入が可能、失敗時はロールバック
  • キューごとの同時実行数制御 や、完了ジョブの履歴保存・結果保存も可能
  • cronスケジューリング やジョブ実行制御機能を標準搭載
  • OSS版(Oban-py)と商用版(Oban-py-pro)を提供
    • OSSは シングルスレッドasyncioバルク操作非対応レスキュー精度低め など制限あり
    • Pro版は プロセスプール で真の並列処理、バルク操作、スマートな監視・ユニークジョブなど追加
  • OSS版は小規模・評価用途に最適、スケールするならPro版推奨

ジョブ投入から実行までの流れ

  • ジョブ関数定義と投入
    • @job(queue="default")デコレータでジョブ定義、enqueueで投入
  • データベースにジョブをINSERT、stateはavailable
  • PostgreSQLのNOTIFY で全ノードに通知、各ノードのStagerが起動
    • ノードごとに担当キューが異なり、対象キューのみ反応
  • Producerがイベント受信、asyncio.Eventで待機解除
  • ジョブ取得時にFOR UPDATE SKIP LOCKED を利用
    • 他Producerとの競合時もロック済みジョブをスキップし高速並列化
  • 取得ジョブの状態をexecutingに変更し、asyncタスクとして実行
  • add_done_callbackで完了時コールバック を登録、成功失敗問わず処理
  • Executorがジョブ関数を解決し実行、結果をパターンマッチ
    • Exceptionならリトライや破棄、Cancelならキャンセル、Snoozeなら再スケジュール、正常なら完了
  • 完了ジョブはバッチでACK、次のジョブ取得へ

並列処理とPro版の違い

  • OSS版は asyncioの協調並列 のみ、CPUバウンドな処理はブロック
  • Pro版は プロセスプール でマルチコア並列処理が可能、特別な設定不要
  • バルクインサート/ACKもPro版のみ対応

バックグラウンドプロセス

  • リーダー選出
    • クラスタ内で1ノードのみがジョブの掃除や救済を担当
    • PostgreSQLのINSERT ... ON CONFLICT + TTLで シンプルなリーダー選出
    • リーダーは定期的にリースを更新、ダウン時は自動で他ノードに引き継ぎ
  • 孤立ジョブ救済(Lifeline)
    • Workerやコンテナクラッシュ時、executing状態で長時間放置されたジョブを救済
    • OSS版は 時間ベース のみで判定、長時間ジョブは二重実行リスク有
    • rescue_afterは最大ジョブ実行時間より長く設定推奨、ワーカーは 冪等性 が必須
  • Pruner(古いジョブの自動削除)
    • 完了・キャンセル・破棄済みジョブをmax_ageより古いものから順次削除
    • LIMIT指定でテーブル肥大防止とパフォーマンス維持

まとめ・今後の展望

  • Oban-pyはPostgreSQLだけで堅牢なジョブ管理 を実現
  • Elixir版の思想をPythonでも忠実に再現
  • Pro版でさらに高機能・高性能な運用が可能
  • シンプルな設計とPostgreSQL活用 で外部依存を最小化
  • 今後は エラー処理・リトライ・定期ジョブ なども深掘り予定

Hackerたちの意見

私はSidekiqを作ったんだけど、Obanはそれを基にしてるんだ。ShannonとParker、これをリリースしたことおめでとう!何年か前に同じような決断をしたことがあるんだ。Rubyに集中するか、それともSidekiqを他の言語に持っていくか。気づいたのは、すべての言語の専門家にはなれないってこと。Sidekiq.jsやSidekiq.pyとかね。だから、別の方向に進むことにして、Faktoryを作ったんだ。[0] これはアーキテクチャをひっくり返して、キューライフサイクルを内部で実装する中央サーバーを提供するもの。言語特有のクライアントはずっとシンプルになって、各言語のオープンソースコミュニティによってメンテナンスされるようになるんだ。例えば、faktory-rs[1]みたいにね。欠点は、Faktoryが特定のコミュニティに焦点を当てていないから、特定の言語でイディオム的な例を提供するのが難しいってこと。違う方向だけど、特定のコミュニティに集中すれば、より良い結果が得られるかもしれないね。時間が経てばわかるよ!

彼らはどちらもResqueを基にしていると言った方が正確じゃない?

もしかしたらそういうつもりじゃなかったかもしれないけど、あなたのコメントは自分のものを売り込むために議論を乗っ取ろうとしているように見えるよ。ここでは一般的にあまり好意的に見られないよ。

「based on」はちょっと言い過ぎかな。Sidekiqは、Obanがサポートしているワークフローやクロン、パーティショニング、依存ジョブ、失敗処理などに比べると、かなりシンプルだよね。

ありがとう、マイク!あなたはインスピレーションだよ。パーカーと私は、人生や言語において異なる強みを持っている。私たちは、この相互運用性がPythonとElixirの両方にもたらすものにコミットしているよ。

これは見るのがクールなことだけど、逆に進んでほしいな。Pythonで作られているBI/ML/DSのパイプラインやワークフローがElixirに来てほしいんだ(それに続いてElixirもね)。勢いがどこにあるかはわかるけど、機能的でフォールトトレラント、かつ同時実行性の高い作業を支えるのに、自然に高い同時実行性とエラーが発生しやすいものがあるのは、もっと自然なフィット感があると思う。

同意だね、Claude CodeはElixirでめっちゃうまくやってるよ。TS/Pythonが注目されてるけどね: https://youtu.be/iV1EcfZSdCM?si=KAJW26GVaBqZjR3M これが、イディオマティックなElixirを書いたり、良いパターンを使ったりするのに役立つよ: https://skills.sh/agoodway/.claude/elixir-genius

うちの会社でもこれを考えてたんだ。celeryを使ってるけど、あんまり良くない。仕事はこなせるけど、問題があるんだよね。Obanのことは今まで聞いたことなかったし、考えてたのはTemporalだったけど、それは必要以上に感じる。Obanが軽いのはいいね。両方使ったことがある人、簡単に比較してくれない?ありがとう!

すごく、すごく違うツールだけど、似たような領域をカバーしてるね。Temporal - 厳格なワークフロー要件があって、物事が完了することを保証したいなら、追加の複雑さを受け入れる覚悟が必要。もし銀行とかなら、すごく良い選択だと思う。Oban - DBバックのワーカーキューで、スレッド外でタスクを処理する。Temporalができる保証を提供するわけじゃないから、すべてのプッシュ/プルをファーストクラスの市民に抽象化していないんだ。ワークフローに似た機能を提供しているけど、信頼性の高いものを求めるなら、自分でそれを強化することになるよ(Celery+Sidekiqの経験に基づいて)。両方を使った経験があるから、今取り組んでいるシステムに両方があれば嬉しいな。今の仕事では、すべてのバックグラウンド処理にTemporalを使わざるを得ないから、小さなタスクにはただのボイラープレートが多いんだ。

7〜8年Goで分散システムに取り組んでから、やっとPythonのウェブ/API開発に戻ってきたところだよ。2017年に知っていたことを元にDjango+CeleryのMVPを作ったけど、最近Celeryに対する「嫌悪感」をよく見るんだ。Celeryでどんな問題に直面した?信頼性が低くなった?扱いにくくなった?

2年ほど前にCeleryからPrefectに移行して、すごく満足してるよ。でも、うちのは数千のタスクを処理する小規模な運用だから、何百万も扱ってるわけじゃない。可視性や追跡の面では大きな違いがあるよ。ぜひおすすめしたい。多くのユースケースをカバーする重厚なツールだけど、普段のニーズにはシンプルなProcessWorkerを使ってて、重いMLタスクにはECSワーカーを使ってるんだ。

Obanは、データベースだけを使ってジョブを挿入して処理することを可能にします。ユーザーを作成するのと同じデータベーストランザクション内で確認メールを送るジョブを挿入できます。一つでも失敗したら、すべてがロールバックされます。これはすごく重要な機能です。多くの人がリレーショナルデータベースをワーカーキューとして使うべきではないと言うけど、トランザクションの重要性を見落としているんだ。「トランザクションがコミットされたらこの作業をキューに入れ、失敗したらキューに入れない」と言えるのは本当に便利です。Brandur Leachが数年前に素晴らしい記事を書いていて、https://brandur.org/job-drain では、別のキューシステムがあっても、トランザクションの一部として更新できる一時的なデータベーステーブルにキュータスクをログするべきだと説明しています。

これは「トランザクショナルアウトボックスパターン」と呼ばれているよ!

いいポイントだね。こういう取引の考え方は思いつかなかったよ。

これ、めっちゃいい機能だと思う。だけど、Obanの代わりにpg_timetableを使ってるよ: https://cybertec-postgresql.github.io/pg_timetable/v6.x/

Debeziumは、WALに基づいたキューを動かすために正確に作られたんだ。

でも、みんなPostgresを何でも使おうとする人も多いよね。

俺たちはAIを使ったアプリビルダーを作ってるんだ。Elixir、Phoenix、そしてもちろんOBANを使ってる。これって超パワーを感じるよね。君が言ってることは、長時間動くAIプロセスの時代には特に重要だと思う。デプロイをするだけでも、エージェントのオーケストレーションにプレッシャーがかかるからね。でも、OBANがあれば、耐久性を高めるための素晴らしい方法があるんだ。ちなみに、これ全部「無料」で組み込まれてるんだよ。他の言語のエコシステムでは、こういう形の耐久性にお金を払う人もいるからね(例えばTemporalみたいに)。

Obanの人たちは、何年も素晴らしい、よく設計された仕事をしてきたよね。Elixirにとっては本当に唯一の選択肢だと思う。ただ、プロサブスクリプションの背後にプロセスプールをロックするのはちょっと混乱するな。これはCPythonのアーキテクチャを考えると基本的な機能であって、あってもなくてもいいものじゃないから。Oban Proで月135ドルで宣伝しているのは、以下の通り。

  • すべてのオープンソース機能
  • マルチプロセス実行
  • ワークフロー
  • グローバルおよびレート制限
  • ユニークジョブ
  • バルク操作
  • 暗号化されたソース(30/90日リフレッシュ)
  • 1アプリケーション
  • 専用サポート

ここで自分のことをちょっと自慢させてもらうけど、100%無料のChancyを例に挙げるね - https://github.com/tktech/chancy。初めから同じワーカーがasyncio、プロセス、スレッド、サブインタープリタをミックスして使えるんだ。ワークフロー、レート制限、ユニークジョブ、バルク操作、トランザクショナルエンキューイングなどもサポートしてる。これらの機能をOSS版に移して、既存の選択肢と競争力を持たせて、専用サポートやもっと伝統的な「エンタープライズ」機能に集中するのはどうかな?それは月135ドルの価値があると思うし(Obanの開発者たちは問題に対して世界クラスのサポートを提供してる)。PythonエコシステムにはElixirよりも多くの選択肢があるから、TemporalやTrigger、Prefect、Dagster、Airflowなどと競争してるんだよね。

ワークフロー、レート制限、ユニークジョブ、バルク操作、トランザクショナルエンキューイングなどをサポートしている。これらの機能をOSS版に移して、既存の選択肢と競争力を持たせて、専用サポートやもっと伝統的な「エンタープライズ」機能に集中するのはどうかな?それは月135ドルの価値があると思うし(Obanの開発者たちは問題に対して世界クラスのサポートを提供してる)。興味や使用状況によっては、いくつかの機能をOSS版に移すかもしれない。無料にするのは逆よりもずっと簡単だからね。ElixirのPro専用機能のいくつかは以前にOSSに移ったことがあるし、このプロジェクトの結果として、追加の機能も移る予定だよ。サポート専用のオプションは、私たちの経験ではあまり効果がなかったけど、Pythonでは違うかもしれないね。 PythonエコシステムにはElixirよりも多くの選択肢があるから、TemporalやTrigger、Prefect、Dagster、Airflowなどと競争してるんだよね。Pythonエコシステムにはもっとたくさんの選択肢があるよね。

共有してくれてありがとう、面白いプロジェクトだね!私が気になったのは、かなりコアな機能がProティアの背後に隠れていることだよ。参考までに、同じアイデアを完全にOSSで実装している以前のプロジェクトがいくつかあるよ。特にPostgresをバックエンドにした耐久性のある実行に関しては、

  1. DBOSはPostgresの上に耐久性のあるワークフローとキューを構築した(注:私はDBOSの共同創設者です)。最近の議論はこちらで確認できるよ: https://news.ycombinator.com/item?id=44840693
  2. Absurdも関連するデザインを探求している: https://news.ycombinator.com/item?id=45797228

全体的に、外部オーケストレーターではなく、データベース中心の耐久性のあるワークフローに向かう人が増えているのは励みになるね。決定論、リカバリーセマンティクス、DXについてはまだまだデザインの余地があるし、ここで実験している他の人から学ぶのが楽しみだよ。

OSSでアイデアを実装している他のプロジェクトもあるけど、Elixirでも同じことだよね。DAGやワークフローを必ずしも発明したわけじゃないけど、Elixir側の耐久性のある実装はDBOSよりも数年前に始まったんだ。私たちは、Obanが提供するものへのアドオンとして考えているんだ、全体の製品としてではなくね。完全にオープンソースの提供をして、サポートを販売するのは本当に夢のようだ。もしかしたら、私たちもそこにたどり着けるかもね。

うーん。OSSとプロの機能ゲートにはあまり気にしないけど、「Proバージョンは生産者の生存を追跡するためにスマートなハートビートを使用する」ってのはあまり好きじゃないな。QoL機能と信頼性の機能には違いがあると思うし、少なくとも私にとっては、OSSプロジェクトで採用する理由が見つからない。残念だな、これ以外は素晴らしいのに。

一般的なRedisやRabbitMQを使った耐久性のあるキューだと、予期しないシャットダウンの後にジョブを取り戻せる保証はないんだ。その引用もちょっと間違ってるよ—プロデューサーの生存状態は同じように追跡されるし、違うのは「孤立した」ジョブをどう救出するかだけなんだ。

OSSのObanにはいくつか制限があって、Pro版では自動的に解除されるよ。シングルスレッドのasyncio実行だから、同時処理はできるけど本当の意味での並列処理じゃない。CPUに依存するジョブはイベントループをブロックしちゃうから、試す価値もないかな。Celeryのインターフェースはちょっと微妙だけど、もう慣れちゃったし、リソースが許す限り縦にも横にも無限に並列処理できるんだ。asyncioもあまり好きじゃないし、ジョブキューを使うなら必要ないと思ってる。編集:もう少し調べたら、複数のワーカーノードを立ち上げられるみたいで、最初に思ってたほど悪くないかも。

最近、Postgresはジョブ処理に十分速いのかな?今は何億というジョブを処理してるし、数年前のボリュームがその一部だった頃、Postgres + QueからRedis + Sidekiqに移行したらパフォーマンスが大幅に向上したんだ。ここ数年でそれが変わったのかな?

何百億って、どのくらいの時間枠での話?俺はRails/Solid QueueとPostgresを使ったシステムで、1日20Mのジョブを処理してるけど、月45ドルのVMで余裕たっぷりだよ。

Celeryをおすすめするよ。Celeryはシンプルで柔軟、信頼性の高い分散システムで、大量のメッセージを処理できるし、そのシステムを維持するためのツールも提供してくれる。ちなみに、これも無料だよ。