프로젝트를 마치고, 면접 준비를 하다보니 Node.js가 싱글 쓰레드 인줄 알고 있었는데, Node도 여러 스레드를 가지고 있지만!, 자바스크립트 실행하는 스레드가 하나라 싱글 스레드라는 걸 알게되었습니다.
그래서 Node.js가 어떻게 돌아가는지 정확하게 집고넘어갈려고, 찾아보았습니다.
Node.js는 내장 라이브러리와 V8엔진 그리고 libuv로 구성됩니다. 논 블로킹 I/O 모델은 모두 이 libuv 라이브러리 구현됩니다.
각각 어떤 역할을 하는지 알아보겠습니다.
비동기 작업에 중요한 libuv 라이브러리입니다. libuv는 C++로 작성된, Node.js가 사용하는 비동기 I/O라이브러리로 비동기 작업이 어떤 커널이 지원하는지 확인합니다.
우리가 코드를 작성하고 코드를 실행한다면 Node.js내부적으로는 이러한 일이 일어납니다.
이 libuv 라이브러리에서 핵심은 uv_io와 이벤트 루프입니다. 자바스크립트 엔진이 아닌, 구동하는 환경에서 가지고 있는 장치입니다.
이벤트루프는 timers => pending callbacks => idle, prepare -> poll => check => close callbacks 순으로 호출됩니다.
각각의 단계는 자신만의 큐를 가지고있습니다. 우리가 등록한 작업 각각의 유형에 맞는 큐에 등록이됩니다. 이 큐가 실행되고 완료되면 다시 이벤트루프로 넘어와 실행됩니다.
각 단계에서 다음 단계로 넘어가는 것을 '틱(tick)'이라고 합니다.
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가 있다고 했습니다.
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)
libuv 에 파일 읽기와 같은 비동기 작업을 요청하면 커널이 지원하는지 확인합니다. 지원한다면 libuv가 대신 커널에게 비동기적으로 요청하지만, 지원하지않는다면 자신만의 워커 스레드가 담긴 스레드 풀을 사용합니다.
기본적으로 4개의 스레드를 가지는 스레드 풀을 생성합니다.
thread pool에서 처리되는 작업들은 다음과 같습니다.
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/