Александр Крижановский, natsys lab
DESCRIPTION
HighLoad++ 2013TRANSCRIPT
Про сокеты и миллионы пакетов в секунду с одного 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