Originally published on nejckorasa.github.io.
チームがモノリスからマイクロサービスとイベント駆動の非同期システムに移行すると、かつては他の誰かの問題だった種類の問題を引き継ぐことになります。途中で失敗する作業、2回実行してはならないステップ、作業完了前に返る呼び出しなどです。Temporalは耐久性のある実行エンジンで、これらの多くを扱います。複数ステップのプロセスを定義すると、ワーカーが途中でクラッシュしてもプロセスが完了するまで実行されることが保証されます。
私は銀行の資金移動コア(元帳、決済、クレジットカード)で分散システムを構築するのに10年近く携わってきました。その多くはTemporal上で、短いリクエスト駆動のワークフローから数週間続くものまであります。これは、その移行を行うチームに提供したいハイレベルなガイドです。出荷前に内面化すべき原則であり、完全なチュートリアルではありません。これらのほとんどはTemporal自体に関するものではありません。非同期への移行が要求する習慣であり、Temporalはそれをスキップするとすぐに罰を与えるのです。
Durable Execution: 解決する問題
分散作業は途中で失敗します。サービスAを呼び出して成功し、サービスBを呼び出してタイムアウトし、ポッドがCの前に死ぬとします。すると未完了の作業と、どこまで進んだかの記憶がない状態になります。通常の修正はステータス列の山、スタックした行を見つけるcronジョブ、各ステップごとに手動で作成した再試行ロジックです。
Temporalの約束は、開始したプロセスが最後まで実行されることです。ランタイムの図:Temporalサービス(独自のクラスター)があり、アプリはそれをポーリングしてコードを実行するworkerプロセスを実行します。ワークフローが実行されると、Temporalはすべてのステップをイベントhistoryに記録します。ワーカーが死ぬと、別のワーカーがワークフローを引き継ぎ、そのhistoryをreplayして状態を再構築し、失敗したものを再試行しながら中断したところから続けます。
historyが真実のソースであり、クラッシュから生き残ります。以下のルールのほとんどはこの一つの事実から導かれます。
黄金律:ワークフローが決定し、アクティビティが実行する
Temporalには2種類のコードがあります。
- ワークフローはオーケストレーションです。何をどの順序で実行するかを指示します:Aを呼び出し、次にBを呼び出し、待機し、次にCを呼び出す。
- アクティビティは実際の作業です。各々が実質的なことを行います。サービス呼び出し、DB書き込み、Kafka発行などであり、それぞれが独立して再試行されます。
ルール:すべてのビジネスロジックと外部との接触はアクティビティに置きます。ワークフローは退屈で読みやすいステップのリストのままにします。
これはreplayから導かれます。ワーカーは記録されたhistoryに対してコードを再実行することでワークフローを再構築するため、そのコードはdeterministicでなければなりません。同じhistoryに対して再実行すると、同じ決定を下します。生のクロック読み取り、rand()、ワークフロー自体のネットワーク呼び出しはできません(SDKは時間の決定論的バージョンと乱数を提供します)。すべての非決定論的なものはアクティビティに入れ、その結果が記録されreplayされます。
Temporalを1つのサービス内に留める
これが最も強く主張したいもので、Temporal自身のマーケティングが陥れる罠だからです。
Temporalでは、あるサービスでワークフローを定義し、そのアクティビティをどこでも(10のサービスでも)実行できます。1つのワークフローがあなたの全資産をオーケストレーションするのは素晴らしいように聞こえ、まさにその約束に大きなプッシュがあったのを見たことがあります。
これは結合の罠です。アクティビティが別のサービスに存在する瞬間、そのサービスはTemporal統合と、さもなければ必要としない共有namespaceを必要とします。さらに悪いことに、そのアクティビティの入出力スキーマがあなたのワークフローに結びつけられます。同僚がアクティビティの形状を変更すると、history内の古い形式の結果が新しいコードに逆シリアル化できなくなり、進行中のワークフローが壊れます。2つのチームのデプロイを、どちらも見ることのできないメカニズムを通じて結合してしまったのです。
境界をサービスで引きます。ワークフローとそのすべてのアクティビティは1つのサービスが所有します。アクティビティが別のサービスを必要とするときは、普通のHTTP呼び出しやKafkaイベントを発行します。みんなが理解する退屈で普遍的なものです。Temporalは単一サービスのimplementation detailとなり、ワークフローが2つのチームにまたがることはありません。
アカウント作成が良い例です。ワークフロー全体は4つのアクティビティです:
- 作成開始をマーク - 自分のDBに書き込む。
- 決済プロセッサにアカウントを登録 - HTTP。
- 元帳にアカウントを作成 - HTTP。
-
作成完了をマークし
account_createdイベントを発行 - DBに書き込み、Kafkaに発行。
ワークフローは順序を指定するだけです。各アクティビティが作業を行い、必要なものをIDでロードするため、リッチデータがサービス境界を越えて運ばれる必要はありません。
ワークフローは非同期:結果を通知する
リクエスト/レスポンスから来る場合、これはメンタルシフトです。ワークフローを開始するとほぼ即座に返ります。作業が完了するのを待つことはありません。作業はワーカー上でバックグラウンドで実行されるため、呼び出し元はレスポンスから結果を読み取れません。結果は別の方法で返さなければなりません。
それがアカウント作成ワークフローがaccount_createdを発行して終わる理由です。イベントは装飾ではありません。アカウントが実際に作成されたことを誰もが知る方法です。反応する必要がある人(ウェルカムメールの送信、カードのプロビジョニング)は、そのイベントを購読し、あなたの呼び出しをブロックするのではありません。イベント駆動システムには適していますが、呼び出し元が同じリクエスト内で回答を必要とする場合には不適切です。
失敗パスも通知しますが、パスがある場合に限ります。無限再試行ではワークフローは成功するまで動き続ける可能性があるため、報告すべき終端の失敗はありません。代わりにN回試行後に諦める場合は、そのパスで何か発行し、下流が決して来ない成功を待たないようにします。
アクティビティ:べき等、再試行可能、そして例外の罠
Temporalはアクティビティを自動的に再試行します。指数バックオフと無制限の試行で成功するまでです。それを継承します。要求するものではありません。そして2つのことが続きます。
第一に、べき等性は正しさであり、衛生ではありません。 アクティビティは複数回実行される可能性があり、2回実行すると間違ったことをするなら、それをヒットするのは時間の問題です。元帳ではこれは具体的です。トランザクションを投稿するアクティビティが2回実行されると、お金が2回動きます。修正は、再試行する呼び出し元背後の普通のものです。操作のIDに紐づいた安定したキー(トランザクションIDプラスステップ)で書き込みを重複排除します。試行ごとに新しいUUIDではありません。それではチェックを無効にします。すべてのアクティビティは同じ形状になります。IDで状態をロードし、そのキーがすでに着地していれば早期リターンし、作業を行い、コミットします。
第二に、失敗をシグナルする方法が再試行するかどうかを決定します。 例外を発生させると「再試行せよ」を意味するので、意図的に行います:
- 一時的失敗(タイムアウト、503):例外を発生させます。再試行して回復します。スタックした方が失敗したより良い。
- 期待される「いいえ」(拒否、すでに存在):例外を発生させません。通常の結果として返し、ワークフローを継続させます。決して変わらないビジネス結果に対して例外を投げると、永久的な答えに対して無限ループを構築してしまいます。これがトピック全体で最も鋭いエッジです。
- 本当に回復不能な失敗(不正な入力):高速に失敗させるためにnon-retryableエラーを発生させます。
「停止するか永遠に再試行するか」について、私のデフォルトは無限再試行から始めて監視することです。ワークフローが成功する必要がある場合、それに良いアラートを加えるのが最もシンプルに動作するものであり、修正をデプロイすれば自己修復します。しかし、無限試行は試行ごとのStartToCloseタイムアウトがある場合にのみ安全です。クラッシュせずにハングするアクティビティは再試行する失敗を生成せず、永遠に座り続けます。それが本当のフットガンであり、無限再試行ではありません。
ワークフローIDは無料の並行性ロック
ワークフローを開始するときにワークフローIDを設定し、Temporalは指定されたIDを持つワークフローが一度に1つしか実行されないことを保証します。構築する必要のなかったシリアライゼーションロックであり、IDスキームが粒度を設定します:account_id-create-accountはその操作だけをシリアライズし、account_idだけは(アカウントごとのスループットを犠牲にして)そのアカウントのすべての操作をシリアライズします。
私は元帳の前に置いたアカウントライフサイクルサービスでこれを使いました。特定の更新は厳密にシリアライズする必要があり、アカウントIDでワークフローをキーにすることでロックテーブルなしでそれを得ました。IDがロックです。(ワークフローが実行されている間のみ保持します。クローズ後の再利用はポリシーによって管理されます。)
Temporalはオーケストレーションする。状態を保持しない
Temporalはプロセスを実行します。状態が存在する場所ではありません。ワークフローが運ぶライブ状態も、何が起こったかの耐久性のある記録もです。2つの習慣がその境界をきれいに保ちます。
進行中のコンテキストを最小限に保つ。 ワークフローにデータをロードしてアクティビティにスナップショットを渡さないようにします。ワークフローはIDを渡し、各アクティビティが実行時にDBから必要なものをロードし、どのように進めるかを決定させます(べき等性チェックを含む)。早期にキャプチャされたスナップショットは古くなり、それを書き戻すと同時更新を上書きします。IDを渡すことはそれを回避し、機密データをペイロードから外し、イベントhistoryを小さく保ちます。
永続状態を自分のDBに保持し、Temporalには保持しない。 Temporalは保持ウィンドウの間historyを保持し、その後エージングアウトします。したがって、6ヶ月後に誰かがアカウントに何が起こったかを尋ねたときに到達するものではありません。これは2つのことをカバーします。ドメイン状態(残高、ステータス、レポーティングとメールの背後のデータ)は明らかにあなたのデータベースに属します。しかし、ワークフロー自身の進捗もそうです。最初のアクティビティがcreation_startedを書き込み、最後のアクティビティがcreation_completedを書き込みます。これによりTemporalは純粋にランタイムオーケストレーションを行い、DBが記録を所有します。
進捗を書き下ろすことは、重要なアラームも与えます。Temporalの組み込みレイテンシメトリックはワークフローが終了したときにのみ発火します。オープンなものがどのくらいの時間実行されているかのメトリックはありません。そこで私たちは独自のものを保持しました。毎時のジョブがcompletedに到達していない開始行を閾値内でフラグ付けしました。些細なDBルックアップです。しかし、それはレコードを書き込んだからこそです。(ワークフロータイプごとに意味のあるSLAでアラートし、巨大な再試行カウントでスタックしたアクティビティでもアラートします。自己解決しない失敗を隠している無限再試行です。)
これがdual-write問題です。2つのシステム(ここではTemporalとDB)への2つの書き込みでトランザクションを共有できないため、その間のクラッシュは2つを同期から外します。それらをアトミックにすることはできないので、生きられる失敗を選ぶように順序付けます。ワークフローを最初に開始します。Temporalは返る前に耐久的に記録し、その最初のアクティビティに「started」行を書き込ませます。クラッシュは、行が着地するまで再試行するワークフローを意味し、ワークフローが決して処理しない孤立した行ではありません。DBとKafkaのバージョンは以下のKafkaセクションに登場し、outboxパターンがそれを完全に除去できます。
ワークフローの進化:前方修正と長時間実行の罠
ワークフローは変更するのにコストのかかる部分なので、小さく保ちます。賢い分岐はなく、直線で、複雑さはアクティビティに押し下げます。
これはバグを修正するときに報われます。アクティビティのバグは痛くない: コードを修正してデプロイすると、次の再試行が新しいコードを実行します。ワークフロー定義は変更されていないため、決定論は危険にさらされません。ワークフローのバグは難しいケース: 進行中のワークフローは古いhistoryをreplayしており、新しいオーケストレーションコードはそれから乖離する可能性があります。したがって、頭脳をアクティビティに置き、前方修正するように設計します。
その難しいケースは長時間実行のワークフローで永久的になります。これは運用上の痛みの最大の源です。私は数週間実行されるアカウントクローズワークフローに携わりました。残高をクローズする前に規制上の保留です。ウィンドウが開いている時間が長いほど、下位のものが変わる機会が多くなり、その間ずっとそのコードパスをライブで維持します。30日前にオープンしたままのワークフローは、安全にリタイアできない定義を持つものです。
したがって、数週間のワークフローは飛行中に変更するための計画が必要です。古いものを排出するか、古い実行が古いパスを取るようにその場でパッチを当てます。Temporalはこれを和らげます。Continue-As-Newは長いワークフローを新しいhistoryにチェックポイントして境界を保ち、変更可能にし、耐久性のあるtimerとsignalがポーリングループの代わりにネイティブに待機をモデル化します。しかし本当の教訓はよりシンプルです。リスクはワークフローがオープンしている時間の長さに比例するので、短く保ちます。
Kafkaコンシューマーで十分なときを知る
明白な質問は「なぜKafkaを使わないのか?」です。正直な答え:Kafkaでマルチステッププロセスをオーケストレーションできます。メッセージを読み、作業を行い、オフセットをコミットし、次を発行します。キャッチは、すべての機械を自分で構築することです。再試行、デッドレターキュー、バックオフ、ステップごとの状態、何かがスタックした場所の可視性です。
Temporalはそれに加えて、Kafkaで手動で作成するアクティビティごとの再試行設定を提供します。アクティビティAはバックオフで10分ごとに再試行、Bは再試行不可、Cは5回で上限。Durable executionは本質的に、すべてのステップ間でチェックポイントするKafkaコンシューマーであり、すでにあなたのために書かれています。
では、追加のプラットフォームが価値があるのはいつでしょうか? フローが待機、監視を必要とする、またはチェックポイントする複数の副作用があるときです。それ以下。1つの副作用、待機なし、再試行後デッドレターで十分。コンシューマーが正しいツールであり、Temporalはオーバーヘッドです。
コンシューマールートに行く場合、コミットの順序がゲーム全体です:
read msg → do work → commit DB → emit event → ack input (last)
Enter fullscreen mode Exit fullscreen mode
DBコミットの後(その前)に発行します。コミット失敗は幽霊イベントを残します。そして入力を最後にackします(ackしてからクラッシュすると作業が失われます)。これはDBとKafkaのdual write再びです。順序付けはより小さな失敗を選びますが、コミットと発行の間のクラッシュは依然としてイベントをドロップします。そのギャップを完全に閉じるには、transactional outbox patternを使います。状態変更と同じトランザクションでイベントをoutboxテーブルに書き込み、後でリレーが発行するようにします。Temporalはこれを箱から出して提供します。コンシューマーはそれを構築させます。
これらのどれもKafkaを敗者にしません。Kafkaが勝つのはfire-and-forgetドメインイベントです。「アカウント作成」を発行し、下流がそれで何をするかを所有しません。Temporalは所有するワークフロー用で、すべてのステップが終わりまで到達することを気にするものです。
1つの代替案:DBOS
実行と運用するための別クラスターがためらいを与える部分である場合、DBOSは一見の価値があります。Durable executionのより軽量なアプローチです。運用する独立したサービスの代わりに、すでに実行しているPostgresデータベースにワークフロー状態を永続化するライブラリです。Temporalのより豊富なツールとスケールを、はるかに少ない運用サーフェスと交換します。ワークフローが控えめで、別のクラスターを所有したくない場合に合理的な取引です。
始め方
上記のすべては1つのアイデアの結果です:historyが真実のソースであり、ワーカーはいつでもそれをreplayできる。 決定論、アクティビティにロジックを置くこと、べき等再試行、ドメイン状態を自分のDBに置くこと。これらはすべてそのことから導かれます。それを得れば、残りはチェックリストのように感じられなくなります。
最も痛みを節約するプロセス習慣:本番で直接反復することを避ける。ローカルdevサーバーを立ち上げ(temporal CLIがコマンドで1つを提供)、ワークフローをエンドツーエンドで実行させ、小さなことから始めます。そしてWeb UIを活用します。ワークフローの完全なhistoryを検査し、手動でkillまたはreplayするのは、queue-plus-dead-letterセットアップが決して与えない具体的なものの1つです。
If you found this useful, I write about distributed systems and engineering practices at nejckorasa.github.io. You can also find me on LinkedIn and X.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.