지난 주 호눅스님의 강의에서 C10K 문제가 1999년부터 화두에 오르기 시작했고, 그 부분을 해결하기 위한 방법으로 등장한 것이 node.js라는 이야기를 들었다. 노드 js의 비동기 논블로킹이라는 특징이 C10K 문제를 해결하는데에 있어서 무엇이 도움이 되고, 스프링과 비교했을 때의 이점이 무엇인지 정확히 받아들여지지가 않았다.
그래서 이번 포스팅에서는 C10K와 node.js의 싱글 스레드, 논블로킹의 특징이 어떻게 C10K의 문제를 해결하는가에 대해 작성해보겠다.
C10K 문제
C10K란 동시 사용자 1만명(Concurrent 10K users)가 접속하는 서버를 어떻게 구현해야할 것인가에 대한 문제이다.
C10K라는 문제가 대두되었을 때는 1999년으로, 지금으로부터 약 25년 전이다. 당시 기준으로는 매우 큰 규모의 서버를 운영하는 도전 과제였다. (인터넷이 제대로 보급되기 시작한 1995년 상황을 고려하면, 1만 명이 접속하는 서비스는 당시에는 매우 큰 규모의 서비스로 인식되었을 것이다.)
아파치 서버, 프로세스 및 스레드의 한계
C10K 문제를 처음 맞닥뜨렸을 때, 주로 사용하던 서버는 아파치 서버(1995년 개발)였다. 아파치 서버가 C10K를 해결하지 못한 이유가 무엇일까?
당시 아파치 서버는 요청이 들어올 때마다 새로운 프로세스를 생성하는 Pre-fork 방식을 사용했다. 즉, 클라이언트가 서버에 요청을 보낼 때마다 새로운 프로세스를 만들어서 해당 요청을 처리하게 되는데, 이는 서버 자원을 매우 비효율적으로 사용하게 만든다. 프로세스를 생성하는 데는 큰 메모리와 CPU 자원이 필요하고, 프로세스 간의 컨텍스트 스위칭 비용도 무시할 수 없다.
아파치 서버는 이후 Worker 모듈을 도입하여 스레드 기반 모델로 전환하였지만, 여전히 스레드가 시스템 자원을 소모하는 문제는 존재했다.
스레드는 프로세스보다는 가볍지만, 1만 개의 스레드를 생성하게 되면 각 스레드가 자신만의 메모리 공간을 할당받게 되고, 컨텍스트 스위칭이 빈번하게 발생하면서 CPU 자원 소모와 성능 저하를 초래한다.
특히 다중 스레드 환경에서 흔히 발생하는, 스레드 간의 경합이 일어나면 처리 속도가 더욱 느려지는 현상이 발생했다.
스레드의 비용
스레드는 각기 독립적인 스택 메모리를 필요로 한다. 예를 들어, 자바 64비트 VM의 경우 하나의 스레드는 기본적으로 약 1MB의 메모리를 사용한다.
1만 개의 스레드를 생성하면 약 10GB의 메모리가 필요한데, 이것이 단순히 메모리 문제에 그치는 것이 아니라, 각 스레드가 CPU의 실행 시간을 차지하기 위해 끊임없이 경합을 벌인다.
이는 결국 컨텍스트 스위칭 비용을 증가시키고, 서버의 성능을 저하시킨다. 스레드가 많아질수록 이 경합은 심화되어, 결국 서버의 처리 능력에 한계를 불러일으킨다.
컨텍스트 스위칭
여러 개의 스레드가 동시에 있다면, CPU는 각 스레드의 작업을 조금씩 번갈아 가며 처리해야하는데, 이 과정에서 자주 스레드를 전환하게 되고, 이를 위해 스레드의 상태(레지스터 값, 메모리 상태 등)를 저장하고 불러오는 시간이 추가로 필요하게 된다. 이를 컨텍스트 스위칭 오버헤드라고 한다.
Node.js와 논블로킹 I/O
이러한 문제를 해결하기 위해 등장한 것이 바로 Node.js의 비동기 논블로킹 I/O 모델이다.
Node.js는 싱글 스레드 기반으로 동작하면서, 하나의 이벤트 루프를 통해 비동기적으로 I/O 작업을 처리한다. 이것이 가능하게 된 핵심은 바로 epoll()과 같은 논블로킹 I/O 기술 덕분이다.
이 기술은 운영 체제 레벨에서 소켓이 비어 있는지, 즉 데이터가 도착했는지 여부를 감지하고 이를 이벤트로 처리할 수 있게 한다.
Node.js는 요청이 들어올 때마다 새로운 프로세스나 스레드를 생성하지 않고, 비동기 방식으로 작업을 처리함으로써 메모리와 CPU 자원을 훨씬 효율적으로 사용한다. 이를 통해 스레드나 프로세스에서 발생하는 컨텍스트 스위칭의 부담도 최소화할 수 있었다.
여기서 다루진 않았지만, nginx는 비동기 I/O를 통해 아파치와 비교했을 때 효율적인 성능을 보인다. 그냥 비동기 처리가 메모리를 얼마나 아낄 수 있는지 정도로만 참고하자.
다음 포스팅에서 이러한 epoll()이 node.js에서 어떻게 구현되어있는지를 살펴볼 것이다.
참고자료
https://oliveyoung.tech/blog/2023-10-02/c10-problem/
https://kosaf04pyh.tistory.com/339
https://kosaf04pyh.tistory.com/340
https://nodejs.org/en/learn/getting-started/introduction-to-nodejs
'JavaScript > JS를 파헤쳐보자' 카테고리의 다른 글
libuv의 이벤트 루프를 파헤쳐보자 (1) - libuv와 이벤트 루프 실행 흐름 (0) | 2025.01.08 |
---|---|
[JS] 자바스크립트의 비동기 처리 (0) | 2024.07.30 |
Node.js는 완전한 싱글 스레드일까? (0) | 2024.07.26 |
Call Stack과 클로저함수 (0) | 2024.07.24 |
자바스크립트는 컴파일러 언어일까 인터프리터 언어일까? (1) | 2024.07.20 |