비동기 코어: Flask-SocketIO에서 Eventlet과 Gevent 이해하기

발행: (2025년 12월 16일 오전 06:29 GMT+9)
11 min read
원문: Dev.to

Source: Dev.to

The Blocking Problem

표준 스레딩이 규모에 따라 왜 실패하는지를 먼저 분석해야 Eventlet과 Gevent의 필요성을 이해할 수 있습니다. 전통적인 WSGI 배포(예: sync 또는 gthread 워커를 사용하는 Gunicorn)에서는 동시성이 OS 수준의 스레드 또는 프로세스와 1:1로 매핑됩니다.

Flask‑SocketIO 서버가 표준 OS 스레드를 사용해 WebSocket 연결을 관리한다면 두 가지 주요 병목 현상이 발생합니다:

  • Memory Overhead: 일반적인 Linux 스레드는 보통 8 MB의 스택 크기를 예약합니다. 가상 메모리 관리가 즉각적인 물리적 비용을 완화하지만, 커밋 차지와 커널 구조(Thread Control Blocks)는 여전히 무거운 발자국을 남깁니다. 10 000개의 유휴 WebSocket 클라이언트를 위해 10 000개의 스레드를 생성한다면 이론적으로 약 80 GB의 주소 가능한 메모리 공간이 필요하게 되며, CPU 한계에 도달하기도 전에 자원 고갈이 발생합니다.
  • Context Switching Latency: OS 커널 스케줄러는 선점형 멀티태스킹을 사용해 스레드 실행을 관리합니다. 스레드 수가 증가함에 따라 스케줄러는 다음에 실행할 스레드를 결정하는 데 CPU 사이클의 비중이 점점 늘어나게 됩니다(컨텍스트 스위칭). 이러한 “thrashing”은 처리량을 크게 저하시킵니다.

게다가 Python의 Global Interpreter Lock(GIL)은 한 번에 하나의 스레드만 Python 바이트코드를 실행하도록 보장합니다. I/O 작업(예: 소켓 메시지 대기)이 GIL을 해제하긴 하지만, 수천 개 스레드를 관리하는 오버헤드는 여전히 금지적입니다.

Greenlets Explained

Eventlet과 Gevent는 greenlet C‑extension 라이브러리를 통해 코루틴(협력형 사용자‑공간 스레드)을 구현함으로써 차단 문제를 해결합니다. OS 스레드와 달리 greenlet은 커널 개입 없이 전적으로 사용자 공간에서 관리됩니다.

The Mechanism: Stack Slicing

greenlet의 기술적 핵심은 호출 스택을 관리하는 방식에 있습니다. CPython 인터프리터는 함수 호출에 표준 C 스택을 사용합니다. 함수가 I/O에서 블록될 때 실행을 중간에 멈추려면 스택 상태를 보존해야 합니다.

greenlet이 컨텍스트를 전환(양보)할 때:

  1. Stack Slicing: 라이브러리는 현재 greenlet의 C 스택 일부를 CPU의 스택 포인터에서 힙에 있는 버퍼로 복사합니다.
  2. Stack Restoration: 대상 greenlet이 저장해 둔 스택을 힙에서 다시 C 스택으로 복사합니다.
  3. Instruction Pointer Update: 명령 포인터를 업데이트해 대상 greenlet이 중단된 지점부터 실행을 재개하도록 합니다.

이 “트램폴린” 메커니즘 덕분에 Python은 중첩된 함수 호출 깊숙이 들어가 있는 상태에서도(심지어 C‑extension 경계까지) 스택이 무한히 커지는 일 없이 실행을 일시 중지할 수 있습니다.

Efficiency

greenlet은 동일한 OS 스레드와 프로세스 메모리를 공유하므로 컨텍스트 스위치는 시스템 콜이 아닌 memcpy 연산만으로 이루어집니다. 따라서 컨텍스트‑스위치 시간이 OS 스레드의 마이크로초 수준에서 나노초 수준으로 감소합니다. 또한 greenlet의 초기 스택 크기는 매우 작아(몇 킬로바이트 수준) 하나의 프로세스가 수만 개의 동시 greenlet을 호스팅할 수 있습니다.

Monkey Patching: The “Magic” Integration

표준 Python 라이브러리(socket, time 등)는 차단(blocking)됩니다. 일반 Flask 라우트에서 time.sleep(10)이나 socket.recv()를 호출하면 전체 OS 스레드가 멈춥니다. Eventlet/Gevent가 단일 OS 스레드 위에서 동작하기 때문에, 하나의 차단 호출만으로도 서버 전체가 정지해 모든 클라이언트가 멈추게 됩니다.

