I/O 멀티플렉싱 실전 가이드 | select, poll, epoll, kqueue, IOCP

I/O 멀티플렉싱 실전 가이드 | select, poll, epoll, kqueue, IOCP

이 글의 핵심

C10K 문제를 논할 때 빠지지 않는 주제가 I/O 멀티플렉싱이다. 여기서는 select·poll부터 epoll·kqueue·IOCP까지, “언제 무엇이 유리한지”를 코드 흐름과 함께 짚는다.


목차

  1. I/O 멀티플렉싱이란?
  2. Blocking I/O의 문제점
  3. select - 기본 멀티플렉싱
  4. poll - select 개선
  5. epoll - Linux 고성능
  6. kqueue - BSD/macOS
  7. IOCP - Windows 고성능
  8. 성능 비교 및 선택 가이드

사전 지식 (초보자를 위한 기초)

1. 소켓(Socket)이란?

소켓은 네트워크 통신의 양 끝점이다. 연결·송신·수신·종료 같은 동작은 전부 이 fd를 통해 이뤄진다.

간단한 서버 예제:

다음은 c를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 서버 소켓 생성
int server_fd = socket(AF_INET, SOCK_STREAM, 0);

// 주소 바인딩
bind(server_fd, ...);

// 연결 대기
listen(server_fd, 10);

// 클라이언트 연결 수락
int client_fd = accept(server_fd, ...);

// 데이터 받기
char buffer[1024];
recv(client_fd, buffer, sizeof(buffer), 0);

// 데이터 보내기
send(client_fd, "Hello", 5, 0);

// 연결 종료
close(client_fd);

2. Blocking vs Non-blocking I/O

Blocking I/O (블로킹)

아래 코드는 c를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// recv()는 데이터가 올 때까지 대기 (멈춤!)
char buffer[1024];
int n = recv(client_fd, buffer, sizeof(buffer), 0);
// ↑ 데이터가 오기 전까지 여기서 멈춤
printf("Received: %s\n", buffer);

아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

타임라인:

시간 →
0초: recv() 호출
1초: (대기 중...)
2초: (대기 중...)
3초: 데이터 도착! recv() 반환
3초: printf() 실행

문제: 데이터를 기다리는 동안 아무것도 못함!

Non-blocking I/O (논블로킹)

아래 코드는 c를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 소켓을 Non-blocking으로 설정
fcntl(client_fd, F_SETFL, O_NONBLOCK);

// recv()가 즉시 반환
int n = recv(client_fd, buffer, sizeof(buffer), 0);
if (n == -1 && errno == EAGAIN) {
    // 데이터가 없음, 다른 일 할 수 있음
    printf("No data yet, doing other work...\n");
}

3. 동시 접속 처리 방법

방법 1: 프로세스/스레드 per 연결

아래 코드는 c를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 클라이언트마다 스레드 생성
while (1) {
    int client_fd = accept(server_fd, ...);
    pthread_create(&thread, NULL, handle_client, &client_fd);
}

// 문제: 클라이언트 10,000명 = 스레드 10,000개 (메모리 부족!)

방법 2: I/O 멀티플렉싱 (이 글의 주제!)

아래 코드는 c를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 하나의 스레드로 여러 소켓 동시 처리
while (1) {
    // 여러 소켓 중 준비된 것만 처리
    int ready = select(max_fd, &read_fds, ...);
    
    for (int fd = 0; fd < max_fd; fd++) {
        if (FD_ISSET(fd, &read_fds)) {
            // 이 소켓에 데이터 있음
            handle_client(fd);
        }
    }
}

// 장점: 스레드 1개로 10,000개 연결 처리 가능!

1. I/O 멀티플렉싱이란?

정의

I/O 멀티플렉싱은 하나의 스레드로 여러 소켓의 준비 상태를 한 번에 기다렸다가, 준비된 것만 처리하는 기법이다. 블로킹으로 한 연결에 묶이지 않게 하려는 목적이 크다.

동작 원리

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

┌──────────────────────────────────────┐
│         애플리케이션 (1개 스레드)      │
└──────────────┬───────────────────────┘

               │ select/epoll/kqueue
               │ "어떤 소켓이 준비됐나요?"

┌──────────────▼───────────────────────┐
│            커널 (OS)                  │
│  Socket 1: 데이터 있음                │
│  Socket 2: 대기 중                    │
│  Socket 3: 데이터 있음                │
│  Socket 4: 대기 중                    │
└──────────────┬───────────────────────┘

               │ "Socket 1, 3이 준비됐어요"

┌──────────────▼───────────────────────┐
│         애플리케이션                   │
│  Socket 1 처리 → recv()               │
│  Socket 3 처리 → recv()               │
└──────────────────────────────────────┘

2. Blocking I/O의 문제점

단일 클라이언트 처리

다음은 c를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 간단한 에코 서버
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, ...);
listen(server_fd, 10);

while (1) {
    int client_fd = accept(server_fd, ...);  // 연결 대기 (Blocking)
    
    char buffer[1024];
    int n = recv(client_fd, buffer, sizeof(buffer), 0);  // 데이터 대기 (Blocking)
    
    send(client_fd, buffer, n, 0);  // 에코
    close(client_fd);
}

// 문제: 한 번에 1명만 처리 가능!
// 클라이언트 A가 recv()에서 대기 중이면
// 클라이언트 B는 accept()조차 못함

멀티 스레드 방식

