HN 日本語サマリー

← 一覧へ戻る
インフラ・DevOps

Jepsen: Orkes Conductor 5.3.2

Jepsen: Orkes Conductor 5.3.2 (jepsen.io)

47 pointsby aphyr1 コメント

要約

Jepsenは、ワークフローオーケストレーターであるOrkes Conductorの信頼性に関する分析結果を公開しました。この分析では、Conductor 5.3.2、5.4.0、5.5.0-rc1/rc2のバージョンを対象に、Jepsenテストライブラリを用いて、システム障害やネットワーク障害下でのデータ整合性、タスク実行の正確性などを検証しました。テストの結果、一部のバージョンでデータ損失の可能性が示唆されましたが、全体としてConductorは堅牢なワークフロー管理システムであることが確認されました。

全文翻訳

Orkes Conductor 5.3.2

Orkes Conductorは、ユーザー定義タスクを実行し、それらの間でデータをルーティングするワークフローオーケストレーションプラットフォームです。私たちはOrkes Conductorのバージョン5.3.2から5.5.0-rc2までをテストし、個々のタスクの予期せぬ頻繁な並行実行、ワークフロー実行を大幅に遅延させる可能性のある1つの問題、startWorkflowのエラーやnull IDに関する4つの軽微な問題、そしてConductorがワークフロー実行を永久に放棄または忘却させたり、タスクが実行されていないにもかかわらず完了とマークしたりする3つの深刻な問題を発見しました。Orkesは、すべての深刻な安全上の問題に対するパッチを適用し、並行実行を削減し、2つのstartWorkflowエラーを修正したバージョン5.5.6をリリースしました。他の2つのstartWorkflowエラーは調査中です。遅延実行はConductorの設計パラメータと一致しており、対処する必要はありません。Orkesはこの作業に関する共同ブログ記事を公開しています。この調査はOrkes Inc.の資金提供を受け、Jepsen倫理規定に従って実施されました。

1 背景

ワークフローオーケストレーターは、小さなタスクから構成される計算を実行するソフトウェアシステムです。タスクはワークフロー内で接続され、ワークフローは一部のタスクの出力を他のタスクの入力にルーティングし、どのタスクがいつスケジュール可能かを決定します。オーケストレーターはワークフローとその特定の実行を管理し、タスクを実行し、状態を永続化し、各実行を完了に導こうとします。

Conductorは、元々Netflixによって構築され2016年にオープンソース化されたワークフローシステムです1。オープンソース版であるConductor OSSは、現在Conductor OSS Foundationによってメンテナンスされています。この財団は、NetflixでConductorの構築に貢献した創設者たちによって率いられているOrkesによって主導されています。Orkesは、追加機能を持つConductorのホスト版も提供しており、これをOrkes Conductorと呼びます。この分析は、私たちがConductorと略称するOrkes Conductorに焦点を当てています。

Conductorでは、ワークフロー定義は、どのタスクを実行する必要があるか、その順序、入力と出力はどうあるべきかなどを定義するJSON構造です。各ワークフロー定義は、名前とバージョンによって識別されます。例えば、猫を飼うためのワークフロー定義は、まず申請を完了し、次に獣医の予約をスケジュールし、獣医から予防接種記録を受け取るという手順を指定するかもしれません。ワークフローは入力(猫の名前など)を受け取り、出力(養子縁組証明書など)を生成することもできます。

ワークフロー定義は何度も実行される可能性があります。例えば、飼われる猫ごとに1回実行されるかもしれません。各インスタンスはワークフロー実行と呼ばれますが、実行を短縮して「ワークフロー」と呼ぶこともよくあります。各実行は、一意のワークフローIDによって識別されます。実行中に、タスクも複数回試行される可能性があります(例:失敗した場合)、各試行には一意の実行IDが割り当てられます。

一部のタスクはConductorに組み込まれています。HTTPリクエストの実行やKafkaのようなログへのイベント送信などのシステムタスクがあります。その他はオペレーターと呼ばれ、条件分岐、フォーク/ジョイン、ループなどの制御フローを提供します。オペレーターはECMAScriptコードを実行してユーザー定義のロジックを実行できます。最後に、システムで「ビジネスロジック」を実行するためにユーザーが記述するワーカータスクがあります。

ワークフローの実行を開始するには、ConductorへのAPI呼び出し、メッセージブローカーを介したイベントの送信、またはConductorの組み込みcronスタイルのスケジューラを使用できます。確認されると、Conductorはワークフローのタスクをスケジュールし、完了に導こうとします。

1.1 アーキテクチャ

