Node.js libuv

Vorhandenheit ·2022년 6월 23일
0

JS/Node 

목록 보기
46/63
post-thumbnail
post-custom-banner

Node.js libuv

프로젝트를 마치고, 면접 준비를 하다보니 Node.js가 싱글 쓰레드 인줄 알고 있었는데, Node도 여러 스레드를 가지고 있지만!, 자바스크립트 실행하는 스레드가 하나라 싱글 스레드라는 걸 알게되었습니다.
그래서 Node.js가 어떻게 돌아가는지 정확하게 집고넘어갈려고, 찾아보았습니다.

Node.js는 내장 라이브러리와 V8엔진 그리고 libuv로 구성됩니다. 논 블로킹 I/O 모델은 모두 이 libuv 라이브러리 구현됩니다.
각각 어떤 역할을 하는지 알아보겠습니다.

1. libuv

비동기 작업에 중요한 libuv 라이브러리입니다. libuv는 C++로 작성된, Node.js가 사용하는 비동기 I/O라이브러리로 비동기 작업이 어떤 커널이 지원하는지 확인합니다.

우리가 코드를 작성하고 코드를 실행한다면 Node.js내부적으로는 이러한 일이 일어납니다.

  1. js로 코드를 작성하고 실행하게 되면 스택에 코드가 쌓입니다.
  2. 이때 스택에 쌓인 코드를 실행하게 되면 libuv를 호출합니다.
  3. libuv는 비동기 처리를 할지 , 동기 처리를 할지 검사 후, 시스템 API를 이용하거나 쓰레드 풀에 생성된 쓰레드에게 작업을 위임합니다.
  4. 작업이 완료되면 콜백 함수를 테스크 큐에 넘겨줍니다.
  5. 이벤트 루프는 콜스택에 쌓여있는 함수가 없을 때, 테스크 큐에 대기하고 있던 콜백함수를 콜스택으로 넘겨줍니다.
  6. 콜스택에 쌓인 콜백 함수가 실행되고, 콜스택에서 제거됩니다

(1) 이벤트루프

이 libuv 라이브러리에서 핵심은 uv_io와 이벤트 루프입니다. 자바스크립트 엔진이 아닌, 구동하는 환경에서 가지고 있는 장치입니다.

이벤트루프는 timers => pending callbacks => idle, prepare -> poll => check => close callbacks 순으로 호출됩니다.
각각의 단계는 자신만의 큐를 가지고있습니다. 우리가 등록한 작업 각각의 유형에 맞는 큐에 등록이됩니다. 이 큐가 실행되고 완료되면 다시 이벤트루프로 넘어와 실행됩니다.

각 단계에서 다음 단계로 넘어가는 것을 '틱(tick)'이라고 합니다.

  • timers
    타이머에 관한 비동기 작업을 관리합니다. 예를 들어setTimeout , setInterval에 등록된 콜백을 관리합니다. 이 타이머 콜백은 min-heap 자료구조 기반으로 구성되어있습니다.

min-heap 자료구조
데이터를 완전 이진 트리 형태로 관리하면서 최댓값 또는 최솟값을 찾아내는 효율적 자료구조입니다. 최소힙은 최소값을 찾아내는데 최적화되어있습니다. 떄문에 이를 사용하면 실행할 수 있는 가장 이른 타이머를 손쉽게 찾을 수 있습니다.

  • pending callbacks
    이 단계는 , 이전 이벤트 루프 반복에서 수행되지 못했던 콜백들이 있습니다. 처리하지 못하고 넘어간 작업을을 쌓아놓고 실행하는 단계입니다.
    이벤트 루프에 pending_queue에 들어가있는 콜백들을 실행합니다. 에러 핸들러 콜백도 여기로 들어오게됩니다.

  • idle, prerare
    매틱마다 실행되며, node.js 내부 관리를 위해 사용한다고 합니다.

  • poll
    거의 모든 콜백이 여기에 해당됩니다. ( ex) 데이터베이스에 쿼리를 보낸 후 결과가 왔을 떄 실행되는 콜백, HTTP 요청을 보낸 후 응답이 왔을 때 실행되는 콜백, 파일을 비동기로 다시 읽고 다 읽었을 때 실행되는 콜백)
    여기서 watcher_queue를 이용하여 관리합니다. 해당 큐를 사용하는 이유는 비동기 작업이 완료되었을 경우, 순서를 보장하기 위해서입니다.
    watcher은 FD(File Descriptor)를 가지고 있습니다. 운영체제가 FD가 준비되었다고 알리면 event loop 이에 해당하는 watcher를 찾을 수 있고 콜백을 실행할 수 있습니다.

  • check
    setImmediate()로 등록된 콜백을 관리하기위한 단계입니다

  • close callbacks
    close, destroy 와 같은 이벤트 타입 콜백을 처리합니다.

