[번역] NodeJS의 비동기 IO

Sonny·2025년 1월 6일
24

Article

목록 보기
28/28
post-thumbnail

Nodejs 내부 자세히 알아보기 (블로킹, 논 블로킹 IO, select/poll/epoll, event loop)

원문: https://medium.com/@manikmudholkar831995/async-io-in-nodejs-a57fe9c3ccc6

By Manik Mudholkar (시니어 소프트웨어 엔지니어)

이 글은 시니어 엔지니어를 위한 고급 Node.js 시리즈의 두 번째 글입니다. 이 글에서는 비동기 IO가 무엇인지, 작동 방식에 대해 자세히 설명하겠습니다. 시니어 엔지니어를 위한 고급 Node.js 시리즈의 다른 글은 아래에서 확인할 수 있습니다.

시리즈 로드맵

목차

우리가 이미 알고 있는 것처럼 Node.js는 확장성이 뛰어납니다. 하지만 많은 사람들이 "왜 그리고 어떻게?"라는 질문에 대해서는 여전히 만족할 만한 답을 찾지 못하고 있습니다. 온라인에서 찾을 수 있는 대부분의 자료들은 내부 동작 방식을 제대로 설명하지 않거나, 잘못된 정보를 제공하기 때문입니다. 이번 글에서는 왜 Node.js가 빠른지에 대한 저의 생각을 설명해보려고 합니다.

논블로킹 IO의 필요성 이해하기

서버 애플리케이션은 특정 주소와 포트에 연결되어 소켓을 형성합니다. 애플리케이션에 요청을 보내면 연결이 생성되는데, 연결은 파일 디스크립터를 통해 접근됩니다. 클라이언트가 서버로 데이터를 전송할 때도 동일한 주소와 포트를 사용합니다. 이후 운영체제는 해당 데이터를 적절한 파일 디스크립터와 연결하여 커널 버퍼에 저장합니다. 그리고 애플리케이션은 커널 버퍼에 쌓인 데이터를 읽어, 지정된 사용자 메모리 영역으로 옮겨야 합니다.

만약 데이터를 받기 위한 요청을 보냈는데 클라이언트가 아직 소켓에 데이터를 쓰지 않았다면 어떻게 해야 할까요? 데이터를 읽기 위해서는 데이터가 사용 가능할 때까지 기다려야 합니다. 하지만 이로 인해 발생하는 대기 시간은 CPU가 다른 중요한 작업에 스레드를 활용할 수 있는 기회를 빼앗기 때문에, CPU의 귀중한 리소스를 낭비하게 됩니다.

데이터를 기다리거나, 파일에 쓰거나, 무언가를 읽는 등 어떤 작업을 실행하든 스레드가 필요합니다. 이러한 동기식 모델을 사용하여 애플리케이션을 확장하는 것은 어려울 수 있으며, 특히 Node.js에서는 메인 스레드가 차단되면 애플리케이션의 성능에 치명적인 영향을 미칠 수 있습니다. 읽기나 쓰기를 요구하는 요청이 엄청나게 많다면 CPU는 대부분의 시간을 대기하는 데 사용하게 되어 리소스가 낭비됩니다.

파일에서 읽을 때도 상황은 비슷합니다. 네트워크 호출에서처럼 데이터를 읽고 쓰기 위해 기다릴 필요는 없지만, 실제로 데이터를 읽고 쓰는 동안 스레드는 차단됩니다. DNS(도메인이나 호스트 이름을 네트워크 주소로 확인하는 프로토콜) 해석도 마찬가지로 네트워크 요청임에도 불구하고 블로킹 작업에 해당합니다. 이는 많은 프레임워크와 런타임이 OS에서 제공하는 동기식 DNS 구현체를 사용하기 때문이며 이 과정에서 스레드가 차단되기 때문입니다. 따라서 파일을 읽는 것과 마찬가지로 DNS 해석 또한 블로킹 작업에 해당합니다.

Node.js에서의 논블로킹 IO

이러한 문제를 해결하기 위해 Node.js는 비동기 논블로킹 I/O와 스레드 풀을 도입했습니다.

