Contents

Nodejs 스레드

Nodejs 스레드

자바스크립트 코드가 실행될 때 보다, IO 이벤트가 발생할 때 까지 기다리는 시간이 훨씬 더 많다.

node index.js 한 후,
lsof -i:[사용하는 포트 번호]
ex) lsof -i:3000 하면 해당 프로세스의 pId를 확인할 수 있다.

ps -M [pId] -> 스레드의 개수를 확인 가능

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
USER   PID   TT   %CPU STAT PRI     STIME     UTIME COMMAND
we   43531 s003    0.0 S    31T   0:00.12   0:00.41 node index.js
     43531         0.0 S    31T   0:00.00   0:00.00
     43531         0.0 S    31T   0:00.00   0:00.01
     43531         0.0 S    31T   0:00.00   0:00.00
     43531         0.0 S    31T   0:00.00   0:00.00
     43531         0.0 S    31T   0:00.00   0:00.01
     43531         0.0 S    31T   0:00.00   0:00.00
     43531         0.0 S    31T   0:00.00   0:00.00
     43531         0.0 S    31T   0:00.00   0:00.00
     43531         0.0 S    31T   0:00.00   0:00.00
     43531         0.0 S    31T   0:00.00   0:00.00

node.js가 사용하는 libuv의 모듈은 내부적으로 thread pool을 두어 I/O 작업을 스레드로 처리
-> eventloop 빠르게 작업 진행 가능

MultiThread: 서버의 요청 처리를 스레드에서 처리하도록 하여 병렬처리를 가능하도록 하는 방식.
스레드는 서버 CPU 자원을 시분할 형태로 나누어 가짐으로써 독립 실행이 가능하며 다른 요청을 동시에 받을 수 있게 한다.

동기 방식은 IO 처리를 Blocking 하는데 지금까지는 이 문제를 스레드로 처리.
비동기 방식으로도 처리할 수 있다. 비동기 방식은 하나의 요청 처리가 완료되기 전에 제어권을 다음 요청으로 넘긴다.
따라서 IO 처리인 경우 Blocking되지 않으며 다음 요청을 처리할 수 있다.

Node.js는 비동기 IO를 지원하며 싱글 스레드 기반으로 동작.
노드 서버는 비동기 방식으로 요청을 처리하므로 요청 처리하면서 다음 요청을 받을 수 있다.
병렬 처리를 스레드로 처리하지 않으므로 멀티 스레드가 갖는 근원적인 문제에서 자유롭다.

비동기 처리는 이벤트 방식으로 풀어진다. 이벤트가 발생하며 서버 내부에 메시지 형태로 전달된다.
event loop가 처리하는 동안 제어권은 다음 요청으로 넘어가고 처리가 완료되면 콜백을 호출하여 처리 완료를 호출 측에 알려준다.

출처: Node.js의 스레드

싱글 스레드(Single Thread)란 말 그대로 하나의 스레드만을 사용하여 여러 작업요청을 처리하는 방법이다.
앞서 정리한 것처럼 I/O작업과 같은 경우 비동기방식으로 처리하고
그 동안 다른 작업을 처리하는 방식이다.
한개의 콜스택으로 명령을 처리하는 Node.js는 이런면에서 싱글 스레드라고 할 수 있다.

스레드 풀(Thread Pool)

멀티 스레드(Multi Thread)모델의 경우 스레드 풀을 두고 요청을 처리할 때 스레드를 기반으로 처리한다.
앞서 정리한 것 처럼 대부분의 작업은 콜스택을 통해 처리되며 Queue를 이용해 비동기 작업을 처리하지만 I/O, 네트워크등의 작업은 OS에게 작업은 넘겨주는 논블로킹방식으로 동작한다.

하지만, OS에서 지원하지 않는 비동기작업이나 특정 I/O작업은
libuv에서 처리하게되며 이는 내부적으로 운영되는 스레드 풀을 이용하여 논블로킹을 유지한다.

이벤트 루프 멀티플랙싱(Multiplexing)

많은 수의 작업이 요청되는 경우 어떻게 처리할 지에 대한 고민이 생길 수 있다.
이벤트 루프의 멀티 플렉싱(Multiplexing)에서 그 답을 찾을 수 있다고 한다.

  • 멀티 플렉싱
    하나의 통신 채널을 통해 다량의 데이터를 전송하는데 사용되는 기술이다.
    즉, 매 요청마다 새로운 프로세스나 스레드를 생성하는 것이 아니라 요청의 갯수와 상관없이 한개의 프로세스나 스레드를 이용하여 작업을 처리하는 방법이다.
    ex) 주파수 분할, 시분할 등

  • Node.js의 모든 작업처리는 단일 콜스택에서 이루어지고 비동기 처리는 Queue를 이용하며 둘은 하나의 쓰레드로 이루어진 이벤트 루프를 통해 동작한다.

    이벤트 루프는 멀티플렉싱(Multiplexing)방식으로 동작한다. 여러 개의 소켓이 동시에 연결되어 있고, 이들을 관찰하면서 들어오는 작업을 처리하는 방식이다.

    기존의 웹서버의 경우엔 요청이 들어오면 이를 처리하기위한 프로세스나 쓰레드를 생성하는데
    이를 대기하는 시간이 발생하고 (Thread waiting)이 과정에서 병목현상이 발생한다.

    하지만 Node.js의 경우에는 다량의 작업이 요청되어도 쓰레드대기가 발생하지 않고
    작업의 처리는 다른 쓰레드로 넘겨버리기 때문에 바로 다른 작업 요청을 받을 수 있다.
    -> CPU의 영향을 많이 받는 요청의 경우 한정된 쓰레드에서 I/O 작업이 처리되며
    이를 이벤트루프는 대기하기 때문에 이후 발생하는 다른 Request들에 병목현상이 발생한다.