다음은 c를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void* handle_client(void* arg) {
    int client_fd = *(int*)arg;
    
    char buffer[1024];
    int n = recv(client_fd, buffer, sizeof(buffer), 0);
    send(client_fd, buffer, n, 0);
    
    close(client_fd);
    return NULL;
}

int main() {
    while (1) {
        int client_fd = accept(server_fd, ...);
        
        pthread_t thread;
        pthread_create(&thread, NULL, handle_client, &client_fd);
        pthread_detach(thread);
    }
}

// 문제:
// - 클라이언트 10,000명 = 스레드 10,000개
// - 메모리: 10,000 × 8MB = 80GB (스택 크기)
// - 컨텍스트 스위칭 오버헤드

3. select - 기본 멀티플렉싱

select란?

select는 가장 오래된 I/O 멀티플렉싱 방법이다. (1983년 BSD Unix)

아래 코드는 c를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

int select(
    int nfds,                  // 최대 fd + 1
    fd_set *readfds,           // 읽기 준비 확인할 fd 집합
    fd_set *writefds,          // 쓰기 준비 확인할 fd 집합
    fd_set *exceptfds,         // 예외 확인할 fd 집합
    struct timeval *timeout    // 타임아웃
);

select 예제

다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define MAX_CLIENTS 1024
#define PORT 8080

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 10);
    
    fd_set master_fds;  // 모든 소켓 저장
    FD_ZERO(&master_fds);
    FD_SET(server_fd, &master_fds);
    
    int max_fd = server_fd;
    
    printf("Server listening on port %d\n", PORT);
    
    while (1) {
        fd_set read_fds = master_fds;  // 복사 (select가 수정함)
        
        // 준비된 소켓 대기
        int ready = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
        
        if (ready < 0) {
            perror("select");
            break;
        }
        
        // 모든 소켓 확인
        for (int fd = 0; fd <= max_fd; fd++) {
            if (!FD_ISSET(fd, &read_fds)) {
                continue;  // 준비 안됨
            }
            
            if (fd == server_fd) {
                // 새 연결
                int client_fd = accept(server_fd, NULL, NULL);
                FD_SET(client_fd, &master_fds);
                
                if (client_fd > max_fd) {
                    max_fd = client_fd;
                }
                
                printf("New client: %d\n", client_fd);
            } else {
                // 기존 클라이언트 데이터
                char buffer[1024];
                int n = recv(fd, buffer, sizeof(buffer), 0);
                
                if (n <= 0) {
                    // 연결 종료
                    printf("Client %d disconnected\n", fd);
                    close(fd);
                    FD_CLR(fd, &master_fds);
                } else {
                    // 에코
                    send(fd, buffer, n, 0);
                }
            }
        }
    }
    
    close(server_fd);
    return 0;
}

select의 장단점

장점: POSIX 계열에서 널리 있고 API가 단순하며 타임아웃을 걸기 쉽다.

단점: FD_SETSIZE 근처에서 한계가 보이고, 매 호출마다 fd 집합을 다시 잡아야 해서 연결이 많을수록 부담이 커진다.

select 성능 문제

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

클라이언트 1,000명 연결:

매 select() 호출마다:
1. fd_set 복사 (1,000개)
2. 커널이 1,000개 모두 확인
3. 애플리케이션이 1,000개 순회

→ O(n) 복잡도, 비효율적!

4. poll - select 개선

poll이란?

poll은 select의 FD_SETSIZE 제한을 없앤 개선 버전이다. (1986년 SVR3)

아래 코드는 c를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

int poll(
    struct pollfd *fds,  // fd 배열
    nfds_t nfds,         // 배열 크기
    int timeout          // 타임아웃 (ms)
);

struct pollfd {
    int fd;         // 파일 디스크립터
    short events;   // 확인할 이벤트 (POLLIN, POLLOUT)
    short revents;  // 발생한 이벤트
};

poll 예제

다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <poll.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>

#define MAX_CLIENTS 10000
#define PORT 8080

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 10);
    
    struct pollfd fds[MAX_CLIENTS];
    int nfds = 1;
    
    // 서버 소켓 추가
    fds[0].fd = server_fd;
    fds[0].events = POLLIN;
    
    printf("Server listening on port %d\n", PORT);
    
    while (1) {
        // 준비된 소켓 대기
        int ready = poll(fds, nfds, -1);
        
        if (ready < 0) {
            perror("poll");
            break;
        }
        
        // 모든 소켓 확인
        for (int i = 0; i < nfds; i++) {
            if (fds[i].revents == 0) {
                continue;  // 이벤트 없음
            }
            
            if (fds[i].fd == server_fd) {
                // 새 연결
                int client_fd = accept(server_fd, NULL, NULL);
                
                fds[nfds].fd = client_fd;
                fds[nfds].events = POLLIN;
                nfds++;
                
                printf("New client: %d (total: %d)\n", client_fd, nfds - 1);
            } else {
                // 기존 클라이언트 데이터
                char buffer[1024];
                int n = recv(fds[i].fd, buffer, sizeof(buffer), 0);
                
                if (n <= 0) {
                    // 연결 종료
                    printf("Client %d disconnected\n", fds[i].fd);
                    close(fds[i].fd);
                    
                    // 배열에서 제거 (마지막 요소와 교체)
                    fds[i] = fds[nfds - 1];
                    nfds--;
                    i--;  // 다시 확인
                } else {
                    // 에코
                    send(fds[i].fd, buffer, n, 0);
                }
            }
        }
    }
    
    close(server_fd);
    return 0;
}

poll vs select

항목selectpoll
fd 개수 제한1024 (FD_SETSIZE)무제한
fd_set 복사필요불필요
성능O(n)O(n)
이식성높음높음

