개요

목표 이벤트 루프, 스레드풀, 커널이 실제 CPU 코어와 어떻게 상호작용하는지 CPU 관점에서 명확히 이해하기 Node.js가 어디서 CPU를 쓰고 어디서 기다리는지 구분해야 병목을 제대로 잡을 수 있음

CPU 코어와 OS 스레드의 물리적 의미

하드웨어와 OS 레벨 정의를 먼저 정리

  • CPU 코어 1개 ≈ 특정 시점에 물리적으로 실행 가능한 OS 스레드 1개
  • 스레드 OS 스케줄러가 CPU를 할당하는 최소 단위
  • 동시성 vs 병렬성
    • 코어 1개면 수백 스레드도 시분할로 번갈아 실행되는 동시성
    • 코어 여러 개일 때만 실제 병렬 실행 가능

CPU 관점의 핵심 비용

  1. 실행 유저 코드나 커널 코드가 실제로 CPU 사이클 소모
  2. 대기 I O 응답 등으로 CPU 미사용 상태 Blocked
  3. 문맥 교환 OS가 실행 중인 스레드를 바꾸는 과정 레지스터 저장 복원, 캐시 오염 등 오버헤드 발생

Node.js의 실행 모델 Actors

Node.js 프로세스가 CPU 자원을 소비하는 주체 세 가지로 단순화 가능

메인 스레드 Event Loop

  • 정체 Node.js 시작 시 생성되는 단 하나의 OS 스레드
  • 역할 V8 엔진 위 JS 실행, 콜백 처리, 비동기 요청 등록
  • CPU 특징 대부분 사용자 모드에서 CPU 사용 이 스레드가 막히면 애플리케이션 전체가 멈춘 것으로 체감됨

커널 Linux Kernel

  • 정체 OS의 핵심 컴포넌트, 하드웨어 제어
  • 역할 TCP IP 스택, 파일 시스템, epoll 기반 이벤트 통지 처리
  • CPU 특징 시스템 콜 수행 시 유저 모드와 커널 모드 전환이 일어나며 CPU 사용

libuv 스레드풀 Thread Pool

  • 정체 Node.js 프로세스 내부의 별도 OS 스레드 그룹 기본 4개
  • 역할 OS가 비동기를 직접 제공하지 않거나 CPU 연산이 큰 작업을 오프로딩
    • 파일 I O 완전 비동기 파일 접근은 OS마다 제약 존재
    • 일부 암호화 crypto, 압축 zlib, TLS 연산 일부
    • dns.lookup 시스템 리졸버 호출
  • CPU 특징 메인 스레드와 별개로 스케줄링되어 멀티 코어 환경에서 병렬 처리 가능

시나리오별 CPU 자원 흐름

A) 순수 CPU 연산 CPU bound

예 거대한 루프, 복잡한 정규식, 대용량 JSON.parse, 암호화 해싱

  1. 흐름 메인 스레드가 V8 위에서 코드를 연속 실행
  2. CPU 상태 메인 스레드 CPU 점유율 100% 근접
  3. 문제점 메인 스레드 점유로 완료된 I O 이벤트나 타이머 콜백 처리가 지연되어 기아 Starvation 발생 사용자는 Node가 멈춘 것으로 인지
  4. 해결 worker_threads로 오프로딩하거나 외부 프로세스로 분리

B) 네트워크 I O Network bound

예 HTTP 요청 응답, DB 쿼리 대기

  1. 흐름
  • 요청 메인 스레드가 write 시스템 콜 호출 짧은 CPU 사용
  • 대기 커널이 NIC로 패킷 전송 후 응답 대기 메인 스레드는 Non blocking으로 다른 일 처리
  • 수신 패킷 도착 시 커널 인터럽트 처리 후 epoll로 이벤트 준비
  • 처리 메인 스레드가 깨어나 콜백 실행
  1. CPU 상태
  • 네트워크 대기 시간 동안 CPU 사용량 거의 0
  • 수천 연결도 실제 패킷 처리 시점에만 CPU 사용 집중
  1. 특징 스레드풀 불사용 Node.js가 가장 효율적인 영역

C) 파일 I O Disk bound

예 fs.readFile, fs.writeFile Async

