FEATURED · 精选文章

Activepieces 生产容器 CPU 飙高排查:原地开启 V8 Inspector 采集 CPU Profile 的完整实战指南

发布时间 / 2026/9/13 6:42:36
来源 / 创域科博编辑部
栏目 / 资讯中心
Activepieces 生产容器 CPU 飙高排查:原地开启 V8 Inspector 采集 CPU Profile 的完整实战指南 Activepieces 生产容器 CPU 飙高排查原地开启 V8 Inspector 采集 CPU Profile 的完整实战指南【免费下载链接】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本指南对应仓库中的运维技能文档 .agents/skills/profile-app-cpu/SKILL.md。Activepieces 的 Worker 是 CPU 消耗大户它在进程内 fork 引擎、用isolated-vm执行用户代码、轮询 BullMQ 队列并维护 action-run 代码缓存。当自建用户反馈容器 CPU 打满健康检查超时但进程还活着时本文提供一条不重启容器的定位路径先排除数据库、再区分 JS 线程 / GC / libuv 线程池、用SIGUSR1原地打开 V8 Inspector、经 CDP 采样 30 秒最后把.cpuprofile聚合回真正拥有 CPU 时间的那个函数。读完本文你将掌握如何判断瓶颈究竟在应用还是数据库如何在docker stats之外按线程看清 CPU 去向如何在不重启、不挂调试器的情况下对运行中的 Node 进程采集 CPU Profile以及如何避免被 V8 按调用栈拆分的节点误导直接锁定热点函数。核心原则贯穿全文原始 profile 文件留在客户机器上只把聚合后的热点帧列表带走。1. 先排除数据库应用在等 Postgres而不是在忙容器 CPU 打满时直觉会先怀疑数据库。但在等数据库和自己烧 CPU之间pg_stat_activity能给出决定性证据。先看连接池的整体形态包括有多少会话阻塞在客户端一侧select count(*) total, count(*) filter (where stateactive) active, count(*) filter (where stateidle in transaction) idle_in_txn, count(*) filter (where wait_event_typeLock) waiting_on_lock, count(*) filter (where wait_eventClientRead) waiting_on_client from pg_stat_activity;再看每个会话的等待事件——每个会话的 wait event 才是真正做决定的地方select now() - query_start as dur, state, wait_event_type, wait_event, left(query, 120) as q from pg_stat_activity where state idle and query_start is not null order by 1 desc limit 15;判读信号应用是瓶颈的铁证大量会话持续数秒停在wait_eventClientRead且waiting_on_lock为零。ClientRead意味着 Postgres 已经干完了活、正在等客户端来读结果——是事件循环被阻塞数据库只是受害者而非原因。数据库真在挣扎的样子相反出现非零的锁等待或wait_event_type为IO/LWLock的会话dur很长。Activepieces 的 API 与 Worker 都会重度依赖 Postgres流运行状态、队列、持久化packages/server/worker/src/lib/worker.ts 中每个轮询周期都要采集机器信息并发起健康探测任何一次数据库响应延迟都会直接传导到 Worker 的轮询循环上。所以这一步必须最先做且结论要写成DB 有罪/无罪两种可证伪的说法。2. 找出热点线程TIME比%CPU更可靠确认不是数据库后进入容器内部按线程分解 CPU。docker stats的百分比是每核计量的100% 代表吃满一个核因此cpus: 1的容器饱和时读数约 100%即使宿主机还有很多空闲核。PID$(docker inspect -f {{.State.Pid}} container) top -H -b -n 1 -p $PID读TIME列累计 CPU 时间而不是只看瞬时%CPU。三个方向的判读MainThread热→ JS 跑在事件循环上进入第 3 步做 CPU ProfileV8Worker线程热→ 是 GC该看堆压力而不是应用代码libuv-worker热→ fs / crypto / zlib 在线程池里干活。TIME还要和容器存活时间对比27 小时累计 105 CPU 分钟平均约 6%——这是突发型负载不是稳态空转。突发型负载意味着你必须在它真正热的时候采样否则采到的就是一堆(idle)。结合 Activepieces 的架构看packages/server/worker/src/lib/worker.ts 里 Worker 默认按AP_WORKER_CONCURRENCY历史默认 5见配置与 ADR 0004 的过渡兼容说明运行多个轮询循环每个循环在进程内 fork 一个引擎沙箱。用户代码、piece 逻辑、网络请求都在这些引擎里跑任何一个热点都可能在MainThread或libuv-worker上暴露出来。3. 原地打开 InspectorSIGUSR1 无需重启Node 收到SIGUSR1会原地启动 inspector不重启进程、不丢现场——重启恰恰会毁掉你想测量的那个状态。它绑定到容器网络命名空间内的127.0.0.1:9229宿主机和外部都访问不到docker exec container sh -c kill -USR1 1 docker exec container node -e require(http).get({host:127.0.0.1,port:9229,path:/json/version},rr.pipe(process.stdout))第二条命令验证 inspector 已经就绪。上生产前要清楚两个事实Inspector 打开后无法再关闭它会一直活到进程退出。优先选择事后可以回收重建的容器并在事故记录里注明这一点Profiler域只做采样不会像断点那样暂停进程对在线服务的影响可忽略。这与姊妹篇 .agents/skills/profile-worker-memory/SKILL.md 中发SIGUSR1打开 inspector、再通过 CDP 驱动127.0.0.1:9229的手法完全一致——同一套原地诊断基础设施一个用于内存快照一个用于 CPU 采样。4. 采样Node 22 全局 WebSocket 作为 CDP 客户端Node 22 自带全局WebSocketCDP 客户端零依赖。脚本要在容器内运行与目标进程共享网络命名空间。先docker cp进去采样完再docker cp结果出来const http require(http), fs require(fs) const getWs () new Promise((res, rej) http.get({ host: 127.0.0.1, port: 9229, path: /json/list }, r { let d ; r.on(data, c d c) r.on(end, () { try { res(JSON.parse(d)[0].webSocketDebuggerUrl) } catch (e) { rej(new Error(d)) } }) }).on(error, rej)) ;(async () { const ws new WebSocket(await getWs()) let id 0; const pending new Map() await new Promise((r, j) { ws.onopen r; ws.onerror j }) ws.onmessage ev { const m JSON.parse(ev.data); if (pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) } } const send (method, params {}) new Promise(r { const i id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method, params })) }) await send(Profiler.enable) await send(Profiler.setSamplingInterval, { interval: 400 }) await send(Profiler.start) await new Promise(r setTimeout(r, 30000)) const { profile } await send(Profiler.stop) fs.writeFileSync(/tmp/cpu.cpuprofile, JSON.stringify(profile)) console.log(samples profile.samples.length) process.exit(0) })()参数说明Profiler.setSamplingInterval的interval单位是微秒400即 400µs 一个采样点30 秒采样 × 400µs 间隔 ≈5 万个样本足够统计显著又轻到不会扭曲结果脚本以process.exit(0)结束避免残留的 WebSocket 连接把容器挂在退出状态上。一个关键差异提醒profile-worker-memory 中提到Node 的全局WebSocket无法与 V8 inspector 通信——那是针对HeapProfiler 的堆快照流式传输的坑握手成功但 socket 随即死掉需要改用镜像自带的ws包并配置{ perMessageDeflate: false, maxPayload: 0 }。而本文的CPU Profile 走的是Profiler域的 JSON-RPC 消息全局WebSocket完全够用。两者协议路径不同不要混用经验。5. 按函数聚合这是被跳过、也因此误读最多的一步V8 的.cpuprofile会给每个调用栈单独建一个节点。一个热点函数会以几十个小百分比的节点形式散落各处单个看都不起眼——直接看原始 profile 的人几乎必然误判。正确做法是先按(functionName, url, line)汇总 self-timeconst p JSON.parse(require(fs).readFileSync(process.argv[2], utf8)) const byId new Map(p.nodes.map(n [n.id, n])) const self new Map() for (const s of p.samples) self.set(s, (self.get(s) || 0) 1) const agg new Map() for (const [id, c] of self) { const f byId.get(id).callFrame const k ${f.functionName || (anon)} ${f.url}:${f.lineNumber 1} agg.set(k, (agg.get(k) || 0) c) } const total p.samples.length ;[...agg.entries()].sort((a, b) b[1] - a[1]).slice(0, 15) .forEach(([k, c]) console.log((100 * c / total).toFixed(2).padStart(6) % k))聚合结果要和(idle)对照着读49% 的空闲下一个占墙钟 42% 的帧实际吃掉了约 84% 的有效 CPU。汇报时要两个数字一起说——占墙钟 42%占非空闲 CPU 84%这句话才能让结论落地。识别失控递归逐个遍历采样到的调用栈统计同一个函数在单栈上的重复次数。一个长链条上同一帧反复出现、且百分比缓慢递减30 帧内从 22% 递减到 19%的模式是每层成本正比于其下方剩余量的递归——典型的意外 O(N²) 签名。6. 三个高频翻车点Gotchas健康检查超时是事件循环被阻塞的症状不是崩溃。容器显示unhealthy、FailingStreak持续攀升但应用仍在正常服务流量——curl健康检查只是在自己的超时窗口内得不到响应。容器里不断累积的僵尸curl进程ps -eo stat | grep ^Z是被杀掉的健康检查不是内存泄漏。哪个容器显示不健康会随流量轮换不要对告警点名的那个过度投入。Activepieces 的健康检查机制与此完全对得上Worker 侧的健康服务器worker.ts在轮询循环停滞时返回 503而镜像自带的HEALTHCHECKDockerfile用curl -fsS http://localhost:${AP_PORT:-80}/api/v1/health每 10 秒探测一次、5 秒超时——事件循环一旦被长任务堵住这个 5 秒窗口就会连续击穿。docker stats的 CPU 是每核的不是每宿主机的。100% 等于一个满核。对cpus: 1来说那就是上限即使宿主机显示大量空闲。对比第 2 步用TIME判断平均负载两者口径不同不能直接互推。突发型负载下瞬时的docker stats与TIME、load average 都会互相打架。在你 attach 之前几秒先取一个新鲜的docker stats读数来挑选采样目标而不是拿几分钟前的告警快照当依据。7. 与 Activepieces 运行时结合这套流程在仓库里对应的真实场景本文技能文档是仓库运维知识体系的一部分它与代码侧的几处实现互为印证Worker 是采样重点。packages/server/worker/src/lib/worker.ts 启动后通过 Socket.IO 连接 API、启动轮询循环、缓存清扫器和看门狗collectMachineInfo里用systemUsage.getCpuUsage()上报 CPU 占用率。若自建环境出现Worker 页面 CPU 高、但 job 吞吐正常本文的线程分解步骤能立刻区分是轮询循环本身、引擎 fork、还是isolated-vm的 GC 在烧 CPU。健康检查超时在代码里有明确对应Worker 健康服务器只响应/worker/health、/v1/health、/api/v1/health三个路径轮询循环停滞超过 10 分钟POLL_LIVENESS_TIMEOUT_MS即报 503。如果你的unhealthy是这类 503 而非事件循环阻塞那是另一个问题进程管理重启即可本文的 CPU 采样流程不适用。镜像基线与采样前提吻合Dockerfile 基于node:24.14.0-bullseye-slim满足Node 22 自带全局WebSocket的前提镜像内预装procpstop/ps可用与curl健康检查与 inspector 验证可用意味着第 2、3 步的命令在默认镜像里开箱即用。隐私红线与内存篇一脉相承.cpuprofile内嵌函数名、文件路径和脚本片段与堆快照一样属于敏感产物。聚合后的热点帧列表可以带走原始文件必须留在客户机器上与 profile-worker-memory 中快照永不离机的原则一致。8. 总结一份可复用的排查清单先查pg_stat_activitywait_eventClientRead 零锁等待 应用阻塞数据库无罪按线程分解top -H读TIME区分 MainThreadJS/ V8WorkerGC/ libuv-workerfs/crypto/zlib原地开 inspectorkill -USR1 1绑定容器内127.0.0.1:9229外部不可达采样 30 秒容器内跑零依赖 CDP 脚本400µs 间隔 ≈ 5 万样本按(functionName, url, line)聚合先求和 self-time再对照(idle)折算占非空闲 CPU 比例识别递归沿调用栈找同帧重复、百分比缓慢递减的 O(N²) 签名善后只带走聚合结果删除容器内/tmp/cpu.cpuprofile与辅助脚本优先回收该容器inspector 无法关闭。这套流程的完整技能定义触发条件与适用场景见 SKILL.md 的 frontmatter其姊妹篇 profile-worker-memory 覆盖内存/堆方向的同类诊断两者共同构成 Activepieces 自建环境容器不健康但进程还活着场景下的标准调查入口。【免费下载链接】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),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