Node.js
⇒ 자바스크립트를 브라우저 밖에서도 실행할 수 있도록 하는 자바스크립트 런타임
비동기 이벤트 주도 javascript runtime, Node.js는 확장성있는 네트워크 애플리케이션을 만들 수 있도록 설계
배경 지식
- Process ⇒ 메모리에 올라와 실행되고 있는 프로그램의 인스턴스
- Thread ⇒ 프로세스 내에서 할당받은 실행 단위,
- 스레드는 프로세스 내의 메모리 공간을 공유하게 된다.
Thread and Blocking
- Single Thread + blocking
- 프로세스 내에서 하나의 스레드가 하나의 요청만을 수행하게 된다.
- 특정 요청이 진행중이면 다른 요청은은 병렬적으로 수행할 수 없다.
- Multi Thread + blocking
- 스레드 풀에서 실행의 요청만큼 스레드에 배정하여 작업을 수행한다.
- 병렬적으로 처리하기 때문에 작은 작업들을 한번에 처리하기 좋아보이지만
- 효율성에서 큰 단점이 있다.
- 동시 request가 과하게 많아지면 많은 스레드가 필요해서 서버 과부하가 발생하고
- 반면, 요청이 배정된 스레드보다 적다면 유휴 스레드가 생겨난다.
Single Thread, Non Bloking ⇒ Node.js
- Node.js는 Single Thread, Non Blocking
- Async I/O 을 + Non-Blocking으로 동작이가능하다.
- Async란 작업순서를 보장하지 않는것을 의미하며 Non-Blocking은 실행흐름을 막지 않는 것을 의미한다. Async와 Non-Blocking은 완전히 일치하는 개념은 아니지만 일단은 넘어가자.
- IO 작업을 요청 한 후, 결과를 기다리지 않고 다른 작업들을 수행한다. 이를 통하여, Multi-Thread의 단점인 cpu core가 놀고 있는 상황이 거의 없어진다.
- 따라서, 대용량 데이터 처리, 스트리밍 작업, 즉 IO에 집중된 작업에서는 Multi-Thread 에 비하여 높은 성능을 보인다.
- 반면, Non-Blocking은 IO작업에 한정되기 때문에, IO작업이 아닌 CPU연산이 필요한 작업이라면 Blocking이 발생하게 된다.
- Multi-Thread의 경우에는 특정 Thread가 Blocking 작업을 처리하는 동안, 다른 Thread에서 다른 작업을 처리하면 되는 반면, Single Thread인 Node에서는 프로그램 자체가 Blocking된다.
How to async IO processing
- Enent Loop ⇒ Node.js에서는 V8의 런타임 스펙에 포함되어있다. 브라우저에서는 각 브라우저 별 자체 엔진에 포함되어있다.
- JS는 Memory Heap + Call Stack으로 동작한다.
- Memory Heap에는 일반적으로 변수값들을 저장하는 공간이고, Call Stack에는 실행순서를 관리한다.
- JS는 동기적으로 코드를 실행시키고, 비동기 작업(IO)들은 작업 Callback Queue에 넣어둔다.
- 이때 Callback Queue에는 실행 비동기 작업 뿐만아니라, 이후 동작할 Callback 함수를 넣어준다. 그래서 Callback Queue.
- Callback Queue에서 작업이 완료되면 JS의 Call Stack에 Callback을 등록시키고 Js는 Call Stack을 처리해 나간다.
I/O 작업?
- 그래서 IO 작업이 뭐길래
- 일반적인 IO작업이란 연산에 필요한 데이터를 저장장치 ⇒ CPU로 이동시키는 작업
- IO작업은 폰노이만 구조에서 어쩔수 없이 시간이 걸리는 작업이다.
- CPU연산이 아닌 하드웨어적 처리가 필요한 작업이다. 레지스터, 메모리의 영역
- Cpu의 연산에 필요한 Thread가 아닌 IO burst에서 별도로 처리된다.
- Node.js에서 IO들은 운영체제의 커널 , 혹은LIbuv의 Thread Pool에서 담당한다.
- Libuv는 OS에서 처리할 작업과, 그렇지 않은 작업들을 Thread Pool로 분기한다.
- 커널, Thread Pool에서 작업이 완료되면 이벤트 루프에 Callback을 등록한다.
- IO에서도 blocking이 필수적인 경우가 있음(filesystem)⇒ libuv는 스레드풀을 사용, 즉 멀티스레드.
Node.js and Streaming
- 영상 스트리밍 이라하면 일반적으로 대용량 파일을 다루는 경우가 많음.(neflix)
- 일반적인 경우 스트리밍 서비스는 단순 IO 작업
- Blocking 환경의 경우에는 영상이 서버에 모두 올라가서 전달이 완료될 때까지 스레드를 점유하기 때문에 적합하지 않음.
- Non-Blocking 환경에서는 파일이 큰것과는 별개로 영상 스트리밍 서비스 자체는 cpu연산이 크게 필요하지 않은 단순 IO 작업이기 때문에 NODE.js에서 큰 효율.
- 또한 node에서는 스트림을 기본으로 지원하기 때문에 ,큰 영상파일을 작은단위의 스트림, 작은 파일 단위의 이벤트로 처리할 수 있다.
NODE.js and Encoding?
- 반면, 이미지 인코딩 같은 경우에는 cpu연산이 매우 많이 필요한 작업.
- IO작업들은 비동기로 처리가 가능하지만, 파일 인코딩과 같은 작업은 고성능 cpu연산이 필요한 작업은 그렇지 않다.
- 때문에 파일 인코딩이 끝날때 까지 서버가 멈춰버리는(Event loop) 현상이 발생한다.
NODE.js : what to do
- Node.js는 오직 IO에 집중된 작업을 처리해야한다.
- 암호화,복호화 같은 복잡한 연산은 NODE에 장점이 없고, eventloop가 멈추는 현상을 발생시킨다.
- 만약 위와같은 작업이 필요하다면, 다른 언어로 작성되어 비동기적인 처리가 가능한 방식으로 사용해야한다.
fs:readFilevsreadFileSync**readFile: 파일을 읽어들이는 동안 대기하지 않고 다음 작업을 수행한 후 callback을 수행한다. ⇒ non-blockingreadFileSync: 파일 읽기가 끝날때 까지 기다린 후 다음 작업을 실행한다. ⇒ blocking- 특별한 경우가 아니라면 어떤걸 써야하는지 알 수 있다.
Multi Core Node.js
- IO에 집중된 프로그램을 구성하면, CPU가 IO에 blocking되는 일은 없다.
- 비동기 IO에서의 장점은 Single Core에서의 효율이며, 다른 코어에 작업을 분산 할 수없다.
- 멀티코어 시대에서 Single Core로는 일반적인 서비스 로드를 받아 낼 수 없기에 일반적으로 아래 두 가지 선택지를 고민하게 된다.
- Multiprocessing vs Multithreading
- 당연하게도 Single Thread가 핵심인 Node는 MultiThread 방식을 택할 수 없다.
Multiprocessing Single-Thread Program
- 멀티프로세싱은 싱글스레드 프로그램들을 통해서도 달성이 가능하다.
- 리눅스 계열 운영체제에서는 fork라는 API를 통해 자신과 동일한 프로세스를 생성할 수 있다.
- 머신의 코어 개수와 동일한 개수의 프로세스를 fork해서 작업을 수행한다면 머신의 모든 코어들은 거의 대부분의 시간을 해당 코드를 실행하는데 할애할 것이다.
- 머신의 코어 개수보다 적은 수를 fork한다면 남은 코어들은 OS의 다른 작업들을 위해 사용될 것이고, 더 많은 수를 fork한다면 같은 코어에서 같은 작엄을 위해 Context Switching이 발생하면서 비효율적인 멀티프로세싱이 될 것이다.
Stateless Server
- 이러한 방식으로 여러 프로세스를 복제해 실행할 때도 주의해야 할 점은서버의 상태가 프로세스간에 공유되지 않는다는 것이다.
- 서로 다른 프로세스는 서로 다른 메모리 영역을 할당받고 서로의 영역에 간섭할 수 없다. 따라서 지역 변수 또는 객체로서 선언된 값들은 같은 입력에 대해서 항상 같은 결과를 보장하지 않는다.
let a = 0;
app.get('/', (req, res) => {
a += 1;
res.send(a);
});
- 두 개의 fork된 프로세스가 같은 입력을 핸들하는 상황이라면,
a의 값은 두 프로세스에서 다를 것이다. 만약 프로세스 0이 해당 API에 대해서 4번 응답하고 그 후 프로세스 1이 1번 응답했다면, 사용자는 다섯 번째 요청에 대해서 1이라는 이상한 답을 받을 것이다. - 따라서 이러한 방식으로 작성되는 서버는 stateless해야 한다. 즉, mutable한 변수 등 사용자의 입력 외의 상태를 가져서는 안된다.
- NodeJS 프레임워크들은 콜백 패턴과 함수형 프로그래밍을 권장하기 때문에 let 대신 const를 쓰고 메모리 누수에 주의하는 등의 몇 가지 규칙만 엄수한다면 서버를 stateless하게 작성하는 것은 어렵지 않다.
- 만약 사용자의 입력에 따라 일관적이고 영구적인 기록을 남겨야 한다면, 별도의 DB를 두자. 만약 세션 등의 중요하진 않지만 임시로 들고 있어야 할 데이터를 처리해야 한다면, 지역변수 대신 Redis 등의 인메모리 DB를 활용하자.
- DB와의 커뮤니케이션은 아주 일반적인 IO 작업의 일종이고, NodeJS가 특화된 분야이기도 하다.
Cluster
- Node.js에서는 cluster api를 사용하여 프로세스를 fork 할 수 있다. 초기 node 명령어로 실행시
isMastercluster가 실행되며, fork()를 통해서isWorkercluster를 만들 수 있다. 일반적으로 다음과 같이 구현하여 사용한다.
const cluster = require("cluster");
const http = require("http");
const numCPUs = require("os").cpus().length;
if (cluster.isMaster) { // 마스터 프로세스 식별
console.log(`마스터 프로세스 아이디: ${process.pid}`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork(); // 코어 갯수만큼 워커 프로세스 생성
}
cluster.on("exit", (worker, code, signal) => {
console.log(`${worker.process.pid}가 종료됐습니다.`);
cluster.fork();
});
} else {
http
.createServer((req, res) => {
res.write("<h1>hello node.js</h1>");
res.end("<p>hello! cluster</p>");
setTimeout(() => {
process.exit();
}, 1000);
})
.listen(8080);
console.log(`${process.pid}번 워커 실행`);
}
참고
- https://engineering.linecorp.com/ko/blog/pm2-nodejs/
- https://medium.com/@vdongbin/node-js-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC-single-thread-event-driven-non-blocking-i-o-event-loop-ce97e58a8e21
- https://blog.appleseed.dev/post/nodejs-non-blocking-io-and-multicore-processing/