Deep Engineering

ЗАМЕР

bench/acceptq/practice.py

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

Цитируется в статье
/ru/interview/sre/accept-queue
Как запустить
python3 bench/acceptq/backlog.py  > bench/acceptq/runs/backlog.txt
python3 bench/acceptq/practice.py > bench/acceptq/runs/practice.txt

Запись прогона

Замеры для урока «Очередь accept»

Файл Что делает
backlog.py четыре наблюдения: соединение с сервером, который не звал accept; сколько соединений вмещает очередь при backlog 1 и 8; как выглядит переполнение со стороны клиента; и что делает с очередью somaxconn
practice.py ответы к задачам урока: исход connect, число отправленных байт, чем кончился цикл подключений и вместимость очереди

Запуск из корня репозитория:

python3 bench/acceptq/backlog.py  > bench/acceptq/runs/backlog.txt
python3 bench/acceptq/practice.py > bench/acceptq/runs/practice.txt

Что воспроизводимо

Успешный connect к серверу, который не вызывал accept; вместимость очереди backlog + 1; таймаут вместо ECONNREFUSED при переполнении; молчаливое урезание backlog до somaxconn.

Не воспроизводимо: миллисекунды подключения, номера портов и значение somaxconn конкретной машины.

Требования к среде

ss из iproute2 — без него строки про очередь напечатают not visible in ss. Четвёртый блок временно понижает /proc/sys/net/core/somaxconn и возвращает исходное значение; без права записи он честно скажет not permitted.

Числа сняты на CPython 3.11.15, Linux 6.18.44, loopback.

Скрипт

100 строк
"""Практика к уроку про очередь accept: три ответа и одна вместимость.

ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(scripts/validate-practice.mjs): показанный читателю код обязан построчно быть
в скрипте, верный вариант — дословно встречаться в записи прогона, а
заявленное число — печататься самой программой.

ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Три места, где разговор обычно сбивается: соединение с
сервером, который ни разу не вызвал `accept`, всё равно устанавливается;
клиент успевает отправить данные; а при переполнении очереди он получает не
отказ, а ожидание. Плюс сама вместимость очереди при `backlog=1`.

ЗАПУСК: python3 bench/acceptq/practice.py
Вывод: runs/practice.txt
"""

import errno
import os
import socket
import sys

HOST = "127.0.0.1"


def server(backlog: int) -> tuple[socket.socket, int]:
    srv = socket.socket()
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((HOST, 0))
    srv.listen(backlog)
    return srv, srv.getsockname()[1]


def connect_result(port: int) -> tuple[str, int]:
    """Что получит клиент от сервера, который accept не вызывал ни разу."""
    client = socket.socket()
    client.settimeout(1.0)
    try:
        client.connect((HOST, port))
    except OSError as exc:
        client.close()
        return errno.errorcode.get(exc.errno, str(exc.errno)), 0
    sent = client.send(b"GET / HTTP/1.0\r\n\r\n")
    client.close()
    return "success", sent


def fill(port: int, limit: int = 40, timeout: float = 0.6) -> tuple[int, str]:
    """Подключается, пока подключается: счёт и то, чем всё кончилось."""
    kept = []
    for _ in range(limit):
        client = socket.socket()
        client.settimeout(timeout)
        try:
            client.connect((HOST, port))
        except socket.timeout:
            client.close()
            return len(kept), "timeout"
        except OSError as exc:
            client.close()
            return len(kept), errno.errorcode.get(exc.errno, str(exc.errno))
        kept.append(client)
    for client in kept:
        client.close()
    return len(kept), "no refusal"


def main() -> None:
    print(f"Python {sys.version.split()[0]} · Linux {os.uname().release}")
    print()

    # --- Часть 1: три ответа о поведении --------------------------------
    srv, port = server(backlog=1)
    outcome, sent = connect_result(port)
    print(outcome)
    print(sent)
    srv.close()

    # Отдельный сервер для счёта: соединение из первой части осталось в
    # очереди и после закрытия клиентом — сервер его так и не забрал.
    srv, port = server(backlog=1)
    filled, ending = fill(port)
    print(ending)

    # --- Часть 2: сколько соединений держит очередь при backlog=1 --------
    #
    # ПОЧЕМУ НЕ ЕДИНИЦА. `backlog` задаёт длину очереди готовых соединений,
    # но ядро Linux пускает в неё на одно соединение больше. Почему именно так
    # устроена проверка — из прогона не видно, и здесь об этом не говорится.
    # Число печатается прогоном, а не выводится из рассуждения — именно потому,
    # что рассуждение здесь ошибается.
    print()
    print(f"listen backlog                       1")
    print(f"connections the kernel took          {float(filled):.1f}")
    print(f"how the loop ended                   {ending}")
    srv.close()


if __name__ == "__main__":
    main()