Про сокеты и миллионы
пакетов в секунду с одного
CPU ядра
Александр Крижановский
О сокетах (серверных)
●

●

●

●

●

●

путь пакета в Linux с адаптера в TCP-сокет;
установление новых соединений,
мультиплексирование и чтение из сокетов;
как ускорить прикладной сервер (оптимизация
accept(2), MSI-X и RPS/RFS, GRO);
можно быстрее: Oracle Reliable Datagram Sockets
еще быстрее: переход к полностью синхронным
сокетам
В основном интересовала скорость установления
соединений
Путь пакета (recv)
1.RSS (Receive Side
Scaling)
2.MSI-X очереди
3.DCA (Direct Cache
Access)
=> User/kernel:
копирования
=> асинхронность
получения и чтения
Типовой сценарий
    listen(listen_sd, 5);
    epoll_ctl(wd, EPOLL_CTL_ADD, listen_sd, &ev);
    while (1) {
        n = epoll_wait(wd, ev, 64, ­1);
        for (int i = 0; i < n; ++i) {
            if (ev[i].data.fd == listen_sd) {
                new_sd = accept(listen_sd, NULL, NULL);
            } else {
                recv(ev[i].data.fd, msg, READ_SZ, 0);
            }
        }
    }
Чтение из сокета: recvfrom(2)
●

●

sockfd_lookup_light() - захватывает файловый
дескриптор, получает соответствующий сокет
sock­>ops­>recvmsg() => tcp_recvmsg()
–

skb_queue_walk(&sk­>sk_receive_queue, skb) —
проходим по списку буферов
●
skb_copy_datagram_iovec() - копируем
●
tcp_cleanup_rbuf() - очищаем место в буфере
и отправляем ACK
●
tcp_rcv_space_adjust() - пересчитываем
свободное место в сокетном буфере
Мультиплексирование:
epoll_wait(2)
●

●

●

epoll_wait() → ep_poll() → 
__add_wait_queue_exclusive() — встаем в очередь
ожидания и засыпаем
epoll_ctl(EPOLL_CTL_ADD) → ep_insert() → 
tcp_poll() — добавляет сокет в очередь ожидания
tcp_v4_rcv() → tcp_v4_do_rcv() → 
tcp_rcv_state_process() → tcp_data_queue() → 
sk­>sk_data_ready()
–

ep_poll_callback() - разбудить процессы в
очереди ожидания
Установление новых соединений:
listen(2) & accept(2)
●

●

●

inet_csk_listen_start() - аллоцирует место в
очереди, равное backlog (второй аргумент
listen(2))
inet_csk_accept() → 
inet_csk_wait_for_connect() → wait (schedule)
tcp_v4_do_rcv() → tcp_child_process()
–

–

tcp_rcv_state_process()
●
tcp_set_state(sk, TCP_ESTABLISHED);
sk­>sk_state_change(sk);
parent­>sk_data_ready(parent, 0);
Пример переключения контекста:
«Наш клиент?»
1.epoll_wait(2)
2.accept(2)
3.getpeername(2)
4.=> не наш клиент: close(2)
7 контекст свитчей
Хорошо: user/kernel context switch не инвалидирует
кэши
Пример переключения контекста:
«Наш клиент?»
1.epoll_wait(2)
2.accept(2)
3.getpeername(2)
4.=> не наш клиент: close(2)
7 контекст свитчей
Хорошо: user/kernel context switch не инвалидирует
кэши
Что если приходит сразу много
клиентов?
    listen(listen_sd, 5);
    epoll_ctl(wd, EPOLL_CTL_ADD, listen_sd, &ev);
    while (1) {
        n = epoll_wait(wd, ev, 64, ­1);
        for (int i = 0; i < n; ++i) {
            if (ev[i].data.fd == listen_sd)
                new_sd = accept(listen_sd, NULL, NULL);
        }
    }
Оптимизация accept(2)
BRECHT T., PARIAG D., GAMMO L. «accept()able strategies for improving web
server performance.»
    listen(listen_sd, 1000);
    fcntl(sd, F_SETFL, flags | O_NONBLOCK);
    epoll_ctl(wd, EPOLL_CTL_ADD, listen_sd, &ev)  
    while (1) {
        n = epoll_wait(wd, ev, 64, ­1);
        for (int i = 0; i < n; ++i) {
            if (ev[i].data.fd == listen_sd)
                do {
                    new_sd = accept(listen_sd, NULL, NULL);
                } while (new_sd >= 0);
        }
    }
MSI-X, RPS, RFS
1. RSS (Receive Packet
Steering)
2. RFS (Receive Flow
Steering)
3. MSI-X не всегда хорошо
балансирует трафик =>
RPS
4. Но MSI-X - «железный»
Сокетные каллбеки
●

●

●

●

●

●

sk_data_ready — вызывается при получении
данных на сокете
sk_state_change — изменение состояния сокета
sk_write_space — у сокета появилось место в
буфере записи
sk_error_report — ошибка на сокете
sk_backlog_rcv — чтение из очереди отложенных
сегментов (!tcp_low_latency)
sk_destruct — вызывается на удалении сокета
Oracle Reliable Datagram Sockets
(RDS)
Ядерная реализация сокетов (linux/net/rds):
●

нет копирований и переключений контекстов

●

основная работа происходит на калбеках сокетов

Функции, необходимые accept(), могут спать => wait
queue
RDS accept
    int rds_tcp_accept_one(struct socket *sock) {
        sock­>ops­>accept(sock, new_sock, O_NONBLOCK);
    }
    void rds_tcp_accept_worker(struct work_struct *work) {
        while (rds_tcp_accept_one(rds_tcp_listen_sock) == 0);
    }
    DECLARE_WORK(rds_tcp_listen_work, rds_tcp_accept_worker);
    void rds_tcp_listen_data_ready(struct sock *sk, int bytes) {
        if (sk­>sk_state == TCP_LISTEN)
            queue_work(rds_wq, &rds_tcp_listen_work);
    }
    sock­>sk­>sk_data_ready = rds_tcp_listen_data_ready;
Synchronous Sockets
●

●

●

●

Всё делается только в softirq
Хорошо подходят для большого числа
короткоживущих соединений
Нужен патч ядра
Могут быть вынесены в некрасивую библиотеку
ядра:
–
–

upcalls & downcalls
Но код с ней в ~2 раза короче (200 vs 364)
Synchronous Sockets: пример
    struct { SsProto proto;} my_proto; 
    struct socket *listen_sock;
    int my_read(void *proto, char *data, int len) {
        /* Read application level data. */
    }
    int my_conn_new(struct sock *sock) {
        /* Handle a new TCP connection */
    }
    SsHooks ssocket_hooks = {.connection_new = my_connection_new};
    int my_init(void) {
        ss_hooks_register(&ssocket_hooks);
        ss_proto_push_handler((SsProto *)&my_proto, my_read);
        ss_tcp_set_listen(listen_sock­>sk, (SsProto *)&my_proto);
    }
Скорость установления
соединений
Запросы в скунду в зависимости
от числа соединений
Спасибо!
Synchronous Sockets (+патч):
https://github.com/krizhanovsky/sync_socket

ak@natsys-lab.com

Александр Крижановский, NatSys Lab