-
[NodeJS] Libuv ThreadPool 과 비동기 처리 관계 알아보기 ( UV_THREADPOOL_SIZE )Language/Nodejs 2024. 8. 13. 06:26반응형
- 목차
Libuv Thread Pool 이란 ?
Node.js 는 하나의 Thread 에서 동작하는 구조로 유명하지만 실제로는 libuv 의 Thread Pool 에 의해서 여러 쓰레드들이 함께 실행됩니다.
특히 Node.js 에 내장된 비동기 함수들이 여러 Thread Pool 에서 멀티 쓰레드 구조로 실행됩니다.
실제로 Node.js 프로세스를 실행하게 되면 아래와 같이 여러개의 Thread 가 확인됩니다.
root@2153f4656997:/usr/src/app# ps -T -p 29 PID SPID TTY TIME CMD 29 29 pts/0 00:00:00 node 29 30 pts/0 00:00:00 node 29 31 pts/0 00:00:00 node 29 32 pts/0 00:00:00 node 29 33 pts/0 00:00:00 node 29 34 pts/0 00:00:00 node 29 35 pts/0 00:00:00 node 29 36 pts/0 00:00:00 node아래의 이미지는 Node.js Process 의 Thread 구조입니다.
Libuv 모듈에 의해서 아래와 같이 여러 개의 Thread 들이 생성 및 실행되며, 이는 UV_THREADPOOL_SIZE 환경 변수에 의해서 결정됩니다.

출처 : https://levelup.gitconnected.com/nodejs-runtime-environment-libuv-library-event-loop-thread-pool-5f5ecadc0318 일반적으로 fs 모듈에서 제공되는 여러 함수들이 Thread Pool 에서 실행됩니다.
그리고 Libuv Thread 에서 실행된 결과가 Event Loop 를 통해서 Main Thread 로 이동되는 구조입니다.
그래서 Libuv Thread Pool 에 의해서 비동기 처리 로직들이 동시 수행이 가능하지만,
처리된 결과가 Event Loop 을 통해서 Main Thread 에서 처리되기에 이 부분에서 병목이 발생할 수 있습니다.
UV_THREADPOOL_SIZE 와 처리 속도 관계.
Node.js 에서 제공되는 내장 비동기 함수들은 C 언어로 작성됩니다.
그리고 이 함수들은 내부적으로 Libuv 모듈의 Thread 에서 실행되도록 구성됩니다.
특히 File System 과 관련된 fs 모듈의 함수들이 이와 관련 깊습니다.
10mb 정도의 파일을 처리하는 소요되는 시간을 Thread Pool Size 와 관련하여 실험해보도록 합니다.
Thread Pool 사이즈와 벤치마킹.
아래의 명령어는 1개 / 10개의 Thread 로 10MB 의 파일을 50 회 처리하는데에 걸리는 시간을 출력합니다.
export UV_THREADPOOL_SIZE=1 && node benchmark.js export UV_THREADPOOL_SIZE=10 && node benchmark.jsUV_THREADPOOL_SIZE = 1 total-time: 457.736ms UV_THREADPOOL_SIZE = 10 total-time: 38.487ms소요되는 시간은 위의 결과와 같이 10배 정도 차이가 납니다.
리소스 환경에 따라서 결과를 다를 순 있겠지만, File System 과 같이 비동기 처리 함수들은 Thread Pool 갯수 설정에 영향을 받게 됩니다.
하지만 File System 이외에 사용할 수 있는 경우는 ?
Node.js 는 강력한 Network 통신은 Libuv Thread Pool 을 사용하지는 않고, Kernel 의 Epoll 과 관련된 시스템 콜을 사용합니다.
즉, TCP 소켓에 데이터가 추가된다면 이를 커널이 처리하고 Node.js Process 의 Event Loop 에게 넘겨주게 됩니다.
따라서 네트워크 통신에서는 Libuv Thread Pool 의 득을 볼 순 없습니다.
Node.js 에서 File System 과 관련된 처리를 적용하는 Application 은 드물기 때문에 Thread Pool 사이즈를 최적화하는 것보다
Multi Processing 구조로 여러 Node.js Process 를 스케일 아웃하는 방식이 적합해 보입니다.
관련된 명령어.
yes "Hello World" | head -c 10M > testfile.log cat <<EOF> benchmark.js const fs = require("fs"); const path = "./testfile.log"; const totalTasks = 50; let completed = 0; console.log("UV_THREADPOOL_SIZE =", process.env.UV_THREADPOOL_SIZE || "default (4)"); console.time("total-time"); for (let i = 0; i < totalTasks; i++) { fs.readFile(path, () => { completed++; if (completed === totalTasks) { console.timeEnd("total-time"); } }); } EOF export UV_THREADPOOL_SIZE=1 && node benchmark.js export UV_THREADPOOL_SIZE=10 && node benchmark.js반응형