소켓 IO에서는 어떻게 처리될까요?

  • 읽을 데이터가 아직 버퍼에 준비되지 않았다면 블로킹이 발생할 수 있습니다. Node.js는 이를 방지하기 위해 fcntl 시스템 콜을 사용하여 소켓을 논블로킹 모드로 전환합니다. fcntl은 소켓을 논블로킹 모드로 전환하여, 데이터가 준비되지 않았을 때도 스레드를 차단하지 않고 다른 작업을 수행하다가 나중에 다시 돌아올 수 있게 합니다(폴링 방식).
  • 서버가 100개의 연결에 대해 데이터를 폴링하는 경우를 예로 들어보겠습니다. 각 연결은 자신만의 고유한 파일 디스크립터를 갖고 있으므로, 서버는 100개의 파일 디스크립터를 모니터링하여 데이터를 읽을 수 있는지 확인해야 합니다.
  • 이처럼 소켓 IO 문제를 해결하기 위해 Node.js는 Linux의 epoll, MacOS의 Kqueue와 같은 OS 유틸리티를 활용합니다. 다수의 파일 디스크립터를 효율적으로 모니터링하기 위해 epoll(어떤 파일 디스크립터에 읽기/쓰기 가능한 데이터가 있는지 알려줌)을 사용하는데, 비동기 논블로킹 I/O 덕분에 스레드는 클라이언트가 소켓에 데이터 쓰기를 기다리지 않아도 됩니다. 대신 epoll이 읽기나 쓰기가 가능한 데이터가 있는 파일 디스크립터를 식별합니다.
  • 데이터를 사용할 수 있게 되면 해당 파일 디스크립터에서 데이터를 읽어 커널 메모리로 전송하고, 이후 사용자 전용 메모리에 전송됩니다.
  • Node.js 코드를 작성할 때는 소켓이나 연결에서 직접 데이터를 읽지 않습니다. 대신 데이터가 준비되었음을 알리는 이벤트를 수신하고, 해당 데이터로 콜백 함수가 실행됩니다.

select/poll/epoll은 데이터가 사용 가능한지 여부만 알려줄 뿐입니다. 실제 IO 작업을 수행하기 위해서는 여전히 read/write/recv/send과 같은 블로킹 시스템 콜을 사용해야 합니다.

select/epoll/read의 작동 방식을 더 자세히 알아봅시다.

파일 IO 및 DNS 해석은 어떻게 처리될까요?

논블로킹이 네트워크 호출에 대한 문제는 해결하지만, 파일 I/O나 DNS 해석은 어떨까요? 이때 스레드 풀이 유용하게 사용됩니다.

  • 파일 IO와 DNS 해석은 본질적으로 동기식 작업이기 때문에, 이러한 작업들은 스레드 풀의 스레드를 사용하여 처리합니다.
  • Node.js의 crypto 라이브러리와 같은 CPU 집약적인 라이브러리들도 스레드 풀을 사용합니다.
  • 따라서 메인 스레드에서 블로킹 작업을 수행하는 대신, 해당 작업을 다른 스레드에 위임합니다.

이 모든 것은 Node.js가 사용하는 lib_uv 라이브러리에서 구현되어 있습니다.

select, poll 그리고 epoll에 대해 더 자세히 알아보기

웹 서버의 관점에서 생각해봅시다. accept 시스템 콜로 연결을 받을 때마다 해당 연결을 나타내는 새로운 파일 디스크립터를 얻게 되며, 수천 개의 연결이 동시에 열려있을 수 있습니다. 이러한 연결들에서 새로운 데이터가 도착했는지 확인하고 이에 대응하려면 CPU 시간을 낭비하며 "지금 업데이트가 있나요? 지금은요? 지금은 어떤가요?"라고 계속 물어보는 대신, Linux 커널에게 "여기 100개의 파일 디스크립터가 있는데, 이 중 하나라도 업데이트되면 알려줘!"라고 요청하는 것이 더 효율적입니다.

