업데이트:

태그: , ,

카테고리:

개론

  • 이전에 클론코딩 몇번을 하면서 자연스럽게 JS와 친해졌고, NodeJS기반 백엔드 프레임워크를 다뤄볼 수 있었다.
  • 그런데, 그때는 왜 이 코드가 이렇게 동작하는지 전혀 이해가 가지 않았다.
  • 가령, 콜백함수라던지, async, await등의 비동기처리 함수들은 그냥 나중에 처리되는구나하고 말았었다.
  • 이번에 프로젝트 프레임워크로 nest, express를 사용하게 됐는데, 이를위해 NodeJS의 기본적인 동작방식에 대해 공부해보려한다.



Node.js란?

  • V8 엔진을 기반으로한 server-side platform
  • 쉽고, 빠르고, 확장성있는 네트워크 어플리케이션을 위한 JS runtime platform이다.
  • 특징으로는 event-driven, non-blocking I/O이 있다.
  • 그리고 다양한 JS 모듈 라이브러리를 제공해 웹 어플리케이션 개발을 쉽게 한다.



역사

  • NodeJS의 시스템 아키텍쳐는 Nginx의 아키텍쳐와 매우 닮아있다.
  • apache의 경우 예전에는 프로세스를 fork함으로써 request들을 처리했었다고한다.(이제는 MPM worker로 처리)
    • 지금은 apache는 쓰레드도 만들어서 처리하긴 하지만, multithread 웹서버는 병행적인 커넥션을 관리하는건 쉽지 않기에 apache가 고전하고 있는 이유.
  • Nginx또한 비동기, 이벤트기반, non-blocking 이다.



의존성

1. V8

  • 정의
    • V8은 c++로 작성된 구글의 오픈소스 고성능 JS 및 WebAssembly 엔진이다.
    • V8은 JS소스코드를 컴파일, 실행, 객체에 대한 메모리 할당 처리, 필요하지 않은 객체를 Garbage Collect한다.
    • 이 가비지 컬렉션은 V8 성능의 핵심 중 하나이다.


  • client side(browser)
    • JS는 브라우저의 DOM모델 객체를 조작하는 것과같은 작업에 사용되는 client-side 스크립팅에 사용된다.
      • 브라우저를 사용하는 유저 = client
      • client는 브라우저의 화면 상에서 버튼을 누르고, 값을 입력한다.
      • fdf, cub3d 등에서 사용한 mlx를 사용해보면 사용자의 입력이나 마우스위치 하나하나가 mlx의 X-window 내에서 감지되고, 이를 기반으로 브라우저가 이벤트를 감지한다는 것을 알 수 있다.
    • DOM은 일반적으로 브라우저가 V8에 전달한다.
    • V8을 사용하면 모든 C++ 프로그램의 기능, 개체를 JS 코드에 노출할 수 있다.
  • V8은 메모리와 관련한 작업을 할 뿐, 네트워크, 파일 작업등의 비동기 처리를 하진 않는다. 이는 libuv에서 한다.


2. libuv

소개

  • 비동기 I/O처리를 지원하는 라이브러리.
    • 커널을 추상화해서 Wrapping하고있음.
  • Node.js 뿐만아니라 Luvit, Julia, Neovim등에서 사용한다.
  • handle, stream을 통해 socket과 다른 개체들에 대한 높은 수준의 추상화 제공.
  • 네트워크와 파일시스템, 병행성 제어 또한 제공한다.
  • 노드의 특징인 이벤트 기반(event-driven), Non-Blocking 비동기I/O를 지원하는게 libuv이다.
  • 또, 쓰레드풀을 가지고 있는데, Node가 요청한 비동기 작업들이 여기에 할당된다.
  • ⭐️노드JS를 단일쓰레드 이벤트기반이라고 하지만, 싱글쓰레드만 사용하는 것은 아니다.
    • 비동기작업은 libuv의 쓰레드풀에서 처리하거나
    • 추가적인 워커쓰레드를 생성해 작업을 처리할 수 있기때문이다.
    • 싱글쓰레드라는 것은 프로그래머가 직접 제어할 수 있는 쓰레드가 코드를 실행하는 메인쓰레드 1개뿐이기 때문이다.


libuv와 커널간의 관계

  • 커널에서 어떤 비동기 작업을 지원해주는지 알고있기 때문에, 비동기작업을 발견하면 커널로 작업을 offload해버린다.
  • 이후, 커널에서 system call로 작업 종료를 알리면 이벤트루프에 callback을 등록한다.
  • 커널이 지원하지 않는 작업은 별도의 스레드에 작업을 offload한다.

스크린샷 2022-10-28 오후 8 54 41


  • 이 밖에도 http를 파싱하는 llhttp, 비동기 DNS요청을 위한 c-ares, https를 구성하는 tls, 암호화를 위한 crpyto를 제공하는 OpenSSL, 압축해제와 압축을 위한 zlip등이 있다.



