Node.js란

이수현·2022년 6월 3일
0

TIL

목록 보기
20/23

📚Node.js

Node란

Node란 비동기 이벤트 기반 Javascript 런타임 환경이다.(프레임워크나 라이브러리가 아님!)

Node.js Core concepts

Blocking-Non-Blocking

블로킹 논블로킹 살펴보기

블로킹은 Node.js 프로세스에서 추가적인 자바스크립트의 실행을 위해 자바스크립트가 아닌 작업이 완료될 때까지 기다려야만 하는 상황이다. 이는 이벤트 루프가 블로킹 작업을 하는 동안 자바스크립트 실행을 계속할 수 없기 때문이다.

Node.js에서, I/O 등의 자바스크립트가 아닌 작업을 기다리는 것보다 CPU 집약적인 작업 때문에 나쁜 성는을 보여주는 자바스크립트는 보통 블로킹이라고 부르지 않는다.

  • libuv를 사용하는 Node.js 표준 라이브러리의 동기 메서드가 가장 대표적인 블로킹 작업이다.
  • Node.js 표준 라이브러리의 모든 I/O 메서드는 논블로킹인 비동기 방식을 제공하고 콜백 함수를 받는다.
  • 일부 메서드는 같은 작업을 하는 블로킹 메서드도 가지는데, 이는 이름 마지막에 Sync가 붙는다.
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // 파일을 읽을 때까지 여기서 블로킹
----------------------------------------------
const fs = require('fs');
fs.readFile('/file.md', (err, data) => { // 비동기 메서드
  if (err) throw err;
});

첫 번째 예제가 코드는 더 간단해 보이지만, file.md라는 파일을 전부 읽을 때까지 다른 자바스크립트 실행이 블로킹되는 단점이 있다.

  • 첫 번째 예제는 동기 메서드를 사용했고, 동기 메서드에서 오류가 발생하면 반드시 처리해줘야 하고 그렇지 않으면 프로세스는 죽을 것이다.
  • 비동기 메서드는 코드에 나왔듯이 에러를 던질지 아닐지는 작성자에게 달려있다.
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // 파일을 읽을 때까지 여기서 블로킹
console.log(data);
moreWork(); // console.log 이후 실행
----------------------------------------------
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
moreWork(); // console.log 이전에 실행

위 첫 번째 예제에서 console.log는 moreWork() 호출 전에 호출될 것이다. 반면에 두 번째 예제에서는 readFile()이 비동기 메서드이고 논블로킹이므로 계속 자바스크립트를 실행하고 moreWork()가 먼저 호출될 것이다.

  • 이 처럼 파일 읽기가 완료되기를 기다리지 않고 moreWork()를 실행할 수 있도록 한 것은 높은 스루풋(throughput=처리율)을 가능하게 하는 핵심 디자인이다.

동시성과 스루풋(throughput)

Node.js에서 자바스크립트 실행이 싱글 스레드이므로 동시성은 다른 작업이 완료된 후에 자바스크립트 콜백함수를 실행하는 이벤트 루프의 능력을 의미한다.

  • 동시에 실행돼야 하는 모든 코드는 I/O 등의 자바스크립트가 아닌 작업이 일어나는 동안 이벤트 루프가 계속 실행될 수 있도록 해야 한다.
  • 예시로 웹서버로의 요청이 완료되기까지 50ms가 걸리고 50ms 중 45ms는 비동기로 실행될 수 있는 데이터베이스 I/O인 상황을 생각해보자.
  • 논블로킹 비동기 작업을 사용하면 요청마다 45ms는 다른 요청을 처리할 수 있게 된다.
  • 이는 블로킹 메서드 대신 논블로킹 메서드를 사용함으로써 확연히 다른 성능의 차이가 난다.
  • 이벤트 루프는 동시 작업을 다루기 위해 부가적인 스레드를 만드는 다른 언어의 모델과는 다르다.

Node.js Event-Loop,Timer, process.nextTick()

-비동기 참고링크: 비동기/이벤트루프

이벤트 루프

이벤트 루프는 가능하다면 언제나 시스템 커널에 작업을 넘겨서 Node.js가 논블로킹 I/O 작업을 수행하도록 해준다.(자바스크립트가 싱글 스레드임에도 불구하고)

  • 대부분의 현대 커널은 멀티 스레드이므로 백그라운드에서 다수의 작업을 실행할 수 있다.
  • 이러한 작업 중 하나가 완료되면 커널이 Node.js에게 알려줘 적절한 콜백은 poll 큐에 추가할 수 있게하여 결국 실행되게 한다.

