エージェント時代に向けたWorkflows制御プレーンの再設計
2026-04-15 Luís Duarte Mia Malden André Venceslau 8分で読める
概要
Workflowsを最初に構築した際、マルチステップアプリケーション用の耐久実行エンジンは、ユーザーのサインアップや注文など、人間のアクションによってワークフローがトリガーされる世界を想定していました。オンボーディングフローのようなユースケースでは、ワークフローは1人あたり1つのインスタンスのみをサポートすればよく、人間がクリックできる速度には限界があります。
しかし実際には、ワークロードとアクセスパターンに定量的な変化が見られました。人間がトリガーするワークフローは減少し、マシン速度で作成されるエージェントがトリガーするワークフローが増加しています。
エージェント駆動型ワークフローへの移行
エージェントが永続的で自律的なインフラストラクチャとなり、数時間から数日間ユーザーに代わって動作するにつれて、実行する作業のための耐久的で非同期の実行エンジンが必要になります。Workflowsはまさにそれを提供します。
- 各ステップは独立して再試行可能
- ワークフローは人間によるループ承認のために一時停止可能
- 各インスタンスは障害を乗り越えて進捗を失わない
さらに、ワークフロー自体がエージェントループの実装に使用され、エージェントを管理して生存させる耐久的なハーネスとして機能しています。Agents SDK統合により、エージェントがワークフローインスタンスを簡単にスポーンし、リアルタイムの進捗を取得できるようになりました。
単一のエージェントセッションで数十のワークフローをキックオフでき、複数のエージェントが同時に実行されると、数秒で数千のインスタンスが作成されます。Project Thinkが利用可能になったため、この速度はさらに増加すると予想されます。
スケーラビリティの向上
Workflows上でエージェントとアプリケーションをスケーリングするのを支援するため、以下のサポートを発表します。
- 50,000の同時インスタンス(並行して実行されているワークフロー実行の数)- 元々は4,500
- アカウントあたり300インスタンス/秒の作成 - 以前は100
- ワークフローあたり200万のキューに入ったインスタンス(作成または起動され、同時実行スロットを待機しているインスタンス)- 100万から増加
V1: Workflows制御プレーンの初期アーキテクチャ
Workflowsは当社独自の開発者プラットフォーム上に完全に構築されました。基本的に、ワークフローは一連の耐久ステップであり、各ステップは独立して再試行可能で、タスクを実行したり、外部イベントを待機したり、所定の時間まで休止したりできます。
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const data = await step.do("fetch-data", async () => {
return fetchFromAPI();
});
const approval = await step.waitForEvent("approval", {
type: "approval",
timeout: "24 hours",
});
await step.do("process-and-save", async () => {
return store(transform(data));
});
}
}
各インスタンスをトリガーし、そのロジックを実行し、メタデータを保存するために、SQLiteバックアップのDurable Objectsを活用しました。これは分散システム内での調整とストレージのためのシンプルながら強力なプリミティブです。
制御プレーンでは、一部のDurable Objects(実際のワークフローインスタンスを実行するEngineなど)は1:1の比率でインスタンスごとにスピンアップされます。一方、AccountはアカウントレベルのDurable Objectで、そのアカウントのすべてのワークフローとワークフローインスタンスを管理します。
V1の制限
Workflowsをベータ版で立ち上げた後、顧客が急速に製品の使用をスケーリングしているのを見て喜びましたが、すべてのアカウントレベル情報を保存する単一のDurable Objectがボトルネックを引き起こしていることに気付きました。
多くの顧客は1分あたり数百から数千のワークフローインスタンスを作成および実行する必要があり、これは元のアーキテクチャのAccountを圧倒する可能性がありました。元のレート制限(4,500同時実行スロットと10秒あたり100インスタンス作成)はこの制限の結果でした。
V1制御プレーンでは、これらの制限はハードキャップでした。Accountに依存するすべての操作(作成、更新、リスト)は、その単一のDOを通過する必要がありました。高同時実行ワークロードを持つユーザーは、任意の時点で数千のインスタンスが開始および終了し、Accountへの毎秒数千のリクエストに蓄積される可能性がありました。
V2: より高いスループットのための水平スケーリング
新しいバージョンでは、高ボリュームワークフローに最適化することを目標に、すべての単一操作を一から再考しました。最終的に、Workflowsは開発者が必要とするもの(毎秒数千のインスタンス作成または同時に実行される数百万のインスタンス)をサポートするようにスケーリングする必要があります。
また、V2がハードキャップを課すV1制限ではなく、柔軟な制限を許可し、切り替えて継続的に増加させることができることを確認したいと考えました。
多くの設計反復の後、新しいアーキテクチャの以下の柱に落ち着きました。
アーキテクチャの原則
- 特定のインスタンスの存在の真実のソースはそのEngineであり、他には何もない
- インスタンスライフサイクルと生存メカニズムは、ワークフローごとに水平スケーラブルで、多くのリージョン全体に分散している必要がある
- 新しいAccountシングルトンは最小限の必要なメタデータのみを保存し、同時リクエストの不変最大値を持つ必要がある
V2制御プレーンの新しいコンポーネント
V2制御プレーンのスケーラビリティを向上させることができた2つの新しい重要なコンポーネントがあります。
SousChef
SousChefは、Accountの「副官」です。以前は、Accountは特定のアカウント内のすべてのワークフロー全体のすべてのインスタンスのメタデータとライフサイクルを管理していました。SousChefは、特定のワークフロー内のインスタンスのサブセットのメタデータとライフサイクルを追跡するために導入されました。
アカウント内では、SousChefの分布がより効率的で管理可能な方法でAccountに報告できます。このデザインの追加の利点として、既にアカウントごとの分離がありましたが、各SousChefが1つの特定のワークフローのみを処理するため、同じアカウント内で「ワークフローごと」の分離も無意識に得られました。
Gatekeeper
Gatekeeperは、同時実行「スロット」(同時実行制限から派生)をアカウント内のすべてのSousChef全体に分散するメカニズムです。リーシングシステムとして機能します。
インスタンスが作成されると、そのアカウント内のSousChefの1つにランダムに割り当てられます。その後、SousChefはAccountにリクエストを送信してそのインスタンスをトリガーします。スロットが付与されるか、インスタンスがキューに入ります。スロットが付与されると、SousChefはインスタンスの実行をトリガーし、インスタンスが決してスタックしないという責任を引き受けます。
Gatekeeperは、Engineが決してAccountをオーバーロードしないようにするために必要でした(V1での差し迫ったリスク)。そのため、SousChefとそのAccount間のすべての通信は定期的なサイクルで発生し、1秒に1回です。各サイクルはすべてのスロットリクエストもバッチ処理し、1つのJSRPCコールのみが行われることを保証します。
これにより、インスタンス作成レートが最も重要なコンポーネントであるAccountをオーバーロードまたは影響を与えることはできません。(余談ですが、SousChefカウントが高すぎる場合、呼び出しをレート制限するか、異なる時間帯に異なるSousChef全体に分散させます)。
また、この定期的なプロパティにより、古いインスタンスの公平性を保持し、多くのSousChef全体でmax-min公平性を確保でき、すべてが進行できるようになります。たとえば、インスタンスが起動する場合、新しく作成されたインスタンスよりもスロットの優先順位を付ける必要がありますが、各SousChefは独自のインスタンスがスタックしないようにします。
インスタンス作成フロー
このアーキテクチャはより分散しており、したがってより拡張可能です。インスタンスが作成されると、リクエストパスは次のようになります。
- 制御プレーンバージョンを確認
- ワークフローとバージョン詳細のキャッシュされたバージョンがその場所で利用可能かどうかを確認
- 利用できない場合は、Accountをチェックしてワークフロー名、一意のID、バージョンを取得し、その情報をキャッシュ
- 必要なメタデータのみ(インスタンスペイロード、作成日)を独自のEngineに保存
Engineが制御プレーンに存在することをどのように伝えるのでしょうか?それはインスタンスメタデータが設定された後、バックグラウンドで発生します。Durable Objectのバックグラウンド操作は、削除またはサーバー障害により失敗する可能性があるため、作成ホットパスでEngineに「アラーム」も設定します。
バックグラウンドタスクが完了しない場合、アラームはインスタンスが開始されることを保証します。Durable Objectアラームにより、Durable Objectインスタンスは将来の細粒度の時間に起動され、少なくとも1回の実行モデルで、自動再試行が組み込まれています。
ホットパスから操作を削除しながら、すべてが計画通りに実行されることを確認するために、バックグラウンド「タスク」とアラームのこの組み合わせを広範に使用します。これにより、インスタンスの作成などの重要な操作を高速に保ちながら、信頼性を決して損なわないようにします。
V2の利点
スケールのロック解除以外に、この制御プレーンのバージョンは以下を意味します。
- インスタンスリスティングのパフォーマンスが高速化され、カーソルページネーションと実際に一貫性がある
- インスタンスの任意の操作は正確に1つのネットワークホップを実行します(そのEngineに直接移動できるため、眼球リクエストレイテンシーを可能な限り小さく保証します)
- より多くのインスタンスが実際に正しく動作していることを確認できます(定時に実行され、正しくない場合は修正し、Engineが実行を続行するのに遅れないようにします)
V1からV2への移行
より高いボリュームのユーザーロードを処理できるWorkflows制御プレーンの新しいバージョンができたので、「退屈な」部分を実行する必要がありました。顧客とインスタンスを新しいシステムに移行することです。
Cloudflareのスケールでは、これ自体が問題になるため、「退屈な」部分が最大の課題になります。1年のマークをはるか前に、Workflowsは既に数百万のインスタンスと数千の顧客を蓄積していました。
また、V1の制御プレーンのいくつかの技術的負債は、キューに入ったインスタンスが独自のEngine Durable Objectをまだ作成していない可能性があることを意味し、事態をさらに複雑にしました。
顧客は任意の時点でインスタンスを実行している可能性があるため、この移行は難しいです。SousChefおよびGatekeeperコンポーネントを古いアカウントに追加する方法が必要でしたが、中断やダウンタイムは発生しませんでした。
最終的に、既存のAccounts(AccountOldsと呼びます)をSousChefのように動作するように移行することを決定しました。Account DOsを永続化することで、インスタンスメタデータを保持し、DOをSousChef「DO」に単純に変換しました。
import { SousChef } from "@repo/souschef";
class AccountOld extends DurableObject {
constructor(state: DurableObjectState, env: Env) {
if (this.currentVersion === ControlPlaneVersions.SOUS_CHEFS) {
this.sousChef = new SousChef(this.ctx, this.env);
await this.sousChef.setup()
}
}
async updateInstance(params: UpdateInstanceParams) {
if (this.currentVersion === ControlPlaneVersions.SOUS_CHEFS) {
assert(this.sousChef !== undefined, 'SousChef must exist on v2');
return this.sousChef.updateInstance(params);
}
}
@RequiresVersion<AccountOld>(ControlPlaneVersions.V
}