4 분 소요

Epoll I/O 모델에 대해 소개하는 글입니다.



I/O Models

고성능 서버를 위해 필요한 것

고성능 서버는 가능한 적은 부하로 연결된 클라이언트들에 서비스를 제공합니다.
즉, 단일 요청을 서버 성능 저하(latency 등)없이, 많은 동시 연결을 처리할 수 있어야 합니다.
서버 성능은 애플리케이션 로직도 영향이 크지만, 연결 처리가 중요합니다.


왜 동시 연결일까요? 수명이 짧은 요청의 경우 큰 문제가 아니지만, 요청 처리가 길어질수록
처리 중인 요청들이 누적됩니다.
예를 들어, 서버가 초당 1000 개의 요청을 처리한다면, 서버가 정상 상태에서 10,000 개의
동시 요청을 처리하려면 10초가 걸립니다.
보다 자세한 내용은 C10K 문제를 참고하세요.


일반적으로 서버는 코어들을 활용하여 성능을 향상시킬 수도 있습니다.
그리고 고성능 서버 아키텍처는 다음 성능 저하의 4 가지 요소를 잘 다뤄야 합니다.

  • Data copies
  • Context switches
  • Memory allocation
  • Lock contention



IO

IO는 요청 처리의 기본 요소입니다.

IO 에는 요청 데이터를 read, 응답 데이터를 write 하고 일부 Disk IO
애플리케이션 서버에 대한 네트워크 함수 호출 이 있습니다.


대부분의 예제 네트워킹 코드는 다음과 같습니다.

for conn in open_connections:
    read(conn)

특히 시스템 호출 read 는 기본적으로 blocking 되므로, 원격에서 데이터를 보내지 않으면
계속 대기하여 다른 연결에 서비스를 제공할 수 없습니다.
디스크 IO, 응답 쓰기 등의 Wait 도 마찬가지입니다.
긴 IO 호출에도 불구하고 요청을 처리할 수 있는 방법이 필요합니다.

서버를 위해 I/O 를 처리하는 5가지 모델이 있습니다.
모델을 보기 전에 I/O 를 2단계로 생각하면 도움이 됩니다:

  1. 데이터가 없음/데이터가 컨트롤러(NIC)에 있음 -> 커널에서 데이터를 사용할 수 있게 됩니다.
  2. 커널 내의 데이터 -> 응용 프로그램 메모리의 데이터

이제 다섯 가지 모델에 대해 알아보겠습니다:


1. 블로킹(Blocking) IO

데이터가 도착하여 애플리케이션에 복사될 때까지 시스템 호출이 반환되지 않습니다.
스레드가 차단된 후 다른 작업을 처리하기 위해서는 멀티스레드 환경이 필요합니다.


그림 1. 블로킹 IO.



2. 넌블로킹(Nonblocking) IO

시스템 호출에서 데이터를 사용할 수 없는 경우 블로킹되지 않고, EWOULDBLOCK 을 즉시 반환합니다.
즉, 이제 모든 연결의 루프를 통해서 필요에 따라 서비스를 제공 할 수 있지만, 사용할 수 있는
연결이 없으면 폴링(바쁜 대기, busy waiting)을 끝내고 CPU를 사용할 수 있습니다.


그림 2. 넌블로킹 IO.



3. I/O Multiplexing: Select/Poll/Epoll/Kqueue

한 번에 하나의 파일 디스크립터(File Descriptor 이하 FD)에서만 다루는 대신
많은 파일 디스크립터의 변경 사항을 모니터하는 방법이 필요할 수 있습니다.
다음은 여러 폴링 방법/반복에 대한 설명입니다.


그림 3. I/O Multiplexing.


select 는 FD 목록을 전달하면 각 FD의 상태를 반환합니다.
select는 성능 제한이 상당히 큽니다(예를 들면, 모든 FD의 상태를 반환하므로 관심있는 항목을
찾으려면 루프를 돌아야 합니다. Linux에서 1024 인 FD_SETSIZE 에 의해 제한됩니다.).

pollselect 에서 큰 문제를 해결하여 수 천개의 연결에 대해서 처리할 수 있고
작은 규모의 서버라면 문제가 없습니다.

epoll(linux), kqueue(bsd) 는 추천하는 폴링 옵션입니다.
구현이 다소 복잡하지만 확장성이 훨씬 뛰어납니다.

이들은 모든 FD 이벤트가 가능한 상태면(읽을 수 있는 경우) 모든 FD 이벤트를 리턴하는 레벨 트리거
변경이 발생할 때만 리턴되는 에지 트리거 모드를 제공합니다(무언가를 읽을 수 있게 될 경우).

