ЗАМЕР
bench/timewait/practice.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/sre/time-wait-ports
- Как запустить
sudo python3 bench/timewait/ports.py > bench/timewait/runs/ports.txt sudo python3 bench/timewait/practice.py > bench/timewait/runs/practice.txt
Запись прогона
Замеры для урока «TIME_WAIT и эфемерные порты»
| Файл | Что делает |
|---|---|
ports.py |
пять наблюдений: на чьей стороне возникает TIME_WAIT; сколько он длится и какие настройки на это не влияют; чем кончается попытка открыть больше соединений, чем есть номеров портов; потолок новых соединений в секунду; и что меняет переиспользование одного соединения |
practice.py |
ответы к задачам урока: сторона, на которой остаются сокеты, ошибка при исчерпании портов и посчитанный потолок |
Запуск из корня репозитория:
sudo python3 bench/timewait/ports.py > bench/timewait/runs/ports.txt
sudo python3 bench/timewait/practice.py > bench/timewait/runs/practice.txt
Как устроен замер
Обе стороны соединения живут в одном процессе на loopback, поэтому по выводу
ss видно, на какой из них возник TIME_WAIT: у одной стороны порт сервера
стоит локальным адресом, у другой — адресом собеседника.
Длительность состояния не берётся из документации, а измеряется: скрипт
закрывает соединение и опрашивает ss, пока сокет не исчезнет. Поэтому прогон
идёт около полутора минут.
Исчерпание портов делается не нагрузкой, а сужением диапазона: на время блока
/proc/sys/net/ipv4/ip_local_port_range уменьшается до полусотни номеров и
возвращается в finally. Так отказ наступает за секунды и не зависит от того,
сколько соединений открыто в системе помимо замера.
Что воспроизводимо
Сторона, на которой остаётся TIME_WAIT: вся двадцатка у того, кто закрыл
первым. Точное исчерпание: сколько номеров в диапазоне, столько и соединений,
дальше EADDRNOTAVAIL. Один порт на переиспользуемое соединение независимо от
числа запросов.
Не воспроизводится точное значение потолка новых соединений в секунду: он
считается делением прочитанного диапазона на измеренную длительность, а
длительность между запусками гуляет на доли секунды. В ports.txt и
practice.txt она снята дважды, и числа поэтому чуть разные — это два разных
замера, а не расхождение.
Требования к среде
ss из iproute2 и права записи в /proc/sys/net/ipv4/ip_local_port_range для
третьего блока; без прав он печатает not permitted, а диапазон возвращается в
finally. Если процесс убить между блоками, диапазон останется суженным —
исходное значение видно в начале блока.
Числа сняты на CPython 3.11.15, Linux 6.18.44, loopback.
Скрипт
122 строк"""Практика к уроку про TIME_WAIT: три ответа и один потолок.
ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(scripts/validate-practice.mjs): показанный читателю код обязан построчно быть
в скрипте, верный вариант — дословно встречаться в записи прогона, а
заявленное число — печататься самой программой.
ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Два места, где ошибаются чаще всего. Первое: на чьей
стороне оказывается `TIME_WAIT` и чем кончается попытка открыть больше
соединений, чем есть номеров портов. Второе: сколько новых соединений в секунду
к одному адресу выдерживает клиент — не «примерно много», а число, посчитанное
из двух измеренных величин.
Сервер и вспомогательные функции берутся из `ports.py`, чтобы задача и разбор
урока стояли на одном и том же коде.
ЗАПУСК: sudo python3 bench/timewait/practice.py
Вывод: runs/practice.txt
Прогон идёт около минуты: часть 2 дожидается, пока состояние исчезнет само.
"""
import errno
import os
import subprocess
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ports import ( # noqa: E402
PORT_RANGE,
Echo,
exchange,
in_time_wait,
time_wait_sides,
)
SMALL_RANGE = "50100 50119" # двадцать номеров
def who_holds(closes_first: bool) -> tuple[int, int]:
"""Двадцать обменов подряд: на чьей стороне остаётся TIME_WAIT."""
echo = Echo(closes_first)
for _ in range(20):
exchange(echo.port, wait_for_close=closes_first)
time.sleep(0.4)
sides = time_wait_sides(echo.port)
echo.close()
return sides
def until_ports_run_out() -> tuple[int, str]:
"""Сколько соединений уместится в двадцать номеров портов."""
with open(PORT_RANGE) as handle:
original = handle.read().strip()
echo = Echo(closes_first=False)
made = 0
stopped = "nothing"
try:
with open(PORT_RANGE, "w") as handle:
handle.write(SMALL_RANGE + "\n")
for _ in range(200):
try:
exchange(echo.port, wait_for_close=False)
made += 1
except OSError as exc:
stopped = errno.errorcode.get(exc.errno, str(exc.errno))
break
finally:
with open(PORT_RANGE, "w") as handle:
handle.write(original + "\n")
echo.close()
return made, stopped
def time_wait_seconds() -> float:
"""Сколько живёт одно состояние TIME_WAIT — по часам, а не по документации."""
echo = Echo(closes_first=False)
local = exchange(echo.port, wait_for_close=False)
started = time.perf_counter()
elapsed = -1.0
while time.perf_counter() - started < 150:
time.sleep(0.5)
if not in_time_wait(local):
elapsed = time.perf_counter() - started
break
echo.close()
return elapsed
def main() -> None:
print(f"Python {sys.version.split()[0]} · Linux {os.uname().release} · loopback")
print()
# --- Часть 1: три ответа о поведении --------------------------------
server_side, client_side = who_holds(closes_first=False)
made, stopped = until_ports_run_out()
print(server_side)
print(client_side)
print(stopped)
# --- Часть 2: потолок новых соединений в секунду ---------------------
#
# ПОЧЕМУ ЭТО ДЕЛЕНИЕ ПЕЧАТАЕТ ПРОГРАММА. Обе величины измерены здесь же:
# диапазон прочитан из /proc, длительность снята по часам. Разделить их в
# уме на стороне редакции — ровно тот шаг, где вкрадывается ошибка,
# которую потом нечем поймать.
seconds = time_wait_seconds()
with open(PORT_RANGE) as handle:
low, high = (int(x) for x in handle.read().split())
span = high - low + 1
print()
print(f"connections made with 20 port numbers {made}")
print(f"port numbers in ip_local_port_range {span}")
print(f"seconds one port stays in TIME_WAIT {seconds:.1f}")
print(f"new connections per second, ceiling {span / seconds:.1f}")
if __name__ == "__main__":
main()