자리를 비켜 줄 줄 모르는 옛날 도구를 그 창구에서 쓰면 어떻게 될까요? 다른 사람들에게는 "차례가 되면 부르겠다"고 해 놓고, 정작 자기는 답이 올 때까지 창구에 앉아 기다리는 셈입니다.
기초에서 먼저 보기이벤트루프는 언제 유리하고 언제 불리할까?세상의 모든 라이브러리가 논블로킹인 것은 아닙니다. 오래된 디비 드라이버, 동기 파일 입출력, 일부 HTTP 클라이언트는 결과가 올 때까지 그냥 멈춰 섭니다. 이런 함수를 이벤트루프 위에서 부르면 무슨 일이 벌어질까요? 아래 토글로 직접 확인해 보세요.
이벤트루프에는 스레드가 하나뿐입니다. 그 스레드가 블로킹 함수를 부르면 7단원에서 본 것처럼 통째로 Waiting 상태가 되어 CPU를 떠납니다. 대기 목록에 이벤트가 아무리 쌓여 있어도 꺼내 줄 사람이 없습니다.
토글을 켜면 완료 속도가 급격히 떨어집니다. 수십 개의 요청을 동시에 굴리던 서버가, 한 번에 하나씩만 처리하는 서버로 되돌아갑니다. 처리량이 스레드 수(=1)로 제한되기 때문입니다.
리액티브 스택에 블로킹 디비 드라이버 한 줄, Node에 동기 파일 읽기 한 줄이면 충분합니다. 그래서 이벤트루프 기반 프레임워크는 "이 스택에서는 모든 IO가 논블로킹이어야 한다"는 규칙을 강하게 요구합니다. 불가피하게 블로킹 라이브러리를 써야 한다면 반드시 별도의 스레드풀로 격리해야 합니다.
블로킹 호출은 문법적으로 아무 문제가 없습니다. 컴파일도 되고 테스트도 통과합니다. 부하가 낮을 때는 심지어 잘 동작합니다. 문제가 드러나는 것은 동시 요청이 늘어난 순간뿐입니다. 타입 시스템이 막아주지 않기 때문에, 리액티브 스택에 동기 DB 드라이버 한 줄, 코루틴 안에 동기 HTTP 클라이언트 한 줄이 자연스럽게 섞여 들어갑니다.
여기서 반드시 구분해야 할 것이 있습니다. 블로킹된 스레드는 CPU를 쓰지 않습니다. 그래서 CPU 사용률은 낮은데 서버는 아무것도 처리하지 못하는 상태가 됩니다. 게다가 그 스레드가 DB 커넥션이나 락을 쥔 채 멈춰 있으면 그 자원까지 함께 묶입니다. 실제 장애의 상당수가 이 연쇄입니다 — 느린 외부 호출 하나가 스레드를 묶고, 스레드가 커넥션을 묶고, 커넥션 풀이 마르면 그 서비스의 모든 요청이 죽습니다.
블로킹을 완전히 없앨 수 없다면 두 가지를 반드시 갖춰야 합니다. 하나는 격리입니다. 블로킹 작업을 전용 스레드풀로 몰아넣어 그 풀이 고갈되더라도 나머지 기능은 살아남게 합니다. 다른 하나는 타임아웃입니다. 타임아웃 없는 블로킹 호출은 상대가 응답하지 않는 한 영원히 자원을 쥡니다. 상대의 장애가 곧 내 장애가 되는 가장 흔한 경로입니다.
시뮬레이터에서 해보기
이 단원의 용어
루프 위의 블로킹 한 줄 = 서버 전체의 정지.
남은 문제
여기까지가 방법과 함정입니다. 그런데 이 방법들은 왜 하필 이 순서로 태어났을까요? →