我们在最初构建 Workflows(支持多步骤应用的持久化执行引擎)时,其设计理念是用于用户注册或下单等人工操作触发的工作流。对于用户引导流程等用例,工作流只需支持每人一个实例,人类用户的点击速度毕竟有限。
随着时间的推移,我们实际观察到的是工作负载和访问模式发生了量化转变:人工操作触发的工作流减少,智能体触发的工作流增加,且此类工作流以机器速度创建。
由于智能体成为持久、自主的基础设施,代表用户运行数小时或数天,它们需要一个持久、异步的执行引擎来处理其正在进行的工作。Workflows 恰好具备这项功能:每个步骤均可独立重试,可暂停工作流进行人工干预审批,并且每个实例在发生故障后仍能继续运行而不丢失进度。
此外,工作流本身用于实施智能体循环,并充当管理和维持智能体持续运行的持久工具。Cloudflare Agents SDK 集成加速了这个进程,帮助智能体轻松生成工作流实例并获取实时进度信息。现在,单个智能体会话可以启动数十个工作流,多个智能体并发运行则意味着可以在几秒钟内创建数千个实例。随着 Project Think 的推出,我们预计创新速度将只会提高。
为了帮助开发人员在 Workflows 上扩展智能体和应用,Cloudflare 很高兴地宣布我们现在支持:
50,000 个并发实例(并行运行的工作流执行数量),最初为 4,500 个
每个账户每秒创建 300 个实例,之前为 100 个
每个工作流支持 200 万个已加入队列实例(即:已创建或已唤醒且正在等待并发槽位的实例),之前的上限为 100 万个
我们根据使用数据和基本原理重新设计了 Workflows 控制平面,以支持这些增长。在第一版控制平面中,单个 Durable Object (DO) 可以充当整个账户的中央注册表和协调器。在第二版中,我们构建了两个新组件,以帮助横向扩展系统并缓解第一版引入的瓶颈,然后将所有客户(包括实时流量)无缝迁移到新版本。
正如我们在公开测试版博客文章中所述,Workflows 完全基于 Cloudflare 开发人员平台构建。从根本上说,工作流是一系列持久步骤,每个步骤都可以单独重试,能够执行任务、等待外部事件或休眠到预定时间。
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 是一个账户级持久对象,用于管理该账户的所有工作流和工作流实例。
如需了解关于第一版控制平面的更多信息,请参阅我们的 Workflows 公告博客文章。
在 Workflows 进入测试阶段后,我们欣喜地看到客户迅速扩展了对这款产品的使用,但我们同时也意识到,使用单个 Durable Object 来存储所有账户级信息会造成瓶颈。许多客户需要每分钟创建并执行数百个甚至数千个工作流实例,这很容易导致我们原有架构中的 Account 不堪重负。最初的速率限制(4,500 个并发槽位以及每 10 秒创建 100 个实例)正是基于这一限制而设定。
在第一版控制平面上,这些限制是硬上限。所有依赖于 Account 的操作都必须经过单个 DO,包括创建、更新和列出。对于拥有高并发工作负载的用户,任何时刻都可能会启动和结束数千个实例,导致 Account 每秒收到数千个请求。为了解决这个问题,我们重构了工作流控制平面,使其能够横向扩展以支持更高的并发和创建速率限制。
在新版本中,我们从底层重新设计了每一个操作,目标是优化高容量工作流。归根结底,Workflows 应能够扩展以支持开发人员的需求:无论是每秒创建数千个实例,还是一次运行数百万个实例。我们还希望确保第二版考虑到灵活调整的限制,以便我们可以切换这些限制并持续提高,而不是像第一版那样设置硬上限。经过多次设计迭代,我们最终确定了新架构的以下支柱:
第二版控制平面中新增了两个关键组件,让我们能够提高 Workflows 的可扩展性:SousChef 与 Gatekeeper。第一个组件 SousChef 是 Account 的“副指挥”。回想一下,前文所述的 Account 管理着指定账户内所有工作流中所有实例的元数据和生命周期。引入 SousChef 是为了跟踪指定工作流中实例子集的元数据和生命周期。账户内分布的多个 SousChef 可以更高效、更便捷地向 Account 报告信息。(这种设计的另一个好处是:我们不仅实现了按账号隔离,而且还意外地在同一账户内实现了“按工作流”隔离,因为每个 SousChef 仅负责一个特定的工作流。)
第二个组件 Gatekeeper 是一种机制,用于将并发槽位(源自并发限制)分配给账户内的所有 SousChef。它类似于一个租赁系统。创建一个实例后,它会被随机分配给该账户内的某个 SousChef。然后,SousChef 向 Account 发起请求以触发该实例。账户要么为实例分配一个槽位,要么将其加入队列。获得分配的槽位后,SousChef 会触发实例执行并确保该实例不会被卡住。
Gatekeeper 的作用是确保 Engine 不会使 Account 过载(这是第一版中一个迫在眉睫的风险);因此,SousChef 与其 Account 之间的所有通信都以每秒一次的周期进行,每个周期还会批量处理所有槽位请求,以确保只进行一次 JSRPC 调用。这将确保实例创建率永远不会使最重要的组件 Account 过载或受到影响(顺便说一句:如果 SousChef 实例数量过高,我们会对调用请求进行速率限制,或将其分散到不同 SousChef 实例,并在不同的时间段内执行)。此外,这种周期性特性让我们能够维护旧实例的公平性,并确保多个 SousChef 之间实现最大-最小公平性原则,从而让所有 SousChef 都能取得进展。例如,如果某个实例被唤醒,它应优先于新建的实例获得槽位,但每个 SousChef 会确保自己的实例不会被卡住。
这种架构更加分布式,因此,更具可扩展性。现在,创建一个实例后,请求路径如下:
检查控制平面版本
检查该位置是否有工作流的缓存版本以及版本详细信息可用
如果没有,则检查 Account 以获取工作流名称、唯一 ID 和版本,并缓存这些信息
仅将必要的元数据(例如实例有效负载、创建日期)存储到其自身的 Engine
那么,Engine 如何告知控制平面它已存在呢?这会在实例元数据设置完成后在后台发生。由于针对 Durable Object 的后台操作可能因清理或服务器故障而失败,因此,我们还会在创建热路径的 Engine 上设置“报警”。这样一来,如果后台任务没有完成,报警会确保启动实例。
Durable Object 警报允许在未来的某个特定时间点以至少一次的执行模型唤醒 Durable Object 实例,且内置自动重试功能。我们广泛使用这种后台“任务”与警报组合,将操作从热路径中移除,同时确保一切按计划进行。这就是我们在不牺牲可靠性的前提下,保持快速执行创建实例等关键操作的方法。
除了扩展功能之外,第二版控制平面还意味着:
鉴于我们已拥有能够处理更高用户负载的新版 Workflows 控制平面,接下来我们需要完成“枯燥乏味”的部分:将客户和实例迁移到新系统。对于 Cloudflare 平台规模来说,这本身就是一个难题,因此,“枯燥乏味”的部分反而变成了最大的挑战。Workflows 上线不到一年,已经积累了数百万个实例和数千个客户。此外,第一版控制平面存在一些技术债务,这意味着加入队列的实例可能尚未创建自己的 Engine Durable Object,这进一步加剧了问题的复杂性。
这样的迁移非常棘手,因为客户可能随时都在运行实例;我们需要一种方法将 SousChef 和 Gatekeeper 组件添加到旧账户,而不造成任何中断或停机。
最终,我们决定将现有 Account(我们称之为 AccountOld)迁移到与 SousChef 相同的行为模式。通过持久化 Account 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
}
}
我们可以实例化 AccountOld 中的 SousChef 类,因为 SousChef 和 AccountOld DO 用于跟踪实例元数据的 SQL 表是相同的。因此,我们只需决定使用哪个版本的代码即可。如果不是这样的话,我们将不得不迁移数百万个实例的元数据,这可能会导致每个账户的迁移更加困难、耗时更长。那么,如何进行迁移呢?
首先,我们准备好切换 AccountOld DO,使其行为类似于 SousChef(这意味着创建一个包含上述代码片段的版本)。然后,我们按账户启用第二版控制平面,这会大致同时触发以下三个步骤:
现在,所有新实例创建请求均路由到新的 SousChef(收到第一个请求后创建 SousChef),新实例再也不路由到 AccountOld;
AccountOld DO 开始切换,使其行为类似于 SousChef;
新 Account DO 启动,并包含相应的元数据。
在将所有账户迁移到新版控制平面后,便可以随着 AccountOld DO 实例保留期的到期将其废止。AccountOld 上所有账户的所有实例都迁移完毕之后,便可以永久关闭这些 DO。整个迁移过程零停机,就像在驾驶途中更换汽车轮胎一样。
如果您是 Workflows 新手,请查看我们的入门指南,或者使用 Workflows 构建第一个持久智能体。
如果您的用例所需限制比我们新的默认设置更高(并发限制为 50,000 个槽位,账户级创建速率限制为每秒 300 个实例,每个工作流 100 个实例),请通过客户团队或填写 Workers 限制请求表单联系我们。您还可以在我们的 Discord 服务器上提出反馈和功能请求,或分享您的 Workflows 使用体验。