news 2026/9/13 13:23:38

Activepieces 沙箱池演进决策剖析:纯 execute() 形态的由来、废弃与遗产

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Activepieces 沙箱池演进决策剖析:纯 execute() 形态的由来、废弃与遗产

Activepieces 沙箱池演进决策剖析:纯 execute() 形态的由来、废弃与遗产

【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces

Activepieces 的沙箱执行架构经历了一次从"远程执行池"到"Worker 即沙箱"的显著转型。本文以决策记录 000013-sandbox-pool-is-a-pure-execute-cloud-run-pool-superseded.md 为核心,完整还原这场决策的前因后果:为什么沙箱池被设计为"纯粹的 execute()",为什么基于 Cloud Run 的远程池方案最终被放弃,以及这项决策留下的两个重要遗产(pieces 链接分发机制与 Worker→App 回路由规则)如何在今天的源码中继续生效。读完本文,你将理解 Activepieces 执行引擎的输入物化边界、AP_FRONTEND_URLPUBLIC_URL的分工逻辑,以及如何从一份"已废弃"的 ADR 中提取仍然有效的架构原则。

一、决策原文:沙箱池是一个纯 execute(),Cloud Run 池方案已被取代

这份 ADR 的status字段明确标记为superseded(被取代),但它的价值恰恰在于记录了一次"失败探索"的完整逻辑链。决策核心可以压缩为一句话:

沙箱池是一个纯粹的execute()——它不持有apiClient,不持有应用连接(app connection),所有输入都以请求/响应数据的形式传入(flow bundle + pieces 以 S3 引用形式传递)。Worker 是唯一的 Resolver,在调用execute()之前完成输入物化。GCP Cloud Run 曾经就是这样的池(concurrency 1,背后一个/execute端点),但那个远程池如今已不存在。

这一段包含四个关键信息点:

  1. 纯 execute 契约:池对执行环境的唯一职责是"拿到完整输入 → 执行 → 返回结果",不需要主动访问任何外部服务;
  2. 无连接持有:池不持有apiClient,不做鉴权、不拉取数据,所有需要的数据由调用方(Worker)提前备好;
  3. 输入物化前置:把"flow bundle + pieces 引用"解析成可执行形态的工作,由 Worker 侧的 Resolver 完成;
  4. 历史坐标:Cloud Run 曾是这种池的落地载体,现已移除。

二、背景:为什么曾想跑到 Cloud Run 上

决策的 Context 部分交代了探索动机与物理约束:

曾探索过把池远程运行在 Cloud Run 上,但 Cloud Run 无法保持按请求(per-request)的亲和性——因此生命周期无法跨网络拆分为 provision/run/dispose 三个阶段,只能是一次自包含的调用。

这段技术推理非常关键。一个沙箱的理想生命周期是:预备(provision)→ 执行(run)→ 清理(dispose)。在本地进程内,这三个阶段可以分步进行,甚至可以把一个沙箱复用于多次执行。但 Cloud Run 这类无服务器容器平台对每个请求的处理是独立的:请求之间没有亲和性,同一容器可能被回收、扩容或调度到别的实例,你无法假设"上一个请求预备好的沙箱还在"。因此,跨网络的远程池方案被迫把整个生命周期压缩成一次自包含调用——这正是"纯 execute()"形态诞生的直接原因。

三、为什么选纯 execute():两个被否决的替代方案

3.1 纯 execute 的优势

execute()让本地与远程拥有完全一致的签名,且不需要应用连接(所有输入都是传入的数据)。

"同一签名"意味着:无论沙箱跑在本地进程内,还是(如果未来需要)跑在远程主机上,调用方看到的接口都是一样的。这把"部署位置"从接口设计问题中剥离出来,是一种典型的依赖倒置——执行环境不关心数据从哪来,只关心拿到的是完整、健康的输入。

3.2 被否决的方案一:Cloud Run 反向连接 App

已否决:Cloud Run 打开自己的 socket 连回 App(等于在一个无状态运行时上钉住一条持久连接)。

