用worker_threads处理图片缩略图,我踩到的内存坑

上周在 Node 18.16 上跑一个批量生成缩略图的服务,输入是 2000 张 5MB 左右的 JPEG,输出 200x200 的 webp。用 sharp 在主线程一张张处理,内存峰值直接飙到 1.2GB,处理速度还慢。后来换成 worker_threads 池,峰值降到 400MB 左右,吞吐量大概翻了一倍。

做法不复杂:先建一个固定大小的 worker 池,大小取 os.cpus().length - 1,避免把主线程饿死。主线程只负责读文件和分发路径,worker 里用 sharp 处理。

// main.js
import { Worker } from 'node:worker_threads';
import os from 'node:os';

const poolSize = Math.max(1, os.cpus().length - 1);
const workers = [];
for (let i = 0; i < poolSize; i++) {
  workers.push(new Worker('./worker.js'));
}

let idx = 0;
function next() {
  if (idx >= files.length) return Promise.resolve();
  const file = files[idx++];
  const worker = workers[idx % poolSize];
  return new Promise((resolve, reject) => {
    worker.once('message', resolve);
    worker.once('error', reject);
    worker.postMessage(file);
  }).then(next);
}

worker 里这样写:

// worker.js
import { parentPort } from 'node:worker_threads';
import sharp from 'sharp';

parentPort.on('message', async (file) => {
  try {
    await sharp(file)
      .resize(200, 200, { fit: 'inside' })
      .webp({ quality: 80 })
      .toFile(file.replace(/\.jpg$/, '.webp'));
    parentPort.postMessage({ ok: true });
  } catch (e) {
    parentPort.postMessage({ ok: false, error: e.message });
  }
});

为什么能省内存:sharp 底层是 libvips,它有自己的线程池和缓存,默认缓存上限是 50MB,但每个 worker 实例都会独立占一份缓存和 V8 堆。主线程串行处理时,这些缓存和堆反复增长,GC 跟不上;拆成多个 worker 后,每个 worker 的堆更小,缓存也分散了,整体峰值反而下降。代价是进程启动时多花几十毫秒创建 worker,且 sharp 的缓存参数需要调,我设了 sharp.cache(false) 和 sharp.concurrency(1),否则 worker 内部还会再开线程,跟池抢 CPU。

哪里会失效:如果单张图特别大(比如 50MB 的 TIFF),worker 池反而会让内存峰值叠加,因为每个 worker 同时持有一张大图。这时候应该限制并发数,或者用 sharp 的流式接口逐块处理。另外,worker 里抛出的异常如果不 catch,整个 worker 会退出,池子就少了一个,我一开始没处理,跑一半发现吞吐掉得厉害,查了半天。

你们在处理大量图片时,是倾向于用 worker 池还是直接上队列加多进程?有没有遇到 sharp 缓存跟 worker 抢资源的情况?

0
赞同度 100% · 0 条评论 · 9 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。