poll의 개선점:

  • ✅ FD_SETSIZE 제한 제거
  • ✅ fd_set 복사 불필요

여전한 문제:

  • ❌ O(n) 성능 (모든 fd 순회)

5. epoll - Linux 고성능

epoll이란?

epoll은 Linux의 고성능 I/O 멀티플렉싱이다. (Linux 2.5.44, 2002년)

핵심 차이:

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

select/poll:
- 매번 모든 fd를 커널에 전달
- 커널이 모든 fd 확인 (O(n))
- 애플리케이션이 모든 fd 순회 (O(n))

epoll:
- fd를 커널에 한 번만 등록
- 커널이 준비된 fd만 반환 (O(1))
- 애플리케이션이 준비된 fd만 처리 (O(k), k = 준비된 개수)

epoll API

다음은 c를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 1. epoll 인스턴스 생성
int epoll_fd = epoll_create1(0);

// 2. fd 등록
struct epoll_event event;
event.events = EPOLLIN;  // 읽기 이벤트
event.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);

// 3. 이벤트 대기
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);

// 4. 준비된 fd만 처리
for (int i = 0; i < n; i++) {
    int fd = events[i].data.fd;
    // 처리
}

epoll 예제

다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

#define MAX_EVENTS 1024
#define PORT 8080

// Non-blocking 설정
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    // 서버 소켓 생성
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    set_nonblocking(server_fd);
    
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, SOMAXCONN);
    
    // epoll 생성
    int epoll_fd = epoll_create1(0);
    
    // 서버 소켓 등록
    struct epoll_event event;
    event.events = EPOLLIN | EPOLLET;  // Edge-Triggered
    event.data.fd = server_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
    
    struct epoll_event events[MAX_EVENTS];
    
    printf("Server listening on port %d\n", PORT);
    
    while (1) {
        // 이벤트 대기
        int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        
        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            
            if (fd == server_fd) {
                // 새 연결
                while (1) {
                    int client_fd = accept(server_fd, NULL, NULL);
                    if (client_fd < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;  // 더 이상 연결 없음
                        }
                        perror("accept");
                        break;
                    }
                    
                    set_nonblocking(client_fd);
                    
                    // 클라이언트 소켓 등록
                    event.events = EPOLLIN | EPOLLET;
                    event.data.fd = client_fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
                    
                    printf("New client: %d\n", client_fd);
                }
            } else {
                // 클라이언트 데이터
                char buffer[1024];
                
                while (1) {
                    int n = recv(fd, buffer, sizeof(buffer), 0);
                    
                    if (n < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;  // 더 이상 데이터 없음
                        }
                        perror("recv");
                        break;
                    }
                    
                    if (n == 0) {
                        // 연결 종료
                        printf("Client %d disconnected\n", fd);
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                        close(fd);
                        break;
                    }
                    
                    // 에코
                    send(fd, buffer, n, 0);
                }
            }
        }
    }
    
    close(server_fd);
    close(epoll_fd);
    return 0;
}

Edge-Triggered vs Level-Triggered

Level-Triggered (LT, 기본값)

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

데이터가 있는 동안 계속 알림

시간 →
0초: 데이터 100바이트 도착
0초: epoll_wait() 반환 → 이벤트 알림
1초: 50바이트만 읽음
1초: epoll_wait() 반환 → 이벤트 알림 (아직 50바이트 남음)
2초: 50바이트 읽음
2초: epoll_wait() 대기 (데이터 없음)

Edge-Triggered (ET, 고성능)

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

데이터 상태가 변할 때만 알림

시간 →
0초: 데이터 100바이트 도착
0초: epoll_wait() 반환 → 이벤트 알림
1초: 50바이트만 읽음
1초: epoll_wait() 대기 (알림 없음! 주의!)
→ 반드시 EAGAIN까지 읽어야 함!

ET 모드 사용 시 주의:

다음은 c를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 ET 사용
int n = recv(fd, buffer, sizeof(buffer), 0);
send(fd, buffer, n, 0);
// 문제: 데이터가 더 있어도 알림 안옴!

// ✅ 올바른 ET 사용
while (1) {
    int n = recv(fd, buffer, sizeof(buffer), 0);
    if (n < 0) {
        if (errno == EAGAIN) break;  // 더 이상 없음
        // 에러 처리
    }
    if (n == 0) break;  // 연결 종료
    send(fd, buffer, n, 0);
}

6. kqueue - BSD/macOS

kqueue란?

kqueue는 FreeBSD·macOS의 고성능 이벤트 알림 메커니즘이다. (FreeBSD 4.1, 2000년)

epoll과의 차이:

  • epoll: I/O 이벤트만
  • kqueue: I/O + 파일 변경 + 시그널 + 타이머 등 모든 이벤트

kqueue API

다음은 c를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 1. kqueue 생성
int kq = kqueue();

// 2. 이벤트 등록
struct kevent change;
EV_SET(&change, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);

// 3. 이벤트 대기
struct kevent events[MAX_EVENTS];
int n = kevent(kq, NULL, 0, events, MAX_EVENTS, NULL);

// 4. 이벤트 처리
for (int i = 0; i < n; i++) {
    int fd = events[i].ident;
    // 처리
}

kqueue 예제

다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <sys/event.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

#define MAX_EVENTS 1024
#define PORT 8080

