MEASUREMENT
bench/nameres/practice.py
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/sre/name-resolution
- How to run it
sudo python3 bench/nameres/resolve.py > bench/nameres/runs/resolve.txt sudo python3 bench/nameres/practice.py > bench/nameres/runs/practice.txt
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Разрешение имени»
| Файл | Что делает |
|---|---|
resolve.py |
пять наблюдений: откуда приходит ответ (файл или сеть); есть ли кеш в процессе; цена имени, которое не разрешается; сколько запросов уходит при AF_UNSPEC и при AF_INET; что делают поисковые области и точка на конце имени |
practice.py |
ответы к задачам урока: сколько имён спрошено с поисковой областью и без неё, сколько запросов уходит на неуказанное семейство и во что обходится ненайденное имя при настройках по умолчанию |
Запуск из корня репозитория:
sudo python3 bench/nameres/resolve.py > bench/nameres/runs/resolve.txt
sudo python3 bench/nameres/practice.py > bench/nameres/runs/practice.txt
Как устроен замер
Настоящий сервер имён для блоков 3–5 не нужен и вреден: его времена — это
чужая сеть, а не поведение библиотеки. Вместо него скрипт поднимает свой
сервер на 127.0.0.1:53, который принимает запросы и не отвечает ни на один,
и на время замера подменяет /etc/resolv.conf. Всё, что при этом видно, —
расписание повторов самой libc.
Исходный /etc/resolv.conf копируется в /tmp до первой подмены и
возвращается в finally. Если процесс всё же убить между блоками, файл
останется подменённым — копия лежит в /tmp/resolv.conf.bench-backup.
Что воспроизводимо
Цена ненайденного имени: timeout × attempts × число серверов, секунда в
секунду. Число запросов: два при AF_UNSPEC, один при AF_INET; два имени
при поисковой области и одно при точке на конце. Порядок источников — из
nsswitch.conf.
Не воспроизводимы миллисекунды настоящих запросов: в этой среде они гуляют от
2,2 до 29,3 мс, поэтому ни одно утверждение урока на них не опирается. Не
воспроизводится и то, какой именно адрес вернётся для example.com, — важно
лишь, что за шесть вызовов подряд он менялся.
Требования к среде
Блокам 3–5 нужны права root: bind на порт 53 и запись в
/etc/resolv.conf. Без них скрипт печатает not permitted и выполняет
первые два блока. Нужен также рабочий выход в сеть для блоков 1 и 2 — без
него example.com не разрешится и строки покажут gaierror.
Числа сняты на CPython 3.11.15, Linux 6.18.44, glibc с hosts: files dns.
Script
123 lines"""Практика к уроку про разрешение имени: три ответа и одна цена.
ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(scripts/validate-practice.mjs): показанный читателю код обязан построчно быть
в скрипте, верный вариант — дословно встречаться в записи прогона, а
заявленное число — печататься самой программой.
ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Два места, где ошибаются чаще всего. Первое: сколько
имён на самом деле спрашивается, когда в resolv.conf есть поисковая область, —
и что с этим делает точка на конце. Второе: во что обходится имя, которого
нет, при настройках по умолчанию — не «примерно долго», а точное число секунд,
которое считается из трёх величин.
Сервер имён и подмена resolv.conf берутся из `resolve.py`, чтобы задача и
разбор урока стояли на одном и том же коде.
ЗАПУСК: sudo python3 bench/nameres/practice.py
Вывод: runs/practice.txt
"""
import os
import shutil
import socket
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from resolve import ( # noqa: E402
BACKUP,
PROBE,
RESOLV,
SilentNameServer,
write_resolv,
)
SEARCH = "search svc.cluster.local cluster.local"
def asked_for(server: SilentNameServer, name: str, ndots: int) -> list[str]:
"""Какие имена libc спросила у сервера, пока искала одно."""
write_resolv(
f"nameserver 127.0.0.1\n{SEARCH}\noptions timeout:1 attempts:1 ndots:{ndots}\n"
)
server.seen.clear()
try:
socket.getaddrinfo(name, 80, socket.AF_INET, socket.SOCK_STREAM)
except OSError:
pass
return [n for n in server.seen if PROBE in n]
def family_queries(server: SilentNameServer) -> int:
"""Сколько запросов уходит на одно имя, когда семейство не задано."""
write_resolv("nameserver 127.0.0.1\noptions timeout:1 attempts:1 ndots:15\n")
server.seen.clear()
try:
socket.getaddrinfo(f"{PROBE}.invalid", 80, socket.AF_UNSPEC, socket.SOCK_STREAM)
except OSError:
pass
return len([n for n in server.seen if PROBE in n])
def default_cost(server: SilentNameServer) -> float:
"""Цена имени, которого нет, при настройках по умолчанию — в секундах.
ПОЧЕМУ ИМЕННО ЭТИ ТРИ ЧИСЛА. `timeout:2 attempts:3` и два сервера — это
то, что лежит в resolv.conf у контейнера, откуда снят урок. Ответ считает
не редакция, а прогон: секунды печатаются замером, а не умножением в уме.
"""
write_resolv(
"nameserver 127.0.0.1\nnameserver 127.0.0.1\noptions timeout:2 attempts:3\n"
)
server.seen.clear()
started = time.perf_counter()
try:
socket.getaddrinfo(f"{PROBE}.invalid", 80, socket.AF_INET, socket.SOCK_STREAM)
except OSError:
pass
return time.perf_counter() - started
def main() -> None:
print(f"Python {sys.version.split()[0]} · Linux {os.uname().release}")
print()
try:
shutil.copyfile(RESOLV, BACKUP)
server = SilentNameServer()
except OSError as exc:
print(f"not permitted: {type(exc).__name__}")
print("run as root: the exercises need :53 and /etc/resolv.conf")
return
try:
# --- Часть 1: три ответа о поведении --------------------------------
relative = asked_for(server, f"{PROBE}.internal", ndots=5)
absolute = asked_for(server, f"{PROBE}.internal.", ndots=5)
families = family_queries(server)
print(len(relative))
print(len(absolute))
print(families)
# --- Часть 2: цена имени, которого нет -------------------------------
#
# ПОЧЕМУ СЕКУНДЫ, А НЕ МИЛЛИСЕКУНДЫ. Здесь нет ни сети, ни диска:
# всё время состоит из выдержек, которые libc отсчитывает по часам.
# Разброс между запусками — единицы миллисекунд на двенадцать секунд,
# поэтому округление до десятых честно, а не удобно.
seconds = default_cost(server)
print()
print(f"resolv.conf: timeout 2")
print(f"resolv.conf: attempts 3")
print(f"resolv.conf: nameservers 2")
print(f"one name that does not resolve, s {seconds:.1f}")
finally:
shutil.copyfile(BACKUP, RESOLV)
server.close()
if __name__ == "__main__":
main()