このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
弊社が最初に開発したマルチステップアプリケーション用の耐久性のある実行エンジンであるWorkflowsは、ユーザーのサインアップや注文などの人間のアクションによってワークフローがトリガーされる世界を想定してデザインされました。オンボーディングフローのようなユースケースの場合、Workflowsは1人あたり1つのインスタンスをサポートするだけで、ユーザーはとても速くクリックできるようになりました。
時間の経過とともに、私たちが実際に見ているのは、ワークロードとアクセスパターンの定量的な変化です。人間がトリガーするワークフローが減少し、機械的なスピードで作成されたエージェントがトリガーするワークフローが増えています。
エージェントがユーザーに代わって数時間または数日にわたって動作する、永続的で自律的なインフラストラクチャになるにつれ、ユーザーが行っている作業には、耐久性のある非同期実行エンジンが必要になります。Workflowsは、まさに、それを提供します。すべてのステップを個別に再試行でき、ワークフローはヒューマンインザループの承認のために一時停止することができ、各インスタンスは進行を失うことなく障害を乗り越えることができます。
さらに、ワークフロー自体がエージェントループの実装に使用され、エージェントを管理し、維持するための耐久性のある手段として機能しています。当社の Agents SDK統合により、これが加速され、エージェントはワークフローインスタンスを簡単に生成し、リアルタイムの進捗状況を把握できるようになりました。単一のエージェントセッションで数十のワークフローを開始でき、多くのエージェントが同時に実行されるということは、数秒で数千のインスタンスが作成されることを意味します。Project Thinkが利用可能になったことで、活動の速さは加速の一途になるでしょう。
開発者がWorkflows上でエージェントとアプリケーションを拡張できるように、サポート開始を発表できることを嬉しく思います。
同時インスタンス数は50,000(並行して実行されるワークフローの実行数)、当初は4,500
アカウントあたり300インスタンス/秒(以前は100回)作成
ワークフローあたり200万件のキューイングされたインスタンス(作成または起動され、同時実行スロットを待っているインスタンスを意味します)、100万件から増加
こうした増加に対応するために、使用状況データと第一原則からWorkflowsコントロールプレーンを再設計しました。コントロールプレーンのV1では、単一のDurable Object(DO)がアカウント全体の中央レジストリとコーディネーターとして機能します。V2では、システムを水平に拡張し、V1がもたらしたボトルネックを軽減するために、2つの新しいコンポーネントを構築しました。その後、ライブトラフィックを持つすべてのお客様を新バージョンにシームレスに移行できます。
当社のパブリックベータ版に関するブログ投稿で説明されているように、当社は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は、分散システム内での調整とストレージのための、シンプルながらも強力なプリミティブです。
コントロールプレーンでは、一部のDurable Objects(ステップ、リトライ、スリープロジックを含む実際のワークフローインスタンスを実行するエンジンなど)が、インスタンスあたり1:1の割合で起動されます。一方、アカウントはアカウントレベルのDurable Objectで、そのアカウントのすべてのワークフローとワークフローインスタンスを管理します。
V1コントロールプレーンの詳細については、弊社のWorkflows発表のブログ記事をご覧ください。
Workflowsのベータ版を公開した後、お客様がすぐに利用を拡大できるのを見てワクワクしましたが、一方で、アカウントレベルの情報をすべて保存するために単一のDurable Objectがあることがボトルネックになっていることも判明しました。多くのお客様は、1分間に数百、あるいは数千のWorkflowインスタンスを作成して実行する必要があり、これにより元のアーキテクチャではすぐにアカウントが過負荷状態になる可能性がありました。本来のレート制限(4,500の同時実行スロット、10秒あたり100のインスタンス作成)は、この制限によるものでした。
V1コントロールプレーンでは、これらの制限は厳しい上限でした。作成、更新、一覧取得を含む、アカウントに依存するすべての操作は、1つのDOを通過する必要がありました。高い同時実行ワークロードを持つユーザーの場合、数千のインスタンスが任意の時点で開始および終了し、アカウントへの毎秒最大数千のリクエストが構築される可能性があります。これを解決するために、私たちはワークフローのコントロールプレーンを再設計し、コンカレンシーと作成レート制限を高くするために水平にスケールできるようにしました。
新バージョンでは、大量のワークフローに最適化する目的で、あらゆる操作をゼロから再考しました。最終的に、Workflowsは開発者が何千ものインスタンスを作成するのか、一度に数百万のインスタンスを実行するのかにかかわらず、開発者が必要とするものすべてをサポートするために拡張できるべきです。また、V2には、V1に課された厳しい上限ではなく、柔軟な制限を設け、切り替えたり、利用を拡大し続けることができるようにしたかったこともあります。何度も設計を反復した結果、新しいアーキテクチャの柱として確定しました。
V2コントロールプレーンには、Workflowsのスケーラビリティを改善することを可能にした、SousChefとGatekeeperの2つの新しい重要なコンポーネントがあります。最初のコンポーネントであるSousChefは、アカウントの「副司令官」です。以前は、アカウントは、特定のアカウント内のすべてのワークフローのすべてのインスタンスのメタデータとライフサイクルを管理していました。SousChefは、特定のワークフロー内のインスタンスのサブセット上でメタデータとライフサイクルを追跡するために導入されました。1つのアカウント内で、SousChefsの配布は、より効率的で管理しやすい方法でアカウントに報告することができます。SousChef(この設計のさらなる利点:各SourceChefは1つの特定のワークフローだけを処理するため、同じアカウント内で不意に「ワークフローごと」の分離を持つことができました)。
2番目のコンポーネントであるGatekeeperは、アカウント内のすべてのSousChefsに同時実行「スロット」(同時実行制限から導き出される)を配布するメカニズムです。リースシステムとして機能します。インスタンスが作成されると、そのアカウント内のSousChefsのいずれかにランダムに割り当てられます。次に、SousChefはアカウントにリクエストを行い、そのインスタンスをトリガーします。スロットが付与されるか、インスタンスがキューに入れられます。スロットが付与されると、SousChefはインスタンスの実行をトリガーし、インスタンスがフリーズしないように責任を負います。
Gatekeeperは、Enginesがアカウントを過負荷にしないようにするために必要でした(V1における差し迫ったリスク)。そのため、SousChefsとアカウント間のすべての通信は、1秒に1回、定期的なサイクルで行われます。各サイクルでは、すべてのスロットリクエストもバッチ処理され、JSRPCコールが1回だけ行われるようにします。これにより、インスタンス作成レートが最も重要なコンポーネントであるアカウントに過負荷を与えたり、影響を与えたりすることはありえません(余談ですが、SousChefのカウントが高すぎると、レート制限呼び出しを行ったり、異なる期間に複数のSousChefsに分散したりします)。また、この定期的プロパティにより、古いインスタンスの公平性を維持し、多くのSousChefsを通じて最大最小の公平性を確保することで、すべてを進行させることができます。例えば、インスタンスが起動した場合、新しく作成されたインスタンスよりもスロットを優先する必要がありますが、各SousChefは、それぞれのインスタンスがスタックしないようにします。
このアーキテクチャはより分散されているため、より拡張性があります。これで、インスタンスが作成されると、リクエストパスは次のようになります。
コントロールプレーンバージョンの確認
ワークフローのキャッシュされたバージョンとバージョンの詳細が、その場所で利用可能かどうかを確認する
そうでない場合は、アカウントを確認して、ワークフロー名、一意のID、バージョンを取得し、その情報をキャッシュします。
必要なメタデータ(インスタンスペイロード、作成日)だけを独自のEngineに保存
では、Engine はどのようにしてコントロールプレーンに存在を伝えるのでしょうか?これは、インスタンスのメタデータが設定された後、バックグラウンドで行われます。Durable Objectに対するバックグラウンド操作は、立ち退きやサーバー障害のために失敗することがあるため、作成のホットパスでEngineにアラームも設定します。そうすることで、バックグラウンドタスクが終了しない場合でも、アラームはインスタンスが開始されることを保証します。
Durable Object Alarmsでアラームを設定すると、少なくとも1回の実行モデルと自動リトライが組み込まれているため、将来的に一定のタイミングでDurable Objectインスタンスを起動することができます。当社は、このバックグラウンド「タスク」とアラートの組み合わせを幅広く使用して、ホットパスから操作を削除しつつ、すべてが計画通りに行われるようにします。このようにして、信頼性を損なうことなく、インスタンスの作成のような重要な操作を高速に保っています。
このバージョンのコントロールプレーンは、拡張性以外にも、次のことを意味します:
インスタンスリストのパフォーマンスが向上し、実際にブラウザページ移動と一貫性があります。
インスタンスに対する操作は、厳密に1つのネットワークホップを行います(Engineに直接アクセスできるため、エンドユーザーのリクエストの遅延を可能な限り小さく抑えることができます)。
より多くのインスタンスが、実際に(時間通りに実行されることで)同時に正しく動作していることを確認できます(そうでない場合は修正し、Enginesが実行を継続するのに遅れないようにします)。
より多くのユーザー負荷に対応できる新バージョンのWorkflowsコントロールプレーンを手に入れたので、顧客とインスタンスを新しいシステムに移行するという「ボーリング」作業を行う必要がありました。Cloudflareの規模では、それ自体が問題となるため、「ボーリング」の部分が最大の課題となります。Workflowsは、公開して1年を迎える前に、すでに数百万のインスタンスと数千の顧客を獲得していました。また、V1のコントロールプレーンに技術的負債があることもあり、キューに入れられたインスタンスに独自のEngine Durable Objectがまだ作成されていない可能性があり、問題をさらに複雑にしています。
お客様はいつでも実行中のインスタンスを持っている可能性があるため、このような移行は難しくなります。中断やダウンタイムを発生させることなく、SousChefとGatekeeperのコンポーネントを古いアカウントに追加する方法が必要でした。
最終的に、SourceChefsのように動作するように、既存のアカウント(AccountOldsと呼びます)を移行することにしました。アカウントDOを保持することで、インスタンスのメタデータを維持し、DOをSousChefの「DO」に変換するだけです。
// You might be wondering what's this SousChef class? This is the SousChef DO class!
import { SousChef } from "@repo/souschef";
class AccountOld extends DurableObject {
constructor(state: DurableObjectState, env: Env) {
// We added the following snippet to the end of our AccountOld DO's
// constructor. This ensures that if we want, we can use any primitive
// that is available on SousChef DO
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);
}
// old logic remains the same
}
@RequiresVersion<AccountOld>(ControlPlaneVersions.V1)
async getMetadata() {
// this method can only be run if
// this.currentVersion === ControlPlaneVersions.V1
}
}
SousChefs と AccountOld DOの両方にあるインスタンスメタデータを追跡するSQLテーブルは同じであるため、 AccountOld 内で SousChef クラスをインスタンス化することができます。そのため、使用するコードのバージョンを決定するだけでいいのです。もしこれがそうでなければ、何百万ものインスタンスのメタデータを移行しなければならなかったでしょうから、各アカウントの移行はより困難で、より時間のかかるものになっていたでしょう。では、移行はどのように行われたのでしょうか?
まず、AccountOld DOをSousChefsとして動作するように切り替える準備をしました(これは、上記のスニペットのバージョンを含むリリースを作成することを意味しました)。次に、アカウントごとにコントロールプレーンV2を有効にし、次の3つのステップをほぼ同時にトリガーしました。
すべての新しいインスタンス作成リクエストは、新しいSousChefsにルーティングされるようになりました(SousChefsは最初のリクエストを受信したときに作成されます)。新しいインスタンスがAccountOldに再び移動することはありません。
AccountOld DOは、SousChefsのように振る舞うために、自分自身を移行し始めます。
新しいアカウントDOは、対応するメタデータとともに作成されます。
すべてのアカウントを新しいコントロールプレーンバージョンに移行した後、インスタンス保持期間が切れた時点でAccountOld DOを廃止することができました。AccountOldsの全アカウントのすべてのインスタンスを移行したら、それらのDOを永久にダウンさせることができました。移行は、ダウンタイムもなく、まるで運転中の車のホイールを交換しているようなプロセスで完了しました。
Workflowsを初めてご利用になる方は、弊社の入門ガイドをお試しいただくか、Workflowsで最初の耐久性のあるエージェントを構築してください。
お客様の用途で、新しいデフォルトより高い制限(同時実行制限は50,000スロット、アカウントレベルの作成レート制限は1秒あたり300インスタンス、ワークフローあたり100個)が必要な場合は、アカウントチームまたはWorkers制限リクエストフォームからご連絡ください。また、フィードバックや機能のリクエスト、Discordサーバー上でWorkflowsをどのように使っているかを共有することもできます。