void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    // 서버 소켓 생성
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    set_nonblocking(server_fd);
    
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, SOMAXCONN);
    
    // kqueue 생성
    int kq = kqueue();
    
    // 서버 소켓 등록
    struct kevent change;
    EV_SET(&change, server_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
    kevent(kq, &change, 1, NULL, 0, NULL);
    
    struct kevent events[MAX_EVENTS];
    
    printf("Server listening on port %d\n", PORT);
    
    while (1) {
        // 이벤트 대기
        int n = kevent(kq, NULL, 0, events, MAX_EVENTS, NULL);
        
        for (int i = 0; i < n; i++) {
            int fd = events[i].ident;
            
            if (fd == server_fd) {
                // 새 연결
                while (1) {
                    int client_fd = accept(server_fd, NULL, NULL);
                    if (client_fd < 0) break;
                    
                    set_nonblocking(client_fd);
                    
                    // 클라이언트 소켓 등록
                    EV_SET(&change, client_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
                    kevent(kq, &change, 1, NULL, 0, NULL);
                    
                    printf("New client: %d\n", client_fd);
                }
            } else if (events[i].filter == EVFILT_READ) {
                // 클라이언트 데이터
                char buffer[1024];
                int n = recv(fd, buffer, sizeof(buffer), 0);
                
                if (n <= 0) {
                    // 연결 종료
                    printf("Client %d disconnected\n", fd);
                    close(fd);
                } else {
                    // 에코
                    send(fd, buffer, n, 0);
                }
            }
        }
    }
    
    close(server_fd);
    close(kq);
    return 0;
}

kqueue 고급 기능

파일 변경 감지:

아래 코드는 c를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 파일 변경 감지
int fd = open("config.txt", O_RDONLY);

struct kevent change;
EV_SET(&change, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, 
       NOTE_WRITE | NOTE_DELETE, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);

// 파일이 수정되면 이벤트 발생

타이머:

아래 코드는 c를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 1초 타이머
struct kevent change;
EV_SET(&change, 1, EVFILT_TIMER, EV_ADD, 0, 1000, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);

// 1초마다 이벤트 발생

7. IOCP - Windows 고성능

IOCP란?

IOCP (I/O Completion Port)는 Windows의 고성능 비동기 I/O다.

핵심 차이:

아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

select/epoll/kqueue (Readiness Notification):
- "이 소켓에 데이터가 있어요" 알림
- 애플리케이션이 recv() 호출
- 동기적 처리

IOCP (Completion Notification):
- 애플리케이션이 비동기 recv() 요청
- 커널이 데이터를 버퍼에 복사
- "recv() 완료됐어요" 알림
- 완전히 비동기적

IOCP 동작 원리

아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

1. 애플리케이션: "데이터 받아줘" (WSARecv 호출)

2. 커널: "알았어, 나중에 알려줄게" (즉시 반환)

3. 애플리케이션: 다른 일 함

4. 커널: 데이터 도착! 버퍼에 복사

5. 커널: Completion Port에 알림

6. 애플리케이션: GetQueuedCompletionStatus() → "완료됐어요!"

IOCP 예제

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <winsock2.h>
#include <windows.h>
#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFFER_SIZE 1024

// 소켓별 데이터
struct SocketData {
    SOCKET socket;
    WSAOVERLAPPED overlapped;
    WSABUF wsaBuf;
    char buffer[BUFFER_SIZE];
    DWORD flags;
};

int main() {
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    
    // 서버 소켓 생성
    SOCKET server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    
    bind(server_socket, (sockaddr*)&addr, sizeof(addr));
    listen(server_socket, SOMAXCONN);
    
    // IOCP 생성
    HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
    
    // 서버 소켓을 IOCP에 연결
    CreateIoCompletionPort((HANDLE)server_socket, iocp, (ULONG_PTR)server_socket, 0);
    
    printf("Server listening on port %d\n", PORT);
    
    // Accept 시작
    SOCKET client_socket = accept(server_socket, NULL, NULL);
    
    while (1) {
        DWORD bytes_transferred;
        ULONG_PTR completion_key;
        LPOVERLAPPED overlapped;
        
        // 완료된 I/O 대기
        BOOL result = GetQueuedCompletionStatus(
            iocp,
            &bytes_transferred,
            &completion_key,
            &overlapped,
            INFINITE
        );
        
        if (!result) {
            printf("GetQueuedCompletionStatus failed\n");
            continue;
        }
        
        SocketData* socket_data = CONTAINING_RECORD(overlapped, SocketData, overlapped);
        
        if (bytes_transferred == 0) {
            // 연결 종료
            printf("Client disconnected\n");
            closesocket(socket_data->socket);
            delete socket_data;
            continue;
        }
        
        // 데이터 처리 (에코)
        socket_data->wsaBuf.len = bytes_transferred;
        WSASend(
            socket_data->socket,
            &socket_data->wsaBuf,
            1,
            NULL,
            0,
            &socket_data->overlapped,
            NULL
        );
        
        // 다음 recv 요청
        ZeroMemory(&socket_data->overlapped, sizeof(OVERLAPPED));
        socket_data->wsaBuf.len = BUFFER_SIZE;
        socket_data->flags = 0;
        
        WSARecv(
            socket_data->socket,
            &socket_data->wsaBuf,
            1,
            NULL,
            &socket_data->flags,
            &socket_data->overlapped,
            NULL
        );
    }
    
    closesocket(server_socket);
    CloseHandle(iocp);
    WSACleanup();
    return 0;
}

IOCP 스레드 풀

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Worker 스레드
DWORD WINAPI WorkerThread(LPVOID param) {
    HANDLE iocp = (HANDLE)param;
    
    while (1) {
        DWORD bytes_transferred;
        ULONG_PTR completion_key;
        LPOVERLAPPED overlapped;
        
        GetQueuedCompletionStatus(iocp, &bytes_transferred, 
                                  &completion_key, &overlapped, INFINITE);
        
        // I/O 처리
        // ...
    }
    
    return 0;
}

int main() {
    HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
    
    // CPU 코어 수만큼 스레드 생성
    SYSTEM_INFO sysInfo;
    GetSystemInfo(&sysInfo);
    int num_threads = sysInfo.dwNumberOfProcessors * 2;
    
    for (int i = 0; i < num_threads; i++) {
        CreateThread(NULL, 0, WorkerThread, iocp, 0, NULL);
    }
    
    // 메인 스레드는 accept만 처리
    while (1) {
        SOCKET client = accept(server_socket, NULL, NULL);
        CreateIoCompletionPort((HANDLE)client, iocp, (ULONG_PTR)client, 0);
        
        // 비동기 recv 시작
        // ...
    }
}

8. 성능 비교

벤치마크 결과

테스트 환경:

  • 동시 연결: 10,000개
  • 메시지 크기: 1KB
  • 에코 서버

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

처리량(요청/초, 예시):

1. IOCP (Windows):    250,000
2. epoll (Linux):     200,000
3. kqueue (macOS):    180,000
4. poll:               50,000
5. select:             30,000  

CPU 사용률(예시):

1. IOCP:    20%
2. epoll:   25%
3. kqueue:  30%
4. poll:    60%
5. select:  80%  

복잡도 비교

메커니즘등록대기처리전체
selectO(n)O(n)O(n)O(n)
pollO(1)O(n)O(n)O(n)
epollO(1)O(1)O(k)O(k)
kqueueO(1)O(1)O(k)O(k)
IOCPO(1)O(1)O(k)O(k)

k = 준비된 이벤트 수

연결 수에 따른 성능

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

연결 수: 100개
- select:  충분히 빠름
- epoll:   약간 더 빠름
- 차이:    거의 없음

연결 수: 1,000개
- select:  느려지기 시작
- epoll:   여전히 빠름
- 차이:    2~3배

연결 수: 10,000개
- select:  매우 느림 (FD_SETSIZE 초과)
- epoll:   여전히 빠름
- 차이:    10배 이상

연결 수: 100,000개
- select:  불가능
- epoll:   가능 (C10K 문제 해결)
- 차이:    무한대

9. 플랫폼별 선택 가이드

선택 기준

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Linux:
✅ epoll (최고 성능)
- 대규모 연결 (10,000+)
- 프로덕션 서버

Windows:
✅ IOCP (최고 성능)
- 완전 비동기
- 스레드 풀 활용

macOS/FreeBSD:
✅ kqueue (최고 성능)
- epoll과 유사한 성능
- 다양한 이벤트 지원

크로스 플랫폼:
✅ libuv, libevent, Boost.Asio
- 플랫폼별 최적 메커니즘 자동 선택
- Node.js, Nginx가 사용

라이브러리 추천

libuv (Node.js 사용)

다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <uv.h>

void on_read(uv_stream_t* client, ssize_t nread, const uv_buf_t* buf) {
    if (nread < 0) {
        uv_close((uv_handle_t*)client, NULL);
        return;
    }
    
    // 에코
    uv_write_t* req = malloc(sizeof(uv_write_t));
    uv_buf_t wrbuf = uv_buf_init(buf->base, nread);
    uv_write(req, client, &wrbuf, 1, NULL);
}

void on_connection(uv_stream_t* server, int status) {
    uv_tcp_t* client = malloc(sizeof(uv_tcp_t));
    uv_tcp_init(uv_default_loop(), client);
    
    if (uv_accept(server, (uv_stream_t*)client) == 0) {
        uv_read_start((uv_stream_t*)client, alloc_buffer, on_read);
    }
}

int main() {
    uv_tcp_t server;
    uv_tcp_init(uv_default_loop(), &server);
    
    struct sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", 8080, &addr);
    
    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
    uv_listen((uv_stream_t*)&server, 128, on_connection);
    
    printf("Server listening on port 8080\n");
    uv_run(uv_default_loop(), UV_RUN_DEFAULT);
    
    return 0;
}

Boost.Asio (C++)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <boost/asio.hpp>
#include <iostream>

using boost::asio::ip::tcp;

class Session : public std::enable_shared_from_this<Session> {
public:
    Session(tcp::socket socket) : socket_(std::move(socket)) {}
    
    void start() {
        do_read();
    }
    
private:
    void do_read() {
        auto self(shared_from_this());
        socket_.async_read_some(
            boost::asio::buffer(buffer_),
            [this, self](boost::system::error_code ec, std::size_t length) {
                if (!ec) {
                    do_write(length);
                }
            }
        );
    }
    
    void do_write(std::size_t length) {
        auto self(shared_from_this());
        boost::asio::async_write(
            socket_,
            boost::asio::buffer(buffer_, length),
            [this, self](boost::system::error_code ec, std::size_t) {
                if (!ec) {
                    do_read();
                }
            }
        );
    }
    
    tcp::socket socket_;
    char buffer_[1024];
};

class Server {
public:
    Server(boost::asio::io_context& io_context, short port)
        : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)) {
        do_accept();
    }
    