event loop

  • node의 핵심
  • NodeJS는단일 쓰레드 기반이지만, event와 callback함수를 통해 concurrency를 지원한다.
  • NodeJS는 시작과 동시에 단일 쓰레드가 생성되고, 이 쓰레드에서 모든 코드를 처리한다.
  • 더 정확하게는, event loop의 단일 인스턴스가 생성되고, 1개 쓰레드에 배치된다.
  • 그렇다고해서 단일쓰레드만 사용하는건 아니다.
  • 이런 event loop를 제공하는게 libuv이다.
  • 자바스크립트를 공부하면서 이벤트 큐나 스택 등에 대해 들어봤다면, 그 개념은 매우 추상화된 개념이고, 아래가 더 정확하다.


event loop를 사용하는 이유

스크린샷 2022-11-02 오후 10 07 45

  • Node는 observer패턴을 사용해 이벤트 루프를 생성한다.
  • 이벤트루프는 단일스레드가 특정 시점에 수행해야하는 작업을 결정한다.
  • 메인스레드가 이벤트루프와 어플리케이션 코드를 실행한다.
  • 하지만 I/O, 네트워크, 암호화, 압축 등의 시간이 오래걸리는 작업은 운영체제의 커널 혹은 쓰레드풀에서 작업을 위임한다.
    • 이런 작업을 libuv가 처리한다.
  • Node.js의 모든 API는 비동기적이고, 단일쓰레드로 처리된다.
  • 즉, Node.js는 백그라운드에서 여러 스레드를 사용해 비동기 코드를 실행하는 단일 스레드 언어인 것이다


event loop 내부구조

스크린샷 2022-11-02 오후 10 07 14

  • 각 단계에는 실행할 콜백의 FIFO Queue가 있다.(실제 구현체는 큐가 아닐수도 있다.)
  • 그리고 이 큐에는 실행되어야하는 callback 함수가 담겨있다.
  • JS 실행은 이 이벤트루프의 임의의 단계에서 수행될 수 있다.
    • 이벤트 루프가 각 단계에 돌입할때마다 그에 맞는 처리를 한다.
  • tick : 각 단계의 큐가 비어있거나 해당 단계에서 수행할 수 있는 callback 제한에 달하면 다음 단계로 넘어가게된다.


각 phase의 역할

  1. Timer
    • setTimeout(), setInterval()로 스케쥴링한 콜백이 들어간다.
    • callback함수가 들어가는게 아닌, min-heap으로 관리되는 타이머가 들어가며, 이 시간이 지난 callback을 수행한다.


  1. Pending
    • 이전 loop에서 완료되지 않은 I/O callback들이 여기서 실행된다.
    • TCP handler에 쓰려고하고, 해당 작업이 완료되면 callback이 이 큐에 들어온다.
    • error callback도 여기로 들어온다.


  1. idle, prepare
    • 내부적으로만 사용된다.
    • idle(유휴)는 이름만 그렇고 매 tick마다 사용된다.
    • Prepare도 polling이 시작되기 전에 바로 수행된다.


  1. poll
    • eventloop 중 가장 중요하다.
    • 새로운 I/O 이벤트를 받거나, I/O관련 콜백을 실행한다.
    • socket 생성과 같은 새로운 연결과 데이터 등을 이 phase에서 받는다.
    • setImmediate(), timer관련 콜백, close callback을 제외한 모든 콜백들이 여기서 실행.
    • 필요하면 node는 여기서 block한다.
    • I/O 작업을 처리하는 큐를 poll이라고 하는데, 예외적으로 poll의 queue를 처리하는 동안에는 다른 Poll 이벤트가 쌓일 수 있다.
    • poll phase의 작업은 2가지로 나눌 수 있다.
      1. poll phase에 붙은 watch_queue에 뭔가 들어와있다면, 큐가 비거나, 시스템 한도까지 내부의 것들을 동기적으로 수행한다.
      2. 큐가 비워지면, Node는 새로운 connection을 기다리는데, 이 기다리는 시간은 다양한 요인에 의해 결정된다.


  1. check
    • setImmediate() callback을 위한 phase
    • 왜 한개의 callback만을 위한 phase가 있는지 이해하려면, poll phase의 작업때문이고, workflow를 이해해야한다.


  1. close
    • socket.on('close', ()=>{})와 같은 callback들이 여기서 다뤄진다.


  • nextTickQueue, microTaskQueue
    • event loop(libuv)가 아닌, nodejs에 구현되어있다.
    • 루프의 일부분은 아니지만, 여기 담긴 callback들은 event loop의 어느 phase에서든지 수행될 수 있다.
    • 가장 높은 우선순위를 가진다.
      • c/c++과 JS의 경계를 넘나들때마다 가능한한 빨리 처리된다.
    • nextTickQueue는 process.nextTick() api를 사용해 호출한 callback을 들고있다.
    • microTaskQueue는 해결된 promise들을 들고있다.



댓글남기기