Deep Engineering

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()