Python 3.11 gevent 성능은 greenlet 패치 버전에 좌우된다 - 3.2.4는 느리고 3.4.0+는 회복한다
요약
- "Python 3.11이 gevent에 좋다/나쁘다"는 Python 버전만으로 결정되지 않고 greenlet 패치 버전에 좌우된다. greenlet 3.2.4는 3.11에서 context switch가 +60% 느리지만 3.4.0+에서 회복된다.
- 마이크로벤치에서 greenlet 3.2.4→3.5.1 bump가 switch 453→266ns로 회귀를 해소했으나, prod 저부하 카나리에선 DB per-call이 그대로였다 — ns 절약은 ms 쿼리 + 한가한 hub 앞에서 묻힌다.
본문
배경. gevent 기반 Django API 서버를 Python 3.10→3.11로 올린 뒤 New Relic에서 DB/external 시간이 늘었다. 원인 후보는 인터프리터, cp310→cp311로 재빌드된 gevent/greenlet, NR 계측 래퍼 wrapt(1.13.3→1.17.3) 셋. gevent/greenlet 버전 문자열은 동일(25.9.1 / 3.2.4)했지만 cp 빌드가 바뀌었다.
로컬 마이크로벤치로 분해 (arm64, gevent 25.9.1 고정, 항목별 중앙값). 측정 항목: greenlet.switch() 라운드트립, gevent monkey-patch된 loopback 소켓 왕복, wrapt 래핑 호출, 순수 함수 호출.
| 항목 | 3.10 (greenlet 3.2.4) | 3.11 (greenlet 3.2.4) | 3.11 (greenlet 3.5.1) |
|---|---|---|---|
| greenlet switch | 283 ns | 453 ns (+60%) | 266 ns (회복) |
| gevent 소켓 왕복 | 28.9 µs | 36.4 µs (+26%) | 26.6 µs |
| wrapt 래퍼 오버헤드 | 272 ns | 289 ns (+6%) | — |
| 순수 함수 호출(CPU) | 46.6 ns | 37.9 ns (−19%) | — |
- 범인은 greenlet, wrapt 아님. wrapt 1.13.3→1.17.3은 +6%로 무의미. 인터프리터 순수 CPU는 오히려 −19%(3.11이 빠름).
- gevent 버전은 무관, greenlet이 결정. greenlet 3.5.1이면 gevent 25.9.1이든 26.5.0이든 switch ~266ns로 동일.
- 즉 3.11에서 느려진 건 greenlet 3.2.4의 switch 경로이고, 3.4.0+에서 최적화돼 사라진다.
메커니즘. CPython 3.11이 프레임을 _PyInterpreterFrame 경량 구조체로 재설계(Faster CPython). greenlet은 C 스택을 통째로 swap하므로 switch마다 이 프레임 상태 저장/복원이 더 비싸졌다. greenlet 3.2.x는 미대응이라 +60%, 3.4.0+는 새 프레임 구조에 맞춰 최적화해 회복한다. (이래서 greenlet 3.4.0으로 측정하면 "3.11이 3.10보다 switch −5%"로 정반대 결론이 나온다 — python 3.11 이후부턴 gevent 성능이 악화될 수 있다. 의 벤치는 3.4.0 기준.)
그러나 prod에서 재현 안 됨 (가장 중요). 같은 OS(bookworm)에 greenlet만 3.5.1로 bump한 이미지를 운영 트래픽 카나리로 띄워 stock(3.2.4)과 비교했더니, 오후 저부하에서 DB per-call median이 2.38 ≈ 2.35ms로 동일했다. 이유:
- switch 절약은 ns 단위인데 실제 DB 쿼리는 ms 단위 → 한 쿼리 옆에서 0.01% 수준.
- gevent의 datastore wall-clock 부풀림은 hub가 바쁠 때(고동시성) 증폭되는데, 저부하에선 hub가 한가해 switch 비용이 누적되지 않는다.
- 원래 회귀(+36%)는 아침 피크에서 나온 것이므로, 마이크로벤치가 가리킨 fix가 prod에서 유효한지는 회귀가 난 부하 조건에서만 검증된다. 저부하 카나리는 거짓 음성을 낸다. (→ 변수 하나만 바꾼 카나리를 같은 타깃그룹에 동시 투입하면 부하 교란 없이 회귀 원인을 격리한다)
실무 정리.
- 3.11 + gevent를 유지한다면 greenlet을 **3.4.0+**로 올려 switch 회귀를 피한다(코드 변경 없이 lock 핀 한 줄).
- 단 그 효과가 실제 레이턴시로 나타나는지는 피크 부하 카나리로 확인해야 한다. 마이크로벤치 수치를 prod 이득으로 곧장 환산하지 말 것.
- 3.12+는 프레임 변경이 더 깊어 greenlet이 상쇄 불가 → gevent→asyncio 전환 선행 필요.
관련 노트
- python 3.11 이후부턴 gevent 성능이 악화될 수 있다.
- 변수 하나만 바꾼 카나리를 같은 타깃그룹에 동시 투입하면 부하 교란 없이 회귀 원인을 격리한다
- python 3.12부턴 cpython stack이 PyInterpreterFrame으로 관리된다.
- greenlet의 동작 방식
- 스택은 함수의 실행 정보를 프레임 단위로 저장한다