본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 고성능 서버를 위한 select, poll, epoll 3가지 차이와 해결 방법

by Papa Martino V 2026. 3. 18.
728x90

select, poll, epoll
select, poll, epoll

 

네트워크 프로그래밍에서 수만 개의 동시 접속을 처리하는 'C10K 문제'를 해결하는 것은 개발자의 숙명과도 같습니다. 파이썬(Python) 환경에서 다중 클라이언트 요청을 효율적으로 관리하기 위해 우리는 I/O 멀티플렉싱 기술을 사용합니다. 본 가이드에서는 시스템 호출 방식인 select, poll, epoll의 구조적 차이점을 심층 분석하고, 파이썬의 selectors 모듈이 이를 어떻게 추상화하여 최적의 성능을 끌어내는지 전문적인 식견을 바탕으로 설명합니다.


1. I/O 멀티플렉싱의 진화: 왜 epoll인가?

초기 유닉스 시스템에서 사용되던 select 방식은 관리해야 할 파일 디스크립터(FD)를 선언하고, 변화가 생길 때까지 전체를 루프(Loop) 돌며 확인하는 방식이었습니다. 하지만 접속자가 늘어날수록 이 '전수 조사' 비용은 기하급수적으로 증가합니다. 이를 개선하기 위해 등장한 것이 poll이며, 리눅스 환경에서 혁신적인 성능 향상을 가져온 것이 바로 epoll입니다.

주요 모델별 핵심 메커니즘 비교

비교 항목 select poll epoll (Linux 전용)
최대 FD 수 제한 있음 (보통 1024개) 무제한 (메모리 허용치까지) 무제한
검사 방식 전체 FD 순회 (O(n)) 전체 FD 순회 (O(n)) 이벤트 발생 FD만 확인 (O(1))
데이터 복사 매 호출 시 커널로 복사 매 호출 시 커널로 복사 최초 등록 시에만 복사
효율성 낮음 (연결이 많을수록 급감) 보통 매우 높음

2. 파이썬 selectors 모듈의 역할과 필요성

각 운영체제(OS)마다 지원하는 고성능 I/O 알림 메커니즘이 다릅니다. 리눅스는 epoll, BSD/macOS는 kqueue, 윈도우는 selectIOCP를 사용하죠. 개발자가 서비스 환경마다 코드를 다르게 작성하는 것은 매우 비효율적입니다.

파이썬의 selectors 모듈은 이러한 플랫폼별 차이를 해결하기 위해 등장했습니다. 이 모듈은 시스템에서 사용 가능한 가장 효율적인 I/O 멀티플렉싱 방식을 자동으로 선택하는 DefaultSelector 클래스를 제공합니다. 즉, 개발자는 저수준의 시스템 콜을 고민할 필요 없이 단일화된 인터페이스로 고성능 비동기 서버를 구축할 수 있습니다.


3. [Practical Example] selectors를 활용한 Echo 서버 구현

다음은 selectors 모듈을 사용하여 수천 개의 클라이언트를 동시 처리할 수 있는 구조의 파이썬 코드 예시입니다. 이 코드는 시스템이 epoll을 지원하면 자동으로 epoll을 사용합니다.


import selectors
import socket

# 시스템에 최적화된 셀렉터 선택
sel = selectors.DefaultSelector()

def accept_wrapper(sock):
    conn, addr = sock.accept()
    print(f"Accepted connection from {addr}")
    conn.setblocking(False)
    # 읽기 이벤트를 감시하기 위해 등록
    data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"")
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    sel.register(conn, events, data=data)

def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)
        if recv_data:
            data.outb += recv_data
        else:
            print(f"Closing connection to {data.addr}")
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if data.outb:
            sent = sock.send(data.outb)
            data.outb = data.outb[sent:]

# 서버 소켓 설정
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind(('localhost', 65432))
lsock.listen()
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)

try:
    while True:
        events = sel.select(timeout=None)
        for key, mask in events:
            if key.data is None:
                accept_wrapper(key.fileobj)
            else:
                service_connection(key, mask)
except KeyboardInterrupt:
    print("Caught keyboard interrupt, exiting")
finally:
    sel.close()

4. 성능 병목 해결을 위한 전문가의 제언

대규모 트래픽 환경에서 select를 사용하면 CPU 점유율이 100%까지 치솟는 현상을 목격하게 됩니다. 이는 커널과 유저 공간 사이에서 FD 리스트를 반복적으로 복사하기 때문입니다. 해결 방법은 명확합니다.

  • 리눅스 서버 환경: 반드시 epoll 기반의 selectors를 사용하십시오.
  • 파일 디스크립터 관리: ulimit -n 설정을 통해 프로세스가 가질 수 있는 최대 파일 열기 제한을 늘려야 합니다.
  • Edge Triggered vs Level Triggered: epoll의 작동 방식 중 상태 변화 시에만 알림을 주는 Edge Triggered 방식을 이해하면 더 정교한 제어가 가능합니다.

5. 결론: 어떤 방식을 선택해야 하는가?

현대적인 파이썬 애플리케이션 개발에서 개별 시스템 콜(select, poll)을 직접 호출하는 것은 권장되지 않습니다. selectors.DefaultSelector를 사용하면 코드의 이식성을 유지하면서도 운영 환경에서 최상의 성능(epoll/kqueue)을 보장받을 수 있습니다. 이는 유지보수 비용을 줄이고 시스템의 안정성을 높이는 핵심적인 전략입니다.


참고 문헌 및 출처

  1. Python 3.12 Documentation - selectors: High-level I/O multiplexing
  2. The C10K Problem by Dan Kegel
  3. Linux Programmer's Manual - epoll(7), poll(2), select(2)
  4. "Hands-On Network Programming with Python" by Sam Bull
728x90