serversim
학습시뮬레이터템플릿가격
…
…
메뉴 열기
학습↗시뮬레이터↗템플릿↗가격↗
내 Documents문서
목차/13단원13 / 16

함정 ① — CPU-bound 하나가 루프를 얼린다

자리 하나로 여러 일을 번갈아 처리하는 서버에서, 가장 위험한 요청은 어떤 것일까요? 모두가 짧게 쓰고 자리를 비켜 주기로 한 창구에, 한 사람이 앉아 30분짜리 서류를 쓰기 시작한 상황입니다.

기초에서 먼저 보기이벤트루프는 언제 유리하고 언제 불리할까?→

이제 그 약속을 깨보겠습니다. 요청 하나가 CPU를 오래 쓰는 작업(큰 이미지 변환, 무거운 암호화, 거대한 JSON 파싱)을 한다고 해봅시다. 왼쪽은 스레드풀, 오른쪽은 이벤트루프로 같은 요청 흐름을 처리합니다. 빨간색이 그 무거운 작업입니다.

① 스레드풀은 왜 버티나

6단원의 선점 덕분입니다. 무거운 작업이 CPU를 오래 쥐고 있으면 타이머 인터럽트가 강제로 회수해 다른 스레드에게 넘깁니다. 그래서 레인 하나가 바빠질 뿐, 나머지 요청들은 사이사이로 계속 빠져나갑니다. 운영체제가 공평함을 보장해 주는 셈입니다.

② 이벤트루프는 왜 무너지나

이벤트루프에는 그런 강제 장치가 없습니다. 콜백 하나를 시작하면 그 콜백이 끝날 때까지 다음 이벤트를 꺼내지 못합니다(run-to-completion). 빨간 작업이 CPU를 쥔 동안 나머지 요청은 큐에 그대로 쌓인 채 아무 진전이 없습니다. 오른쪽 패널의 완료 숫자가 멈춰 있는 것을 확인하세요.

③ 그래서 실무에서는

이벤트루프 기반 서버(Node.js, WebFlux 등)에서는 무거운 계산을 이벤트루프 스레드에서 하면 안 됩니다. 별도의 워커 스레드나 별도 프로세스로 넘기거나, 작업을 잘게 쪼개 중간중간 양보해야 합니다. "코루틴/Node가 불리한 작업이 무엇이냐"는 면접 질문의 정답이 바로 이 장면입니다.

④ 얼마나 무거워야 문제인가 — 숫자로

감이 잘 안 오면 이렇게 계산합니다. 이벤트루프에서 콜백 하나가 10밀리초를 쓰면 그 루프는 초당 최대 100개밖에 처리하지 못합니다. 50밀리초면 20개입니다. 게다가 뒤에 온 요청은 앞의 콜백이 끝날 때까지 통째로 기다리므로, 꼬리 지연시간이 가장 무거운 콜백의 실행시간에 그대로 묶입니다. 이벤트루프에서는 평균이 아니라 최악의 콜백 하나가 전체 품질을 결정합니다.

⑤ 어떻게 발견하나

CPU 사용률만 봐서는 잘 보이지 않습니다. 코어는 100%로 바쁜데 처리량이 늘지 않는 모양이기 때문입니다. 그래서 런타임들은 별도의 지표를 줍니다. Node는 이벤트루프 지연을, 리액터 계열은 블로킹 호출 탐지기를, JVM은 JFR로 스레드 상태를 보여줍니다. 루프 한 바퀴가 도는 데 걸린 시간이 튀는 구간을 찾으면 범인이 나옵니다.

⑥ 해결은 세 가지뿐

첫째, 무거운 계산을 루프 밖으로 내보냅니다 — 워커 스레드, 별도 프로세스, 또는 큐를 통한 비동기 처리입니다. 둘째, 쪼개서 양보합니다. 큰 반복을 조각내고 조각 사이마다 루프에 제어를 돌려주면 다른 요청이 끼어들 틈이 생깁니다. 셋째, 애초에 모델을 바꿉니다 — 요청당 계산이 무거운 서비스라면 스레드풀이 더 맞습니다. 운영체제의 선점이 공평함을 공짜로 보장해 주기 때문입니다.

속도CPU-bound 주입

시뮬레이터에서 해보기

  1. 1.무거운 요청이 서버 전체를 어떻게 흔드는지 프리셋으로 확인 — '3-tier 기본'을 열고 client 노드의 RPS 슬라이더를 봅니다(기본 1,000, Spring 카드 CPU 63%).
  2. 2.RPS를 1,500으로 올리면 Spring 카드의 CPU가 91%로, 1,700에서는 CPU 99%(KPI 패널 p99 291ms)로 치솟습니다 — 앱 한 대의 진짜 상한이 이 근처입니다.
  3. 3.2,000까지 올리면 accept queue가 넘쳐 queue-full 드롭이 시작되고 KPI 패널의 Success가 89.2%로 떨어집니다 — Drop rate와 Success를 함께 보세요.
시뮬레이터에서 실험해보기 →

이 단원의 용어

run-to-completion
한번 시작한 콜백을 끝까지 실행하는 방식. 중간에 뺏기지 않는다.
협력적 스케줄링
각자가 알아서 양보해야 돌아가는 방식. 이벤트루프가 여기 해당한다.
이벤트루프 블로킹
무거운 작업이 루프를 점유해 다른 모든 요청이 지연되는 현상.
이벤트루프 지연
루프 한 바퀴가 늦어진 정도. 이 함정을 잡는 가장 직접적인 지표.
오프로드
무거운 작업을 루프 밖(워커·별도 프로세스)으로 넘기는 것.

협력적 스케줄링은, 협력하지 않는 코드 앞에서 무너진다.

남은 문제

함정이 하나 더 있습니다. 이번엔 CPU가 아니라 "잘못된 기다림"입니다 →

← 이전 단원

12단원. 코루틴 / async-await — 콜백을 동기 모양으로

다음 단원 →

14단원. 함정 ② — 루프 위의 블로킹 호출