Node.js 里用 worker_threads 处理 CPU 密集任务时,为什么你的主线程还是卡住了
线上有个图片处理服务,用户上传一张 4MB 左右的 JPEG,服务端要生成三档缩略图并做一次水印合成。一开始用的是 sharp 的同步链式调用,在测试环境跑得挺顺,结果上线后一到下午高峰,接口 P99 直接飙到 3 秒以上,健康检查也开始超时。第一反应是把整个处理逻辑塞进 worker_threads,毕竟 sharp 本身是 libvips 的绑定,跑在 worker 里应该能把主线程释放出来。改完之后吞吐确实上了一个台阶,但监控上还是能看到事件循环延迟有周期性尖峰,偶尔能到 400ms 以上。排查下来发现 worker 的创建方式、消息传递的数据形态、以及线程池默认大小这三处都有坑,而且它们叠在一起时,主线程依然会被拖住。这篇文章把这段改造里踩到的几个真实问题拆开讲清楚,包括 worker 复用的正确姿势、Transferable 和 Structured Clone 对序列化开销的影响,以及怎么用 event loop delay 把问题量化出来。 先说 worker 的创建方式。很多人会在每次请求进来时 `new Worker('./image-worker.js')`,用完就 `terminate()`。这个写法看起来最干净,实际上 worker 的启动成本不低:要加载模块、初始化 libvips、分配线程池,单次冷启动在普通云主机上测下来大概 30~80ms。请求一多,这些启动动作会挤在主线程的 event loop 里排队,再加上频繁创建销毁带来的 GC 压力,延迟尖峰就出来了。合理的做法是维护一个固定大小的 worker 池,启动时预热,请求来了从池里取空闲的实例,处理完归还。下面是一个最小可用的池实现,没有依赖任何三方库: ```js const { Worker } = require('node:worker_threads'); const path = require('node:path'); class WorkerPool { constructor(size, workerPath) { this.size = size; this.workerPath = workerPath; this.idle = []; this.queue = []; this.workers = new Set(); for (let i = 0; i { const task = worker._currentTask; if (!task) return; if (msg.error) task.reject(new Error(msg.error)); else task.resolve(msg.result); worker._currentTask = null; this.#release(worker); }); worker.on('error', (err) => { const task = worker._currentTask; if (task) { task.reject(err); worker._currentTask = null; } this.workers.delete(worker); this.#spawn(); }); this.workers.add(worker); this.idle.push(worker); } #release(worker) { if (this.queue.length) { const task = this.queue.shift(); worker._currentTask = task; worker.postMessage(task.payload); } else { this.idle.push(worker); } } run(payload) { return new Promise((resolve, reject) => { const worker = this.idle.pop(); const task = { resolve, reject, payload }; if (worker) { worker._currentTask = task; worker.postMessage(payload); } else { this.queue.push(task); } }); } } const pool = new WorkerPool(4, path.join(__dirname, 'image-worker.js')); ``` worker 侧的代码很直白,收到 buffer 就调 sharp,处理完把结果 postMessage 回去。但这里有个容易被忽略的点:`postMessage` 默认走的是 Structured Clone 算法,一个 4MB 的 Buffer 会被完整拷贝一份,拷贝发生在主线程发消息和 worker 收消息两个环节。高峰期每秒几十个请求,光序列化就能把主线程的 CPU 吃满。解决办法是 Buffer 底层是 ArrayBuffer,只要把这块内存的所有权转移出去,就不需要拷贝: ```js // image-worker.js const { parentPort } = require('node:worker_threads'); const sharp = require('sharp'); parentPort.on('message', async (payload) => { try { const { buffer, width, height } = payload; const result = await sharp(buffer) .resize(width, height, { fit: 'inside' }) .jpeg({ quality: 82 }) .toBuffer(); const ab = result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength); parentPort.postMessage( { result: ab }, [ab] ); } catch (err) { parentPort.postMessage({ error: err.message }); } }); ``` 主线程发送时也要带上 transfer list: ```js const ab = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); pool.run({ buffer: ab, width: 800, height: 800 }, [ab]); ``` 注意 transfer 之后主线程这边的 ArrayBuffer 会被 detach,长度变成 0,如果后续还要拿原始 buffer 做别的事,必须先拷贝一份再转移。曾经因为忘了这点,上传原图的逻辑在 worker 返回后读到了空 buffer,调试了半小时才发现是 detach 惹的祸。 另一个影响事件循环延迟的因素是 libvips 自己的线程池。sharp 内部用 libvips,libvips 默认会根据 CPU 核数开一组工作线程,如果用 `UV_THREADPOOL_SIZE` 去调 Node 的线程池,对 sharp 没有效果。真正起作用的是环境变量 `VIPS_CONCURRENCY`,在容器里如果 CPU limit 配得比实际核数高,libvips 会按宿主机核数开线程,反而造成线程争抢。我们最后是显式设成每个 worker 单线程: ```bash VIPS_CONCURRENCY=1 node server.js ``` worker 池本身承担并发,单个 worker 内部再把 libvips 的并发压到 1,这样每个容器实例的 CPU 占用是可预测的,扩容时也不会因为线程数失控而互相踩。 改完之后还得有手段验证效果,不能靠体感。Node 从 18 开始提供了 `performance.eventLoopUtilization()`,可以周期性采样主线程的事件循环利用率,方法本身开销很低,放在一个 `setInterval` 里就行: ```js const { performance } = require('node:perf_hooks'); let last = performance.eventLoopUtilization(); setInterval(() => { const now = performance.eventLoopUtilization(); const delta = performance.eventLoopUtilization(now, last); if (delta.utilization > 0.85) { console.warn('event loop busy', delta.utilization.toFixed(2)); } last = now; }, 1000).unref(); ``` 这个指标比单纯看 CPU 更贴近用户感知,因为事件循环被占住时,CPU 使用率可能并不高,请求却全都卡在队列里。把 worker 池大小、消息体积、libvips 并发这三项对齐之后,接口 P99 稳定在 400ms 左右,健康检查再没报过超时。后面如果还要继续压延迟,下一个该动的地方是把缩略图结果直接写进对象存储的流里,省掉 worker 到主线程再回传 buffer 这一趟。 推荐查看天气:http://www.tianqi556.com/chengyu/85.html 推荐查看天气:http://www.tianqi556.com/chengyu/86.html 推荐查看天气:http://www.tianqi556.com/chengyu/87.html 推荐查看天气:http://www.tianqi556.com/chengyu/88.html 推荐查看天气:http://www.tianqi556.com/chengyu/89.html 推荐查看天气:http://www.tianqi556.com/chengyu/90.html 推荐查看天气:http://www.tianqi556.com/chengyu/91.html 推荐查看天气:http://www.tianqi556.com/chengyu/92.html 推荐查看天气:http://www.tianqi556.com/chengyu/93.html 推荐查看天气:http://www.tianqi556.com/chengyu/94.html 推荐查看天气:http://www.tianqi556.com/chengyu/95.html 推荐查看天气:http://www.tianqi556.com/chengyu/96.html 推荐查看天气:http://www.tianqi556.com/chengyu/97.html 推荐查看天气:http://www.tianqi556.com/chengyu/98.html 推荐查看天气:http://www.tianqi556.com/chengyu/99.html 推荐查看天气:http://www.tianqi556.com/chengyu/100.html 推荐查看天气:http://www.tianqi556.com/chengyu/101.html 推荐查看天气:http://www.tianqi556.com/chengyu/102.html 推荐查看天气:http://www.tianqi556.com/chengyu/103.html 推荐查看天气:http://www.tianqi556.com/chengyu/104.html 推荐查看天气:http://www.tianqi556.com/chengyu/105.html 推荐查看天气:http://www.tianqi556.com/chengyu/106.html 推荐查看天气:http://www.tianqi556.com/chengyu/107.html 推荐查看天气:http://www.tianqi556.com/chengyu/108.html 推荐查看天气:http://www.tianqi556.com/chengyu/109.html 推荐查看天气:http://www.tianqi556.com/chengyu/110.html 推荐查看天气:http://www.tianqi556.com/chengyu/111.html 推荐查看天气:http://www.tianqi556.com/chengyu/112.html 推荐查看天气:http://www.tianqi556.com/chengyu/113.html 推荐查看天气:http://www.tianqi556.com/chengyu/114.html 推荐查看天气:http://www.tianqi556.com/chengyu/115.html 推荐查看天气:http://www.tianqi556.com/chengyu/116.html 推荐查看天气:http://www.tianqi556.com/chengyu/117.html 推荐查看天气:http://www.tianqi556.com/chengyu/118.html 推荐查看天气:http://www.tianqi556.com/chengyu/119.html 推荐查看天气:http://www.tianqi556.com/chengyu/120.html 推荐查看天气:http://www.tianqi556.com/chengyu/121.html 推荐查看天气:http://www.tianqi556.com/chengyu/122.html 推荐查看天气:http://www.tianqi556.com/chengyu/123.html 推荐查看天气:http://www.tianqi556.com/chengyu/124.html 推荐查看天气:http://www.tianqi556.com/chengyu/125.html 推荐查看天气:http://www.tianqi556.com/chengyu/126.html 推荐查看天气:http://www.tianqi556.com/chengyu/127.html 推荐查看天气:http://www.tianqi556.com/chengyu/128.html 推荐查看天气:http://www.tianqi556.com/chengyu/129.html 推荐查看天气:http://www.tianqi556.com/chengyu/130.html 推荐查看天气:http://www.tianqi556.com/chengyu/131.html 推荐查看天气:http://www.tianqi556.com/chengyu/132.html 推荐查看天气:http://www.tianqi556.com/chengyu/133.html 推荐查看天气:http://www.tianqi556.com/chengyu/134.html 推荐查看天气:http://www.tianqi556.com/chengyu/135.html 推荐查看天气:http://www.tianqi556.com/chengyu/136.html 推荐查看天气:http://www.tianqi556.com/chengyu/137.html 推荐查看天气:http://www.tianqi556.com/chengyu/138.html 推荐查看天气:http://www.tianqi556.com/chengyu/139.html 推荐查看天气:http://www.tianqi556.com/chengyu/140.html 推荐查看天气:http://www.tianqi556.com/chengyu/141.html 推荐查看天气:http://www.tianqi556.com/chengyu/142.html 推荐查看天气:http://www.tianqi556.com/chengyu/143.html 推荐查看天气:http://www.tianqi556.com/chengyu/144.html 推荐查看天气:http://www.tianqi556.com/chengyu/145.html 推荐查看天气:http://www.tianqi556.com/chengyu/146.html 推荐查看天气:http://www.tianqi556.com/chengyu/147.html 推荐查看天气:http://www.tianqi556.com/chengyu/148.html 推荐查看天气:http://www.tianqi556.com/chengyu/149.html 推荐查看天气:http://www.tianqi556.com/chengyu/150.html 推荐查看天气:http://www.tianqi556.com/chengyu/151.html 推荐查看天气:http://www.tianqi556.com/chengyu/152.html 推荐查看天气:http://www.tianqi556.com/chengyu/153.html 推荐查看天气:http://www.tianqi556.com/chengyu/154.html 推荐查看天气:http://www.tianqi556.com/chengyu/155.html 推荐查看天气:http://www.tianqi556.com/chengyu/156.html 推荐查看天气:http://www.tianqi556.com/chengyu/157.html 推荐查看天气:http://www.tianqi556.com/chengyu/158.html 推荐查看天气:http://www.tianqi556.com/chengyu/159.html 推荐查看天气:http://www.tianqi556.com/chengyu/160.html 推荐查看天气:http://www.tianqi556.com/chengyu/161.html 推荐查看天气:http://www.tianqi556.com/chengyu/162.html 推荐查看天气:http://www.tianqi556.com/chengyu/164.html 推荐查看天气:http://www.tianqi556.com/chengyu/165.html 推荐查看天气:http://www.tianqi556.com/chengyu/166.html 推荐查看天气:http://www.tianqi556.com/chengyu/167.html 推荐查看天气:http://www.tianqi556.com/chengyu/168.html 推荐查看天气:http://www.tianqi556.com/chengyu/169.html 推荐查看天气:http://www.tianqi556.com/chengyu/170.html 推荐查看天气:http://www.tianqi556.com/chengyu/171.html 推荐查看天气:http://www.tianqi556.com/chengyu/172.html 推荐查看天气:http://www.tianqi556.com/chengyu/173.html 推荐查看天气:http://www.tianqi556.com/chengyu/174.html 推荐查看天气:http://www.tianqi556.com/chengyu/175.html 推荐查看天气:http://www.tianqi556.com/chengyu/176.html 推荐查看天气:http://www.tianqi556.com/chengyu/177.html 推荐查看天气:http://www.tianqi556.com/chengyu/178.html 推荐查看天气:http://www.tianqi556.com/chengyu/179.html 推荐查看天气:http://www.tianqi556.com/chengyu/180.html 推荐查看天气:http://www.tianqi556.com/chengyu/181.html 推荐查看天气:http://www.tianqi556.com/chengyu/182.html 推荐查看天气:http://www.tianqi556.com/chengyu/183.html 推荐查看天气:http://www.tianqi556.com/chengyu/184.html 推荐查看天气:http://www.tianqi556.com/chengyu/185.html 推荐查看天气:http://www.tianqi556.com/chengyu/186.html 推荐查看天气:http://www.tianqi556.com/chengyu/187.html 推荐查看天气:http://www.tianqi556.com/chengyu/188.html 推荐查看天气:http://www.tianqi556.com/chengyu/189.html 推荐查看天气:http://www.tianqi556.com/chengyu/190.html 推荐查看天气:http://www.tianqi556.com/chengyu/191.html 推荐查看天气:http://www.tianqi556.com/chengyu/192.html 推荐查看天气:http://www.tianqi556.com/chengyu/193.html 推荐查看天气:http://www.tianqi556.com/chengyu/194.html 推荐查看天气:http://www.tianqi556.com/chengyu/195.html 推荐查看天气:http://www.tianqi556.com/chengyu/196.html 推荐查看天气:http://www.tianqi556.com/chengyu/197.html 推荐查看天气:http://www.tianqi556.com/chengyu/198.html 推荐查看天气:http://www.tianqi556.com/chengyu/199.html 推荐查看天气:http://www.tianqi556.com/chengyu/200.html 推荐查看天气:http://www.tianqi556.com/chengyu/201.html 推荐查看天气:http://www.tianqi556.com/chengyu/202.html 推荐查看天气:http://www.tianqi556.com/chengyu/203.html 推荐查看天气:http://www.tianqi556.com/chengyu/204.html 推荐查看天气:http://www.tianqi556.com/chengyu/205.html 推荐查看天气:http://www.tianqi556.com/chengyu/206.html 推荐查看天气:http://www.tianqi556.com/chengyu/207.html 推荐查看天气:http://www.tianqi556.com/chengyu/208.html 推荐查看天气:http://www.tianqi556.com/chengyu/209.html 推荐查看天气:http://www.tianqi556.com/chengyu/210.html 推荐查看天气:http://www.tianqi556.com/chengyu/211.html 推荐查看天气:http://www.tianqi556.com/chengyu/212.html 推荐查看天气:http://www.tianqi556.com/chengyu/213.html 推荐查看天气:http://www.tianqi556.com/chengyu/214.html 推荐查看天气:http://www.tianqi556.com/chengyu/215.html 推荐查看天气:http://www.tianqi556.com/chengyu/216.html 推荐查看天气:http://www.tianqi556.com/chengyu/217.html 推荐查看天气:http://www.tianqi556.com/chengyu/218.html 推荐查看天气:http://www.tianqi556.com/chengyu/219.html 推荐查看天气:http://www.tianqi556.com/chengyu/220.html 推荐查看天气:http://www.tianqi556.com/chengyu/221.html 推荐查看天气:http://www.tianqi556.com/chengyu/222.html 推荐查看天气:http://www.tianqi556.com/chengyu/223.html 推荐查看天气:http://www.tianqi556.com/chengyu/224.html 推荐查看天气:http://www.tianqi556.com/chengyu/225.html 推荐查看天气:http://www.tianqi556.com/chengyu/226.html 推荐查看天气:http://www.tianqi556.com/chengyu/227.html 推荐查看天气:http://www.tianqi556.com/chengyu/228.html 推荐查看天气:http://www.tianqi556.com/chengyu/229.html 推荐查看天气:http://www.tianqi556.com/chengyu/230.html 推荐查看天气:http://www.tianqi556.com/chengyu/231.html 推荐查看天气:http://www.tianqi556.com/chengyu/232.html 推荐查看天气:http://www.tianqi556.com/chengyu/233.html 推荐查看天气:http://www.tianqi556.com/chengyu/234.html 推荐查看天气:http://www.tianqi556.com/chengyu/235.html 推荐查看天气:http://www.tianqi556.com/chengyu/236.html 推荐查看天气:http://www.tianqi556.com/chengyu/237.html 推荐查看天气:http://www.tianqi556.com/chengyu/238.html 推荐查看天气:http://www.tianqi556.com/chengyu/239.html 推荐查看天气:http://www.tianqi556.com/chengyu/240.html 推荐查看天气:http://www.tianqi556.com/chengyu/241.html 推荐查看天气:http://www.tianqi556.com/chengyu/242.html 推荐查看天气:http://www.tianqi556.com/chengyu/243.html 推荐查看天气:http://www.tianqi556.com/chengyu/244.html 推荐查看天气:http://www.tianqi556.com/chengyu/245.html 推荐查看天气:http://www.tianqi556.com/chengyu/246.html 推荐查看天气:http://www.tianqi556.com/chengyu/247.html 推荐查看天气:http://www.tianqi556.com/chengyu/248.html 推荐查看天气:http://www.tianqi556.com/chengyu/249.html 推荐查看天气:http://www.tianqi556.com/chengyu/250.html 推荐查看天气:http://www.tianqi556.com/chengyu/251.html 推荐查看天气:http://www.tianqi556.com/chengyu/252.html 推荐查看天气:http://www.tianqi556.com/chengyu/253.html 推荐查看天气:http://www.tianqi556.com/chengyu/254.html 推荐查看天气:http://www.tianqi556.com/chengyu/255.html 推荐查看天气:http://www.tianqi556.com/chengyu/256.html 推荐查看天气:http://www.tianqi556.com/chengyu/257.html 推荐查看天气:http://www.tianqi556.com/chengyu/258.html 推荐查看天气:http://www.tianqi556.com/chengyu/259.html 推荐查看天气:http://www.tianqi556.com/chengyu/260.html 推荐查看天气:http://www.tianqi556.com/chengyu/261.html 推荐查看天气:http://www.tianqi556.com/chengyu/262.html 推荐查看天气:http://www.tianqi556.com/chengyu/263.html 推荐查看天气:http://www.tianqi556.com/chengyu/264.html 推荐查看天气:http://www.tianqi556.com/chengyu/265.html 推荐查看天气:http://www.tianqi556.com/chengyu/266.html 推荐查看天气:http://www.tianqi556.com/chengyu/267.html 推荐查看天气:http://www.tianqi556.com/chengyu/268.html 推荐查看天气:http://www.tianqi556.com/chengyu/269.html 推荐查看天气:http://www.tianqi556.com/chengyu/270.html 推荐查看天气:http://www.tianqi556.com/chengyu/271.html 推荐查看天气:http://www.tianqi556.com/chengyu/272.html 推荐查看天气:http://www.tianqi556.com/chengyu/273.html 推荐查看天气:http://www.tianqi556.com/chengyu/274.html 推荐查看天气:http://www.tianqi556.com/chengyu/275.html 推荐查看天气:http://www.tianqi556.com/chengyu/276.html 推荐查看天气:http://www.tianqi556.com/chengyu/277.html 推荐查看天气:http://www.tianqi556.com/chengyu/278.html 推荐查看天气:http://www.tianqi556.com/chengyu/279.html 推荐查看天气:http://www.tianqi556.com/chengyu/280.html 推荐查看天气:http://www.tianqi556.com/chengyu/281.html 推荐查看天气:http://www.tianqi556.com/chengyu/282.html 推荐查看天气:http://www.tianqi556.com/chengyu/283.html 推荐查看天气:http://www.tianqi556.com/chengyu/284.html 推荐查看天气:http://www.tianqi556.com/chengyu/285.html