Давайте снова поговорим об отставании TCP

TCP/IP оптимизация производительности

Что касается трехэтапного рукопожатия, то здесь есть много деталей, которые подробно не были представлены в предыдущей статье.В этой статье мы используем параметр отставания для более глубокого изучения процесса установления соединения. Прочитав эту статью, вы освоите следующие знания:

  • Что такое невыполненные работы, полусвязные очереди и полносвязные очереди?
  • Как ядро ​​Linux вычисляет полусвязные очереди и полносвязные очереди
  • Почему только SomaxConn и TCP_MAX_SYN_BACKLOG, которые изменяют систему, не работают для окончательного размера очереди
  • Как использовать зонд systemtap для получения информации об очередях полу- и полных соединений текущей системы
  • По какому принципу работает инструмент ss в библиотеке iprouter
  • Как быстро имитировать переполнение полусвязной очереди, переполнение полносвязной очереди

Примечание. Код и тесты в этой статье были выполнены для версии ядра 3.10.0-514.16.1.el7.x86_64.

Основные понятия полусвязных очередей и полносвязных очередей

Чтобы понять отставание, нам нужно понять, что происходит за функциями прослушивания и принятия. Параметр невыполненной работы связан с функцией прослушивания. Определение функции прослушивания выглядит следующим образом:

int listen(int sockfd, int backlog);

Когда сервер вызывает функцию Listen, состояние TCP изменяется с состояния Close на Listen, и ядро ​​создает две очереди:

  • Неполная очередь соединений, также известная как очередь SYN.
  • Завершенная очередь соединений, также известная как очередь принятия

Как показано ниже.

Далее мы подробно представим содержимое, связанное с этими двумя очередями.

Полусвязная очередь (SYN Queue)

Когда клиент инициирует SYN на сервер, сервер вернет ACK и свой собственный SYN после его получения. В это время TCP на стороне сервера переходит из состояния прослушивания в состояние SYN_RCVD (SYN получено). В это время информация о соединении будет помещена в «очередь полусоединений». Очередь полусоединений также называется SYN. Очередь, в которой хранятся "входящие SYN.пакеты".

После того, как сервер отвечает на пакет Syn + ACK, он ждет клиента, чтобы ответить ACK, и запускает таймер. Если ACK не получен после тайм-аута, он будет повторно передавать SYN + ACK. Количество повторных передач определяется Значение TCP_SYNACK_RETRIES. На Centos это значение равно 5.

После получения ACK от клиента сервер запускаетсяпытатьсяДобавьте его в другую полностью подключенную очередь (Accept Queue).

Расчет размера очереди semi-join

Здесь инструмент SystemTap используется для вставки системного зонда, и после получения пакета SYN выводится размер текущей очереди SYN и общий размер очереди полуобъединения.

Поток обработки получения пакета SYN сокетом в состоянии прослушивания TCP выглядит следующим образом.

tcp_v4_rcv
  ->tcp_v4_do_rcv
    -> tcp_v4_conn_request

Здесь вводится метод tcp_v4_conn_request, и код выглядит следующим образом.

probe kernel.function("tcp_v4_conn_request") {
    tcphdr = __get_skb_tcphdr($skb);
    dport = __tcp_skb_dport(tcphdr);
    if (dport == 9090)
    {
        printf("reach here\n");
        // 当前 syn 排队队列的大小
        syn_qlen = @cast($sk, "struct inet_connection_sock")->icsk_accept_queue->listen_opt->qlen;
        // syn 队列总长度 log 值
        max_syn_qlen_log = @cast($sk, "struct inet_connection_sock")->icsk_accept_queue->listen_opt->max_qlen_log;
        // syn 队列总长度,2^n
        max_syn_qlen = (1 << max_syn_qlen_log);
        printf("syn queue: syn_qlen=%d, max_syn_qlen_log=%d, max_syn_qlen=%d\n",
         syn_qlen, max_syn_qlen_log, max_syn_qlen);
        // max_acc_qlen = $sk->sk_max_ack_backlog;
        // printf("accept queue length limit: %d\n", max_acc_qlen)
        print_backtrace();
    }
}