Conductorには5つの主要コンポーネントがあります。Conductorサーバーはステートレスなプロセスで、ワークフローを管理し、実行をスケジュールします。これらはAPIとユーザーインターフェースを提供し、外部サービスと対話し、状態を追跡します。ワークフローの定義、その実行の状態、およびタスクの結果は、Postgres、Cassandra、またはRedisのようなデータベースに保存されます。Conductorはまた、キュー(多くの場合Redis)を使用して、さまざまなプロセスに作業をディスパッチし、一時的な状態を保存します。最後に、ステートレスワーカーの2つのプールがあります。Conductorの組み込みタスクを実行する標準ワーカーと、さまざまな言語のSDKを使用してユーザーが記述したカスタムワーカーです。すべてのワーカーはConductorサーバーに接続し、サーバーからタスクをポーリングし、それを実行し、結果をサーバーに返します。

各コンポーネントは独立してスケールできます。任意の数のサーバーとワーカーを並行して実行でき、ノードはいつでもキルして再起動できます。バージョン5.3.2では、データベースとキューの両方が、確認されたすべての書き込みを保持するために必要でした。しかし、バージョン5.4.0では、メインのデサイダーキュー以外のすべてのキューにロスのあるストレージシステムの使用を可能にする新しいオプションが導入されました。

1.2 保証

Conductorは「少なくとも1回のメッセージ配信」を保証し、すべてのメッセージが永続的であり、タスクワーカーに1回以上配信されることを保証します。Conductorはまた、ワークフローが最終的に終了することを保証します。「ワークフローが開始された場合、すべてのタスクが完了する限り、それは完了まで実行されます。」これは、ワークフローとタスクが必ずしも成功裏に実行されることを意味するわけではありません。タスクはリトライ回数が尽きたり、リトライ不可能なエラーで失敗したり、明示的にキャンセルされたりする可能性があります。ワークフロー実行にも同様の状態遷移が適用されます。しかし、ワークフローは最終的にターミナルステータス(COMPLETED、FAILED、TIMED_OUT、またはTERMINATED)に収束するはずです。

タスクが実行される際、各試行には一意の実行IDと、既に実行された試行回数を示す一意のリトライカウントが割り当てられます。

Orkesによると、各ワークフロー実行について、タスクの各インスタンスは最大1つの可視的な成功実行を持ちます。タスクは複数回成功裏に実行される可能性があります。例えば、Conductorサーバーがタスク実行の完了を永続化する直前にデータベースへの接続を失った場合などです。しかし、成功したタスクの出力が可視的になれば、どのタスク、ワークフロー、またはAPI呼び出しも異なる出力を観測することはありません2。

2 テスト設計

これらのプロパティを検証するために、私たちはJepsenテストライブラリとバージョン5.0.1のConductor Java SDKを使用して、Orkes Conductor用のテストハーネスを構築しました。このハーネスを使用して、Orkes Conductor 5.3.2、5.4.0、および5.5.0-rc1と-rc2をテストしました。

Orkesは通常、Conductorをクラウド環境にデプロイし、プライマリデータベースとしてPostgres(またはそれと互換性のあるデータベース、例えば自社開発のParticleDB)を使用し、キューとしてRedis(またはValkeyのような互換性のあるサービス)を使用します。これらのデプロイメントを近似するために、コンテナ内および個別のEC2 VMの両方で10台のDebianマシンからなるクラスターをセットアップしました。データベースには単一のPostgresノード、キューには単一のRedisノードまたはRedis Cluster、残りのノードはConductorサーバー、Conductorの標準ワーカー、およびJava SDKを使用して作成したカスタムワーカーに均等に分割しました。

私たちのテストワークロードは、カスタムワーカーが処理する単一タスクを持つ単一のワークフローを定義しました。タスクは最長30秒スリープして作業をシミュレートし、その後ランダムに生成された出力を返しました。また、テストハーネスで実行をログに記録しました。これにより、どのタスクがいつ実行され、その観測された入力と意図された出力が何であったかを正確に知ることができました。

テストは3つのフェーズで進行しました。メインフェーズ中、テストハーネスは一意の入力でstartWorkflow()を呼び出し、どの実行が進行中か、どの実行が終了したかを追跡し、getWorkflow()を使用して特定の実行の状態を確認しました。startWorkflow()の呼び出しがタイムアウトまたは失敗した場合に実行を検出するために、定期的にgetRunningWorkflow()を呼び出して、進行中のすべての実行をリストしました。メインフェーズ中には、プロセスの停止やキル、ConductorサーバーとPostgres、Redis、ワーカーノード間のネットワークパーティションなど、さまざまな障害を注入しました。