如果池自己维护一条到 App 的长连接(如 Socket.IO),它就能像本地 Worker 一样拉取数据。但这违背了无服务器平台的假设:长连接需要跨请求保持状态,而 Cloud Run 实例的存活与请求无关,连接随时可能被回收,运维上也无法保证这条连接只被单个实例使用。

3.3 被否决的方案二:大体积产物内联进请求体

已否决:把沉重的产物(artifact)内联塞进请求体(选择了 S3 引用,由池直接拉取)。

一个 flow bundle 可能包含编译后的代码、多个 piece 包,体积可达数 MB 甚至更大。把它们以 base64 内联进 HTTP 请求体,既浪费带宽,也拖慢冷启动。最终选择的是"S3 引用":请求体里只携带指向 S3 对象的 URL,池在需要时自行拉取。这与 000006-pieces-are-distributed-as-links-resolved-lazily.md 中"一切皆链接"的思路一脉相承。

四、源码验证:Worker 是唯一的 Resolver,池只消费物化好的输入

4.1 Resolver 的职责边界

决策中"Worker 是唯一 Resolver"这一条,在源码中有非常清晰的落点。resolver.ts 的文件头注释直接点明了设计意图:

The Resolver is the worker-side, Runtime-Kind-independent half of the seam. It owns the only apiClient and turns a job into a fully-materialized ProvisionInput beforeexecuteis ever called, so the pool only sees healthy, complete inputs.

翻译过来:Resolver 是 Worker 侧、与运行时种类无关的那半边接缝,它独占唯一的apiClient,在execute()被调用之前就把一个 job 变成完全物化的ProvisionInput,因此池只看到健康、完整的输入。

createResolver的实现可以看到物化的具体动作:

  • 通过flowProvisioning.resolve()获取FlowVersion,并解析该 flow 用到的所有 piece 与代码步骤;
  • 若命中已发布的 flow bundle,则直接使用 bundle 内的flowVersionpieces(代码已物化);
  • 冷路径下先在本机编译代码步骤,全部成功后才发布 bundle(publishBundle),编译失败不会让运行失败——池会从源码重新编译;
  • 最终组装出ProvisionInputplatformIdflowVersionIdpiecescodespublicApiUrlengineToken

也就是说,池(沙箱)拿到的ProvisionInput已经是"纯数据":不再需要 Resolver 之外的任何网络调用。

4.2 execute() 的槽位生命周期

sandbox.ts 中的createSandboxRuntime给出了纯execute()在本地实现中的完整生命周期,注释同样点明与 ADR 的对应关系:

execute owns the slot lifecycle: acquire -> provision -> run -> release on success / invalidate on throw(execute拥有槽位生命周期:获取 → 预备 → 运行 → 成功时释放 / 抛错时作废)。

关键实现要点:

  • createSandboxRuntime({ concurrency, ... })按并发数创建等量的SandboxManager(每个 box 一个 manager);
  • execute()workerIndex路由到对应 manager,acquire()取沙箱;
  • localExecutionCache.provision(...)把 pieces / 代码 / publicApiUrl / engineToken 落到本地缓存,失败则manager.invalidate()并抛出;
  • 之后才sandbox.start()(fork 引擎子进程 + 启动)与sandbox.execute(operationType, operation, ...)
  • 成功release(),异常invalidate()

这印证了"池只消费已解析输入"的论断:execute的参数provision是调用方(worker.ts 中的createResolver)物化好的结果,沙箱内部不再触碰apiClient

4.3 Worker 侧的接线

worker.ts 的executeJob()展示了两个 URL 的最终分流:

const apiUrl = getApiUrl() const { PUBLIC_URL: publicUrl } = await workerSettings.waitForSettings() const publicApiUrl = ensurePublicApiUrl(publicUrl) // The engine forks in-process inside this worker, so it reaches the app over the worker's own // app URL (cluster-internal when co-located with the app, the public URL for a standalone worker). const internalApiUrl = apiUrl const ctx: JobContext = { apiClient, runtime, resolver: createResolver({...}), internalApiUrl, publicApiUrl, ... }

其中internalApiUrl承担"引擎→App"的所有回调,而publicApiUrl只作为字符串注入与 bundle 下载地址。这正是决策 Consequences 中"Worker→app routing"一节在代码里的投影。