이벤트 루프는 이 6단계를 라운드 로빈 방식으로 순회하며 동작합니다.

라운드 로빈
그룹 내에 있는 모든 요소를 합리적인 순서에 입각하여 뽑는 방식으로 , 리스트의 맨 위에서 아래로 가며 하나씩 뽑고 끝나면 다시 맨위로 돌아가는 식으로 진행됩니다. 일정 규칙에 따라 여러개의 페이즈들을 계속해서 순회합니다.
컴퓨터 운영에서, 컴퓨터 자원을 사용할 수 있는 기회를 프로그램 프로세스들에게 공정하게 부여하기 위한 한 방법으로서, 각 프로세스에 일정시간을 할당하고, 할당된 시간이 지나면 그 프로세스는 잠시 보류한 뒤 다른 프로세스에게 기회를 주고, 또 그 다음 프로세스에게 하는 식으로, 돌아가며 기회를 부여하는 운영방식이 있는데, 이를 흔히 라운드 로빈 프로세스 스케줄링이라고 부른다.

Queue

위에 각 단계마다 각각의 queue가 있다고 했습니다.

  • Expired timers and intervals queue : setTimeout, setInterval을 사용한 콜백

  • IO Events Queue : 완료된 I/O 이벤트

  • Immediates Queue : setImmediate 함수를 사용하여 추가된 콜백

  • Close Handlers Queue : 모든 close 이벤트 핸들러

  • nextTickQueue : Timer관련 비동기 콜백 함수들이 저장됩니다.

  • microTaskQueue : Promise등의 비동기 콜벡함수들이 저장됩니다.

진행순서

setTimeout(() => {
    console.log(1)
    process.nextTick(() => {
        console.log(3)
    })
    Promise.resolve().then(() => console.log(4))
}, 0)
setTimeout(() => {
    console.log(2)
}, 0)
  1. Node.js가 Timer Phase에 진입
  2. 우선 Timer Phase에 있는 큐를 확인하고 console.log(1) 실행
  3. process.nextTick과 Promise.resolve를 호출해 nextTickQueue와 microTaskQueue에 콜백을 등록
  4. 현재 실행하고 있는 작업이 끝났으므로 Node.js는 nextTickQueue와 microTaskQueue에 작업이 있음을 확인 -> Timer Phase의 큐를 확인하지 않고 우선순위가 높은 nextTickQueue 부터 확인
  5. console.log(3) 출력
  6. Node.js는 nextTickQueue가 비었음을 확인하고 우선순위가 낮은 microTaskQueue 확인
    console.log(4) 출력
  7. microTaskQueue가 비었음을 확인하고 다시 Node.js는 Timer Phase에 있는 큐를 확인하고 console.log(2) 실핼
  8. 현재 실행하고 있는 작업이 끝났으므로 Node.js는 nextTickQueue와 microTaskQueue에 작업이 있음을 확인 -> Timer Phase의 큐가 비었음을 확인하고 Pending Callbacks Phase로 이동

(2) Thread pool

libuv 에 파일 읽기와 같은 비동기 작업을 요청하면 커널이 지원하는지 확인합니다. 지원한다면 libuv가 대신 커널에게 비동기적으로 요청하지만, 지원하지않는다면 자신만의 워커 스레드가 담긴 스레드 풀을 사용합니다.
기본적으로 4개의 스레드를 가지는 스레드 풀을 생성합니다.

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 제외
  • ...

Single thread

node.js는 single thread라는 건, event loop가 single thread라는 걸 의미하며 여러개의 thread를 사용한다는 건 libuv thread pool을 의미합니다.

출처

https://www.korecmblog.com/node-js-event-loop/
https://darrengwon.tistory.com/953
https://so0choi.github.io/2020/09/15/Nodejs/Node-js13/
https://www.uniever.space/263ffd78-794c-45a7-8279-ba2120b69044
https://evan-moon.github.io/2019/08/01/nodejs-event-loop-workflow/

profile
읽고 기록하고 고민하고 사용하고 개발하자!
post-custom-banner

0개의 댓글