Zettelkasten

Python 3.11 gevent 성능은 greenlet 패치 버전에 좌우된다 - 3.2.4는 느리고 3.4.0+는 회복한다

·수정 1

요약

  • "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로 동일했다. 이유:

  1. switch 절약은 ns 단위인데 실제 DB 쿼리는 ms 단위 → 한 쿼리 옆에서 0.01% 수준.
  2. gevent의 datastore wall-clock 부풀림은 hub가 바쁠 때(고동시성) 증폭되는데, 저부하에선 hub가 한가해 switch 비용이 누적되지 않는다.

실무 정리.

  • 3.11 + gevent를 유지한다면 greenlet을 **3.4.0+**로 올려 switch 회귀를 피한다(코드 변경 없이 lock 핀 한 줄).
  • 단 그 효과가 실제 레이턴시로 나타나는지는 피크 부하 카나리로 확인해야 한다. 마이크로벤치 수치를 prod 이득으로 곧장 환산하지 말 것.
  • 3.12+는 프레임 변경이 더 깊어 greenlet이 상쇄 불가 → gevent→asyncio 전환 선행 필요.

관련 노트

참고