リカバリフェーズ中、テストはネットワークパーティションを終了させ、プロセスを再起動・再開させ、クラスターが回復するのを待ちました。システムが安定状態に収束した時期を把握するために、Conductorのデサイダーキューの深さをポーリングしました。このキューがゼロに達すると、Conductorは他に何もすることがなくなり、テストは最終フェーズに進みました。

最終フェーズ中、テストはこれまで観測したすべてのワークフロー実行の状態を読み取りました。その後、さまざまな予期しない動作を検索しました:

バックエンドエラー:

一部のリクエストが予期しないバックエンドエラーを返しました。

並行実行:

2つ以上のワーカーが、単一ワークフロー実行内で同じタスクを同時に実行しようとしました。

異なるタスク入力:

同じワークフロー実行内の同じタスクの2つのビューが異なる入力を持ちました。または、タスクの入力がワークフローの入力と一致しませんでした。

異なるワークフロー入力:

単一ワークフロー実行が2つの異なる入力を持ちました。

異なるワークフロー出力:

単一ワークフロー実行が2つの異なる出力を持ちました。

重複ワークフローID:

startWorkflowへの2つの呼び出しが同じワークフロー実行IDを返しました。

G1a:

startWorkflowリクエストが明確な失敗のように見えるものを返しましたが、要求された入力を持つワークフローが後に一部のリーダーに観測されました。

不可能な出力:

ワークフロー実行の出力が、そのタスクの出力と一致しませんでした。

不完全:

ワークフロー実行が最終フェーズ中にCOMPLETED状態ではありませんでした。

欠落:

読み取りが、既に存在を知っていたワークフロー実行を観測できませんでした。

開始されず:

ワークフローが対応するstartWorkflowリクエストなしで存在しました。

NullワークフローID:

startWorkflow呼び出しがワークフローID文字列ではなくnullを返しました。

タスク未実行:

ワークフローのタスクはいずれも実行されませんでした。

3 結果

