gevent는 I∕O 동시성만 늘리므로 적용 전 워크로드가 CPU bound인지 확인해야 한다
·수정 1회
요약
- gevent가 공짜로 주는 건 "I/O 대기의 중첩"이지 "CPU 처리량"이 아니다. 워커 하나는 여전히 코어 하나 위에서 협력적으로 돈다.
- "web server = I/O bound"라는 통념과 달리, 요청 처리 안에 숨은 파이썬 CPU(파싱·직렬화)가 크면 인스턴스당 진짜 천장은 CPU다. 그래서 gevent 적용 전 워크로드 성격을 먼저 확인해야 한다.
본문
자연스럽지만 위험한 전제
- 비동기 프레임워크가 head-of-line blocking과 C10K를 해결하니, "web server는 I/O bound니까 gevent 적용 시 latency 저하 없이 인스턴스당 동시 처리량이 늘 것"이라 기대하기 쉽다.
- 함정은 gevent가 다중화하는 건 I/O 대기뿐이라는 점이다. 요청 처리 자체에 든 CPU는 그대로 남는다. 요청당 CPU 비용 × RPS 가 1코어 예산에 근접하면 I/O 동시성이 아무리 높아도 거기서 막힌다.
왜 CPU에서 막히면 latency가 비선형으로 나빠지나
- gevent 워커는 단일 OS 스레드 위 협력적 스케줄링이다 → 선점(preempt)이 없다.
- OS 스레드/프로세스는 커널이 CPU를 잘게 나눠주지만, CPU 무거운 그린렛 하나가 yield 없이 코어를 쥐면 나머지 그린렛이 전부 그 뒤에 줄 선다.
- 그래서 CPU가 차오를 때 tail latency가 비선형으로 악화된다. (서버를 줄여 인스턴스당 RPS를 올리면 코어가 포화로 향하며 이 현상이 나타남)
"I/O처럼 보이는 CPU"가 진짜 범인
- DB 호출은 I/O로 보이지만, 순수 파이썬 드라이버(pymysql)의 결과셋 디코딩/파싱은 전부 파이썬 CPU 작업이다. 소켓
recv에서만 yield하고, 받은 바이트를 파싱하는 구간은 hub를 블로킹한다.
- gevent monitoring thread를 켜면 hub switch가 100ms 막히는 케이스로 잡히고, py-spy는 pymysql 파싱을 가리킨다 → "어떤 그린렛이 100ms 동안 yield 없이 CPU를 점유했다"는 직접 증거.
- 구조적 트레이드오프: C 확장 드라이버(mysqlclient)는 monkeypatch와 협력하지 않아, gevent 호환을 위해 순수 파이썬 드라이버를 쓸 수밖에 없는 경우가 많다. gevent를 쓰려는 선택 자체가 CPU 비용을 키운다.
결론 — 적용 전 측정
- "I/O 대기"와 "I/O를 다루는 CPU"를 분리해서 봐야 한다. 커넥션 풀 같은 I/O 쪽을 고쳐도 병목이 CPU로 이동했을 뿐이면 처리량은 안 는다.
- 그러므로 gevent 적용 전, py-spy 등으로 워크로드가 실제 CPU bound인지 I/O bound인지 확인한다. web server라는 라벨만 믿지 말 것.
관련 노트
참고