쿠버네티스, eBPF, 그리고 머신러닝 이야기
by Yang Taehwan
gcc -O2 -Wall -Wextra -pthread echo_epoll_threadpool.c -o echo_server
## 실행
./echo_server
nc 127.0.0.1 8080
struct conn {
int fd;
worker_t *owner;
// pending write buffer
char *wbuf;
size_t wbuf_len;
size_t wbuf_sent;
};
struct fd_node {
int fd;
fd_node_t *next;
};
struct worker {
int id;
int epfd;
int notify_fd;
pthread_t thread;
pthread_mutex_t queue_lock;
fd_node_t *queue_head;
fd_node_t *queue_tail;
conn_t *conns[MAX_FDS];
};
static int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) return -1;
return 0;
}
static int mod_epoll_events(worker_t *w, int fd, uint32_t events) {
struct epoll_event ev;
memset(&ev, 0, sizeof(ev));
ev.events = events;
ev.data.fd = fd;
return epoll_ctl(w->epfd, EPOLL_CTL_MOD, fd, &ev);
}
static int add_epoll_fd(worker_t *w, int fd, uint32_t events) {
struct epoll_event ev;
memset(&ev, 0, sizeof(ev));
ev.events = events;
ev.data.fd = fd;
return epoll_ctl(w->epfd, EPOLL_CTL_ADD, fd, &ev);
}
static int handle_payload(conn_t *c, const char *buf, size_t len) {
if (c->wbuf_len == 0) {
// ...
while (sent < len) {
ssize_t n = send(c->fd, buf + sent, len - sent, 0);
if (n > 0) {
sent += (size_t)n;
continue;
}
// ...
}
// ...
}
static void handle_readable(worker_t *w, conn_t *c) {
// ...
for (;;) {
ssize_t n = recv(c->fd, buf, sizeof(buf), 0);
if (n > 0) {
int rc = handle_payload(c, buf, (size_t)n);
// ...
// epoll 이벤트 재설정
// EPOLLIN 읽기 가능 상태(readable) 감시
// EPOLLET Edge Trigger
// EPOLLRDHUP peer가 FIN 보냄을 감지(half-close)
uint32_t events = EPOLLIN | EPOLLET | EPOLLRDHUP;
// ...
if (mod_epoll_events(w, c->fd, events) == -1) {
// ...
}
continue;
}
if (n == 0) {
// peer closed
// ...
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// ET mode: drained all readable data
break;
}
close_connection(w, c->fd);
return;
}
}
static void handle_writable(worker_t *w, conn_t *c) {
// ...
int rc = flush_write_buffer(c);
// ...
uint32_t events = EPOLLIN | EPOLLET | EPOLLRDHUP;
if (c->wbuf_len > 0) {
events |= EPOLLOUT;
}
if (mod_epoll_events(w, c->fd, events) == -1) {
// ...
}
}
static void enqueue_fd(worker_t *w, int fd) {
fd_node_t *node = (fd_node_t *)malloc(sizeof(fd_node_t));
// ...
node->fd = fd;
node->next = NULL;
// 링크드 리스트에 Insert 연산
pthread_mutex_lock(&w->queue_lock);
if (w->queue_tail) {
w->queue_tail->next = node;
w->queue_tail = node;
} else {
w->queue_head = w->queue_tail = node;
}
pthread_mutex_unlock(&w->queue_lock);
// worker thread를 깨운다.
uint64_t one = 1;
if (write(w->notify_fd, &one, sizeof(one)) == -1) {
// best effort
}
}
static void drain_new_connections(worker_t *w) {
for (;;) {
// 링크드리스트에서 아직 epoll에 등록되지 않은 소켓을 하나 가져온다.
fd_node_t *node = NULL;
pthread_mutex_lock(&w->queue_lock);
if (w->queue_head) {
node = w->queue_head;
w->queue_head = node->next;
if (!w->queue_head) {
w->queue_tail = NULL;
}
}
pthread_mutex_unlock(&w->queue_lock);
// ...
// calloc은 malloc과 달리 메모리를 할당하면서 동시에 0으로 초기화한다.
// conn 객체 생성
conn_t *c = (conn_t *)calloc(1, sizeof(conn_t));
// ...
c->fd = fd;
c->owner = w;
w->conns[fd] = c;
// epoll 등록
if (add_epoll_fd(w, fd, EPOLLIN | EPOLLET | EPOLLRDHUP) == -1) {
// ...
}
// ...
}
}
static void *worker_main(void *arg) {
// ...
while (!stop_flag) {
// 이벤트가 발생할 때까지 잠들어 있는다.
int n = epoll_wait(w->epfd, events, MAX_EVENTS, 1000);
// ...
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
uint32_t ev = events[i].events;
// main thread가 새로운 커넥션이 있다고 알림
if (fd == w->notify_fd) {
// ...
drain_new_connections(w);
continue;
}
// ...
// readable 이벤트 처리
if (ev & EPOLLIN) {
handle_readable(w, c);
// ...
}
// writable 이벤트 처리
if (ev & EPOLLOUT) {
// ...
handle_writable(w, c);
}
}
}
// ...
}
static void init_worker(worker_t *w, int id) {
// ...
// worker 전용 이벤트 감시 객체 생성
w->epfd = epoll_create1(0);
// ...
// worker thread 생성
if (pthread_create(&w->thread, NULL, worker_main, w) != 0) {
// ...
}
}
static int create_server_socket(uint16_t port) {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// ...
int opt = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
// ...
}
// ...
if (set_nonblocking(listen_fd) == -1) {
// ...
}
// ...
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
// ...
}
if (listen(listen_fd, BACKLOG) == -1) {
// ...
}
// ...
}
static void accept_loop(int listen_fd) {
while (!stop_flag) {
// ...
int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
// ...
if (set_nonblocking(client_fd) == -1) {
// ...
}
// ...
// round robin 방식으로 할당
worker_t *w = &workers[next_worker_idx];
next_worker_idx = (next_worker_idx + 1) % worker_count;
enqueue_fd(w, client_fd);
}
}
int main(int argc, char **argv) {
// ...
// 코어 수를 가져온다.
long cores = sysconf(_SC_NPROCESSORS_ONLN);
// ...
worker_count = (int)cores;
// ...
// worker 초기화
for (int i = 0; i < worker_count; i++) {
init_worker(&workers[i], i);
}
// 서버 소켓 생성
int listen_fd = create_server_socket(port);
// ...
// accpet 루프 시작
accept_loop(listen_fd);
// ...
}