Link Search Menu Expand Document

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 : readFile vs readFileSync **
    • readFile : 파일을 읽어들이는 동안 대기하지 않고 다음 작업을 수행한 후 callback을 수행한다. ⇒ non-blocking
    • readFileSync : 파일 읽기가 끝날때 까지 기다린 후 다음 작업을 실행한다. ⇒ 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 명령어로 실행시 isMaster cluster가 실행되며, fork()를 통해서 isWorker cluster를 만들 수 있다. 일반적으로 다음과 같이 구현하여 사용한다.
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/