Node.js가 내부적으로 빠른 속도를 달성하는 방법은 select, poll, epoll 시스템 콜을 통해 파일 디스크립터 목록의 변경 사항을 확인하는 것입니다. 이때 대기 시간을 함께 지정하여 변경 사항이 있는지 확인하게 됩니다. 변경이 감지되면 즉시 알림을 받고, 그렇지 않으면 지정된 타임아웃까지 대기한 후 이벤트 루프의 다음 반복에서 다시 확인합니다. 이 타임아웃은 변경 사항이 없을 때 대기할 시간을 나타냅니다.

selectpoll에서는 연결(파일 디스크립터)의 수가 증가할수록 폴링하는 시간도 선형적으로 증가합니다. 하지만 epoll은 이와는 다르게 자동 밸런싱 이진 탐색 트리인 red black tree를 사용하는 방식이라는 점이 다릅니다. epoll에 파일 디스크립터를 추가하기 시작하면 자동으로 균형을 맞추기 때문에 로그 시간으로 검색할 수 있습니다(파일 디스크립터 수가 증가해도 검색 시간이 크게 증가하지 않음). 아래는 100,000개의 모니터링 작업에 대한 성능 비교 표로, epoll이 확실히 우수한 성능을 보여줍니다.

# operations  |  poll  |  select   | epoll
10            |   0.61 |    0.73   | 0.41
100           |   2.9  |    3.0    | 0.42
1000          |  35.0  |   35.0    | 0.53
10000         | 990.0  |  930.0    | 0.66

epoll 시스템 콜 그룹(epoll_create, epoll_ctl, epoll_wait)을 사용하면 Linux 커널이 파일 디스크립터 목록을 모니터링하고 활동 상태의 변화를 전달받을 수 있습니다.

epoll 사용 방법은 다음과 같습니다.
1. epoll_create를 호출하여 epoll을 사용할 것임을 커널에 알립니다. 커널은 ID를 반환합니다.
2. epoll_ctl을 사용하여 업데이트를 받고 싶은 파일 디스크립터를 커널에 알립니다.
3. epoll_wait를 사용하여 관심 있는 파일 목록의 업데이트를 대기합니다.

select/poll/epoll은 데이터의 사용 가능 여부만 알려줄 뿐, 실제 IO 작업을 수행하려면 여전히 read/write/recv/send와 같은 블로킹 시스템 콜이 필요합니다.

참조

🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article을 구독해주세요!

profile
FrontEnd Developer

4개의 댓글

comment-user-thumbnail
2025년 1월 10일

Certvalue is the top CE-Mark Consultants in egypt for providing CE-Mark Certification in Cairo, Giza, Alexandria and other major cities in egypt with services of implementation.

https://www.certvalue.com/ce-mark-certification-in-egypt/

답글 달기
comment-user-thumbnail
2025년 1월 12일

저도 비슷하게 Node.js의 이벤트 루프에 관해 글을 써봤는데, 관심 있으시면 한 번 읽으러 와주세요!
https://velog.io/@jinkonu/Node.js의-이벤트-루프에-대해-알아보자

답글 달기
comment-user-thumbnail
2025년 1월 15일

Node.js 내부 구조에 대한 심도 있는 글 정말 감사합니다! 🙌
비동기 IO의 작동 원리와 epoll의 성능 비교까지 구체적으로 다뤄주셔서 이해가 훨씬 쉬워졌습니다. 특히, 소켓 IO와 파일 IO, DNS 해석에서 논블로킹 작업이 어떤 방식으로 구현되는지 명확히 설명해주신 점이 인상 깊습니다. 💡

Node.js가 왜 효율적인지에 대한 "어떻게"와 "왜"를 이렇게 잘 풀어주신 덕분에, 이 주제를 공부하는 많은 개발자들에게 큰 도움이 될 것 같아요! 😊

글의 구조도 체계적이고, select/poll/epoll의 차이점과 장단점을 쉽게 이해할 수 있었습니다. 앞으로도 좋은 번역 많이 부탁드릴게요! 🚀✨

답글 달기

Pengen nonton film tanpa buffering? Aku selalu buka nontonfilm88, lancar banget!
https://publishingcentral.com

답글 달기