五、遗产一:pieces 链接分发(ADR 0002)的动机变化

决策 Consequences 明确指出,远程池方案催生或强化了"pieces 是链接"的决策(即 000006-pieces-are-distributed-as-links-resolved-lazily.md,ADR 文件编号 0002):

bundle link 基于publicApiUrl构建,其理由是"从分离的远程池可达"。如今远程池已不存在,这个理由随之消失——但 "links" 决策依然凭借其他优点成立(传输统一的安装方式、S3 惰性预热、自托管者总能拿到可用链接),只是 public-URL 这个基础现在变成了附带性考虑,未来可以迁移到 internal URL。

拆开看,这个"链接分发"决策本身的价值并不依赖远程池:

  • 传输统一:所有 piece(官方 npm 包、S3 缓存对象、自定义 ARCHIVE piece)统一走"一个可下载链接",池用bun install消费,删除旧的 socket 字节通道(getPieceArchive/fetchArchive已被移除);
  • 惰性预热:S3 缓存未命中时立即返回 npm / 文件存储链接,并去重触发一个SYSTEM预热任务(按bundle:<platformId|global>:<name>:<version>去重),避免全量批量同步;
  • 自托管友好:S3 只是纯优化,没有 S3 的自托管者仍能得到可用链接。

这也解释了为什么 ADR 0001/0002 被标记为 superseded 而非删除:它们记录的"为什么",一部分已经失效,但结论本身仍有生命力。ADR 文件保留(2026-07-16 标记),是为了让后人能追溯动机变化的完整脉络。

六、遗产二:Worker→App 回路由 —— AP_FRONTEND_URL 与 PUBLIC_URL 的分工

这是本决策对今天运维影响最大的一条,原文如下:

对于独立 Worker(AP_CONTAINER_TYPE=WORKER),AP_FRONTEND_URL是所有按执行(per-execution)引擎→App 回调的基础(internalApiUrl:worker/project、populated-flows、connections/props、run-progress、file I/O、piece 的serverContext.apiUrl)。publicApiUrl(来自 App 的PUBLIC_URL)只是字符串注入(webhook URL、serverContext.publicUrl)加上缓存用的、provisioning 时刻的 bundle 下载——不是按请求的路径。

6.1 两种 URL,两种职责

  • AP_FRONTEND_URLinternalApiUrl:引擎执行期间所有需要回访 App 的请求都打到这里。包括拉取 project / worker 信息、获取已发布的 flow bundle、解析 connection 与属性(props)、上报 run-progress、文件读写,以及 piece 运行时通过serverContext.apiUrl发起的调用。这些请求每个 flow run 都会发生,属于高频热路径。
  • PUBLIC_URLpublicApiUrl:仅用于两类场景——把"面向用户"的 URL 注入给引擎(如 webhook 地址、serverContext.publicUrl),以及 provisioning 阶段下载 bundle(有缓存,非按请求)。它不参与每次执行的回调。

6.2 指向集群内部服务,规避 hairpin

因此把AP_FRONTEND_URL指向集群内部服务,能让每个回调都不经过公共端点(避免 DNS + TLS + LB 的回环),而且这是安全的——因为面向用户/公共的 URL 独立来自PUBLIC_URL

这在 configs.ts 中有直接实现。getApiUrl()按容器类型分叉:

function getApiUrl(): string { const containerType = system.get(WorkerSystemProp.CONTAINER_TYPE) ?? 'WORKER_AND_APP' if (containerType === 'WORKER_AND_APP') { const port = process.env[WorkerSystemProp.PORT] ?? system.get(WorkerSystemProp.PORT) return `http://127.0.0.1:${port}/api/` } const frontendUrl = system.getOrThrow(WorkerSystemProp.FRONTEND_URL).replace(/\/+$/, '') return frontendUrl + '/api/' }
  • 当 Worker 与 App 同容器(默认WORKER_AND_APP)时,回调直接走http://127.0.0.1:${AP_PORT:-3000}/api/,零网络开销;
  • AP_CONTAINER_TYPE=WORKER(独立 Worker)时,回调走AP_FRONTEND_URL + '/api/'。在 Kubernetes 等集群环境中,把该变量设为http://<app-service>.<namespace>.svc.cluster.local这类集群内部地址,就能让高频回调留在集群内网,避免"出公网再回来"的 hairpin 损耗。