private:
    void do_accept() {
        acceptor_.async_accept(
            [this](boost::system::error_code ec, tcp::socket socket) {
                if (!ec) {
                    std::make_shared<Session>(std::move(socket))->start();
                }
                do_accept();
            }
        );
    }
    
    tcp::acceptor acceptor_;
};

int main() {
    boost::asio::io_context io_context;
    Server server(io_context, 8080);
    
    std::cout << "Server listening on port 8080\n";
    io_context.run();  // 내부적으로 epoll/kqueue/IOCP 사용
    
    return 0;
}

10. 실전 패턴

Reactor 패턴 (epoll, kqueue)

아래 코드는 text를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Reactor 패턴:
- 이벤트 루프
- 이벤트 발생 시 핸들러 호출
- 동기적 처리

┌─────────────────────────────────┐
│        Event Loop               │
│  while (1) {                    │
│    events = epoll_wait()        │
│    for event in events:         │
│      handler(event)             │
│  }                              │
└─────────────────────────────────┘

구현:

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Reactor {
public:
    void register_handler(int fd, std::function<void()> handler) {
        handlers_[fd] = handler;
        
        struct epoll_event event;
        event.events = EPOLLIN | EPOLLET;
        event.data.fd = fd;
        epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &event);
    }
    
    void run() {
        struct epoll_event events[MAX_EVENTS];
        
        while (1) {
            int n = epoll_wait(epoll_fd_, events, MAX_EVENTS, -1);
            
            for (int i = 0; i < n; i++) {
                int fd = events[i].data.fd;
                handlers_[fd]();  // 핸들러 호출
            }
        }
    }
    