Node.js를 시작할 때 이벤트 루프를 초기화하고 제공된 입력 스크립트(또는 REPL=Read-Eval-Print-Loop)를 처리한다.

  • 이때 이 스크립트는 비동기 API를 호출하거나 스케줄링된 타이머를 사용하거나 process.nextTick()을 호출할 수 있다. 그 다음 이벤트 루프 처리를 시작한다.

각 박스는 이벤트 루프의 '단계'를 의미한다.

  • 각 단계는 실행할 콜백의 FIFO(First In First Out)큐를 가진다.
  • 각 단계는 자신만의 방법에 제한적이므로 보통 이벤트 루프가 해당 단계에 진입하면 해당 단계에서 한정된 작업을 수행하고 큐를 모두 소진하거나 콜백의 최대 개수를 실행할 때까지 해당 단계의 큐에서 콜백을 실행한다.
  • 큐를 모두 소진하거나 콜백 제한에 이르면 이벤트 루프는 다음 단계로 이동한다.
  • 이러한 작업이 또 다른 작업을 스케줄링하거나 poll 단계에서 처리된 새로운 이벤트가 커널에 의해 큐에 추가될 수 있으므로 폴링 이벤트를 처리하면서 poll 이벤트를 큐에 추가할 수 있다.
  • 그 결과 오래 실행되는 콜백은 poll 단계가 타이머의 한계 시점보다 더 오래 실행되도록 할 수 있다.
  • timer : 이 단계는 setTimeout()과 setInterval()로 스케줄링한 콜백을 실행한다.
  • pending callback : 다음 루프 반복으로 연기된 I/O 콜백들을 실행한다.
  • idle, prepare : 내부용으로만 사용한다.
  • poll : 새로운 I/O 이벤트를 가져온다. I/O와 연관된 콜백을 실행한다. 적절한 시기에 node는 여기서 블록한다.
  • check : setImmediate() 콜백은 여기서 호출된다.
  • close callbacks : 일부 close 콜백들, 예를 들어 socket.on('close',...)

이벤트 루프가 실행하는사이 Node.js는 다른 비동기 I/O나 타이머를 기다리고 있는지 확인하고 기다리고 있는 것이 없다면 깔끔하게 종료한다.

Don't Block the Event-Loop(or the Worker Pool)

Node.js는 이벤트 루프에서 JavaScript 코드를 실행하고 파일 I/O와 같은 고비용 작업을 처리하기 위해 worker pool을 제공한다. Node.js는 잘 확장되며 때로는 아파치와 같은 보다 무거운 접근 방식보다 낫다고 한다.

  • Node.js의 확장성의 비결은 적은 수의 스레드를 사용하여 많은 클라이언트를 처리한다는 것이다.
  • 만약 Node.js가 더 적은 수의 스레드로 작업을 수행할 수 있다면 스레드에 대한 공간 및 시간 오버헤드를 지불하는 것보다 클라이언트에서 작업하는 시스템의 시간과 메모리를 더 많이 사용할 수 있다.
  • 그러나 Node.js에는 스레드가 몇 개뿐이므로 현명하게 그 스레드들을 사용하기위해 애플리케이션을 구성해야만 한다.
  • Node.js 서버를 빠르게 유지하기 위한 좋은 경험 법칙은 다음과 같다
    : Node.js는 주어진 시간에 각 클라이언트와 관련된 작업이 '작은' 경우 빠르다.
  • 이것은 이벤트 루프의 콜백과 worker pool의 작업에 적용된다.

그렇다면 왜 이벤트 루프와 worker pool 차단을 피해야 할까?

Node.js는 적은 수의 스레드를 사용하여 많은 클라이언트를 처리한다.

  • Node.js에는 두 가지 유형의 스레드가 있다. 하나는 이벤트 루프(메인 루프, 메인 스레드, 이벤트 스레드 등)이고 다른 하나는 worker pool에 있는 k Workers pool이다.
  • 만약 스레드가 콜백(이벤트 루프) 또는 작업(worker)을 실행하는 데 오랜 시간이 걸리는 경우 이를 '차단됨'이라고 한다.
  • 스레드가 한 클라이언트를 대신하여 작동하는 것이 차단되는 동안 다른 클라이언트의 요청을 처리할 수 없다.
  • 이것은 이벤트 루프나 worker pool을 차단하지 않는 두 가지 동기(motivation)를 제공한다.

    • Performance : 두 가지 유형의 스레드에서 정기적으로 많은 작업을 수행하면, 서버의 처리량이 저하된다.
    • Security : 특정 입력에 대해 스레드 중 하나가 차단될 수 있는 경우 악의적인 클라이언트가 이 '악의적인 입력'을 하여 스레드를 차단하고 다른 클라이언트에서 작동하지 않도록 할 수 있다. 이것은 서비스 거부 공격(Dos Attack)이 된다.

