Socket backlog

Преамбула: как сервер принимает подключения?

Сервер делает так: listen_socket_fd = socket(AF_INET, SOCK_STREAM, 0) – создаём сокет; setsockopt(listen_socket_fd, _flag_, _value_) – настраиваем его (необязательно); bind(listen_socket_fd, _address_) – привязываем адрес, где будем слушать; listen(listen_socket_fd, **_backlog_size_**) – говорим, что это «слушающий сокет»; int connected_socket_fd = accept(listen_socket_fd, NULL, 0) – садимся и ждём клиентов.

На блокирующих (по-умолчанию) сокетах приложение-сервер висит в accept() до прихода соединения. Если в setsockopt указано TCP_DEFER_ACCEPT (например, за установку этого флага отвечает параметр deferred в директиве listen в конфигурации Nginx), то управление приложению из вызова accept() возвращается, только когда пришли первые данные. Иначе – сразу после того, как произошел tcp handshake.

Accept() возвращает файл-дескриптор нового сокета – сокета соединения. Это два разных сокета: первый (слушающий) имеет единственное состояние TCP_LISTEN, второй – все состояния, кроме TCP_LISTEN.

Все подключения до момента accept() помещаются в очередь, именуемую backlog. Очередь привязана к слушающему сокету, при этом в параметрах listen() указывается максимальная длина этой очереди – backlog size. При переполнении очереди соединение сразу отбрасывается (при proxy_pass в Nginx, например, получим ошибку 502). Очень часто разные приложения-сервера выносят эту настройку в свою конфигурацию.

Как сказал бы Тони Роббинс будь он админом, а не коучем:

«если приложение перестаёт accept()'ить соединения, то растёт очередь в беклоге».

Это была преамбула, а теперь фабула.

Долгое время админы, пришедшие в Linux с FreeBSD плакали, что нет возможности мониторить очередь сокета, вспоминая netstat -L, которого нет в Linux. Объяснялось это весьма мутной документацией netstat в этой части:

Recv-Q
    The count of bytes not copied by the user program connected to this socket.
Send-Q
    The count of bytes not acknowledged by the remote host.

Я решил посмотреть, что делает ss -ln

socket(PF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG) = 3
sendto(3, "(\0\0\0\24\0\1\3@\342\1\0\0\0\0\0\1\0\0\0\200\4\0\0\0\0\0\0\25\0\0\0"..., 40, 0, NULL, 0) = 40
recvfrom(3, "L\0\0\0\24\0\2\0@\342\1\0\36+\0\0\1\2\7\0'`\0\0\300\3272\35\20\210\377\377"..., 8192, 0, {sa_family=AF_NETLINK, pid=0, groups=00000000},

Ага, информацию ss берёт из ядра через netlink API. Поиск по ключевому слову NETLINK_SOCK_DIAG приводит нас в ман sock_diag(7), в середине которого встречаем искомое:

udiag_rqueue
    For listening sockets: the number of pending
    connections.  The length of the array associated
    equal to this value.

    For established sockets: the amount of data in
    incoming queue.

udiag_wqueue
    For listening sockets: the backlog length which
    equals to the value passed as the second argu‐
    ment to listen(2).

    For established sockets: the amount of memory
    available for sending.

В переводе на русский получается следующее: Если сокет слушающий, то Recv-Q обозначает длину очереди в соединениях, а Send-Q обозначает backlog_size, указанный в listen().

Если сокет – активный сокет соединения, то Recv-Q обозначает количество принятых байт, но не прочитанных приложением. А Send-Q – размер доступного пространства для отправки.

Подтверждается это кусками кода из ядра net/ipv4/tcp_diag.c:

if (sk->sk_state == TCP_LISTEN) {
    r->idiag_rqueue = sk->sk_ack_backlog;
    r->idiag_wqueue = sk->sk_max_ack_backlog;
} else {
    r->idiag_rqueue = max_t(int, tp->rcv_nxt - tp->copied_seq, 0);
    r->idiag_wqueue = tp->write_seq - tp->snd_una;
}

net/unix/diag.c:

if (sk->sk_state == TCP_LISTEN) {
    rql.udiag_rqueue = sk->sk_receive_queue.qlen;
    rql.udiag_wqueue = sk->sk_max_ack_backlog;
} else {
    rql.udiag_rqueue = (u32) unix_inq_len(sk);
    rql.udiag_wqueue = (u32) unix_outq_len(sk);
}

Итого, ss -ln – то, что вам нужно при диагностике подключений к серверу и теперь вы знаете, как интерпретировать его вывод.

Есть вопрос? Напишите в комментариях!