Back to feed
ASH avatar
ASH

2026. 5. 5.·base·

CPU Bound 작업에서 워커의 Concurrency를 늘려도 성능 개선이 거의 없었던 이유

NestJS 백엔드 리팩토링

bullmqnestjsnodejs
ts
@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이 이런형태라면

ts
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를 사용하는걸 권장한다고 한다.

0
Comments

Join the thread

Leave feedback, ask for clarification, or keep a focused discussion attached to this article.

0 comments
No comments yet. Start the first thread for this article.
Current user avatar
Styling with Markdown is supported