A quick review of Node

  • Node.js는 이벤트 기반 아키텍처를 사용한다. 이 아키텍처는 orchestration을 위한 이벤트 루프와 고비용 작업들을 위한 Worker pool이 있다.

이벤트 루프에서 실행되는 코드는 무엇일까?

  1. 코드가 시작되면, Node.js 애플리케이션은 먼저 초기화 단계를 완료하고, 모듈을 요구하고 이벤트에 대한 콜백을 등록한다.
  2. 그런 다음 Node.js 애플리케이션은 이벤트 루프에 들어가 적절한 콜백을 실행하여 들어오는 클라이언트 요청에 응답한다.
  3. 이 콜백은 동기식으로 실행되며 완료된 후 처리를 계속하기 위해 비동기식 요청을 등록할 수 있다.
  4. 이러한 비동기식 요청에 대한 콜백은 이벤트 루프에서도 실행된다.
  5. 그리고 이벤트 루프는 콜백(ex.네트워크 I/O)에 의해 만들어진 non-blocking 비동기 요청을 수행한다.
  6. 요약하면 이벤트 루프는 이벤트에 대해 등록된 JavaScript 콜백을 실행하고 네트워크 I/O와 같은 non-blocking 비동기 요청을 수행하는 역할도 한다.

worker pool에서 실행되는 코드는 무엇일까?

  1. Node.js의 worker pool은 general task submission API를 노출하는 libuv로 구현된다. Node.js는 worker pool을 이용하여 고비용 작업을 처리한다.
  2. 여기에는 운영 체제가 non-blocking 버전을 제공하지 않는 I/O와 특히 CPU를 많이 사용하는 작업이 포함된다.
  3. 다음은 이 worker pool이 사용하는 Node.js 모듈 API다.
  4. I/O-intensive
    1. DNS: dns.lookup(), dns.lookupService().
    2. File System: fs.FSWatcher() 및 명시적으로 동기인 API를 제외한 모든 파일 시스템 API는 libuv의 스레드 풀을 사용한다.

  5. CPU-intensive
    1. Crypto: crypto.pbkdf2(),crypto.scryp(),crypto.randomBytes(),crypto.randomFill(),crypto.generateKeyPair()
    2. Zlib: 명시적으로 동기적인 API를 제외한 모든 zlib API는 libuv의 스레드 풀을 사용한다.
  6. 많은 Node.js 애플리케이션에서 이러한 API는 worker pool의 유일한 source이다.
  7. C++ 추가 기능을 사용하는 애플리케이션 및 모듈은 worker pool에 다른 작업을 제출할 수 있다.
  8. 완전성을 위해 이벤트 루프의 콜백에서 이러한 API 중 하나를 호출할 때 이벤트 루프는 해당 API에 대한 Node.js C++ 바인딩에 입력하고 worker pool에 작업을 제출할 때 약간의 설정 비용을 지불한다.
  9. 이러한 비용은 작업의 전체 비용과 비교할 때 무시할 수 있는 수준이므로 이벤트 루프가 작업을 오프로드한다.
  10. worker pool에 이러한 작업 중 하나를 제출할 때 Node.js는 Node.js C++ 바인딩에서 해당 C++ 함수에 대한 포인터를 제공한다.

Node.js는 다음에 실행할 코드를 어떻게 결정할까?

추상적으로, 이벤트 루프와 worker pool은 각각 pending 이벤트들과 작업들에 대한 큐를 유지한다.

  • 사실, 이벤트 루프는 실제로 큐를 유지하지 않는다..?
  • 대신 epll, kqueue, event ports, IOCP와 같은 메커니즘을 사용하여 운영체제에 모니터링하도록 요청하는 file descriptors의 collection이 있다.
  • 이러한 file descriptors는 네트워크 소켓, 보고 있는 모든 파일 등에 해당한다.
  • 운영체제에서 이러한 file descriptors 중 하나가 준비되었다고 말하면 이벤트 루프는 이를 적절한 이벤트로 변환하고 해당 이벤트와 관련된 콜백을 호출한다.
  • 대조적으로, worker pool은 항목이 처리할 작업인 실제 대기열을 사용한다.
  • worker는 이 대기열에서 작업을 꺼내서 작업하고, 완료되면 작업자는 이벤트 루프에 대해 At least one task is finished 이벤트를 발생시킨다.

애플리케이션 디자인에서 이것이 의미하는 바는 무엇일까?