libevent 는 I/O Multiplexing을 정리된 API로 감싸서 아주 잘 사용할 수 있습니다.

IO Completion Ports 는 윈도우에서의 폴링 방식이라고 생각할 수 있습니다.

polling 방식에서 FD는 넌블로킹으로 설정되어야 합니다.


  • Multiplexing: 공유 매체를 통해서 다수의 신호를 하나의 신호로 송수신하는 방법



4. Signals(SIGIO)

파일 상태를 확인하는 대신 데이터를 사용할 수 있을 때 커널이 신호를 보내도록 요청할 수 있습니다.

SIGIO 를 사용하는 서버는 예가 많지 않습니다.
스레드에서 Signal 을 관리하는 것이 어려울 수 있고, 인터럽트를 통해 애플리케이션을 제어하기
어려울 수 있습니다.


그림 4. Signals(SIGIO).



5. Posix AIO

애플리케이션은 I/O 작업을 시작하고 완료되면 Signal 을 통해 알림을 받습니다.
SIGIO와 비슷하지만 데이터가 애플리케이션 버퍼로 한 번만 이동합니다.


그림 5. Posix AIO.



다음은 Unix Network Programming 에 나온 IO 모델들의 비교입니다.

그림 6. IO 모델 비교.




Epoll I/O

epoll(이벤트 폴)은 확장가능하고 I/O 이벤트 알림 메커니즘을 위한 Linux 커널 시스템 콜입니다.
epoll은 프로세스가 여러 파일 디스크립터를 모니터링하고, I/O가 가능한 경우 알림을 받을 수 있습니다.

epoll은 기존 POSIX select/poll 시스템 콜을 대체하여 많은 파일 디스크립터를 관리하는
응용프로그램에서 더 좋은 성능을 낼 수 있습니다.
(O(n) 시간에 작동하는 이전 시스템 콜과 달리 epoll은 O 시간에 동작합니다.)


epoll은 RB-트리(Red-Black Tree) 데이터 구조를 사용하여
현재 모니터링중인 모든 파일 디스크립터를 추적합니다.




epoll 트리거

그림 1. 레벨 트리거와 엣지 트리거.


epoll 엣지 트리거(Edge-triggered)레벨 트리거(Level-triggered) 모드를 제공 합니다.
엣지 트리거 모드에서는 epoll 객체에 새 이벤트가 추가될 때만 epoll_wait 가 리턴하고,
레벨 트리거 모드에서는 조건이 유지되면 epoll_wait 가 리턴합니다.


예를 들어, epoll에 등록된 파이프가 데이터를 받으면,
epoll_wait 이 리턴되어 읽을 데이터가 있다는 것을 알려줍니다.
reader가 버퍼에서 데이터의 일부만 읽었다고 가정해보면,
레벨 트리거 모드에서는 파이프 버퍼에 읽을 데이터가 있다면
epoll_wait 이 즉시 리턴됩니다.
그러나 엣지 트리거 모드에서 epoll_wait 는 새 데이터가 파이프에 기록된 후에만 리턴합니다.




비판(Criticism)

브라이언 캔 트릴(Bryan Cantrill)은 epoll이 전임자들(iocp, event ports and kqueue)로부터 배웠다면
피할수 있었던 실수가 있다고 지적했습니다.


그러나 그 비판의 상당 부분은 epoll의 EPOLLONESHOTEPOLLEXCLUSIVE 옵션으로 해결되었습니다.
EPOLLONESHOT 은 2004년 2월에 릴리스 된 Linux 커널 기본 버전 2.6.2에 추가되었고,
EPOLLEXCLUSIVE 는 2016년 3월에 릴리스 된 버전 4.5에 추가되었습니다.

  • EPOLLONESHOT: 파일 디스크립터가 epoll_wait 에서 활성화된 이벤트를 가져온 후
    내부에서는 비활성화 되고, 추가 이벤트는 epoll 인터페이스로 보고되지 않습니다.
    사용자는 EPOLL_CTL_MODepoll_ctl() 을 호출하여 새 이벤트 마스크로
    파일 디스크립터를 재장전해야합니다.
  • EPOLLEXCLUSIVE: 이 플래그를 사용하면 공유 fd 이벤트 소스에 여러 epfd에
    연결되어있을 때 단독으로 깨울 수 있습니다.


자세한 내용은 Epoll is fundamentally broken Part1Part2 를 참고하세요.




참고자료

댓글남기기