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

함정 ② — 루프 위의 블로킹 호출

자리를 비켜 줄 줄 모르는 옛날 도구를 그 창구에서 쓰면 어떻게 될까요? 다른 사람들에게는 "차례가 되면 부르겠다"고 해 놓고, 정작 자기는 답이 올 때까지 창구에 앉아 기다리는 셈입니다.

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

세상의 모든 라이브러리가 논블로킹인 것은 아닙니다. 오래된 디비 드라이버, 동기 파일 입출력, 일부 HTTP 클라이언트는 결과가 올 때까지 그냥 멈춰 섭니다. 이런 함수를 이벤트루프 위에서 부르면 무슨 일이 벌어질까요? 아래 토글로 직접 확인해 보세요.

① 유일한 스레드가 잠든다

이벤트루프에는 스레드가 하나뿐입니다. 그 스레드가 블로킹 함수를 부르면 7단원에서 본 것처럼 통째로 Waiting 상태가 되어 CPU를 떠납니다. 대기 목록에 이벤트가 아무리 쌓여 있어도 꺼내 줄 사람이 없습니다.

② 결과: 10단원의 스레드 1개짜리 서버로 퇴화

토글을 켜면 완료 속도가 급격히 떨어집니다. 수십 개의 요청을 동시에 굴리던 서버가, 한 번에 하나씩만 처리하는 서버로 되돌아갑니다. 처리량이 스레드 수(=1)로 제한되기 때문입니다.

③ 실무의 교훈

리액티브 스택에 블로킹 디비 드라이버 한 줄, Node에 동기 파일 읽기 한 줄이면 충분합니다. 그래서 이벤트루프 기반 프레임워크는 "이 스택에서는 모든 IO가 논블로킹이어야 한다"는 규칙을 강하게 요구합니다. 불가피하게 블로킹 라이브러리를 써야 한다면 반드시 별도의 스레드풀로 격리해야 합니다.

④ 왜 이 실수가 반복되나

블로킹 호출은 문법적으로 아무 문제가 없습니다. 컴파일도 되고 테스트도 통과합니다. 부하가 낮을 때는 심지어 잘 동작합니다. 문제가 드러나는 것은 동시 요청이 늘어난 순간뿐입니다. 타입 시스템이 막아주지 않기 때문에, 리액티브 스택에 동기 DB 드라이버 한 줄, 코루틴 안에 동기 HTTP 클라이언트 한 줄이 자연스럽게 섞여 들어갑니다.

⑤ 블로킹은 CPU가 아니라 자원을 묶는다

여기서 반드시 구분해야 할 것이 있습니다. 블로킹된 스레드는 CPU를 쓰지 않습니다. 그래서 CPU 사용률은 낮은데 서버는 아무것도 처리하지 못하는 상태가 됩니다. 게다가 그 스레드가 DB 커넥션이나 락을 쥔 채 멈춰 있으면 그 자원까지 함께 묶입니다. 실제 장애의 상당수가 이 연쇄입니다 — 느린 외부 호출 하나가 스레드를 묶고, 스레드가 커넥션을 묶고, 커넥션 풀이 마르면 그 서비스의 모든 요청이 죽습니다.

⑥ 그래서 격리와 타임아웃

블로킹을 완전히 없앨 수 없다면 두 가지를 반드시 갖춰야 합니다. 하나는 격리입니다. 블로킹 작업을 전용 스레드풀로 몰아넣어 그 풀이 고갈되더라도 나머지 기능은 살아남게 합니다. 다른 하나는 타임아웃입니다. 타임아웃 없는 블로킹 호출은 상대가 응답하지 않는 한 영원히 자원을 쥡니다. 상대의 장애가 곧 내 장애가 되는 가장 흔한 경로입니다.

속도블로킹 호출

시뮬레이터에서 해보기

  1. 1.이 함정의 서버 버전을 프리셋으로 확인 — '외부 의존 부분 장애(cascading failure)' 프리셋을 엽니다. 외부 API 노드가 30% 실패 + 500ms 추가 지연으로 설정되어 있습니다.
  2. 2.500 RPS × 홀드 0.56초(WAN 왕복 60ms + 500ms) = 280개의 동시 요청이 필요한데 Tomcat threads.max는 200뿐 — 인스펙터에서 HTTP client pool의 max connections가 200/200으로 항상 꽉 차 있는 것을 확인하세요.
  3. 3.KPI 패널: Success 50.3%, 드롭은 queue-full 1,285 · downstream-error 1,056 · timeout 1,056 — '스레드가 커넥션을 묶고, 그게 전면 장애로 번진다'는 이 단원의 문장이 숫자로 이렇게 나타납니다. 외부 노드의 fault injection을 인스펙터에서 꺼보면 즉시 100%로 돌아옵니다.
시뮬레이터에서 실험해보기 →

이 단원의 용어

블로킹 드라이버
결과가 올 때까지 호출한 스레드를 멈춰 세우는 라이브러리.
스레드 격리
블로킹 작업을 별도 스레드풀로 넘겨 이벤트루프를 보호하는 기법.
bulkhead
기능별로 스레드풀을 나눠, 한쪽이 고갈돼도 나머지가 살아남게 하는 격리.
커넥션 풀 고갈
블로킹된 스레드들이 커넥션을 쥔 채 멈춰 풀이 비는 상태. 전면 장애로 이어진다.
타임아웃
기다림의 상한. 없으면 상대의 장애가 그대로 내 장애가 된다.

루프 위의 블로킹 한 줄 = 서버 전체의 정지.

남은 문제

여기까지가 방법과 함정입니다. 그런데 이 방법들은 왜 하필 이 순서로 태어났을까요? →

← 이전 단원

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

다음 단원 →

15단원. 어떻게 여기까지 왔나 — 60년의 발전사