How It Works

Monkey patching은 런타임에 표준 라이브러리를 동적으로 수정합니다. eventlet.monkey_patch() 혹은 gevent.monkey.patch_all()을 실행하면 라이브러리는 sys.modules를 다음과 같이 바꿉니다:

  • 표준 socket 클래스를 “green” 소켓 클래스로 교체합니다.
  • threading.Thread를 greenlet 기반 구현으로 교체합니다.

Execution Flow of a “Green” Socket

  1. Intercept: 사용자 코드가 socket.recv()를 호출합니다. Monkey patching 덕분에 OS 버전이 아니라 Gevent/Eventlet 버전이 호출됩니다.
  2. Register: green 소켓은 Hub(중앙 이벤트 루프)에 콜백을 등록합니다. 이 워처는 “파일 디스크립터 X에 읽을 데이터가 생기면 깨워 주세요”라고 Hub에 알립니다.
  3. Yield: green 소켓은 greenlet.switch()를 호출해 현재 요청의 실행을 일시 중지하고 제어권을 Hub에 양보합니다.
  4. Wait: Hub는 고성능 비동기 폴링 메커니즘(보통 Linux에서는 epoll, macOS에서는 kqueue)을 사용해 모든 파일 디스크립터의 I/O 이벤트를 검사합니다.
  5. Resume: 소켓에 데이터가 도착하면 Hub가 이벤트를 감지하고 콜백을 트리거해 원래 greenlet으로 실행을 전환합니다.

Flask 개발자 입장에서는 코드가 동기식(data = sock.recv(1024))처럼 보이지만, 실제로는 비동기적이고 논블로킹으로 동작합니다.

The Risks of Monkey Patching

강력하지만, monkey patching은 중요한 엔지니어링 위험을 동반합니다:

  • C‑Extension Incompatibility: Python 소켓 API를 우회해 직접 OS 시스템 콜을 수행하는 C로 작성된 라이브러리(예: 특정 DB 드라이버나 오래된 gRPC 버전)는 패치할 수 없습니다. 이런 라이브러리가 블록되면 전체 루프가 멈춥니다.
  • Order of Operations: 패치는 socket이나 threading을 임포트하기 이전에 수행되어야 합니다. 늦게 패치하면 앱의 일부는 green 소켓을, 다른 일부는 차단 OS 소켓을 사용하게 되어 “분할 뇌(split brain)” 상황이 발생하고 교착 상태가 생길 수 있습니다.

Choosing Your Fighter: Eventlet vs. Gevent vs. Threading

Flask‑SocketIO를 설정할 때 async_mode를 선택해야 합니다.

Threading

  • Concurrency Model: 표준 OS 스레드.
  • Pros: 최대 호환성. Monkey patching이 필요 없으며 모든 서드파티 라이브러리와 동작합니다.
  • Cons: 확장성 부족. 수백 명의 클라이언트는 감당할 수 있지만, 메모리와 컨텍스트 스위칭 오버헤드 때문에 수천 명에서는 한계에 부딪힙니다.
  • Use Case: 개발, 디버깅, 혹은 트래픽이 적은 내부 도구.

Eventlet

  • Concurrency Model: Greenlet.
  • Architecture: 과거 Flask‑SocketIO의 기본값이었으며, 주로 epoll을 감싸는 순수 파이썬 허브를 사용합니다.
  • Status (2024/2025): Deprecated. Eventlet 프로젝트는 현재 유지보수 모드(“life support”)에 머물고 있습니다. 새로운 기능 개발은 중단됐으며, 최신 Python 버전(3.10+)과의 호환성도 뒤처지는 경향이 있습니다.
  • Performance: 높지만, 원시 처리량 벤치마크에서는 Gevent보다 약간 느립니다.
  • Use Case: 레거시 애플리케이션. 신규 프로젝트에서는 Eventlet 사용을 피하세요.

Gevent

  • Concurrency Model: Greenlet.
  • Architecture: libev(고성능 C 라이브러리)와 Cython 위에 구축되었습니다.
  • Status: Active. Gevent는 활발히 유지보수되고 있으며 견고합니다.
  • Performance: 매우 높음. C 기반 허브와 루프 덕분에 Eventlet보다 낮은 레이턴시와 우수한 성능을 제공합니다.
  • Use Case: 높은 동시성을 요구하는 프로덕션 Flask‑SocketIO 배포에 권장되는 선택지.

Conceptual Benchmark Comparison

Under a workload of 5,000 concurrent WebSocket connections sending “heartbeat” messages… (benchmark details omitted).

Back to Blog

관련 글

더 보기 »