Apache와 같은 a one-thread-per-client system에서는 pending 클라이언트에 고유한 스레드가 할당된다.

  • 만약 하나의 클라이언트를 처리하는 스레드가 차단하면 운영체제가 스레드를 인터럽트하고 다른 클라이언트에게 차례를 준다.
  • 따라서 운영체제는 적은 양의 작업이 필요한 클라이언트가 더 많은 작업을 필요로하는 클라이언트에 의해 불이익 받지 않도록 한다.
  • Node.js는 적은 수의 스레드로 많은 클라이언트를 처리하기 때문에 스레드가 한 클라이언트의 요청 처리를 차단하면 스레드가 콜백 또는 작업을 완료할 때까지 pending 상태인 클라이언트 요청이 차례를 얻지 못할 수 있다.
    -따라서 클라이언트에 대한 공정한 대우는 애플리케이션에 대한 책임이다.
  • 즉, 단일 콜백 또는 작업에서 클라이언트에 대해 너무 많은 작업을 수행해서는 안된다.
  • 이것은 Node.js가 잘 확장될 수 있는 이유의 일부지만, 공정한 스케줄링을 보장할 책임이 있음을 의미하기도 한다.
  • 다음 섹션에서는 이벤트 루프 및 worker pool에 대해 공정한 스케줄링을 보장하는 방법데 대해 알아보자.

Don't block the Event Loop

이벤트 루프는 각각의 새로운 클라이언트 연결을 확인하고 응답 생성을 조정한다.

  • 들어오는 모든 요청과 나가는 응답은 이벤트 루프를 통과한다.
  • 즉, 이벤트 루프가 어느 시점에서 너무 오래 걸리면 모든 현재 및 새 클라이언트가 차례를 얻지 못한다.
  • 이벤트 루프를 절대 차단하지 않도록 해야 한다.
  • 다시 말해서, 각 JavaScript 콜백이 빠르게 완료되어야 한다.
  • 이것은 await, Promise.then() 등에 적용된다.
  • 이를 확인하는 좋은 방법은 콜백의 계산 복잡성에 대해 추론하는 것이다.
  • 콜백이 인수에 관계없이 일정한 수의 단계를 수행하는 경우 항상 pending 상태인 모든 클라이언트에게 공정한 차례를 제공한다.
  • 콜백이 인수에 따라 다른 수의 단계를 수행하는 경우 인수의 길이에 대해 생각해야 한다.

Node.js의 Timers

Node.js의 Timers 모듈에는 일정 시간 후에 코드를 실행하는 함수가 있다. Timers의 모든 메서드는 브라우저 JavaScript API를 Emulate하기 위해 전역으로 사용할 수 있으므로 import할 필요가 없다. 타이머 함수가 언제 실행될지 완전히 이해하려면 Event Loop를 이해하는게 좋다.

  • 지정한 시기에 실행-setTimeout(): setTimeout은 지정한 밀리 초 이후 코드 실행을 스케줄링하는 데 사용할 수 있다. 이 함수는 브라우저 JavaScript API의 window.setTimeout()과 비슷하지만, 코드의 문자열을 실행하려고 전달할 수 없다. 첫 번째 인자로 실행할 함수를 받고, 두 번째 인자로 지연시킬 밀리 초를 숫자로 받는다. 그리고 추가적인 인자는 함수로 전달된다.-setTimeout이 실행되면 Timeout 객체를 반환한다. 이 객체로 실행 동작을 변경할 수 있고, 타임아웃을 취소할 수 있다.

  • 바로 다음에 실행-setImmediate(): setImmediate는 현재 이벤트 루프의 주기 끝에 코드를 실행한다. 이 코드는 현재 이벤트 루프의 모든 I/O 작업 후 다음 이벤트 루프에 스케줄링된 모든 타이머 이전에 실행된다. 이 코드 실행은 setImmediate 함수 호출 뒤에 오는 모든 코드는 setImmediate 함수 인자 이전에 실행된다는 의미로 '바로 다음에' 실행한다고 생각할 수 있다.-setImmediate는 바로 스케줄링된 것을 취소할 수 있는 Immediate 객체를 반환한다.

  • 무한루프 실행-setInterval(): 여러 번 실행해야 하는 코드 블록이 있다면 setInterval을 사용할 수 있다. setInterval은 두 번째 인자로 지정한 밀리 초 단위의 지연시간으로 무한대로 실행할 함수를 인자로 받는다. setTimeout처럼 지연시간 다음에 부가적인 인자를 지정할 수 있고 이는 함수 호출에 전달된다. 또한 setTimeout처럼 작업이 이벤트 루프에서 진행 중일 수 있으므로 지연시간이 보장되지 않는다. 그러므로 대략적인 지연시간으로 생각해야 한다. - setInterval도 setTimeout처럼 설정한 인터벌을 참조하고 수정하는데 사용할 수 있는 Timeout 객체를 반환한다.

추가로 공부할 사항

REDOS

참고자료

https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick/

0개의 댓글