MEASUREMENT
bench/rlimits/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/file-descriptors
- How to run it
python3 bench/rlimits/descriptors.py > bench/rlimits/runs/descriptors.txt python3 bench/rlimits/practice.py > bench/rlimits/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
Замеры для урока «Лимит дескрипторов»
| Файл | Что делает |
|---|---|
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). Версия интерпретатора роли не играет.
Script
89 lines"""Практика к уроку про лимит дескрипторов: три ответа и одно число.
ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(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()