I/O

IO는 Input/Output의 줄임말.
어떤 데이터가 CPU에서 처리되기 위해서는 현재 위치부터 메모리 계층의 최상위에 있는 레지스터까지 전달되어야 한다.
몇몇 데이터 소스는 실행을 심각하게 늦추지 않는 선에서 레지스터까지의 데이터 전달을 보장한다.

하지만 대부분의 데이터 소스는 데이터를 요청했을 때 일정 시간 안에 데이터를 받을 수 있을 거라는 보장이 없기 때문에,
프로그램은 어떤 방식으로든 데이터를 받기까지 실행이 심각하게 늦춰진다고 느껴질 만큼 대기하는 시간이 생긴다.
현대의 컴퓨터 구조에서 전자는 메모리, 즉 RAM과 그 상위 메모리 계층을 가리키고
후자는 디스크 - 하드디스크 및 SSD - 와 네트워크를 통한 데이터 교환을 의미한다.

어떤 IO가 blocking하는지, non-blocking하는지는 주로 후자에 해당하는 디스크 혹은
네트워크로부터 데이터를 가져오려고 할 때 프로그래밍 언어 혹은 런타임이 데이터가 도착하기를 대기하는지에 따라 구분된다.

Blocking I/O

I/O 작업이 진행되는 동안 유저의 프로세스가 결과가 반환되기 전에는 다음 처리로 넘어가지 않는 것.
System call이 들어왔을 때, kernel에서 다른 data를 읽는중이라면 이 kernel이 data를 다 읽을 때까지 기다려야하는데, 이를 Block.
결과적으로 IO와 상관 없는 작업들도 IO 작업이 병목이 되어 CPU 사이클을 낭비하게 된다.

IO 작업은 컴퓨터가 처리하는 작업들 중 가장 오래 걸리는 작업들 중 하나이기 때문에, 모든 개발자들이 IO 성능을 향상시키기 위해 노력.
IO 작업들로 인해 병목이 되는 작업들의 성능을 조금이라도 향상시키기 위해 하드웨어적으로는 하이퍼스레딩3, OS와
소프트웨어적으로는 멀티프로세싱이나 멀티스레딩이 개발되었으며, 덕분에 최신 CPU에 최신 소프트웨어를 구동했을 때 물려있는 작업을 두고 노는 일은 거의 없다.

Non-Blocking IO & Asynchronous programming

Non-blocking은 그 반대로 IO 작업을 막지 않는다. 즉 IO 작업을 요청한 후 결과를 기다리지 않고 다른 작업을 수행한다.
이는 실제로는 IO 작업을 요청한 후 다른 작업에 코어를 양보하는 것처럼 달성된다.
양보를 한 작업과 양보받은 작업 둘 다 일단 작업이 시작되고 나서는 서로의 진행 상황과 상태 값들에 접근할 수 없다.
즉 두 작업은 비동기적으로 수행되므로, non-blocking IO는 비동기 작업을 허용하는 런타임에서만 달성될 수 있다!!!!

[Non-Blocking]**
Non-blocking IO를 달성하고 나면 위에서 지적된 코어가 놀고 있는 상황이 거의 없어진다.
IO에서 데이터를 받아오는 동안 CPU는 공회전하지 않고 다른 작업을 먼저 수행한 다음,
결과가 도착하면 도착하는 대로 그 작업을 다시 수행한다.

이것이 NodeJS가 자랑하는 IO 처리의 효율성 극대화이다.
NodeJS는 기본적으로 V8 엔진의 개발 로드맵을 따라가기 때문에 처음 NodeJS가 나왔을 때는
콜백 패턴 또는 EventEmitter를 구독(subscribe)하는 방법을 주로 썼었다.
최신 JS에서는 Promise, async/await, generator, rxjs 및 기타 비동기 라이브러리 등
다양하고 쉬운 방법으로 비동기 로직을 작성하는 것이 가능하며, 이것이 권장된다.

Sync != Blocking, Async != Non-Blocking

Async와 non-blocking이 주로 짝지어지고 sync와 blocking이 주로 짝지어지지만 이 둘은 완전히 동일한 개념은 아니다. Sync하면서 non-blocking할 수도 있고 async하면서 blocking할 수도 있지만,
비효율적이고 부자연스럽기 때문에 그렇게 쓰이지 않을 뿐이다. 두 개념이 분명히 다르고,
아주 일부 상황에서 이러한 패턴들이 쓰일 수 있다는 것만 짚고 넘어가자.
특히 polling은 ajax 만으로 실시간 소통을 달성하기 위해서 쓰일 정도로 흔한 패턴이다.

멀티 쓰레드

멀티쓰레드는 쓰레드 여러개가 동시에 실행되어 요청을 처리한다는 개념.
하나의 CPU를 여러 쓰레드가 나누어 동작하도록 함으로써 CPU를 공유하는 것 같은 효과를 가져온다.

하지만 Multi Thread에도 한계가 존재합니다. Multi Thread 기반의 서버는
일반적으로 클라이언트가 요청을할 때마다 Thread를 발생시킨다.

즉 Client의 동시접속이 늘어날수록 Thread가 많이생겨 메모리 자원을 많이 소모한다.
또한 스케쥴링을 위해 CPU연산이 늘어나게 되어 성능 저하가 발생할 수 있다.

출처: https://sambalim.tistory.com/42 [삼바의 성장 블로그]