Перейти до вмісту

B3. cgroups і OOM killer

середнійспирається на модуль 12, модуль 17

Кожне обмеження «2 CPU, 512 МіБ» у Docker, Kubernetes чи systemd зводиться до кількох файлів у /sys/fs/cgroup. У цій роботі ви створите таку групу руками, без жодного контейнерного інструменту, і доведете процес у ній до межі пам’яті. Так ви побачите те, що модуль 12 описує словами: malloc майже ніколи не повертає NULL, а нестача пам’яті закінчується несподіваним завершенням процесу.

Після цієї роботи ви зможете:

  • обмежити процесор, пам’ять і ввід-вивід окремому процесу або службі й перевірити, що обмеження діє;
  • відрізнити квоту процесора (cpu.max) від пріоритету (nice) і пояснити, коли потрібне яке;
  • прочитати memory.events і журнал ядра після спрацювання OOM killer і сказати, кого й чому було вбито;
  • пояснити різницю між memory.max і memory.high і вибрати правильне для служби, якій не можна впасти.

Це та сама робота, яку роблять під час розбору інциденту «контейнер перезапустився з OOMKilled»: знайти групу, подивитися лічильники і зрозуміти, обмеження це чи витік.

На віртуальній машині або в контейнері з увімкненою вкладеністю cgroups (див. огляд) створити власну контрольну групу в ієрархії cgroups v2 і показати на ній три види обмежень.

Що зробити:

  1. Група з увімкненими контролерами cpu, memory та io, у якій працює запущений вами процес.
  2. Обмеження процесора квотою: процес у циклі отримує задану частку, і це видно в top.
  3. Обмеження пам’яті через memory.max для програми, що виділяє й записує пам’ять, доки її не вб’є OOM killer у вашій групі.
  4. Те саме з memory.high: процес лишається живим, але гальмує.
  5. Обмеження вводу-виводу через io.max із заміром dd до й після.

Тестову програму для пам’яті пишіть самі, на C або Python: цикл, який виділяє по 10 МіБ і записує в кожен блок хоча б по байту на сторінку.

Що записати у звіт:

  • команди створення групи і вміст cgroup.controllers та cgroup.subtree_control на кожному рівні;
  • частку процесора з top при заданому cpu.max і порівняння з nice 19 на порожній машині;
  • memory.events після OOM і відповідний рядок із journalctl -k;
  • ваше пояснення, чим memory.high відрізняється від memory.max і для якого типу служби ви обрали б кожне.

Готово, коли ви можете підтвердити командою кожне твердження з таблиці «Що треба вміти підтвердити», а звіт містить чотири пункти вище.

  • Прочитайте в модулі 12 розділи «Підкачка на вимогу» і «Як це насправді в Linux» (частину про overcommit і OOM killer), а в модулі 17 розділ «З чого зроблено контейнер».
  • Потрібна система з cgroups v2 і правами root. Vagrant-машина з архіву курсу підходить, у ній уже є stress-ng і cgroup-tools.
  • У контейнері робота піде лише з увімкненою вкладеністю cgroups і достатніми правами. Якщо cgroup.subtree_control не пишеться, переходьте на віртуальну машину.
  1. Розвідка.

    Terminal window
    mount | grep cgroup2
    cat /sys/fs/cgroup/cgroup.controllers
    systemd-cgls | head -20

    Переконайтеся, що це саме v2 (одна ієрархія), і подивіться, як systemd уже розклав усі служби по групах.

  2. Власна група.

    Створіть каталог у /sys/fs/cgroup, увімкніть потрібні контролери в батьківському cgroup.subtree_control, помістіть у групу процес, записавши його PID у cgroup.procs.

    Зверніть увагу на правило внутрішніх вузлів: у v2 процеси можуть лежати лише в листках дерева. Спроба покласти процес у групу, яка має дочірні, дасть помилку, і це очікувана поведінка.

  3. Обмеження процесора.

    cpu.max у форматі «квота період». Запустіть зациклений процес і подивіться в top, що він отримує рівно задану частку.

    Порівняйте з nice: nice задає вагу відносно конкурентів, а cpu.max задає абсолютну квоту (модуль 7). Процес із nice 19 на порожній машині займе 100%, процес із cpu.max 50000 100000 — рівно 50%, навіть якщо більше нікого немає.

  4. Обмеження пам’яті й OOM.

    memory.max на 64 МіБ, потім програма, що виділяє й записує пам’ять по 10 МіБ у циклі. Запис обов’язковий: без нього сторінки фізично не існують (модуль 12).

    Спостерігайте memory.current, memory.events і журнал ядра.

  5. Тиск замість убивства.

    Тепер те саме, але з memory.high замість memory.max. Процес лишається живим: його гальмують, змушуючи звільняти пам’ять. Подивіться на memory.pressure і поясніть різницю у звіті.

  6. Ввід-вивід.

    io.max на конкретний пристрій, потім dd і замір швидкості до й після.

Що треба вміти підтвердити

Section titled “Що треба вміти підтвердити”
Твердження Чим доводиться
Система на cgroups v2 mount | grep cgroup2
Ваша група існує й містить процес cat <група>/cgroup.procs
Процесор обмежено квотою, а не пріоритетом cat <група>/cpu.max + top
Пам’ять обмежено cat <група>/memory.max
OOM спрацював саме у вашій групі cat <група>/memory.events (поле oom_kill)
Ядро це зафіксувало journalctl -k --grep 'Out of memory'
Ви розумієте різницю high і max письмове пояснення у звіті

Контролери не ввімкнені. Файлів cpu.max і memory.max просто немає, поки контролер не дозволено в cgroup.subtree_control батьківської групи.

Процес не потрапляє в групу. У cgroup.procs пишеться один PID за раз, а потоки переносяться через cgroup.threads.

systemd забирає групу назад. Створювати групи руками поруч із деревом systemd можна, але надійніше робити це через systemd-run --scope.

OOM убив не того. Якщо обмеження стоїть на батьківській групі, жертву обиратимуть серед усіх нащадків. Перевірте, що ви обмежили той рівень, який мали на увазі.

Обмежити кількість процесів через pids.max і подивитися, як форк-бомба :(){ :|:& };: перестає бути небезпечною.