ЗАМЕР
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()