February 28th 2023
Contents
Event Loop
Node JS가 내부적으로 어떻게 작동하는지 간단히 먼저 살펴보고, 이러한 이해를 기반으로 조금 더 나은 성능의 코드를 작성할 수 있을지에 대해 고민해 보도록 하겠습니다.
먼저 Node의 기반이 되는 두 프로젝트가 있는데, V8과 libuv 프로젝트 입니다. V8은 웹브라우저 밖에서 자바스크립트 코드가 작동 가능 하도록 해 주며, libuv는 C++ 오픈 소스로 노드가 파일시스템이나, 네트워킹과 같은 OS에 접근할 수 있도록 해 줍니다. 노드의 HTTP나 fs와 같은 API를 사용하면, 내부적으로 libuv의 기능을 이용하게 됩니다.
V8엔진이나 libuv는 자바스크립트 코드가 아니기 때문에 노드가 중간 다리 역할을 해 준다고 생각해 볼 수 있습니다. 그러면 Node 내부에서 위 과정이 어떻게 진행되는지 살펴 보며 Node 내부 동작에 대해 이해를 높여 보도록 할게요.
노드 Crypto 모듈 내 pbkdf2란 함수가 있으며, 패스워드를 Hashing 하는데 사용 돼요. Nodejs의 소스코드 폴더 내 lib 폴더와 src 폴더가 있습니다. lib 폴더는 우리가 Node 프로젝트에서 사용하는 Javascript 모듈을 포함하고 있고, src 폴더는 C++로 구현된 코드가 포함 돼 있어요. 우리가 실제 사용하는 pbkdf2 함수는 node/lib/internal/crypto/pbkdf2.js
경로에서 확인해 볼 수 있어요.
function pbkdf2(password, salt, iterations, keylen, digest, callback) {
// 생략..
// ...
handleError(_pbkdf2(keybuf, password, salt, iterations, digest, wrap), digest);
}
해당 함수 마자막에 _pbkdf2
란 함수를 볼 수 있어요.
// https://github.com/nodejs/node/blob/636c0bb419907188308c01992e546ab1628bc556/lib/internal/crypto/pbkdf2.js#L5
const { pbkdf2: _pbkdf2 } = internalBinding('crypto');
파일 맨 위에서 이 함수가 선언된 부분을 보면 아래와 같이 돼 있어요. 우리가 작업 시 입력한 데이터를 위 함수에 넘겨주고, 실제 hash는 이 함수를 통해 생성이 되는데, 이 함수는 C++로 된 함수에요.
프로세스 쓰레드 nginx
현재 실행되고 있는 프로그램 인스턴스를 프로세스라고 합니다. 스레드는 CPU가 연산할 작업의 기본 단위인데, 하나의 프로세스는 여러개의 스레드를 가질 수 있습니다.
스레드 관련해 스케줄링 개념을 아는 것이 중요합니다. 컴퓨터의 자원은 한정돼 있고, CPU는 한 번에 처리할 수 있는 프로세스가 한정돼 있습니다. 많은 프로세스가 실행중일때는 스케줄링이 더 중요한데, 운영체제는 가능한 지연시간이 생기지 않으며 작업이 진행될 수 있도록 관장합니다.
마우스 커서 이동과 같은 작업을 담당하는 스레드가 있다고 가정하면, 이 스레드 처리가 지연된다면 유저 경험에 좋지 않을 것입니다. 그래서 빨리 처리해야 할 작업을 처리하기 위한 여러 방법이 있겠지만, nodejs와 관련있는 두 가지에 대해서 알아보겠습니다.
첫번째로, CPU 코어를 늘리는 방법입니다. CPU 코어가 늘면 한 번에 처리 가능한 스레드도 많아지기 때문입니다.
두번째로, 하드디스크에서 파일을 읽는 등의 I/O 작업으로 Overtime 발생 시 다른 작업이 이뤄지도록 하는 방법입니다.
두 번째 방법이 node의 이벤트 루프 환경과 관련이 있는데, 위 스레드 및 스케줄링를 연계해 노드의 이벤트 루프에 대해 알아보고자 합니다.
흔히 Node.js를 싱글 스레드 논 블로킹이라고 합니다. Node.js는 하나의 스레드로 동작하지만 I/O 작업이 발생한 경우 이를 비동기적으로 처리할 수 있습니다. 분명 하나의 스레드는 하나의 실행 흐름만을 가지고 있고 파일 읽기와 같이 기다려야 하는 작업을 실행하면 그 작업이 끝나기 전에는 아무것도 할 수 없어야만 합니다.
그러나 Node.js는 하나의 스레드만으로 여러 비동기 작업들을 블로킹 없이 수행할 수 있습니다. Event Loop는 특정 작업을 가능한 경우 시스템 커널에 위임함으로써 이를 가능하게 합니다. OS에서 작업이 완료되면 Node.js에 이를 알리고 적절한 Callback이 poll queue에 추가되도록 합니다.
Node.js의 성능과 관련된 여러 부분들이 Event Loop과 연관 돼 있기 때문에 이에 대한 이해를 높이는 것이 중요합니다. 아래 그림은 Node.js 공식 문서에 포함된 Event Loop 다이아그램 입니다.
Event Loop의 각 단계는 위와 같은 phase로 나누어 지며, 각 phase에는 callback queue가 있습니다. 이벤트 루프에서 각 단계 진입 시 해당 phase에 특정된 작업을 진행하고, callback queue에 담긴 함수를 호출합니다.
timers
pending callbacks
idle, prepare:
poll
check
close callbacks
이벤트 루프가 Poll phase에 진입 후 예정된 timer가 없다면, 아래와 같은 작업이 진행됩니다.
더이상 Poll 큐에 추가된 callback이 없다면, 이벤트루프는 timer에서 실행 가능한 콜백함수가 있는지 확인합니다. 실행 가능한 콜백이 있다면 이벤트루프는 timers 단계로 이동합니다.
Node.js는 싱글스레드 비동기 논블락킹 자바스크립트 런타임입니다. 하지만, 멀티스레드를 사용하는 케이스도 있습니다. Nodejs에는 두 가지 타입의 스레드가 존재합니다. 하나는 Event Loop가 동작하는 메인스레드이고, 다른 하나는 Threadpool이 있습니다. Node.js가 언제 싱글스레드이고 언제 멀티스레드를 이용하는지 알고, 프로그래밍 시 우리의 앱에 어떻게 영향을 줄 수 있는지 인지하는 것이 중요합니다.
Node.js 이벤트 루프는 Single Thread 입니다. 아래 예시 서버의 (_, res) => {...}
는 네트워크 요청에 대한 콜백이며, 이는 노드의 메인 스레드를 사용합니다.
import http from "http";
const server = http.createServer(
(_, res) => {
console.log("Request received");
while(1);
res.end("hello\b\n")
}
);
server.listen(8080, () => {
console.log("listening on 8080");
});
서버 실행 후 복수의 요청을 보내면, 첫 요청은 nodejs 서버 콜백에 닿지만 이후의 요청은 TCP Connection은 이루어 지지만 서버의 콜백에 닿지는 않습니다.
따라서, 메인스레드에서 동작하는 코드에 헤비한 연삭 작업이 이루어지지 않도록 주의를 해야 합니다. 또는, 노드의 클러스터를 사용해 볼 수 있습니다.
Node.js에서 멀티스레드는 I/O Intensive, CPU Intensive 작업 시 사용합니다.
Node.js에서 Blocking이란 메인스레드에서의 콜백이나 스레드풀의 워커에서 오래걸리는 작업을 말합니다. 그리고, 일반적으로 이는 non-javascript 작업이 완료되기를 기다리는 것을 의미합니다. Blocking 요소가 있다면 Node.js의 Event Loop은 해당 Blocking 작업이 완료되기까지 기다립니다. libuv의 동기 메소드가 일반적으로 Blocking 요소에 해당됩니다.
Event Loop
마이크로테스크 이벤트 룹, nextTick은 다른 콜백함수보다 먼저 실행 process.nextTick => promise.resolve()
path resolve path join 상대결로 절대경로
운영중인 서버에서 프로세스가 종료되었을 때 재시작하는 방법
cluster
예기치 못한 에어로 서버가 종료되는 현상을 방지할 수 있어 클러스터링을 적용대 두는 것이 좋음. 물론 에러 자체를 해결해야 함. 실무에서는 pm2 등의 모듈로 cluster 기능을 사용함.