병렬성과 동시성
Source URL: https://docs.bullmq.io/guide/parallelism-and-concurrency
병렬성과 동시성
섹션 제목: “병렬성과 동시성”이 장에서는 병렬 실행과 동시성에 대한 몇 가지 오해를 바로잡고, 이 두 용어가 BullMQ 큐에 어떻게 적용되는지, 그리고 처리해야 하는 작업 유형에 따라 처리량을 최대화하기 위해 이를 어떻게 활용할 수 있는지 설명합니다.
병렬성
섹션 제목: “병렬성”병렬성은 이 두 개념 중 더 단순한 개념으로, 직관적으로 이해되는 의미와 거의 같습니다. 즉, 두 개 이상의 작업을 서로 독립적으로 병렬 실행할 수 있다는 뜻입니다. 멀티코어 프로세서를 사용하거나 여러 머신이 동시에 실행 중인 경우가 이에 해당합니다. 이때 작업이 병렬로 실행될 가능성이 있습니다. 다만 병렬 실행 자체가 CPU 시간을 최대화한다는 보장은 없습니다. 대부분의 소프트웨어는 네트워크 읽기, 디스크 쓰기, 주변 장치를 통한 데이터 송수신 등 느린 IO 작업에 지속적으로 의해 블로킹되기 때문입니다.
하지만 작업이 CPU 집약적이라면, 이를 100% 병렬로 실행할 수 있을 때 가장 높은 성능을 얻을 수 있습니다. 오버헤드가 매우 적기 때문입니다. 다만 이는 일반적인 경우라기보다 예외에 가깝고, 그래서 현대 컴퓨터에서는 대부분의 작업을 동시적으로 실행합니다.
동시성
섹션 제목: “동시성”컴퓨터 과학 맥락에서 동시성은, 사용 가능한 CPU를 작은 시간 조각으로 나누어 여러 작업이 모두 처리 진행을 이어가도록 함으로써 서로 독립적으로 병렬 실행되는 것처럼 보이게 하는 능력을 의미합니다. 따라서 100개의 작업이 동시에 실행된다고 말할 때, 이는 모든 작업이 처리 진행을 이어가고 있다는 뜻이지 정확히 같은 순간에 실행된다는 의미는 아닙니다.
NodeJS Event Loop
섹션 제목: “NodeJS Event Loop”NodeJS가 HTTP 서버에서 요청을 매우 효율적으로 디스패치할 수 있는 이유 중 하나는 단일 루프를 사용하면서도 IO 호출의 비동기 특성을 활용해 엄청난 수의 마이크로태스크를 동시적으로 실행할 수 있기 때문입니다. 예를 들어 데이터베이스에 데이터를 조회하는 호출을 수행하면, 그 호출이 NodeJS 전체를 블로킹하지 않습니다. 대신 다른 코드 조각을 실행하고, 현재 이벤트 루프의 끝에서 비동기 호출 중 완료된 것이 있는지 확인한 뒤 다음 반복에서 이어서 실행합니다.
이 방식은 병렬 실행처럼 보이는 효과를 주지만, 실제로는 그렇지 않습니다. 그럼에도 효율적인 이유는 코드가 IO 비중이 높기 때문이며, 비동기 호출이 끝나기를 기다리며 유휴 상태로 있는 대신 코드를 실행해 CPU 시간을 더 잘 활용할 수 있기 때문입니다.
BullMQ concurrency
섹션 제목: “BullMQ concurrency”BullMQ에서는 워커에 concurrency 설정을 지정할 수 있습니다. 이 설정은 워커 로컬 설정이며 NodeJS event loop를 활용하므로, IO 비중이 높은 작업은 더 높은 처리량의 이점을 얻습니다. 워커가 하나의 작업이 끝날 때까지 기다리지 않고 다음 작업을 가져올 수 있기 때문입니다. 하지만 작업이 IO 중심이 아니라 CPU 집약적인 경우에는 동시성 수치를 높일수록 전체 처리량이 감소한다는 점을 이해하는 것이 매우 중요합니다. 동시에 실행할 수 있는 여지가 크지 않은데도 오버헤드만 추가되기 때문입니다.
스레딩은 어떤가요?
섹션 제목: “스레딩은 어떤가요?”스레드는 운영체제가 동시(그리고 병렬) 실행을 제공하기 위해 사용하는 메커니즘입니다. 같은 CPU에서 실행 중인 스레드 간에는 한 스레드를 다른 스레드로 선점(pre-emption)하고, 여러 CPU 코어가 있으면 스레드를 병렬로 실행할 수 있습니다. 그러나 실제로 현대 OS는 특정 시점에 수백, 많게는 수천 개의 스레드를 실행하므로, 코드를 여러 스레드에서 실행한다고 해서 실제 병렬 실행이 보장되지는 않습니다. 병렬 실행될 가능성이 있을 뿐이며, 이는 해당 OS가 스케줄러를 어떻게 구현했는지에 크게 좌우됩니다. 이 내용은 꽤 복잡하지만, 경험적으로는 CPU를 많이 사용하는 스레드가 2개이고 코어가 최소 2개라면 이 두 스레드는 각자 전용 CPU에서 실행될 가능성이 높다고 볼 수 있습니다.
NodeJS는 스레드를 지원하지만, 이 스레드는 상당히 무겁고 메모리 사용량도 거의 별도의 OS 프로세스 두 개를 실행하는 수준에 가깝다는 점이 중요합니다. 그 이유는 NodeJS 스레드마다 내장 라이브러리가 많이 포함된 완전한 V8 VM이 필요하며, 이로 인해 수십 메가바이트의 메모리가 추가로 사용되기 때문입니다.
그렇다면 BullMQ의 동시성은 어떻게 활용하는 것이 가장 좋을까요?
섹션 제목: “그렇다면 BullMQ의 동시성은 어떻게 활용하는 것이 가장 좋을까요?”BullMQ에서 병렬성과 동시성을 높이는 방법은 2가지입니다. 워커별로 concurrency 계수를 지정하는 방법, 그리고 여러 워커를 병렬로 실행하는 방법입니다.
concurrency 계수는 NodeJS의 event loop를 활용해 작업이 IO 작업을 수행하는 동안 워커가 여러 작업을 동시 처리할 수 있게 합니다. 작업에 IO 연산이 필요하다면 이 값을 상당히 크게 늘릴 수 있습니다. 100~300 정도는 꽤 일반적인 설정이며, 이 값을 정교하게 조정하는 유일한 방법은 워커가 프로덕션 워크로드를 처리하는 양상을 관찰하는 것입니다.
작업이 IO 호출 없이 매우 CPU 집약적이라면 concurrency 값을 크게 둘 이유가 없습니다. 오버헤드만 늘어나기 때문입니다. 다만 BullMQ 자체도 Redis 업데이트와 새 작업 조회 시 IO 작업을 수행하므로, 약간의 concurrency 계수가 CPU 집약적 작업의 처리량을 오히려 개선할 가능성은 있습니다.
둘째로, 원하는 만큼 워커를 실행할 수 있습니다. 각 워커는 사용 가능한 CPU를 갖고 있다면 병렬로 실행됩니다. 머신에 코어가 둘 이상이라면 한 머신에서 여러 워커를 실행할 수 있고, 완전히 다른 머신들에서 워커를 실행할 수도 있습니다. 서로 다른 워커에서 실행되는 작업은 병렬로 실행되므로, 작업이 CPU 집약적이더라도 처리량을 늘릴 수 있으며 일반적으로 워커 수에 비례해 선형적으로 확장됩니다.