Выполните приведенный выше скрипт, используя stap

sudo stap -g syn_backlog.c

Таким образом, после получения пакета SYN можно распечатать количество и общий размер подключений, находящихся в очереди в текущей очереди синхронизации.

Взяв в качестве примера предыдущую эхо-программу, отставание от прослушивания установлено на 10, как показано ниже.

int server_fd = //...

listen(server_fd, 10 /*backlog*/)

Запустите эхо-сервер и послушайте порт 9090. Затем используйте команду nc на другом компьютере для подключения.

nc 10.211.55.10 9090

В этот момент в выводе stap уже можно увидеть текущий Вы можете видеть, что размер очереди синхронизации равен 0, а максимальная длина очереди равна2^4=16

Итак, вы можете видеть, что фактический syn не равенnet.ipv4.tcp_max_syn_backlogЗначение по умолчанию 128, но пользователь передал 10, возведенное в ближайшую степень 2, значение 16.

Далее давайте посмотрим, как рассчитывается код Размер очереди полусоединения связан с тремя значениями:

  • Пользовательский уровень прослушивает входящий отставание
  • системная переменнаяnet.ipv4.tcp_max_syn_backlog, значение по умолчанию – 128.
  • системная переменнаяnet.core.somaxconn, значение по умолчанию – 128.

Конкретный расчет показан в исходном коде ниже.Вызов функции прослушивания сначала введет следующий код.

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    // sysctl_somaxconn 是系统变量 net.core.somaxconn 的值
	int somaxconn = sysctl_somaxconn;
	if ((unsigned int)backlog > somaxconn)
		backlog = somaxconn;
	sock->ops->listen(sock, backlog);
}

SYSCALL_DEFINE2 может быть изучен кодом, если пользователь больше, чем значение, переданное в отставание net.core.somaxconn системных переменных, не вступает в силу отставание, установленное пользователем, системные переменные, по умолчанию 128.

Затем значение отставания будет передано в метод inet_listen()->inet_csk_listen_start()->reqsk_queue_alloc() по очереди. Окончательный расчет выполняется в методе reqsk_queue_alloc. Упрощенный код выглядит следующим образом.

int reqsk_queue_alloc(struct request_sock_queue *queue,
		      unsigned int nr_table_entries)
{
    nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
    nr_table_entries = max_t(u32, nr_table_entries, 8);
    nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
    	
    for (lopt->max_qlen_log = 3;
         (1 << lopt->max_qlen_log) < nr_table_entries;
         lopt->max_qlen_log++);
}

В коде nr_table_entries — это значение отставания, рассчитанное ранее, а sysctl_max_syn_backlog — это значение net.ipv4.tcp_max_syn_backlog. Логика расчета следующая:

  • Меньшее значение nr_table_entries и sysctl_max_syn_backlog присваивается nr_table_entries.
  • Возьмите большее значение между nr_table_entries и 8 и назначьте его nr_table_entries.
  • nr_table_entries + 1 округлить до ближайшей наибольшей степени числа 2
  • Значения значений не больше NR_TABLE_ENTRIES, ближайшие к NR_TABLE_ENTRIES через цикл for

Вот несколько практических примеров.В качестве примера возьмем listen(50).После расчета значения отставания в SYSCALL_DEFINE2, значение min(50,somaxconn), что равно 50.Далее введите расчет функции reqsk_queue_alloc .

// min(50, 128) = 50
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
// max(50, 8) = 50
nr_table_entries = max_t(u32, nr_table_entries, 8);
// roundup_pow_of_two(51) = 64
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
  
max_qlen_log 最小值为 2^3 = 8
for (lopt->max_qlen_log = 3;
     (1 << lopt->max_qlen_log) < nr_table_entries;
     lopt->max_qlen_log++);
