Node.JS의 이벤트루프 코드레벨에서 확인하기(1)

Myukang·2023년 8월 8일
0

NodeJS

목록 보기
1/1

Node.JS의 의존성

NodeJS는 JS를 구동하는 런타임입니다.
그래서 JS를 구동하기위해 다양한 의존성 모듈을 가지고있는데,

  • 메모리 할당 및 가비지 컬렉팅을 하는 V8
  • 비동기 I/O처리를 하는 libuv
  • http를 파싱하는 llhttp
  • 비동기 DNS요청을 위한 c-ares
  • https를 구성하는 tls
  • 암호화를 위한 crpyto를 제공하는 OpenSSL
  • 압축해제와 압축을 위한 zlip

등이 있습니다.

이번에는 내부 이벤트루프를 담당하는 libuv를 확인해보고자합니다.



libuv

소개

  • libuv는 커널을 추상화해서 wrapping하는 구조입니다.

  • handle/stream 등의 개념으로 socket등의 개체를 추상화해서 사용할 수 있습니다.

  • 네트워크/파일시스템/concurrency control도 제공합니다.

  • Node.JS의 특징인 이벤트기반/Non-Blocking 비동기 I/O를 지원하는게 libuv입니다.

  • 자신만의 쓰레드풀을 가지고있어 Node가 요청한 비동기 작업들이 여기서 처리됩니다.

  • kqueue, epoll, iocp 등 kernel event notification mechanism을 사용해 시간이 오래걸리는 작업들을 Non-Blocking으로 처리하며, 이 작업들을 커널에 위임합니다.
    - 운영체제별로 사용할 수 있는 것이 달라서, libuv는 3가지 모두를 구현해 플랫폼에 영향받지 않습니다.

  • 커널에서는 이러한 작업들이 종료되면 이벤트루프에 callback을 등록합니다.

  • 커널이 Non-Blocking을 지원하지 않는 작업은 스레드풀에 작업을 offload합니다.



libuv의 이벤트루프

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

  • 위 그림에서 `nextTickQueue, microTaskQueue는 V8에 구현되어있습니다.
  • promise, async-await, process.nexttick이 담기는 microtask queue는 libuv의 이벤트루프 구성요소가 아닙니다.
  • libuv의 이벤트루프는 macrotask queue + 비동기 I/O작업을 처리하는 루프입니다.

순차적 설명

  1. Node로 스크립트를 실행(node script.js)
  2. V8엔진의 Ignition을 이용해 사용자의 script를 bytecode로 변환
  3. TurboFan으로 최적화
  4. V8 runtime에서 bytecode를 수행, 그리고 Node runtime이 I/O작업, 연산을 수행한다.
  5. 이때 발생하는 I/O, 비동기 작업은 NodeJS runtime이 libuv를 사용해 작업을 offload한다.
    • libuv 공식문서에 보면, libuv가 운영체제의 이벤트 수집, 모니터링을 하기때문에 사용자는 이벤트가 발생할때 호출할 콜백을 등록하기만 하면된다.
  6. libuv는 해당 작업이 종료되면 이벤트 큐에 등록한다.
  7. Node runtime의 수행이 종료되면 이때부터 event loop가 활성화된다.
    • console.log가 어느위치에 있든 setTimeout보다 일찍 수행되는 이유가 이 이유.
    • setTimeout, setInterval등의 time 관련한 작업은 I/O작업처럼 운영체제와 밀접한 관련이 있기에 libuv에서 관리하도록하는 것이다.
    • 그리고 libuv에 콜백함수는 직접 전달되는게 아니라 비동기작업의 파라미터로 전달된다.
    • 그리고 이 콜백함수들은 libuv에서 실행되는 것이 아니며, 해당 콜백함수는 Node runtime에서 V8로 execute한다.



이벤트루프 코드레벨에서 확인하기

  • 루프도 결국 하나의 while루프에 불과합니다.
  • libuv/src/unix/core.c의 384line에 구현되어있습니다.
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int can_sleep;

  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
    //타이머페이즈
    uv__update_time(loop);
    uv__run_timers(loop);

    can_sleep =
        QUEUE_EMPTY(&loop->pending_queue) && QUEUE_EMPTY(&loop->idle_handles);

	//펜딩페이즈
    uv__run_pending(loop);
    //Idle페이즈
    uv__run_idle(loop);
    //Prepare페이즈
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)
      timeout = uv__backend_timeout(loop);

	//Poll페이즈
    uv__io_poll(loop, timeout);

    /* Process immediate callbacks (e.g. write_cb) a small fixed number of
     * times to avoid loop starvation.*/
    for (r = 0; r < 8 && !QUEUE_EMPTY(&loop->pending_queue); r++)
      uv__run_pending(loop);

    /* Run one final update on the provider_idle_time in case uv__io_poll
     * returned because the timeout expired, but no events were received. This
     * call will be ignored if the provider_entry_time was either never set (if
     * the timeout == 0) or was already updated b/c an event was received.
     */
    uv__metrics_update_idle_time(loop);

    //Check페이즈
    uv__run_check(loop);
    //Close페이즈
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      /* UV_RUN_ONCE implies forward progress: at least one callback must have
       * been invoked when it returns. uv__io_poll() can return without doing
       * I/O (meaning: no callbacks) when its timeout expires - which means we
       * have pending timers that satisfy the forward progress constraint.
       *
       * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
       * the check.
       */
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids
   * dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

다음 글에서 이 루프의 구성요소를 코드레벨에서 하나씩 확인해보도록 하겠습니다.

profile
안녕하세요! 반갑습니다

0개의 댓글