Contents

Nodejs Quick Review

Js 스터디 도중 Node.js에 대한 이야기가 여러 번 언급되어 공식 문서를 정리하고자 한다.

https://nodejs.org/en/docs/guides/dont-block-the-event-loop/

요약

  • Node.js는 자바스크립트 코드를 이벤트 루프 안에서 실행한다.
  • 파일 IO와 같은 무거운 작업들은 워커 풀에서 다룬다.
  • Node.js는 스케일이 가능하고, 때로는 Apache와 같은 무거운 접근보다 더 나을 때가 있다. (진짜?)
    • 스케일 업이 쉽게 가능한 이유는 다수의 클라이언트들을 적은 수의 쓰레드로 핸들링하기 때문
    • 적은 쓰레드를 사용한다면 다음의 이점이 있다.
      • OS의 시간과 메모리를 다수의 쓰레드를 관리하는데 사용하지 않지 않고 필요한 작업에 집중할 수 있다.
      • 다수의 쓰레드를 사용한다는 것은 곧 쓰레드 간 컨텍스트 스위칭이 자주 요구된다는 의미이기도 하다.

이벤트 루프와 워커 풀을 블락킹하지 말아야 하는 이유

Node.js는 소수의 쓰레드만을 사용해서 다수의 클라이언트를 핸들링한다. Node.js가 사용하는 쓰레드에는 두 가지 종류가 있다.

  1. 이벤트 루프 (aka main loop, main thread, event thread)
  2. 워커 풀의 k Worker들 (aka thread pool)

쓰레드가 콜백(이벤트 루프)이나 태스크 (워커)를 실행하는데 오랜 시간이 걸린다 == 블락킹 되었다.

쓰레드가 한 클라이언트를 처리하는데 블락킹되었다면 그 동안은 다른 클라이언트들의 요청을 받지 못한다. 이벤트 루프나 워커 풀을 블락킹하지 말아야 하는 이유는 다음과 같다.

  1. 성능 : 만약 주기적으로 무거운 작업을 수행한다면 서버의 전체 throughput이 줄어든다.
  2. 보안 : 만약 특정 입력이 쓰레드를 블락킹시킨다면 악의적인 클라이언트가 악의를 담고 요청을 날려 쓰레드를 블락킹 -> 다른 클라이언트를 처리하지 못하게 함 -> DoS 공격이 가능하다.

Node.js Quick Review

어떤 코드가 이벤트 루프에서 동작하나?

Node.js 어플리케이션은 초기화 phase에서 모듈을 require하고, 이벤트들에 대한 콜백을 등록한다.

Node.js 어플리케이션은 그 다음에 이벤트 루프에 진입하여,

  • 인입되는 클라이언트 요청에 대해 적절한 콜백을 실행하여 응답한다.
  • 이 콜백들은 동기적으로 실행된다.
  • 콜백들이 실행될 때 비동기적인 요청을 등록하여 완료된 이후에 동작을 재개할 수 있다.
  • 비동기적인 요청에 대한 콜백들 또한 이벤트 루프에서 실행된다.

이벤트 루프는 또한 논블락킹 + 비동기 요청들을 콜백으로 실행한다. (Network I/O 등)

요약하자면 이벤트 루프는 이벤트들에 대해 등록된 자바스크립트 콜백들을 실행하고 네트워크 상에서의 IO처럼 논블락킹 + 비동기 요청들을 처리한다.

어떤 코드가 워커 풀에서 동작하나?

Node.js의 워커 풀은 전반적인 task들에 대한 submission API를 제공하는 libuv (공식 문서) 로 구현된다.

Node.js는 워커 풀을 값 비싼 작업들을 처리하기 위해 사용한다.

  • 워커 풀은 운영체제 레벨에서 비동기 버전을 제공하지 않는 IO 작업도 수행한다.
  • 워커 풀은 CPU 집약적인 IO 작업도 수행한다.

