[NODEJS] Nodejs 내부구조에 대해(1)
개론
- 이전에 클론코딩 몇번을 하면서 자연스럽게 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 코드에 노출할 수 있다.
- 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한다.
- 이 밖에도 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를 사용하는 이유
- Node는
observer패턴을 사용해 이벤트 루프를 생성
한다. - 이벤트루프는
단일스레드가 특정 시점에 수행해야하는 작업을 결정
한다. - 메인스레드가 이벤트루프와 어플리케이션 코드를 실행한다.
- 하지만 I/O, 네트워크, 암호화, 압축 등의
시간이 오래걸리는 작업은 운영체제의 커널 혹은 쓰레드풀에서 작업을 위임
한다.- 이런 작업을
libuv가 처리
한다.
- 이런 작업을
- Node.js의 모든 API는 비동기적이고, 단일쓰레드로 처리된다.
즉, Node.js는 백그라운드에서 여러 스레드를 사용해 비동기 코드를 실행하는 단일 스레드 언어인 것이다
event loop 내부구조
- 각 단계에는 실행할 콜백의 FIFO Queue가 있다.(실제 구현체는 큐가 아닐수도 있다.)
- 그리고 이 큐에는 실행되어야하는 callback 함수가 담겨있다.
- JS 실행은 이 이벤트루프의 임의의 단계에서 수행될 수 있다.
- 이벤트 루프가 각 단계에 돌입할때마다 그에 맞는 처리를 한다.
tick
:각 단계의 큐가 비어있거나 해당 단계에서 수행할 수 있는 callback 제한에 달하면
다음 단계로 넘어가게된다.
각 phase의 역할
Timer
setTimeout(), setInterval()
로 스케쥴링한 콜백이 들어간다.- callback함수가 들어가는게 아닌, min-heap으로 관리되는
타이머
가 들어가며, 이 시간이 지난 callback을 수행한다.
Pending
- 이전 loop에서 완료되지 않은 I/O callback들이 여기서 실행된다.
- TCP handler에 쓰려고하고, 해당 작업이 완료되면 callback이 이 큐에 들어온다.
- error callback도 여기로 들어온다.
idle, prepare
- 내부적으로만 사용된다.
- idle(유휴)는 이름만 그렇고 매 tick마다 사용된다.
- Prepare도 polling이 시작되기 전에 바로 수행된다.
poll
- eventloop 중 가장 중요하다.
- 새로운 I/O 이벤트를 받거나, I/O관련 콜백을 실행한다.
- socket 생성과 같은 새로운 연결과 데이터 등을 이 phase에서 받는다.
- setImmediate(), timer관련 콜백, close callback을 제외한 모든 콜백들이 여기서 실행.
- 필요하면 node는 여기서 block한다.
- I/O 작업을 처리하는 큐를
poll
이라고 하는데, 예외적으로 poll의 queue를 처리하는 동안에는 다른 Poll 이벤트가 쌓일 수 있다. - poll phase의 작업은 2가지로 나눌 수 있다.
- poll phase에 붙은 watch_queue에 뭔가 들어와있다면, 큐가 비거나, 시스템 한도까지 내부의 것들을 동기적으로 수행한다.
- 큐가 비워지면, Node는 새로운 connection을 기다리는데, 이 기다리는 시간은 다양한 요인에 의해 결정된다.
check
- setImmediate() callback을 위한 phase
- 왜 한개의 callback만을 위한 phase가 있는지 이해하려면, poll phase의 작업때문이고, workflow를 이해해야한다.
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들을 들고있다.
- REF
댓글남기기