经过 for 循环 max_qlen_log = 2^6 = 64

Окончательные значения размера очереди полусоединения для различных комбинаций somaxconn, max_syn_backlog и отставания приведены ниже.

somaxconn max_syn_backlog listen backlog Размер очереди полусоединения
128 128 5 16
128 128 10 16
128 128 50 64
128 128 128 256
128 128 1000 256
128 128 5000 256
1024 128 128 256
1024 1024 128 256
4096 4096 128 256
4096 4096 4096 8192

можно увидеть:

  • В случае системных параметров без изменения, прослушивания и слепой передачи большой объем невыполненной очереди окончательного полусоединения не будет затронут.
  • При условии, что отставание прослушивания остается неизменным, слепое увеличение somaxconn и max_syn_backlog не повлияет на размер конечной очереди полусоединения.

Имитировать полную очередь полусоединений

Чтобы somaxconn = 128, tcp_max_syn_backlog = 128, listen backlog = 50, например, принцип имитации — второй шаг в трехстороннем рукопожатии, клиент использует iptables, отбрасывая пакет после получения ответа сервера SYN + ACK. Вот экспериментальный сервер 10.211.55.10, клиент 10.211.55.20, с помощью правила iptables увеличиваем клиент, как показано ниже.

sudo  iptables --append INPUT  --match tcp --protocol tcp --src 10.211.55.10 --sport 9090 --tcp-flags SYN SYN --jump DROP

Смысл этого правила в том, чтобы отбросить пакет SYN с ip 10.211.55.10 и исходного порта 9090, как показано на следующем рисунке.

syn-queue-full

Далее используйте ваш любимый язык и начните подключение.Здесь выбирается Go.Код выглядит следующим образом:

func main() {
	for i := 0; i < 2000; i++ {
		go connect()
	}
	time.Sleep(time.Minute * 10)
}
func connect() {
	_, err := net.Dial("tcp4", "10.211.55.10:9090")
	if err != nil {
		fmt.Println(err)
	}
}

Запустите эту программу и используйте netstat на сервере, чтобы просмотреть текущий статус подключения к порту 9090, как показано ниже.

netstat -lnpa | grep :9090  | awk '{print $6}' | sort | uniq -c | sort -rn
     64 SYN_RECV
      1 LISTEN

Можно заметить, что количество соединений в состоянии SYN_RECV увеличивается с 0 до 64, а затем уже не увеличивается, где 64 — размер очереди полусоединений.

Далее мы смотрим на полностью подключенную очередь

Полностью подключенная очередь (Accept Queue)

«Полная очередь соединений» содержит все серверы для завершения трехэтапного рукопожатия, но еще не удалены, принимают вызовы приложения из очереди соединений. В это время сокет находится в состоянии ESTABLISHED. Каждое приложение, вызывающее функцию accept(), удалит коннектор из очереди. Если очередь пуста, accept() обычно блокируется. Полная очередь соединений также относится к очереди принятия.

Вы можете думать об этом процессе как о модели производитель-потребитель. Ядро является производителем, отвечающим за трехстороннее рукопожатие, и соединение после рукопожатия ставится в очередь. Наше приложение является потребителем, берущим соединения из очереди для дальнейшей обработки. В этой модели «производитель-потребитель» задержка в очереди возникает, когда производство идет слишком быстро, а потребление слишком медленно.

Второй параметр отставания функции прослушивания используется для установки размера полной очереди соединений, но это значение отставания не обязательно выбирается, а также ограничивается somaxconn.Будет более подробное содержание для объяснения правил расчета полного размер очереди соединений позже.

int listen(int sockfd, int backlog)

Если полная очередь соединений заполнена, ядро ​​​​отбрасывает подтверждение, отправленное клиентом (прикладной уровень будет думать, что в это время соединение не было полностью установлено)