private:
    int epoll_fd_ = epoll_create1(0);
    std::unordered_map<int, std::function<void()>> handlers_;
};

// 사용
Reactor reactor;

reactor.register_handler(server_fd, [&]() {
    int client_fd = accept(server_fd, NULL, NULL);
    
    reactor.register_handler(client_fd, [client_fd]() {
        char buffer[1024];
        int n = recv(client_fd, buffer, sizeof(buffer), 0);
        if (n > 0) {
            send(client_fd, buffer, n, 0);
        }
    });
});

reactor.run();

Proactor 패턴 (IOCP)

아래 코드는 text를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Proactor 패턴:
- 비동기 I/O 요청
- 완료 시 핸들러 호출
- 완전히 비동기적

┌─────────────────────────────────┐
│        Proactor                 │
│  1. async_read(handler)         │
│  2. 커널이 읽기 수행             │
│  3. 완료 시 handler 호출         │
└─────────────────────────────────┘

11. C10K 문제

C10K 문제란?

C10K (Concurrent 10,000 connections): 동시 접속 10,000명 처리 문제

아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

2000년대 초반:
- 서버 1대로 동시 접속 10,000명 처리 어려움
- select/poll의 O(n) 성능 문제
- 스레드 per 연결 방식의 메모리 문제

해결책:
- epoll (Linux, 2002)
- kqueue (FreeBSD, 2000)
- IOCP (Windows NT 3.5, 1993)

현재:
- 서버 1대로 100만 연결 처리 가능 (C1M)
- Nginx, Node.js 등이 증명

C10K 해결 사례: Nginx

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

Nginx 아키텍처:
- Worker 프로세스: CPU 코어 수만큼
- 각 Worker: epoll로 수만 개 연결 처리
- 비동기 I/O + 이벤트 루프

성능:
- 동시 연결: 100,000+
- 처리량: 50,000 req/s
- 메모리: 수백 MB

12. 실전 최적화

Zero-Copy

일반적인 데이터 전송:

1. 커널 → 애플리케이션 버퍼 (복사 1)
2. 애플리케이션 버퍼 → 커널 (복사 2)

총 2번 복사!

Zero-Copy (sendfile, splice):

아래 코드는 c를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

// Linux sendfile
#include <sys/sendfile.h>

// 파일을 소켓으로 직접 전송 (복사 없음)
off_t offset = 0;
sendfile(client_fd, file_fd, &offset, file_size);

// 커널 내부에서만 처리, 애플리케이션 버퍼 거치지 않음

SO_REUSEPORT (Linux 3.9+)

아래 코드는 c를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 여러 프로세스가 같은 포트 listen
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));

// 커널이 자동으로 로드 밸런싱
// Nginx, HAProxy가 사용

TCP_NODELAY

아래 코드는 c를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// Nagle 알고리즘 비활성화 (지연 감소)
int flag = 1;
setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));

// 작은 패킷도 즉시 전송
// 실시간 애플리케이션 (게임, 채팅)에 필수

13. 실전 서버 아키텍처

단일 스레드 이벤트 루프 (Node.js, Redis)

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

┌─────────────────────────────────┐
│      Main Thread                │
│  ┌───────────────────────────┐  │
│  │     Event Loop            │  │
│  │  - epoll_wait()           │  │
│  │  - 이벤트 처리            │  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘

장점:
- 간단한 구조
- 락 불필요
- 컨텍스트 스위칭 없음

단점:
- CPU 1개만 사용
- CPU 집약적 작업에 부적합

멀티 프로세스 (Nginx)

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

┌─────────────────────────────────┐
│      Master Process             │
│  - 설정 관리                     │
│  - Worker 관리                   │
└──────────┬──────────────────────┘

    ┌──────┼──────┐
    │      │      │
┌───▼──┐ ┌─▼───┐ ┌─▼───┐
│Worker│ │Worker│ │Worker│
│epoll │ │epoll │ │epoll │
└──────┘ └──────┘ └──────┘

장점:
- 멀티 코어 활용
- 프로세스 격리 (안정성)

단점:
- 메모리 사용 증가
- 프로세스 간 통신 필요

