epoll이 select/poll보다 효율적인 이유

1,497 단어·3 분·원문(.md)

네트워크 프로그래밍에서 io 멀티플렉싱 모델인 select, poll, epoll의 차이를 이해하는것이 고성능 서버 구축의 핵심인데

왜 epoll이 압도적을 효율적인지 알아보도록 하겠다.

공통점 부터 알아보면 select/poll, epoll은 둘다 io 멀티플렉싱이라는 기술을 구현하기 위한 시스템 콜이고

어떤 소켓에 데이터가 돛악했는지, 혹은 데이터를 보낼수있는 상태인지 실시간으로 체크하고 아무런 이벤트가 없을때는 프로세스를 sleep으로 두어 cpu자원을 아끼며 감시하던 소켓중에

하나라도 작업준비가 완료되면 프로세스를 wake up 시켜서 지금 일할 준비된 놈들이 나타났다고 알려준다.

커널로의 데이터 복사 비용 memory copy #

가장 직관적인 차이는 감시 대상 목록을 관리하는 방식에 있다.

  • select/poll: 호출할 때마다 감시하고자 하는 파일 디스크립터의 전체 목록을 사용자 공간에서 커널 공간으로 복사해야한다. 매번 수천개의 fd를 커널에 넘겨주는 작업이 cpu에 부담이 된다.
  • epoll: epoll_ctl 함수를 통해 커널 내부의 관심 리스트 interest list를 미리 등록해두고 이후에 epoll_wait를 호출할때는 변경사항이 있는 fd만 주고받으면 되어서 매번 전체 목록을 복사하는 오버헤드가 사라진다.

데이터 구조 및 탐색 시간 복잡도 #

커널이 파일 디스크립터를 관리하고 상태 변화를 확인하는 방식에서 성능 차이가 극명하게 갈린다.

select/poll은 O(N)

  • 단순 배열 poll이나 비트맵 select 형태인데
  • 어떤 데이터가 왓는지 알기 위해서 fd를 처음부터 끝까지 루프해서 확인해야한다.
  • 문제는 연결된 클라이언트가 10000명일때 1명이 데이터를 보냈어도 커널 10000개를 확인해야한다는것.

epoll은 O(1)에 가까움

  • 커널 내부에 레드 블랙 트리와 ready list(더블 링크드 리스트)를 사용한다
    • red black tree: fd의 삽입, 삭제, 검색을 O log N으로 처리한다.
    • Read List: 이벤트가 발생한 fd들만 따로 모아두는 리스트다.
  • 하드웨어 인터럽트를 통해 데이터가 들어오면 커널이 해당 fd를 즉시 read list에 넣는다. epoll wait는 ready list에 담긴 정보만 반환하므로 전체 접속자수와 상관없이 이벤트가 발생한 수에 비례하는 성능을 보인다.

이벤트 감지 방식 level triggered vs edge trigger #

epoll은 select poll에 없는 edge trigger ET모드를 지원한다.

  • Level Trigger: 특정 상태가 유지되는 동안 계속 알림을 준다 데이터가 버퍼에 남아있어도 계속 알림을 줌
  • Edge Trigger: 상태가 변환하는 순간에만 알림을준다 데이터가 새로 도착한 시점에 딱 한번
    • 그래서 장점은 불필요한 시스템 콜 횟수를 획기적으로 줄일 수 있어 극강의 성능 최적화가 가능하다. 그러나 난이도는 상승함 프로그래밍

정리하면 select poll 방식은 누가 데이터를 보냈는지 확인하기 위해 모든 리스트를 탐색하며 epoll은 데이터를 보낸 사람만 확인하는 방식의 차이가 있다.

그래서 복사비용 및 fd 탐색 하는 속도 자체에서 극명한 차이가 있고 ET를 지원해 오버헤드가 적다.

fd 교환 #

fd를 전달하는 이유는 커널에게 내가 관심있는 이 통로들에 대해서 어떤 변화가 생기는지 감시해달라하기 위해서인데

네트워크 소켓이나 파일등 모든 io는 운영체제가 관리한다. 하지만 실제 데이터를 처리하는 것은 유저애플리케이션이다.

본질적으로 유저 프로그램이 나는 1번 소켓에서 데이터가 오길 기다리고 5번 소켓에는 데이터를 보낼 준비가 됐는지 알고싶다 라고 했을때 커널에게 리스트를 넘겨주는것이다.

fd는 유닉스 계열 시스템에서 모든 자원(소켓, 파이프등)은 숫자로 식별된 식별자인 fd로 관리되기 때문에 이걸 쓴다.

select나 epoll이 없다면 우리는 하나의 소켓에서 데이터를 읽기위해 read를 호출하고 데이터가 올때까지 blocking 되어있어야한다.

1000개의 연결이 있다면 1000개의 스레드를 만들거나 하나씩 순서대로 확인해야하는데 매우 비효율적이다.

대신 fd 리스트를 커널에 한꺼번에 전달하면 커널은 이들을 동시에 지켜보다가 어떤 fd에서 이벤트가 발생했는가 신호를 주고 그때 깨워서 실행한다.

SRE/question/q_17.md