Давайте смоделируем ситуацию, когда полностью подключенная очередь заполнена. Поскольку только accept удаляет полностью подключенную очередь, если мы просто слушаем и не вызываем accept, то полностью подключенная очередь может заполниться очень быстро.

Чтобы быть ближе к вызову нижнего уровня, здесь реализовано на языке c, и создан новый файл main.c.

#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>

int main() {
    struct sockaddr_in serv_addr;
    int listen_fd = 0;
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        exit(1);
    }
    bzero(&serv_addr, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);

    if (bind(listen_fd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) == -1) {
        exit(1);
    }
    
    // 设置 backlog 为 50
    if (listen(listen_fd, 50) == -1) {
        exit(1);
    }
    sleep(100000000);
    return 0;
}

скомпилировать и запуститьgcc main.c; ./a.out, используйте предыдущую программу go, чтобы инициировать соединение, и используйте netstat на сервере, чтобы просмотреть состояние соединения tcp.

netstat -lnpa | grep :9090  | awk '{print $6}' | sort | uniq -c | sort -rn
     51 ESTABLISHED
     31 SYN_RECV
      1 LISTEN

Хотя одновременно было отправлено много запросов, фактически только 51 запрос находился в состоянии ESTABLISHED, а большое количество запросов находилось в состоянии SYN_RECV.

Также обратите внимание, что отставание равно 50, но на самом деле 51 соединение находится в состоянии ESTABLISHED, что будет обсуждаться позже.

Клиент использует netstat для проверки наличия сотен TCP-соединений и статуса ESTABLISHED, как показано ниже.

Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 10.211.55.20:37732      10.211.55.10:9090       ESTABLISHED 23618/./connect
tcp        0      0 10.211.55.20:37824      10.211.55.10:9090       ESTABLISHED 23618/./connect
tcp        0      0 10.211.55.20:37740      10.211.55.10:9090       ESTABLISHED 23618/./connect
...

Используя systemstap, вы можете наблюдать за текущей полной очередью соединений в режиме реального времени.Код зонда выглядит следующим образом.

probe kernel.function("tcp_v4_conn_request") {
    tcphdr = __get_skb_tcphdr($skb);
    dport = __tcp_skb_dport(tcphdr);
    if (dport == 9090)
    {
        printf("reach here\n");
        // 当前 syn 排队队列的大小
        syn_qlen = @cast($sk, "struct inet_connection_sock")->icsk_accept_queue->listen_opt->qlen;
        // syn 队列总长度 log 值
        max_syn_qlen_log = @cast($sk, "struct inet_connection_sock")->icsk_accept_queue->listen_opt->max_qlen_log;
        // syn 队列总长度,2^n
        max_syn_qlen = (1 << max_syn_qlen_log);
        printf("syn queue: syn_qlen=%d, max_syn_qlen_log=%d, max_syn_qlen=%d\n",
         syn_qlen, max_syn_qlen_log, max_syn_qlen);
        ack_backlog = $sk->sk_ack_backlog;
        max_ack_backlog = $sk->sk_max_ack_backlog;
        printf("accept queue length, max: %d, current: %d\n", max_ack_backlog, ack_backlog)
    }
}

Используйте stap для выполнения теста, повторно запустите вышеуказанный тест и посмотрите вывод теста ядра.

...
syn queue: syn_qlen=45, max_syn_qlen_log=6, max_syn_qlen=64
accept queue length, max: 50, current: 14
...
syn queue: syn_qlen=2, max_syn_qlen_log=6, max_syn_qlen=64
accept queue length, max: 50, current: 51

Также видно, что размер полной очереди соединений подтвердил наше предыдущее утверждение.

Результат трассировки пакета на стороне сервера следующий:

Далее клиент 10.211.55.20 — это A, а сервер 10.211.55.10 — это B.

  • 1: Клиент A инициирует SYN для порта 9090 сервера B, начиная первый шаг трехэтапного рукопожатия.
  • 2: сервер B немедленно ответил ACK + SYN, в это время сокет сервера B находится в состоянии SYN_RCVD
  • 3: Клиент A получает ACK+SYN от сервера B и отправляет ACK последнего шага трехэтапного рукопожатия на сервер B. В это время он находится в состоянии ESTABLISHED. очередь соединений сервера B заполнена, он отбрасывает ACK, соединение не установлено
  • 4: Сервер B считает, что он не получил ACK, и считает, что его SYN + ACK в 2 были потеряны в процессе передачи, поэтому он начинает повторную передачу и ожидает, что клиент снова ответит на ACK.
  • 5: После того, как клиент A получает BYS + BIN + ACK, он отвечает ACK сразу
  • . 16s), в общей сложности 5 повторных передач занял 31-е годыSYN + ACKПозже сервер B считает, что надежды нет, и через некоторое время система восстанавливает это TCP-соединение.

Количество повторных передач SYN+ACK определяется файлом операционной системы/proc/sys/net/ipv4/tcp_synack_retries, вы можете просмотреть этот файл с котом

cat /proc/sys/net/ipv4/tcp_synack_retries
5

Весь процесс показан на рисунке ниже:

Размер полностью подключенной очереди

Размер полностью подключенной очереди равен наименьшему из значений невыполненной работы и somaxconn, переданных прослушиванием.

Функция для определения того, заполнен ли полный размер очереди соединений, — это метод sk_acceptq_is_full в файле /include/net/sock.h.

static inline bool sk_acceptq_is_full(const struct sock *sk)
{
	return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

Здесь нет ничего плохого, но sk_ack_backlog рассчитывается от 0, поэтому реальный полный размер очереди соединений равен backlog + 1. Если вы укажете для невыполненной работы значение 1, количество подключений, которое может быть размещено, будет равно 2. Раздел 4.5 на стр. 87 «Сетевого программирования Unix, том 1» содержит подробное сравнение взаимосвязи между отставанием каждой операционной системы и фактическим максимальным числом полностью подключенных очередей.

СС команда

Команда ss может просматривать размер полной очереди соединений и количество соединений, ожидающих в настоящее время принятия, выполнения.ss -lntТо есть, например, в приведенном выше примере, где очередь приема заполнена, результат выполнения команды ss выглядит следующим образом.

ss -lnt | grep :9090
State      Recv-Q Send-Q Local Address:Port               Peer Address:Port
LISTEN     51     50           *:9090                     *:*

Для состояния прослушивания сокета Recv-Q представляет собой количество подключений для приема очередей, Edd-Q представляет собой дополнительное подключение к очереди (I.E. Принять очередь).

Давайте взглянем на низкоуровневую реализацию команды ss. Исходный код команды ss находится в проекте iproute2, который умело использует netlink для связи с модулем tcp_diag в стеке протокола TCP для получения подробной информации о сокете. tcp_diag - модуль статистического анализа, который может получить много полезной информации в ядре.Recv-Q и Send-Q в выводе ss получены из модуля tcp_diag.Эти два значения равны idiag_rqueue и idiag_wqueue из структура inet_diag_msg. Исходный код секции tcp_diag показан ниже.

static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r,
			      void *_info)
{
	struct tcp_info *info = _info;

	if (inet_sk_state_load(sk) == TCP_LISTEN) {
	   // 对应 Recv-Q
		r->idiag_rqueue = READ_ONCE(sk->sk_ack_backlog); 
		// 对应 Send-Q
		r->idiag_wqueue = READ_ONCE(sk->sk_max_ack_backlog);	} else if (sk->sk_type == SOCK_STREAM) {
		const struct tcp_sock *tp = tcp_sk(sk);
		r->idiag_rqueue = max_t(int, READ_ONCE(tp->rcv_nxt) -
					     READ_ONCE(tp->copied_seq), 0);
		r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una;
	}
}

