Node.js의 이벤트 루프와 libuv 이해하기

화솔·2023년 11월 22일
1
post-thumbnail
post-custom-banner

주로 Node.js 환경에서 개발하면서, 문득 이벤트 루프와 libuv에 대해 개념 정리가 하고싶어 공부 겸 글을 작성하게 되었다.

싱글 스레드? 논 블로킹 I/O??


Node.js 환경에서 개발을 해본 개발자라면 이 내용들은 숙지를 하고 있을 것이다.

  • 싱글 스레드
  • 이벤트 루프
  • 논 블로킹 I/O

하지만 대부분이 알고있는 싱글 스레드는 하나의 작업이 끝낼 때 까지 기다려야해 동시에 여러 작업을 수행하지 못할 것으로 알려져 있다. 어떻게 Node.js는 하나의 스레드로 여러 비동기 작업들을 블로킹 없이 수행할 수 있는 것일까?

정답은 이벤트 루프(libuv)에


먼저 Node.js의 구성 요소를 파악해야 하는데, Node.js는 런타임 언어이고, 크게 내장 라이브러리, V8 엔진, 이벤트 루프(libuv)로 구성되어 있다.

이 구성 요소 중 Node.js가 하나의 스레드로 여러 작업을 블로킹 없이 수행할 수 있게 하는 핵심 요소는 libuv 라이브러리 덕분이다.

libuv?


C언어 기반의 비동기 I/O 라이브러리.

libuv는 비동기 I/O 작업, 이벤트 처리, 동시성 및 기타 시스템 관련 기능에 대한 크로스 플랫폼 지원을 제공하는 라이브러리이다.

운영체제들의 커널(운영체제의 핵심부로 컴퓨터 자원들을 관리하는 역할)을 추상화해 다양한 운영 체제에서 일관되게 작동한다.

Node.js 에서의 libuv


자바스크립트 엔진은 내부적으로 비동기 처리를 할 수 없다. 때문에 비동기로 처리되는 코드를 만날 경우 libuv 라이브러리를 통해 비동기 작업을 처리하게 된다.

libuv에 비동기 작업을 요청하면 libuv는 uv_io 를 통해 커널이 지원하는지 확인하고, 지원한다면 커널에 요청해 받은 응답을 전달한다.

libuv의 스레드 관련 내용은 더 아래에서 다뤄보겠다.

이벤트 루프


libuv는 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식을 따르고 있다. (이벤트 기반 비동기 처리)

만약 여러 이벤트가 동시에 발생했을 때 어떤 순서로 콜백 함수를 호출할지를 이벤트 루프가 판단해 실행하게 된다.

  • Timer
  • Pending I/O Callbacks
  • Idle, Prepare
  • Poll
  • Check
  • Close Callbacks

목록의 순서대로 호출되며, 각각의 단계는 자신만의 큐를 가지고 있다.
작업을 등록하면 유형에 맞게 각각 큐에 등록이 된다.

(1) Phase별 동작 이해

먼저 페이즈에서 페이즈로 넘어가는걸 Tick 이라고 정의한다.

Timer

루프의 시작을 알리는 페이즈.
주로 setTimeout, setInterval에 등록된 콜백을 관리한다. 여기서는 타이머에 등록된 콜백이 언제 실행될지만 관리하고 실행은 Poll 단계에서 실행된다.

Timer 콜백은 min-heap 자료구조 기반으로 작성되어있다.

min-heap
완전이진트리(complete binary tree)의 일종으로, 최솟값을 빠르게 찾아내기 위한 자료구조. 이를 통해 이벤트 루프가 가장 짧은 지연으로 타이머를 효율적으로 검색할 수 있도록 보장받는다.

Pending Callbacks

다음 페이즈를 실행하기 위해 연기된 콜백을 실행하는 페이즈.

각 페이즈는 모든 작업을 실행하지 않고 일정량만 수행 후 다음 페이즈로 넘어가기 때문에 처리하지 못한 작업(pending_queue)을 수행한다.

Idle, Prepare

이 페이즈는 매 틱마다 수행된다. (Node.js 의 내부 관리를 위해 사용)

Poll

setTimeout, setInterval, setImmediate 로 등록한 콜백을 제외한 모든 콜백이 이 페이즈에서 수행된다.

해당 큐가 비어있지 않다면 작업을 순차적으로 처리하고, 비어있다면 다음 페이즈로 즉시 넘어가지 않고 일정시간 대기한다.

대기하는 시간은 Timer 페이즈에서 실행한 작업이 있는지 검증 후 넘어간다.

해당 페이즈가 검사하는 큐 이름은 watcher_queue 이며,
비동기 작업이 완료되었을 때 순서를 보장하기 위해 사용한다.

Check

setImmediate 로 등록된 콜백을 관리하기 위한 페이즈이다.

Close Callbacks

socket.on('close', cb)

코드와 같이 close, destory 이벤트 타입 콜백을 처리하는 단계이다.

(2) Thread Pool

작업

위에서 말했다싶이 libuv 에 파일 읽기와 같은 비동기 작업을 요청하면 uv_io 를 통해 커널이 지원하는지 확인한다.

만약 지원한다면 libuv가 uv_io를 통해 커널에게 비동기적으로 요청하지만, 아니라면 자신만의 워커 스레드가 담긴 스레드 풀을 사용한다.

Thread Pool에서 처리되는 작업들은 다음과 같다.

file system : fs.FSWatcher()와 synchronous fs 제외
DNS : dns.lookup(), dns.lookupService()
Crypto : crypto.pbkdf2(), crypto.scrypt(), crypto.randomBytes(), crypto.randomFill(),
Zlib : synchronous API 제외

설정

libuv는 기본적으로 4개의 스레드를 가지는 스레드풀을 생성하지만, 환경변수를 통해 스레드를 128개까지 늘릴 수 있다.

참고자료


https://blog.naver.com/pjt3591oo/221976414901
https://www.korecmblog.com/blog/node-js-event-loop#nodejs-event-loop
https://sjh836.tistory.com/149
https://velog.io/@tastestar/Node.js-libuv

post-custom-banner

0개의 댓글