네트워크 프로그래밍이나 DB, NoSQL을 다루다 보면 I/O Multiplexing이라는 용어를 자주 접하게 된다. 오픈 소스가 저수준 아키텍처를 대신해 주다 보니 원리를 이해하지 않고 사용만 하는 경우가 많다. 원리를 알고 사용하면 더 나은 성능을 끌어낼 수 있다.
I/O Multiplexing 대두
네트워크 프로그램을 단순하게 구현하다 보면 I/O 블로킹이 발생해 여러 클라이언트의 접속을 허용하지 못하거나 성능 저하를 겪게 된다. 이를 해결하는 방법은 크게 다음과 같다.
- Fork: 클라이언트 요청이 있을 때마다 프로세스를 복사해 여러 사용자에게 서비스를 제공.
- Threads: 프로세스 대신 스레드를 생성해 여러 사용자에게 서비스를 제공.
- I/O Multiplexing: 여러 소켓에 대해 I/O를 병행 처리하는 기법. 다수의 프로세스나 스레드를 만들지 않고도 여러 파일을 처리할 수 있어 메모리 사용량이 적고 컨텍스트 스위칭이 줄어 성능이 개선된다.
- 비동기 I/O: I/O가 비동기적으로 처리되는 기법. 시그널이 병행되어 존재한다.
- Event Driven I/O: I/O Multiplexing을 추상화한 방식. libev, pyev, libevent 라이브러리가 있다.
I/O Multiplexing을 구현하기 위한 시스템 호출로 select, poll, epoll, kqueue 등이 제공된다. 프로그램에서 여러 파일 디스크립터를 모니터링해 어떤 종류의 I/O 이벤트가 발생했는지 검사하고, 각 파일 디스크립터가 Ready 상태가 되었는지 파악하는 것이 주요 목적이다.
select
- 등록된 file descriptor를 하나씩 체크해야 하고, 커널과 유저 공간 사이에 여러 번의 데이터 복사가 발생한다.
- 관리할 수 있는 file descriptor 수에 제한이 있다.
- 사용이 간단하고 지원 OS가 많아 이식성이 좋다.
file descriptor를 하나씩 체크하기 때문에 O(n)의 계산량이 필요하다. 따라서 관리하는 file descriptor 수가 증가할수록 성능이 떨어진다. 또한 최대 관리 수를 초과하면 사용할 수 없다.
poll
poll은 select와 거의 동일하지만 다음과 같은 차이가 있다.
- 관리할 수 있는 file descriptor 수가 무제한이다.
- 더 낮은 수준의 처리로 system call 호출이 select보다 적지만, 이식성이 나쁘다.
- 접속 수가 늘어나면 fd당 체크 마스크의 크기가 select는 3bit인 데 비해 poll은 64bit 정도이므로, 연결 수가 많아지면 오히려 select보다 성능이 떨어진다.
epoll
Linux 커널 2.6.x 이상에서만 지원되며 특징은 다음과 같다.
- 관리 fd 수가 무제한이다.
- select, poll과 달리 fd의 상태를 커널에서 관리한다.
- fd 세트를 매번 커널에 전달할 필요가 없다.
- 커널이 fd를 관리하므로 커널과 유저 스페이스 간의 통신 오버헤드가 크게 줄어든다.
kqueue
FreeBSD와 macOS 등 BSD 계열 운영체제에서 epoll에 대응하는 I/O Multiplexing 메커니즘이다. kevent 구조체를 통해 파일 디스크립터뿐 아니라 시그널, 타이머, 프로세스 이벤트 등 다양한 이벤트를 단일 인터페이스로 처리할 수 있다는 점이 특징이다.
libevent
파일 디스크립터에서 이벤트가 발생했을 때 지정된 콜백 함수를 실행시켜 주는 라이브러리다. 시스템마다 서로 다른 I/O Multiplexing 방식을 추상화해 준다.
select, poll, epoll, kqueue, event ports를 지원하며, 멀티스레드 환경에서도 사용 가능하다. 파일 디스크립터별로 타임아웃 기능도 갖추고 있어 적은 CPU/메모리 사용량으로 대량의 커넥션을 처리할 수 있다.
다만 이벤트 드리븐 방식으로 개발할 때 코드의 일부가 블로킹되면 서비스 전체가 멈춰버릴 위험이 있다. prefork한 다수의 스레드에서 각각 libevent를 사용해 커넥션을 처리하도록 구성하면 최악의 경우에도 해당 스레드에서 서비스하는 커넥션들만 문제가 생기도록 국한시킬 수 있고, 멀티코어를 최대한 활용할 수도 있다.
활용 팁 및 주의사항
- I/O Multiplexing 방식은 다수의 연결을 처리해야 하거나 각 연결에서 I/O 대기 시간이 길 때 유리하다.
- 이벤트 드리븐 패러다임으로 설계할 때 작업 일부가 블로킹되면 전체 이벤트 루프가 멈출 수 있으므로 주의해야 한다.
- libevent 같은 라이브러리를 활용하면 OS별로 다른 I/O Multiplexing 방식을 추상화해 이식성 있는 코드를 작성할 수 있다.
프로그래밍을 하면 할수록 베이스 지식의 중요성을 느끼게 된다. 이 베이스 지식이 없다면 남이 만들어 놓은 오픈 소스를 사용할 줄만 알 뿐 거기서 벗어나지 못한다. 오픈 소스의 불필요한 오버헤드를 감수해야 하고, 보이지 않는 심각한 버그에 대한 두려움도 함께 안고 가야 한다는 점을 잊어서는 안 된다.