파일 시스템 작업은 libuv 스레드풀로 이관되는 경우가 일반적임 Linux AIO 제약 및 이식성 이슈

  1. 흐름
  • 위임 메인 스레드는 작업을 스레드풀 큐에 넣고 즉시 리턴
  • 작업 스레드풀 스레드가 read write 시스템 콜 수행 후 디스크 응답을 블로킹 대기
  • 완료 작업 완료 시 스레드풀이 메인 스레드에 이벤트 통지
  1. CPU 상태
  • 메인 스레드는 자유롭게 다른 이벤트 처리
  • 스레드풀 스레드는 I O 대기와 메모리 복사 구간에서만 CPU 사용 상태 전환
  1. 주의 스레드풀 크기 기본 4개를 초과하는 동시 파일 요청 시 큐 대기 증가로 지연 확대 UV_THREADPOOL_SIZE 튜닝 필요

예시 최소 스니펫 환경변수 기반 스레드풀 크기 조정

  • UV_THREADPOOL_SIZE=32 node app.js

D) DNS 조회 함정 주의

  • dns.lookup 도메인 OS의 getaddrinfo 호출을 사용 호출 자체는 블로킹 모델이므로 libuv 스레드풀에서 실행 JS는 블로킹되지 않지만 스레드풀 슬롯을 점유
  • dns.resolve 도메인 c-ares 기반 비동기 리졸버 사용 네트워크 I O처럼 동작하며 스레드풀 미사용 커널 비동기 메커니즘과 이벤트 루프로 처리

CPU 관점에서 자주 헷갈리는 개념

Node.js는 싱글 스레드다

정확히는 JS 코드 실행 컨텍스트가 메인 스레드 하나라는 의미 실제 프로세스는 libuv 스레드풀 기본 4개와 V8 내부 스레드 GC, 최적화 등 포함 다수 OS 스레드 사용

Async 작업은 모두 스레드풀에서 돈다

아님 네트워크 I O HTTP, TCP는 스레드풀을 거치지 않고 epoll kqueue 등 커널 비동기를 사용 CPU 효율 우수 파일 I O, 암호화, 압축, DNS lookup 등은 스레드풀 사용

CPU 코어가 많으면 Node.js 하나로 충분한가

아님 메인 스레드는 한 번에 코어 1개만 사용 가능 코어 8개면 프로세스 다중화 cluster 또는 worker_threads로 병렬성 확보 필요

실무 진단 CPU 패턴으로 병목 찾기

모니터링의 CPU 그래프와 지연 시간으로 병목 추론

증상CPU 패턴예상 원인조치
응답 매우 느림CPU 100% Core 1개CPU bound 무한루프, 큰 연산프로파일링으로 핫스팟 식별, 워커 분리
응답 느림CPU 낮음 IdleI O bound 외부 API, DB 지연외부 서비스 상태 점검, 쿼리 튜닝
간헐적 멈춤톱니바퀴 모양 스파이크GC 가비지 컬렉션 또는 Burst 작업메모리 누수 점검, 힙 사이즈 조정
파일 처리 지연CPU 보통, Latency 증가스레드풀 고갈UV_THREADPOOL_SIZE 증가 고려

보강 팁

  • TLS 핸드셰이크의 암 복호화는 crypto 연산으로 스레드풀 사용 가능 네트워크 I O 자체와 분리해 관찰 권장
  • GC 스파이크는 이벤트 루프 지연으로 직결 샘플링 프로파일링과 힙 스냅샷으로 원인 추적 권장
  • 파일 I O가 지배적이면 스토리지 대역폭과 IOPS 지표를 함께 확인 필요

요약

  1. 메인 스레드 보호가 최우선 JS 실행이 막히면 서버 전체가 멈춘 것과 다름없음
  2. I O는 대기 시간을 커널이 책임 Node.js는 이 기다림을 비동기로 효율 처리
  3. 스레드풀은 만능 아님 파일 시스템과 무거운 연산을 위한 보조 수단 네트워크 처리는 스레드풀과 무관하게 더 효율적으로 동작

마무리

CPU 코어와 OS 스레드의 물리적 제약, 커널 비동기 모델, libuv 스레드풀의 역할을 분리해 보면 병목 지점이 선명해짐 워커 스레드 도입, 프로세스 다중화, 스레드풀 튜닝, 외부 I O 지연 제거 순으로 단계적 최적화 권장

참고자료