Из приведенного выше исходного кода мы можем знать, что:

  • Для сокета в состоянии LISTEN Recv-Q соответствует sk_ack_backlog, который указывает количество подключений, которые текущий сокет завершает трехэтапное рукопожатие в ожидании принятия пользовательского процесса.Send-Q соответствует sk_max_ack_backlog, который указывает максимальное количество соединений, которые может разместить текущая очередь полных соединений сокета.
  • Для сокетов в состоянии, отличном от LISTEN, Recv-Q указывает размер очереди приема в байтах, а Send-Q указывает размер очереди отправки в байтах.

разное

Какой размер отставания подходит

Так много было сказано ранее, какой объем отставания является разумным для установки приложения?

Ответ заключается в том, что это тоже зависит от бизнес-сценариев, необходимо выполнить соответствующую настройку.

  • Если у вашего интерфейса очень высокие требования к обработке соединений, или вы проводите стресс-тестирование, то необходимо увеличить это значение
  • Если производительность самого бизнес-интерфейса не очень хорошая, а скорость приема установленного соединения медленная, то регулировать отставание на большее значение бесполезно, это только увеличит вероятность сбоя соединения

Вы можете указать типичное значение невыполненной работы для справки.Значение невыполненной работы по умолчанию для Nginx и Redis равно 511, значение невыполненной работы по умолчанию для Linux — 128, а значение невыполненной работы по умолчанию — 50 для Java.

параметр tcp_abort_on_overflow

По умолчанию после полной очереди подключения заполнен, сервер игнорирует ACK клиента, а затем повторная передачаSYN+ACK, это поведение также можно изменить, это значение задается/proc/sys/net/ipv4/tcp_abort_on_overflowПринять решение.

  • Если tcp_abort_on_overflow равен 0, это означает, что после того, как полная очередь соединений будет заполнена на последнем шаге трехэтапного рукопожатия, сервер отклонит ACK, отправленный клиентом, и затем сервер повторно передаст SYN+ACK.
  • Когда tcp_abort_on_overflow равен 1, это означает, что сервер напрямую отправляет RST клиенту после заполнения полной очереди соединений.

Но возврат RST-пакета клиенту вызовет другую проблему.Клиент не знает, является ли ответ RST-пакета сервером тем, что «на этом порту нет мониторинга процессов» или «на этом порту есть мониторинг процессов, но его очередь заполнена». .

резюме

Эта статья из нашего параметра невыполненной работы является входом для изучения взаимосвязи между очередью половинного соединения, очередь полностью подключена. Краткий обзор.

  • Полуподключенная очередь: когда сервер получает пакет SYN от клиента и отвечает SYN+ACK, но не получил ACK от клиента, он помещает информацию о соединении в полуподключенную очередь. Очереди полуобъединения также известны как очереди SYN.
  • Полная очередь соединений: сервер завершил трехстороннее рукопожатие, но не был снят с принятия. Полностью подключенная очередь также называется очередью принятия.
  • Размер очереди полусоединений связан с отставанием, net.core.somaxconn и net.core.somaxconn, переданными пользователем listen.Точные правила расчета см. в анализе исходного кода выше.
  • Размер полной очереди соединений — это наименьшее значение невыполненной работы и net.core.somaxconn, переданное пользователем listen.

Выводы, упомянутые выше, не должны быть правильными, это и моя точка зрения: важен не вывод, а важен процесс исследования. Я хочу научить вас ловить рыбу и научить вас некоторым инструментам и методам.Если вы сможете изучить некоторые проблемы путем умозаключений, это будет превосходно.

Не верьте произвольно выводам, сделанным в интернет-статьях, в том числе и в моей. Экспериментируйте, чтобы узнать правду, и убедитесь в этом сами.

Проблемы могут сканировать двумерный код после того, как общественность беспокоит мой номер, чтобы связаться со мной.