6.3 配置速查表

结合 configs.ts 中的WorkerSystemProp枚举,独立 Worker 场景下的关键配置如下:

环境变量默认值在独立 Worker(AP_CONTAINER_TYPE=WORKER)下的作用
AP_CONTAINER_TYPEWORKER_AND_APP设为WORKER时启用独立 Worker 模式
AP_FRONTEND_URL无(getOrThrow必填)构成internalApiUrl的全部引擎→App 回调基础,应指向集群内部服务
AP_WORKER_TOKEN连接 App Socket.IO 的鉴权令牌
AP_WORKER_CONCURRENCY5过渡兼容模式的并发数;最终形态是 concurrency 1 + 副本横向扩展
AP_PORT3000同容器模式下的本地回环端口,也是健康检查端口
PUBLIC_URL(App 侧)只作为publicApiUrl字符串注入与 provisioning 期 bundle 下载地址

注意:getApiUrl()AP_FRONTEND_URL通过system.getOrThrow读取,意味着独立 Worker 模式下该变量缺失会直接抛错——这是刻意的 fail-fast 设计,防止回调地址静默失效。

七、现状:纯 execute 形态幸存,远程池彻底退役

决策的最终结论分两层:

被废除的:远程 Cloud Run / 分离池托管方式整体移除。替代方案是 000001-worker-is-the-sandbox-one-job-per-worker-scale-by-replicas.md 确立的"Worker 即沙箱"模型:Worker 与沙箱合并为一个单元,一个 Worker 同时只轮询一个 job(concurrency 1),并行性完全依靠 N 个 Worker 副本横向扩展,每个副本上限 0.5 CPU / 1 GB。该模型同时移除了旧方案最大的运维风险——Docker socket 依赖,并删除了/execute跳转、远程传输、provisioner 与 HTTP 信封等一整条边界。

被保留下来的:纯 execute 的形态。沙箱至今不持有应用连接,只消费已解析的输入——这一点在 resolver.ts 与 sandbox.ts 的代码注释中被反复强调,且与"Worker 是唯一 Resolver"的现状完全一致。

八、给读者与运维者的实践清单

  1. 部署独立 Worker 时:务必显式设置AP_CONTAINER_TYPE=WORKERAP_FRONTEND_URL,并让后者指向集群内部可达的 App 服务地址,而不是公网域名;
  2. 不要把PUBLIC_URLAP_FRONTEND_URL混为一谈:前者服务用户可见 URL(webhook、serverContext.publicUrl),后者服务执行期回调,二者独立配置、互不替代;
  3. 理解"已废弃"不等于"错误":ADR 000013 记录了远程池方案的完整推理链,其核心洞察(输入物化前置、无连接执行、链接分发)至今仍是 Activepieces 执行引擎的设计基石;
  4. 追溯决策时:结合 000001-worker-is-the-sandbox-one-job-per-worker-scale-by-replicas.md 与 000006-pieces-are-distributed-as-links-resolved-lazily.md 一起阅读,可以看到从"远程池"到"Worker 即沙箱"的完整演化弧线——这也是本项目用 superseded ADR 保留历史决策动机的用意所在。

【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 13:22:36

PCL学习的三大认知断层与实战突破路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 13:21:20

零基础6个月转行机器人工程师:从运动学到SLAM的实战路线

如果你点开了这篇文章&#xff0c;说明你心里大概率已经有了问号&#xff1a;脱离系统科班训练&#xff0c;普通人用半年时间能不能挤进机器人工程师这个圈子&#xff1f;我的答案是&#xff1a;能&#xff0c;但有边界。六个月足够把你从“看热闹”变成“能上手干活”&#xf…

作者头像 李华
网站建设 2026/9/13 13:19:29

8位/16位RGB颜色对照与换算:从网页到嵌入式RGB565与PWM调光

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 13:17:07

Python数据可视化实战:常用统计图场景与matplotlib避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华