Node.js 비동기 IO: 논블로킹 I/O와 epoll의 역할

0

Node.js는 뛰어난 확장성과 높은 성능으로 많은 개발자들에게 사랑받고 있습니다. 이러한 성과의 배경에는 비동기 논블로킹 I/O와 효율적인 이벤트 루프 메커니즘이 자리하고 있습니다. 이번 글에서는 Node.js의 비동기 I/O가 무엇인지, 어떻게 작동하는지, 그리고 내부적으로 select, poll, epoll과 같은 시스템 콜을 어떻게 활용하는지에 대해 자세히 알아보겠습니다.

unsplash

비동기 IO의 필요성

서버 애플리케이션은 특정 주소와 포트에 연결되어 소켓을 형성하고, 클라이언트로부터 요청을 받습니다. 이 과정에서 데이터를 읽거나 쓰기 위해 대기하게 되면 CPU 자원이 낭비되기 쉽습니다. 특히, 동기식 모델을 사용할 경우, 많은 요청이 동시에 들어올 때 메인 스레드가 블로킹되어 애플리케이션의 성능에 치명적인 영향을 미칠 수 있습니다. 이러한 문제를 해결하기 위해 비동기 논블로킹 I/O가 필요하게 되었습니다.

Node.js의 논블로킹 I/O 처리 방식

Node.js는 비동기 논블로킹 I/O를 통해 CPU 자원을 효율적으로 활용하며, 높은 확장성을 제공합니다. 이를 위해 libuv 라이브러리를 사용하여 다양한 I/O 작업을 관리합니다.

소켓 I/O 처리

소켓에서 데이터를 읽을 때, 데이터가 아직 버퍼에 준비되지 않았다면 블로킹이 발생할 수 있습니다. Node.js는 이를 방지하기 위해 fcntl 시스템 콜을 사용하여 소켓을 논블로킹 모드로 전환합니다. 이렇게 하면 데이터가 준비되지 않았을 때도 스레드를 차단하지 않고 다른 작업을 수행할 수 있습니다.

예를 들어, 서버가 100개의 연결을 모니터링할 경우, 각 연결은 고유한 파일 디스크립터를 가지며, epoll과 같은 OS 유틸리티를 통해 효율적으로 관리됩니다[epoll은 대규모 파일 디스크립터를 효율적으로 모니터링하여 데이터가 준비되었을 때 즉시 알림을 받습니다.

파일 I/O 및 DNS 해석 처리

파일 I/O와 DNS 해석은 본질적으로 동기식 작업이기 때문에, Node.js는 스레드 풀을 활용하여 이러한 작업을 처리합니다[libuv는 이러한 블로킹 작업을 별도의 스레드에서 수행함으로써 메인 스레드의 블로킹을 방지합니다. 이를 통해 파일 읽기, 쓰기 및 DNS 해석과 같은 작업이 애플리케이션의 성능에 미치는 영향을 최소화할 수 있습니다.

이벤트 루프와 libuv

Node.js의 이벤트 루프는 비동기 작업을 관리하는 핵심 메커니즘입니다. 이벤트 루프는 libuv 라이브러리를 통해 다양한 비동기 작업을 처리하며, 이를 통해 Node.js는 단일 스레드에서도 높은 성능을 유지할 수 있습니다. 이벤트 루프는 다음과 같은 단계를 거쳐 작동합니다:

  • Timers: 타이머가 만료된 콜백을 실행합니다.
  • Pending Callbacks: I/O 콜백을 처리합니다.
  • Idle, Prepare: 내부적인 작업을 수행합니다.
  • Poll: 새로운 I/O 이벤트를 폴링하고 콜백을 실행합니다.
  • Check: `setImmediate` 콜백을 실행합니다.
  • Close Callbacks: 닫기 이벤트 콜백을 처리합니다.

select, poll, epoll의 역할

Node.js는 select, poll, epoll과 같은 시스템 콜을 사용하여 파일 디스크립터의 상태 변화를 모니터링합니다.

  • select: 제한된 수의 파일 디스크립터를 모니터링하며, 파일 디스크립터 수가 증가할수록 폴링 시간이 선형적으로 증가합니다.
  • poll: select와 유사하지만, 더 많은 파일 디스크립터를 효율적으로 처리할 수 있습니다.
  • epoll: 대규모 파일 디스크립터를 효율적으로 모니터링하기 위해 red black tree를 사용하여 로그 시간 복잡도로 검색이 가능합니다. 이는 select와 poll에 비해 훨씬 우수한 성능을 제공합니다.

성능 비교

아래 표는 100,000개의 모니터링 작업에 대한 poll, select, epoll의 성능을 비교한 것입니다.

operations poll select epoll
10
0.61
0.73
0.41
100
2.9
3.0
0.42
1,000
35.0
35.0
0.53
10,000
990.0
930.0
0.66

epoll은 파일 디스크립터 수가 증가해도 검색 시간이 크게 증가하지 않아 대규모 애플리케이션에 적합합니다.

Node.js의 확장성과 성능

Node.js는 libuv를 통해 epoll, fcntl과 같은 시스템 콜을 효과적으로 활용하여 높은 성능과 확장성을 유지합니다. 서버는 다수의 연결을 효율적으로 관리하며, 비동기 I/O 덕분에 CPU 자원을 최적화할 수 있습니다. 또한, libuv는 다양한 플랫폼에서 일관된 비동기 I/O 모델을 제공하여 Node.js 애플리케이션이 다양한 환경에서 안정적으로 동작할 수 있도록 지원합니다.

결론

Node.js의 뛰어난 성능과 확장성은 비동기 논블로킹 I/O와 효율적인 이벤트 루프 메커니즘 덕분입니다[select, poll, epoll과 같은 시스템 콜을 적절히 활용함으로써 Node.js는 대규모 애플리케이션에서도 높은 성능을 유지할 수 있습니다. 이러한 내부 메커니즘을 이해함으로써 더 효율적이고 최적화된 Node.js 애플리케이션을 개발할 수 있습니다.

참고 자료

답글 남기기