ЗАМЕР
bench/cgroups/practice.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/sre/cgroups-oom
- Как запустить
python3 bench/cgroups/oom.py > bench/cgroups/runs/oom.txt python3 bench/cgroups/practice.py > bench/cgroups/runs/practice.txt
Запись прогона
Замеры для урока «cgroups и OOM-killer»
| Файл | Что делает |
|---|---|
oom.py |
пять наблюдений: убийство при лимите 64 МиБ (код выхода, пустые потоки, пик, failcnt); строка ядра с constraint=CONSTRAINT_MEMCG; выбор жертвы между большим и маленьким процессом; та же программа с тем же запросом под лимитом, который её вмещает; и файловый кеш — учитывается группе, но возвращается |
practice.py |
ответы к задачам урока — прогоном, а не рассуждением: три строки первой задачи и пик потребления группы |
Запуск из корня репозитория:
python3 bench/cgroups/oom.py > bench/cgroups/runs/oom.txt
python3 bench/cgroups/practice.py > bench/cgroups/runs/practice.txt
Что в этих числах воспроизводимо, а что нет
Воспроизводимо на любой машине с cgroup v1 и правом создавать группы: пик
потребления, равный лимиту; код выхода −9 (снаружи 137); пустые stdout и
stderr убитого процесса; значение CONSTRAINT_MEMCG в строке ядра; исход
выбора жертвы между большим и маленьким; нулевой failcnt под лимитом,
который вмещает запрос; ненулевой код выхода не появляется при чтении
файла вчетверо больше лимита.
Не воспроизводимо и не должно совпадать: сам failcnt (он зависит от
того, сколько раз ядру удалось освободить память), номера процессов, имена
групп и точный пик в блоке про кеш.
Требования к среде
- Смонтированный
/sys/fs/cgroup/memory(cgroup v1) и право создавать в нём подкаталоги. Во второй версии те же величины называютсяmemory.max,memory.eventsиmemory.peak; скрипты под неё не переписаны. - Читаемый
dmesg— иначе второй блок честно скажетnot readable hereи не станет печатать выдуманное. - Право писать в
/proc/sys/vm/drop_caches. Без него пятый блок напечатаетno (not permitted), и его числа перестанут что-либо значить: кеш, заполненный до прогона, заряжен другой группе.
Числа сняты на CPython 3.11.15, Linux 6.18.44 (x86-64, два ядра, 7 ГиБ). Версия интерпретатора роли не играет: всё, что печатают скрипты, — свойства ядра.
Скрипт
115 строк"""Практика к уроку про cgroups и OOM-killer: три ответа и один пик.
ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(scripts/validate-practice.mjs): показанный читателю код обязан построчно быть
в скрипте, верный вариант — дословно встречаться в записи прогона, а
заявленное число — печататься самой программой.
ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Три вещи, которые в разговоре путают чаще всего: какой
код выхода видно снаружи; что программа успела сказать о своей смерти (ничего);
и кого ядро выбирает жертвой, когда в группе двое. Плюс пик потребления — то
самое место, где интуиция «процесс вырос за лимит и лопнул» расходится с
устройством: за лимит он не выходит ни на байт.
ЗАПУСК: python3 bench/cgroups/practice.py
Вывод: runs/practice.txt
"""
import os
import subprocess
import sys
import time
CG_ROOT = "/sys/fs/cgroup/memory"
MIB = 1024 * 1024
EATER = """
import os, sys, time
group, want_mib = sys.argv[1], int(sys.argv[2])
hold = float(sys.argv[3]) if len(sys.argv) > 3 else 0.0
with open(group + "/cgroup.procs", "w") as fh:
fh.write(str(os.getpid()))
blocks = []
for _ in range(want_mib // 4):
block = bytearray(4 * 1024 * 1024)
for offset in range(0, len(block), 4096):
block[offset] = 1
blocks.append(block)
print("allocated", len(blocks) * 4, "MiB", flush=True)
time.sleep(hold)
"""
def make_group(name: str, limit_mib: int) -> str:
group = f"{CG_ROOT}/{name}"
os.makedirs(group, exist_ok=True)
with open(f"{group}/memory.limit_in_bytes", "w", encoding="utf-8") as fh:
fh.write(str(limit_mib * MIB))
return group
def read(group: str, name: str) -> str:
with open(f"{group}/{name}", encoding="utf-8") as fh:
return fh.read().strip()
def victim_of(group: str) -> str:
"""Кого ядро убьёт в группе, где двое: державшего мало или просящего много."""
small = subprocess.Popen(
[sys.executable, "-c", EATER, group, "40", "30"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
time.sleep(1.5)
big = subprocess.Popen(
[sys.executable, "-c", EATER, group, "600"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
big_code = big.wait()
time.sleep(0.3)
small_alive = small.poll() is None
small.kill()
small.wait()
if big_code == -9 and small_alive:
return "big"
if not small_alive and big_code != -9:
return "small"
return "both"
def main() -> None:
print(f"Python {sys.version.split()[0]} · Linux {os.uname().release} · cgroup v1")
print()
# --- Часть 1: три ответа о поведении --------------------------------
group = make_group("de_practice_kill", 64)
done = subprocess.run(
[sys.executable, "-c", EATER, group, "1600"], capture_output=True, text=True
)
print(128 - done.returncode)
print(done.stderr.strip() or "none")
print(victim_of(make_group("de_practice_victim", 160)))
# --- Часть 2: до какого пика дорастёт группа с лимитом ---------------
#
# ПОЧЕМУ ИМЕННО ПИК. Ошибка, которую задача и ловит: «раз процесс убит за
# превышение, значит он лимит превысил». Не превысил — учёт идёт по
# заселённым страницам, и когда следующая страница не помещается, ядро не
# даёт её занять, а убивает. Максимум за всю жизнь группы поэтому равен
# лимиту, а не больше него.
print()
print(f"limit, MiB {int(read(group, 'memory.limit_in_bytes')) // MIB}")
print(f"peak usage before the kill, MiB {int(read(group, 'memory.max_usage_in_bytes')) / MIB:.1f}")
print(f"times the limit was hit (failcnt) {read(group, 'memory.failcnt')}")
for name in ("de_practice_kill", "de_practice_victim"):
try:
os.rmdir(f"{CG_ROOT}/{name}")
except OSError:
pass
if __name__ == "__main__":
main()