스레드 풀 (IOCP, Boost.Asio)

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

┌─────────────────────────────────┐
│      Main Thread                │
│  - accept() 처리                 │
└──────────┬──────────────────────┘

    ┌──────┼──────┐
    │      │      │
┌───▼──┐ ┌─▼───┐ ┌─▼───┐
│Worker│ │Worker│ │Worker│
│Thread│ │Thread│ │Thread│
│ IOCP │ │ IOCP │ │ IOCP │
└──────┘ └──────┘ └──────┘

장점:
- 멀티 코어 활용
- 메모리 공유 (효율적)

단점:
- 동기화 필요 (락)
- 디버깅 어려움

14. 실전 예제: HTTP 서버

epoll 기반 HTTP 서버

다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define MAX_EVENTS 1024
#define PORT 8080

const char* HTTP_RESPONSE = 
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: text/html\r\n"
    "Content-Length: 13\r\n"
    "\r\n"
    "Hello, World!";

void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    // 서버 소켓 생성
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    set_nonblocking(server_fd);
    
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, SOMAXCONN);
    
    // epoll 생성
    int epoll_fd = epoll_create1(0);
    
    struct epoll_event event;
    event.events = EPOLLIN | EPOLLET;
    event.data.fd = server_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
    
    struct epoll_event events[MAX_EVENTS];
    
    printf("HTTP Server listening on http://localhost:%d\n", PORT);
    
    while (1) {
        int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        
        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            
            if (fd == server_fd) {
                // 새 연결
                while (1) {
                    int client_fd = accept(server_fd, NULL, NULL);
                    if (client_fd < 0) break;
                    
                    set_nonblocking(client_fd);
                    
                    event.events = EPOLLIN | EPOLLET;
                    event.data.fd = client_fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
                }
            } else {
                // HTTP 요청 처리
                char buffer[4096];
                int n = recv(fd, buffer, sizeof(buffer), 0);
                
                if (n <= 0) {
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                } else {
                    // HTTP 응답 전송
                    send(fd, HTTP_RESPONSE, strlen(HTTP_RESPONSE), 0);
                    
                    // Keep-Alive 미지원, 즉시 종료
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                }
            }
        }
    }
    
    close(server_fd);
    close(epoll_fd);
    return 0;
}

컴파일 및 실행:

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

gcc -o http_server http_server.c
./http_server

# 테스트
curl http://localhost:8080
# Hello, World!

# 벤치마크
ab -n 100000 -c 1000 http://localhost:8080/

15. 성능 튜닝

커널 파라미터 최적화

Linux:

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# /etc/sysctl.conf

# 최대 파일 디스크립터
fs.file-max = 1000000

# TCP 버퍼 크기
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# SYN backlog 크기
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 8192

# TIME_WAIT 소켓 재사용
net.ipv4.tcp_tw_reuse = 1

# 적용
sudo sysctl -p

프로세스 제한:

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# /etc/security/limits.conf
* soft nofile 1000000
* hard nofile 1000000

# 확인
ulimit -n

애플리케이션 최적화

1) 버퍼 크기 조정

다음은 간단한 c 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 소켓 버퍼 크기 증가
int buffer_size = 1024 * 1024;  // 1MB
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &buffer_size, sizeof(buffer_size));
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &buffer_size, sizeof(buffer_size));

2) 타임아웃 설정

아래 코드는 c를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 읽기 타임아웃
struct timeval timeout;
timeout.tv_sec = 30;
timeout.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

3) Keep-Alive

아래 코드는 c를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// TCP Keep-Alive 활성화
int keepalive = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));

// Keep-Alive 간격
int keepidle = 60;   // 60초 후 첫 probe
int keepintvl = 10;  // 10초 간격
int keepcnt = 3;     // 3번 실패 시 종료
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));

16. 실전 사례 연구

Nginx (epoll)

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Nginx 설정:

worker_processes auto;  # CPU 코어 수
worker_connections 10000;  # Worker당 최대 연결

events {
    use epoll;
    multi_accept on;
}

성능:
- 동시 연결: 100,000+
- 처리량: 50,000 req/s
- 메모리: 500MB
- CPU: 4코어

Node.js (libuv)

다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Node.js는 내부적으로 libuv 사용
// libuv가 플랫폼별 최적 메커니즘 선택
// - Linux: epoll
// - macOS: kqueue
// - Windows: IOCP

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello World');
});

server.listen(8080);

// 단일 스레드로 수만 개 연결 처리

Redis (epoll/kqueue)

아래 코드는 c를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Redis 이벤트 루프 (ae.c)
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

// 내부적으로 epoll/kqueue 사용
// 단일 스레드로 초당 100,000+ 명령 처리

17. 비교표

종합 비교

항목selectpollepollkqueueIOCP
플랫폼Unix/Linux/WindowsUnix/LinuxLinuxBSD/macOSWindows
최대 fd1024무제한무제한무제한무제한
복잡도O(n)O(n)O(k)O(k)O(k)
성능낮음낮음높음높음매우 높음
이벤트 종류I/OI/OI/OI/O+파일+시그널+타이머I/O
알림 방식ReadinessReadinessReadinessReadinessCompletion
스레드 안전아니오아니오

선택 가이드

다음은 text를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

연결 수 < 100:
→ select/poll (충분히 빠름, 간단함)

연결 수 100~1,000:
→ epoll/kqueue (권장)

연결 수 10,000+:
→ epoll/kqueue/IOCP (필수)

크로스 플랫폼:
→ libuv, libevent, Boost.Asio

