Deep Engineering

ЗАМЕР

bench/rlimits/practice.py

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

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

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

Замеры для урока «Лимит дескрипторов»

Файл Что делает
descriptors.py четыре наблюдения: мягкий и жёсткий лимит и что процесс может с ними сделать; исчерпание лимита (сколько открылось, имя и текст ошибки); что тратит дескриптор — файл, сокет, канал, epoll; наследование лимита потомком
practice.py ответы к задачам урока: имя ошибки, её текст, унаследованный лимит и число открытых файлов

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

python3 bench/rlimits/descriptors.py > bench/rlimits/runs/descriptors.txt
python3 bench/rlimits/practice.py    > bench/rlimits/runs/practice.txt

Что в этих числах воспроизводимо, а что нет

Воспроизводимо на любой машине с Linux: имя ошибки EMFILE и её текст; арифметика «лимит минус уже занятые дескрипторы» — при лимите 64 открывается 61; канал берёт по два номера; мягкий лимит поднимается до жёсткого без перезапуска; жёсткий выше себя не поднимается; потомок стартует с лимитом родителя.

Не воспроизводимо: значения 20000 — это лимит конкретно этой машины. Число дескрипторов, уже занятых процессом на старте, тоже может отличаться, если интерпретатор запущен иначе; поэтому проба вынесена в отдельный процесс, где их ровно три.

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

Никаких особых прав: скрипт только опускает и поднимает собственный мягкий лимит. Единственное, что может не сработать на чужой машине, — последняя строка первого блока: если процесс запущен с правом менять жёсткий лимит, она напечатает allowed here (privileged) вместо отказа.

Числа сняты на CPython 3.11.15, Linux 6.18.44 (x86-64). Версия интерпретатора роли не играет.

Скрипт

89 строк
"""Практика к уроку про лимит дескрипторов: три ответа и одно число.

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

ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Имя ошибки (не ENOMEM и не ENFILE, а EMFILE), её текст
и то, что мягкий лимит наследуется потомком, — три места, где разговор обычно
сбивается. Плюс само число: сколько дескрипторов удаётся открыть при лимите
64, и почему это не 64.

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

import os
import resource
import subprocess
import sys

SOFT = 64

CHILD = """
import errno, os, resource, sys
soft = int(sys.argv[1])
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, resource.getrlimit(resource.RLIMIT_NOFILE)[1]))
held = len(os.listdir("/proc/self/fd")) - 1
keep = []
try:
    while True:
        keep.append(open("/dev/null", "rb"))
except OSError as exc:
    print(errno.errorcode.get(exc.errno, exc.errno))
    print(exc.strerror)
    print(len(keep))
    print(held)
"""


def probe(soft: int) -> list[str]:
    done = subprocess.run(
        [sys.executable, "-c", CHILD, str(soft)], capture_output=True, text=True
    )
    return done.stdout.strip().splitlines()


def inherited_soft(value: int) -> str:
    """Мягкий лимит, с которым стартует потомок, когда родитель поставил себе value."""
    soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
    resource.setrlimit(resource.RLIMIT_NOFILE, (value, hard))
    done = subprocess.run(
        [
            sys.executable,
            "-c",
            "import resource; print(resource.getrlimit(resource.RLIMIT_NOFILE)[0])",
        ],
        capture_output=True,
        text=True,
    )
    resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
    return done.stdout.strip()


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

    # --- Часть 1: три ответа о поведении --------------------------------
    name, message, opened, held = probe(SOFT)
    print(name)
    print(message)
    print(inherited_soft(128))

    # --- Часть 2: сколько дескрипторов помещается в лимит ----------------
    #
    # ПОЧЕМУ НЕ 64. Лимит считает ДЕСКРИПТОРЫ, а не «файлы, которые открыл
    # я»: три из них процесс получил при рождении — ввод, вывод и поток
    # ошибок. Это и есть разница между числом в конфиге и числом соединений,
    # которые сервис успеет принять.
    print()
    print(f"soft limit                           {SOFT}")
    print(f"descriptors held at start            {held}")
    print(f"files opened before the refusal      {float(opened):.1f}")


if __name__ == "__main__":
    main()