3.1 タスクが並行して実行される (#1)

通常の操作では、健全なConductorクラスターは、同じワークフロー実行のために同じタスクを2つのワーカーで同時に実行することが時折ありました。これは、相互排他が非同期ネットワークでは不可能であるため、驚くことではありません。しかし、非常に短命なタスクでさえ同時に実行される可能性があることを念頭に置く価値があります。このテスト実行では、ワークフロー 3919784b-7691-11f1-a292-00163e983f22 の単一タスクが70.110秒にワーカー n9 で開始され、その後70.113秒にワーカー n10 で開始されました。n9では70.114秒に完了し、n10では1ミリ秒未満後に完了しました。

Conductor OSSのドキュメントでは、「複数のサーバーインスタンスが同じワークフローを同時に評価する際の競合状態を防ぐ」分散ロックシステムについて説明しています。Orkes Conductorにも、conductor.workflow.lockLeaseTimeで設定されるリース時間を持つロックメカニズムがあります。このロックは並行実行の頻度を減らすことができますが、完全に防ぐことはできません。

単一タスクワークフロー、デフォルトのリース時間1分、2つのConductorサーバー、3つのワーカー、ほぼゼロのネットワーク遅延、短命なタスク(p50 ~500 µs、p95 ~5 ms、p99 ~10 ms)、および毎秒約1,100ワークフローという条件下で、健全なクラスターでは約6時間に1回並行実行を観測しました。リース時間を1秒に減らすと、数分ごとに並行実行が発生しました。本番環境での頻度は変動する可能性があります。

この問題は、スタックしたタスクを再キューイングするスイーパープロセスにおけるチェック時間から使用時間までのバグによって悪化しました。スイーパーがSCHEDULED状態のタスクを読み取ると想像してください。その後、ワーカーがそのタスクを実行し、データベース内の状態をIN_PROGRESSに変更し、キューからタスクメッセージを削除します。スイーパーはキューをチェックし、(もはや)スケジュールされていないタスクのメッセージがないことを発見します。それを再キューイングし、重複配信につながります。Orkesはこの問題(#1)がバージョン5.5.6で修正されたと報告しています。

Jepsenはまた、OrkesがConductorのドキュメントを更新し、タスクが並行して実行される可能性があることを明確にするよう提案しています。ワーカーがretryCount = 0(最初に実行されたことを示唆)を観測する可能性もありますが、他のワーカーが既にそのタスクを成功裏に実行していた場合でも同様です。

3.2 タスクが実行前に停止する (#2)

PostgresをプライマリDBとし、Redisをキューとして使用した場合、Orkes Conductor 5.3.2では、タスク実行前に少数のワークフローが停止することがよくありました。停止中、各ワークフローはRUNNING状態を示し、ワークフローのタスクは特定のワーカーで実行されているように見えました。しかし、ワーカーのexecute()メソッドはそのタスクに対して呼び出されることはありませんでした。この状態はタスクの応答タイムアウトがトリガーされるまで持続し、その後Conductorはタスクを再スケジュールしました。この動作は、Redisがキルまたは一時停止された場合、Postgresがキルされた場合、サーバーがキルされたりRedis、Postgres、またはワーカーからパーティションされた場合、またはワーカーがキルされた場合など、さまざまな障害に応答して観測されました。

例えば、このテスト実行を考えてみましょう。この実行では、20のワークフロー実行のうち2つが停止しました。ワークフロー実行 3f71d24a-7619-11f1-b911-00163eb95b8c は08:23:43.588にサーバー n3 によって確認され、その唯一のタスクは08:23:43.593にスケジュールされ、08:23:44.615にワーカー n10 でIN_PROGRESSとして記録されました。しかし、ワーカー n10 はそのワークフローIDでWorker.execute(task)を呼び出すことはありませんでした。サーバー n3 は08:24:42にRedisに対する読み取りタイムアウトをログに記録するまで正常に動作しているように見えました。

一般的に、ConductorサーバーがRedisから切断、クラッシュ、またはパーティションされると、1時間(デフォルトの応答タイムアウト)で少数のワークフローが確実に停止しました。ユーザーはresponseTimeoutSecondsパラメータを短縮して遅延を短くすることができます。

タスクは最終的に応答タイムアウトウィンドウで示唆される時間内に実行されたため、これはConductorが主張する保証に違反するものではありません。Orkesはこの動作を変更しないことを選択しました。(#2)。

3.3 NullワークフローID (#3)

ワークフローを作成する際、JavaクライアントのstartWorkflow()メソッドは通常、ワークフローIDを文字列として返しました。しかし、Redisプロセスをキルしたこのテスト実行のように、まれにnullを返すことがありました。

Orkesはこの問題(#3)を調査中です。根本原因は不明ですが、Javaクライアントのバージョン6.0.2には緩和策が含まれています。

3.4 構文エラー (#4)

時折、障害のない健全なクラスターで、startWorkflowへの呼び出しが構文エラーを返しました。リクエストには特別なことは何もなかったようです。それらは、他のすべてのstartWorkflow()呼び出しと全く同じ種類の入力を提供していました。以下は典型的な例です。

{:type    :invoke,
 :process 12,
 :f       :start-workflow,
 :value   {:name "greetings",
           :version 1,
           :input {"name" "jepsen 10"}}}

これはBACKEND_ERROR - ERROR: syntax error at or near "null" Position: 13というエラーを発生させました。

Conductorはワークフロー実行を日付でパーティションされたテーブルに保存します。新しいパーティションが作成またはローテーションされた際、競合状態によりパーティションマネージャーが一時的にシャードからテーブルへの空のマップを観測することがありました。Conductorはその後、その空のマップでシャードを検索し、INSERT INTO null ...のようなSQLステートメントを構築しました。Postgresは構文エラーを返し、そのエラーメッセージがクライアントにリレーされました。バージョン5.5.6では、Conductorは書き込みを受け入れる前にシャードマップが初期化されていることを保証し、この問題を防止するはずです(#4)。

3.5 重複キーエラー (#8)

リリース候補ビルド5.5.0-rc1では、ワークフローの開始が非常にまれにPostgresの重複キーエラーを返すことがありました。例えば、このテスト実行(Postgresのキルと一時停止を伴う)を見てみましょう。単一のstartWorkflow呼び出しがこのエラーを返しました。

BACKEND_ERROR - ERROR: duplicate key value
violates unique constraint
"workflow_archive_shard_00_pkey"

Detail: Key (org_id, workflow_id)=
(0000, 72e49555-81eb-11f1-8ccb-00163eb95b8c)
already exists.

これは5.5.0-rc1に追加されたリトライメカニズムによって引き起こされたようです。Conductorサーバーが新しいワークフロー実行レコードをPostgresに挿入できなかった場合、再試行し、以前の書き込みと競合する可能性がありました。そのエラーメッセージはクライアントにリレーされました。バージョン5.5.6では、Conductorはリトライ時の重複IDエラーを無視するはずです(#8)。

3.6 中断された読み取り (#6)

5.3.2、5.4.0、5.5.0-rc1、および5.5.0-rc2で、PostgresデータベースとRedisキューを使用した場合、Conductorサーバーをキルすると、startWorkflow()への呼び出しがFailed to connect to n3/10.0.3.103:8080のようなエラーをスローすることがよくありました。これは明確な失敗のように聞こえます。結局、クライアントが接続を開けなかった場合、どのようにしてワークフローを開始できたのでしょうか?しかし、これらのワークフローの多くは実際にワーカーによって実行され、getWorkflow呼び出しで観測可能でした。この短い実行では、25,920回のstartWorkflow失敗呼び出しのうち1,752回が実際には成功していました。例:

{:index 65834,
 :time 11146587755,
 :type :fail,
 :process 24,
 :f :start-workflow,
 :value {:name "greetings",
         :version 1,
         :input {"name" "jepsen 8568"}},
 :error
 [:failed-to-connect
  "Failed to connect to n3/10.0.3.103:8080"]}

このワークフローの一意の入力、"jepsen 8568"は、後のワークフロー 559cb7ea-7f07-11f1-9dce-00163eb95b8c の読み取りで観測されました。実際、クライアントはワークフローを作成するための成功したリクエストを発行していましたが、おそらくクライアント側のタイムアウトのため、内部的にリクエストを再試行することを選択しました。そのリトライが接続を開けなかった場合、以前のリクエストタイムアウトを反映するのではなく、その最も最近のエラーのみを返しました。OrkesはJavaクライアントを変更して冪等でないリクエストの再試行を回避し、クライアントエラーにisDefinite()メソッドを追加することを検討しています(#6)。

3.7 放棄されたワークフロー (#5)

バージョン5.3.2、5.4.0、および5.5.0-rc1で、PostgresデータベースとRedisキューを使用した場合、さまざまな障害がConductorに一部のワークフロー実行を永久に放棄させる原因となることがありました。これらのワークフローは無期限にRUNNING状態のままで、タスクは実行されませんでした。しかし、デサイダーキューとタスクタイプのキューの両方が空でした。これは、単一のConductorサーバーをキルした場合、またはサーバーをPostgresまたはRedisから隔離した場合、またはPostgresをキルした場合に観測されました。待機したりノードを再起動したりしても、Conductorは放棄されたワークフローを再実行しませんでした。

このテスト実行を考えてみましょう。15秒のメインフェーズ中にサーバー n4 を1回キルしてから再起動しました。これにより、ワークフロー 70ee12a3-7bd8-11f1-8029-00163e983f22 が放棄されました。テスト終了から数時間後でも、クラスターはそのワークフローをRUNNING状態でリストしていましたが、どのワーカーもそのタスクを実行しようとはしませんでした。

ワークフローが開始されると、ConductorはまずPostgresにワークフローレコードを書き込み、次にワークフローをデサイダーキューにエンキューします。これらの書き込みのいずれかで障害が発生した場合、ワークフローはPostgresに正常に書き込まれたかもしれませんが、キューに存在しない可能性があります。その結果、実行されることはありません。驚くべきことに、スイーパープロセスは、実行中のワークフローを定期的に監視しますが、データベースではなくデサイダーキューのみに依存しています。これは、Conductorがこれらの部分的な書き込みから回復しなかったことを意味します。バージョン5.5.0では、Orkesは放棄された実行を再エンキューするバックグラウンドプロセスを追加しました(#5)。

3.8 失われたワークフロー (#7)

バージョン5.3.2、5.4.0、および5.5.0-rc1では、ConductorサーバーとPostgres間のネットワークパーティション、またはPostgresのキルが、まれにConductorが以前はクライアントから観測可能だったワークフロー実行の存在を忘れる原因となることがありました。Java SDKのgetWorkflow()メソッドへの呼び出しはnullを返し、/api/workflow/<id>へのHTTPリクエストは404エラーを返しました。この状態は無期限に持続しました。失われたワークフローは回復不可能でした。

例えば、PostgresをDBおよびデサイダーキューとして使用し、Redisを通常のキューとして使用したバージョン5.4.0のテストを見てみましょう。ワークフロー 0c426286-80e9-11f1-bb95-00163e983f22 は、テスト開始から239秒後にgetRunningWorkflow()への呼び出しを通じて発見されました。Conductorは、このワークフローが313秒までRUNNING状態であると主張しました。313秒からテスト終了の677秒まで、そのIDに対するgetWorkflowへのすべての呼び出しはnullを返しました。

このワークフロー実行はPostgresにコミットされましたが、サーバーはそれをデサイダーキューに追加できませんでした。おそらくネットワークパーティションのためです。この障害により、サーバーはPostgresへの書き込みを取り消すことを決定し、ワークフローレコードを削除しました。その74秒の間に、その運命づけられたワークフローはクライアントから観測可能でした。バージョン5.5.0では、Conductorはステップが失敗したときに実行を削除しなくなりました(#7)。

3.9 販売中:完了したワークフロー、実行されず (#9)

Postgresがキルされたとき、タスクが実際には一度も実行されていないにもかかわらず、COMPLETED状態のワークフローを時折見つけました3。バージョン5.5.0-rc2のこのテストでは、3つのワークフローが完了しましたが、実行されませんでした。テスト終了時、ワークフロー 774977a0-8324-11f1-911d-00163e983f22 は次のようになりました。

{
  "tasks" : [{
    "pollCount" : 1,
    "reasonForIncompletion" : "responseTimeout:
     10 exceeded for the taskId:
     77499eb5-8324-11f1-911d-00163e983f22
     with Task Definition: greet",
    "retried" : true,
    "taskDefinition" : {
      "retryDelaySeconds" : 5,
      "rateLimitFrequencyInSeconds" : 1,
      "responseTimeoutSeconds" : 10,
      "retryCount" : 2147483647,
      "timeoutPolicy" : "TIME_OUT_WF",
      ...
    },
    "retryCount" : 0,
    "status" : "TIMED_OUT",
    "executed" : false,
    "workerId" : "n7",
    ...
  }],
  "status" : "COMPLETED",
  ...
}

このワークフローの単一タスクはワーカー n7 に割り当てられたとされていましたが、n7 も他のどのワーカーもそれをログに記録していませんでした。このタスクに対してWorker.executeが呼び出された形跡はありませんでした。タスクはTIMED_OUT状態で終了し、そのリトライポリシーはTIME_OUT_WFでした。これは、Conductorエラーハンドリングドキュメントによると、ワークフローもTIMED_OUT状態になるはずでした。それにもかかわらず、ワークフローは実行されなかったにもかかわらず、COMPLETEDとマークされました。

このワークフローは、前述のタスク停止問題(#2)の影響を受けていました。ワーカー n7 はデサイダーキューをポーリングし、Conductorは n7 がタスクを実行するだろうと考えていましたが、(おそらくネットワーク障害のため)n7 は実行を開始しませんでした。その後、サーバー n4 のスイーパータスクはタスクをTIMED_OUTとマークし、リトライのスケジュールを試みました。そのリトライプロセスはPostgresに2つの別々の書き込みを行います。1つは新しいタスクレコードを挿入し、もう1つは元のタスク行を置き換えられたとマークします。Postgresはこの2つの書き込みの間で失敗しましたが、Conductorはその失敗を無視し、新しいタスク行なしで元のタスクを置き換えられたとマークしました。これが、タスク定義ではリトライされるべきとされていたにもかかわらず、タスクが一度もリトライされなかった理由を説明しています。サーバー n3 のスイーパープロセスは、置き換えられたタスクを発見し、すべてのタスクがターミナルステータスを持っていたため、ワークフローはCOMPLETEDであると誤って結論付けました。

これらの2つの書き込みを囲むデータベーストランザクションがあれば、この問題は防止されたでしょう(#9)。しかし、2つの書き込みはコードの異なるレイヤーから来ており、それらをトランザクションにラップするには大幅な変更が必要でした。バージョン5.5.6では、Conductorはリトライタスクレコードの書き込み失敗を無視しなくなりました。

№SummaryEvent RequiredFixed in
#1Unexpectedly concurrent task executionNone5.5.6
#2Tasks stall on most failuresPause, or crash, or partitionAs designed
#3Null workflow IDsRedis crashUnresolved
#4startWorkflow returns syntax errorNone5.5.6
#8startWorkflow returns duplicate key errorPostgres crash and pause5.5.6
#6Aborted readServer crash, or Redis/Postgres crashUnresolved
#5Abandoned workflowsServer or Postgres crash, or partition5.5.0
#7Missing workflowsPostgres crash or partition5.5.0
#9Never-executed workflows marked as completedPostgres crash and partition5.5.6

4 考察

Orkes Conductorのテストでは、タスクが想定よりも頻繁に並行実行されること(#1)、単一ノード障害によって実行が大幅に遅延する可能性があること(#2)が判明しました。また、startWorkflowにおける4つの軽微なエラー(#3、#4、#6、#8)、および確認されたワークフローが実行されない(#5)、完全に消える(#7)、または実行されていないにもかかわらずCOMPLETEDとマークされる(#9)3つの深刻な安全上のエラーも見つかりました。これらの問題の多くを単一プロセスの障害で再現できたり、あるいは健全なクラスターで観測できたりしたことから、Orkes Conductorのプロダクションユーザーが影響を受けている可能性は十分に考えられます。

Conductorはデフォルトの1時間タイムアウト後にタスクの停止から自動的に回復しました。影響を受けたワークフローが完了することを保証するために、ユーザーが何もする必要はありません。問題3、4、8はstartWorkflowがエラーまたはnullワークフローIDを返す原因となりました。クライアントコードがこれらのエラーを検出し、適切にリトライまたは伝播する可能性が高いと思われます。問題6は、成功したstartWorkflow呼び出しが、明白な「接続失敗」エラーを返す原因となりました。クライアントがこれらのエラーを自動的にリトライした場合、単一ワークフローの複数のコピーを意図せず作成した可能性があります。ドキュメントがない場合、すべてのConductorエラーは無期限であると想定するのが最も安全かもしれません。

放棄されたワークフロー(#5)の場合、ユーザーはConductorクラスター内でしばらく実行されていないRUNNING状態のワークフローを検索し、手動で再起動することで回復できる可能性があります。同様に、COMPLETED状態だがタスクが完了していない(#9)ワークフローを検索し、再起動することも可能です。失われたワークフロー(#7)は、Conductorに送信される前にワークフロー実行に関する情報を持っていることが知られている外部システム、例えばアップストリームシステムによって維持されるログを参照することで回復できるかもしれません。

私たちはバージョン5.5.0-rc2までテストできましたが、これには問題5と7の修正が含まれていました。その後Orkesは、問題1、4、8、9の修正を含むバージョン5.5.6をリリースしました。バージョン5.5.6は、私たちが発見した3つの深刻な安全上の問題をすべて解決しているため、ユーザーにはアップグレードを推奨します。タスクの停止(#2)はConductorの約束された動作と一致しており、対処する必要はありません。NullワークフローID(#3)と誤解を招く「接続失敗」エラーメッセージ(#6)は、まだ調査中です。

それ以外の点では、Conductorは期待どおりに機能しているように見えました。ワークフローとタスクの入力と出力は互いに一致しており、タスクの出力は複数回実行されても安定しているように見えました。いつものように、Jepsenは安全検証に実験的なアプローチを取ります。バグの存在を証明することはできますが、その不在を証明することはできません。私たちは問題を見つけるために広範な努力をしていますが、正しさを証明することはできません。

4.1 並行性

ユーザーは、単一ワークフロー内で、単一タスクが複数のワーカー・スレッドによって並行して実行される可能性があることを認識しておくべきです(#1)。私たちのテストでは、健全なクラスターで数分から数時間に1回この動作を観測しましたが、これは比較的少数のワークフローに影響しました。これは、どの分散実行システムにも固有の性質であり、防ぐことはできませんが、その頻度を減らすことは可能です。

相互排他が可能であったとしても、ワーカーの効果が再順序付けされたり、互いにインターリーブされたりすることを防ぐことはできません。これらの効果は非同期ネットワークを介してルーティングされる可能性があるため、任意に遅延または失われる可能性があります。これが、分散システムが相互排他ではなくフェンシングトークンやコンセンサスを使用することが多い理由です。どのワークフローシステムでも、ユーザーはタスクが冪等であり、他のタスクだけでなく、障害ワークフローによって実行されるタスクとも交換可能であることを確認するように注意する必要があります。

例えば、フライトを予約するワークフローを想像してみてください。Conductorは予約タスクを開始しますが、ワーカーがタイムアウトし、Conductorが予約タスクを再度試行します。それもタイムアウトし、Conductorはフライトをキャンセルするために障害ワークフローを開始します。障害ワークフローはフライトをキャンセルするタスクを実行し、フライトがキャンセルされたという確認をフライトサービスから受け取り、終了します。その後、予約タスクによって送信されたリクエストのいずれかがフライトサービスに到着し、フライトが再度予約されます。これは、ワークフローがそれをキャンセルしたように見えたにもかかわらずです。

解決策は、予約操作とキャンセル操作が交換可能であることを保証することです。つまり、どちらの順序で実行しても同じ効果を持つべきです。フライトサービスが既存のフライトをキャンセルするだけでは不十分です。それは、そのフライトの将来の予約リクエストをどれだけ多くても防ぐ必要があります。下流サービスは、これらの交換可能なセマンティクスを提供するために、冪等キーと論理クロックを使用できます。

これらの課題はConductorに固有のものではなく、どの分散オーケストレーションシステムにも当てはまります。元のSaga論文は単一ノードデータベースシステムを対象としていました。分散システムに一般化された場合、新しい障害モードが発生します。

4.2 ストレージの安全性

Conductorは、依存するストレージシステムの安全性と同等です。Conductor 5.3.2では、conductor.db.typeとconductor.queue.typeに使用されるシステムが、確認された書き込みを失わないことが要求されました。5.4.0では、conductor.queue.typeに使用されるシステムは、conductor.queue.decider-queue.typeがそうしない限り、Conductorの安全性を損なうことなく確認された書き込みを失う可能性がありました。これは完全には真実ではありませんでした(#7)が、中心的な考え方は残っています。

単一ノードRedisとRedis Clusterの両方のデプロイメントをテストし、5.3.2でプライマリDBまたはキューにRedisを使用した際に、Redisノードがキルされたり互いにパーティションされたりすると、Conductorが確認されたワークフロー実行を失う可能性があることを発見しました。Postgresのデータ損失はテストしていませんが、同様の効果が想定されるのは合理的だと思われます。OrkesとConductorを自身でデプロイするユーザーにとっての質問は、使用されているRedisとPostgresシステムはどれほど安全かということです。

単一Redisインスタンスはもちろん単一障害点です。ノードのディスクが故障した場合、またはappendfsync alwaysよりも弱い設定で構成されており、ノードが電源を失った場合にデータを失う可能性があります。Redis ClusterとRedis Sentinelは、ネットワークがパーティションされたときに確認された書き込みを失う可能性があります。マネージドRedis互換サービスは、その安全性保証においてさまざまです。ElastiCache ValkeyとElastiCache Redis OSSの両方で書き込み損失に関する警告がありますが、Amazonは--durability syncではこれは発生しないはずだと述べています。Google CloudのMemorystore for Redisも確認された書き込みを失う可能性があり、Azure Managed Redisも永続化および高可用性で同様です。

単一ノードPostgresデプロイメントも同様に、火災やファイルシステム破損のような壊滅的なノード障害に対して脆弱です。一部のPostgresレプリケーションスキームは安全ではありません。Bin Wangが実証したように、Patroniクラスターは、synchronous_mode_strictを使用している場合でも、一部の状況でコミットされた書き込みを失う可能性があります。Citus Cloudは「データは失われません」と主張していますが、最大60秒または16MBの書き込みを失う可能性のある非同期WALストレージシステムに依存しているようです。GoogleのCloud SQL for PostgreSQLのドキュメントは、一部の設定でスプリットブレインとデータ損失の可能性について警告しています。しかし、Postgres互換インターフェースをより強力な安全性保証で提供するいくつかの分散データベースがあります。例えばCockroachDBなどです。要するに、結果は異なる可能性があります。

私たちはプロプライエタリなOrkes Conductorのみをテストしましたが、Conductor OSSも同様にバックエンドストレージサービスの安全性に依存している可能性が高いと思われます。失われたり放棄されたりしたワークフロー実行を避けるために、ユーザーはconductor.db.typeとconductor.queue.typeの選択、およびそれらのそれぞれのシステムの安全性を慎重に評価することを推奨します。

4.3 今後の作業

これらのテストはOrkes Conductorで実行されましたが、標準のConductor Javaクライアントを使用しました。少し作業すれば、テストスイートはConductor OSSの動作も測定できます。私たちはconductor.db.type=postgresとconductor.queue.type=redis_standaloneに焦点を当てましたが、Conductorはさまざまなストレージシステムをサポートしています。今後の研究では、Cassandra、MySQL、SQLiteでの動作を探求することができます。

私たちのテストでは、カスタムワーカーを使用した単一タスクのワークフローのみを評価しました。マルチステップワークフロー、Conductorの組み込みタスクや制御フロータスクを使用するワークフロー、または障害ワークフローはまだ探求していません。これらすべてが、さらなるテストのための有用な道筋となる可能性があります。

プロセスの停止、クラッシュ、ネットワークパーティションでテストしましたが、高ネットワーク遅延やクロックスキューはまだ探求していません。

Orkesチームの貴重なサポートに感謝します。Viren Baraiya、Manan Bhatt、Dale Brady、Jeu George、Dilip Lukose、Miguel Prietoが含まれます。Jepsenはまた、編集サポートのためにIrene Kannyoに感謝します。この調査はOrkes Inc.の資金提供を受け、Jepsen倫理規定に従って実施されました。


  1. 似た名前の関連性のないオーケストレーションシステムが2つあります。Microsoft製で、こちらもConductorという名前のもの、およびPostgres Conductorという名前のものです。関連性のないAIマーケティングサービスであるConductorもあります。↩︎
  2. Conductor内の操作は単一の成功したタスク実行のみを観測するはずですが、タスクはConductor外のシステムに可視的な副作用を持つ可能性があります。したがって、適切に動作するタスクは冪等で決定論的であるべきです。↩︎
  3. この問題はConductorに関する作業が終盤に差し掛かったところで発見されました。残念ながら、他のバージョンでのテストや、それを誘発する可能性のある障害モードを絞り込むことができませんでした。↩︎