워커 풀을 사용하는 Node.js 모듈 API들은 다음과 같다.

  1. IO 집약적인 작업들
    1. DNS : dns.lookup(), dns.lookupService()
    2. File System : 모든 파일 시스템 API들은 워커 풀을 사용한다.
      1. fs.FSWatcher()는 제외
      2. 명시적으로 libuv의 threadpool을 사용하는 경우도 제외
  2. CPU 집약적인 작업들
    1. 암호화 : cryto.pbkdf2(), crypto.scrypt(), crypto.randomBytes(), crypto.randomFill(), crypto.generateKeyPair()
    2. 압축 : 모든 zlib API들은 워커 풀을 사용한다.
      1. 명시적으로 libuv의 threadpool을 사용하는 경우는 제외

대다수의 Node.js 어플리케이션에서 워커 풀을 사용하는 API들은 작업의 소스로 사용하는 경우가 많다. C++ add-on을 사용하는 어플리케이션과 모듈들 또한 워커 풀에 작업들을 추가할 수 있다.

이벤트 루프의 콜백에서 이러한 API를 사용한다면,

  • 이벤트 루프는 Node.js의 (워커 풀을 사용하는 API에 대한) C++ 바인딩에 진입한다. (이 때 셋업 비용이 소모된다)
  • 그 후 워커 풀에 필요한 작업을 제출한다.
  • 전반적으로 바인딩에 진입할 때에 소모되는 리소스 비용은 무시할 수 있는 수준이기 때문에 이런 방식으로 동작한다.
  • 워커 풀에 작업을 제출할 때 Node.js는 C++ 바인딩 안의 해당되는 C++ 함수에 대한 포인터를 제공한다.

Node.js 는 다음에 실행될 코드를 어떻게 결정하나

이벤트 루프와 워커 풀은 기다리는 상태의 이벤트들과 작업들에 대한 각각의 큐를 유지 관리한다.

사실, 이벤트 루프가 실제로 큐를 관리하는 것은 아니다.

  • 대신 이벤트 루프는 운영체제가 모니터링할 파일 디스크립터의 컬렉션을 가지고 있다.
    • Linux : epoll
    • OSX : kqueue
    • Solaris : event ports
    • Windows : IOCP
  • 이러한 파일 디스크립터들은 어떤 요소들에 상응할 수 있냐면..
    • 네트워크 소켓들
    • 보고 있는 모든 파일들
    • 기타 등등..
  • 운영체제가 이런 파일 디스크립터들 중 하나가 준비되었다고 알려주면 이벤트 루프는 운영체제가 알려주는 정보를 해석해서 적절한 이벤트로 변환하고, 이벤트에 연관된 콜백을 실행시킨다.

반면 워커 풀은 처리되길 원하는 작업들에 대해 실제 큐를 사용한다.

  • 워커는 이 큐에서 작업을 하나 뽑아서 작업한다.
  • 워커가 작업을 마치면 이벤트 루프에 대해 ‘하나 이상의 작업이 완료되었음’을 알린다.

Node.js 어플리케이션 디자인

Apache 처럼 클라이언트 - 쓰레드가 1대1 대응이 되는 시스템에서는 운영체제가 클라이언트가 얼마나 작업하길 원하던 상관 없이 리소스를 할당할 수 있는 장점이 있다.

하지만 Node.js에서는 소수의 쓰레드고 다수의 클라이언트를 핸들링하기 때문에 클라이언트의 요청 때문에 쓰레드가 블락되어서는 안된다.

때문에 Node.js 어플리케이션에서는 이를 인지하고 책임져야 한다. 어떠한 클라이언트들에 대해 콜백이나 작업을 수행할 때 너무 과다한 작업을 수행해서는 안된다.

이 이유 때문에 Node.js가 스케일 업에 유리한 면이 있지만 공정한 스케쥴링을 위해서는 어플리케이션 개발자가 어느 정도 신경을 써야 한다는 점이 인상 깊었다.