Windows:
→ IOCP (최고 성능)

Linux:
→ epoll (최고 성능)

macOS:
→ kqueue (최고 성능)

18. 트러블슈팅

자주 발생하는 문제

1) “Too many open files” 에러

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 원인: 파일 디스크립터 제한 초과
# 해결:
ulimit -n 100000

# 영구 설정
echo "* soft nofile 100000" >> /etc/security/limits.conf
echo "* hard nofile 100000" >> /etc/security/limits.conf

2) epoll_wait() 반환 안됨

아래 코드는 c를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 원인: Edge-Triggered 모드에서 데이터 남음
// 해결: EAGAIN까지 읽기
while (1) {
    int n = recv(fd, buffer, sizeof(buffer), 0);
    if (n < 0) {
        if (errno == EAGAIN) break;  // 필수!
    }
    // 처리
}

3) 메모리 누수

아래 코드는 c를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 원인: 연결 종료 시 리소스 미해제
// 해결:
if (n <= 0) {
    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);  // epoll에서 제거
    close(fd);                                      // 소켓 닫기
    free(client_data);                              // 메모리 해제
}

4) 성능 저하

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 원인: 커널 파라미터 미조정
# 해결:
# 1. TCP 버퍼 크기 증가
# 2. backlog 크기 증가
# 3. TIME_WAIT 재사용 활성화

19. 고급 주제

io_uring (Linux 5.1+, 2019)

io_uring은 Linux의 차세대 비동기 I/O다.

아래 코드는 text를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

기존 (epoll):
- 시스템 콜 필요 (커널 ↔ 유저 전환)
- epoll_wait() → recv() → send()

io_uring:
- 공유 메모리 링 버퍼
- 시스템 콜 최소화
- 배치 처리 가능

성능:
- epoll 대비 2~3배 빠름
- 특히 고부하에서 효과적

io_uring 예제:

다음은 c를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <liburing.h>

struct io_uring ring;
io_uring_queue_init(256, &ring, 0);

// 비동기 read 요청
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buffer, sizeof(buffer), 0);
io_uring_submit(&ring);

// 완료 대기
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);

// 결과 처리
int bytes_read = cqe->res;
io_uring_cqe_seen(&ring, cqe);

eBPF + XDP (초고성능)

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

eBPF (Extended Berkeley Packet Filter):
- 커널 내부에서 패킷 처리
- 유저 공간 전환 없음
- DDoS 방어, 로드 밸런싱

XDP (eXpress Data Path):
- 네트워크 드라이버 단계에서 처리
- 초당 수천만 패킷 처리
- Cloudflare, Facebook이 사용

20. 프로덕션 체크리스트

필수 설정

논블로킹 소켓, epoll/kqueue에서 ET 사용 시 읽기 루프 처리, ulimit 등 fd 상한, TCP 버퍼·SO_REUSEADDR·실시간 트래픽이면 TCP_NODELAY, Keep-Alive, EAGAIN/EINTR 처리, 연결 종료 시 리소스 해제, 로그·메트릭은 운영 단계에서 빠지기 쉬우니 미리 자리를 잡는다.

모니터링

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 연결 수 확인
netstat -an | grep ESTABLISHED | wc -l

# 소켓 상태 확인
ss -s

# epoll 사용 확인
strace -e epoll_wait ./server

# 성능 프로파일링
perf record -g ./server
perf report

FAQ

Q1. select는 이제 쓰지 않나?

연결이 적거나 이식성이 최우선이면 select로도 충분한 경우가 있다. 동시 연결이 커지면 epoll·kqueue 쪽이 부담이 적다.

Q2. epoll과 IOCP 중 더 빠른 쪽은?

벤치마크·워크로드마다 달라서 숫자만으로 단정하기 어렵다. Windows면 IOCP, Linux면 epoll처럼 플랫폼에 맞추는 게 먼저다.

Q3. Node.js는 내부적으로 무엇을 쓰나?

libuv가 OS별로 epoll·kqueue·IOCP 등을 고른다. 애플리케이션 코드는 그 위에서 돈다고 보면 된다.

Q4. 멀티스레드와 멀티플렉싱을 같이 쓸 수 있나?

흔하다. accept만 전담하고 워커마다 epoll 루프를 돌리거나, CPU 코어 수에 맞춰 나누는 식으로 설계한다.


요약

핵심 정리

I/O 멀티플렉싱:

  • 하나의 스레드로 여러 I/O 동시 처리
  • 고성능 네트워크 서버의 핵심

메커니즘 비교:

  • select/poll: O(n), 소규모
  • epoll/kqueue: O(k), 대규모
  • IOCP: Completion 기반, Windows

선택 기준:

  • Linux → epoll
  • macOS/BSD → kqueue
  • Windows → IOCP
  • 크로스 플랫폼 → libuv, Boost.Asio

성능:

  • select: ~30,000 req/s
  • epoll: ~200,000 req/s
  • IOCP: ~250,000 req/s

학습 로드맵

블로킹 소켓과 select로 멀티 소켓의 뼈대를 만든 뒤, 논블로킹과 epoll·kqueue로 옮겨 보면 차이가 체감된다. 그다음 HTTP 같은 프로토콜 한 줄, 튜닝·배포, 필요하면 zero-copy·io_uring·프로세스 모델로 넓히면 된다.

다음 글 추천


키워드: I/O Multiplexing, select, poll, epoll, kqueue, IOCP, 비동기 I/O, 네트워크, 고성능, C10K, Reactor, Proactor

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3