2026. 5. 5.·base·
CPU Bound 작업에서 워커의 Concurrency를 늘려도 성능 개선이 거의 없었던 이유
NestJS 백엔드 리팩토링
@Processor('embedding-queue', { concurrency: 3 })
export class EmbeddingWorker extends WorkerHost {
private readonly logger = new Logger(EmbeddingWorker.name);
constructor(
private readonly mlEngine: MLEngine,
private readonly campaignCacheRepository: CampaignCacheRepository
) {
super();
}벡터 임베딩을 담당하는 NodeJS 워커에 concurrency: 3을 부여했는데 성능 개선이 전혀 발생하지 않았다. 나는 당연히 동시성을 늘리는 옵션이라 개선이 생길거라고 생각했었는데..
그럼 이 옵션은 정확히 어떤 일을 하는걸까?
bullMQ의 concurrency옵션은 CPU나 스레드를 늘리는 옵션이 아니라 '동시에 진행 중인 job 수'를 늘리는 옵션이다. 다시말하면, 이 worker가 job을 몇 개까지 active 상태로 동시에 들고 있을 수 있냐를 설정하는 것이라고 볼 수 있다.
예를 들어 concurrency: 5면 워커 하나가
- job1 시작
- job2 시작
- job3 시작
- job4 시작
- job5 시작
까지 가능하다.
여기서 '시작'은 job 하나가 완전히 끝날 때까지 다음 잡을 안가져오는 것이 아니라, Promise가 resolve되기 전에 다른 job도 같이 진행 상태로 올릴 수 있다는 뜻이다. (concurrency옵션을 안주면 디폴트 값이 1이라, 하나의 요청이 처리되어야 다음 요청을 보낸다.)
그래서 concurrency는 별로 쓸데가 없는 옵션인가?
아니다. I/O 작업에서 아주 효과적이다.
만약 job이 이런형태라면
async process(job) {
const a = await redis.get(...);
const b = await db.query(...);
await httpCall(...);
}이 작업을 처리하는데 걸리는 시간은 CPU 계산이 아닌 비동기 응답을 대기하는 시간이 대부분이다.
그래서 concurrency: 1이면 job1이 Redis응답을 기다리는 동안 worker는 다음 잡을 안들고 그냥 기다린다.
반면에 concurrency: 5이면 job1 요청을 보내고 기다리는 동안 job2 요청을 보내고, 그동안 또 job3을 보내서 여러 잡의 대기 시간을 겹처 쓸 수가 있다.
CPU 바운드 작업은 기다림이 없고 메인스레드가 계속 계산을 붙잡고 있는 형태이므로 concurrency: 5라도 실제로는
- job1 계산 중
- 이벤트루프 막힘
- job2~job5를 건드릴 시간이 없음
concurrency가 있어도 겹칠 비동기 대기 시간이 없어 효과가 작거나 오히려 더 나빠질 수가 있다. 그래서 BullMQ 공식문서도 CPU 집약적인 잡은 sandboxed processors를 사용하는걸 권장한다고 한다.
Join the thread
Leave feedback, ask for clarification, or keep a focused discussion attached to this article.