SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-13

Як обмежити пам’ять і CPU процесу в systemd

Налаштуйте MemoryHigh, MemoryMax, CPUQuota і TasksMax у unit systemd. Перевірте помилку OOM kill та неочікуваний запуск сервісу без обмежень.

Обмеження пам’яті та CPU процесу за допомогою drop-in для systemd

Обмежити пам’ять і CPU процесу на Linux VPS можна, додавши кілька рядків до unit-файла, який запускає процес. MemoryMax= — це жорстке обмеження пам’яті. CPUQuota= — обмеження процесорного часу. Обидва параметри забезпечуються cgroup v2 (control groups, version 2) — функцією ядра, яку systemd уже використовує для обліку кожного сервісу на сервері.

sudo systemctl edit myapp.service

Ця команда відкриває drop-in-файл з інструкціями в коментарях. Додайте цей фрагмент перед ними:

[Service]
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
CPUQuota=80%
TasksMax=128
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p MemoryHigh -p MemoryMax -p CPUQuotaPerSecUSec -p TasksMax

systemctl show має повторити ваші значення в одиницях ядра: MemoryMax=805306368 і CPUQuotaPerSecUSec=800ms. Якщо виводиться MemoryMax=infinity, drop-in не завантажено. Перевірте, що файл створено за шляхом /etc/systemd/system/myapp.service.d/override.conf і що він починається із заголовка [Service], оскільки рядок налаштування без секції перед ним змушує systemd записати в журнал Assignment outside of section. Ignoring. і запустити сервіс без жодних обмежень.

Далі в цьому посібнику пояснюється, як вибрати ці значення та які проблеми все ще можуть виникати після їх налаштування.

Чому некерований процес зависає на VPS, навіть не заповнюючи його пам’ять

Процес, який досягає жорсткого ліміту пам’яті, завершується приблизно за секунду, після чого сервіс перезапускається. Це сприятливий сценарій. Несприятливий сценарій — коли нічого не завершується: сервер відповідає на ping, SSH приймає з’єднання, але запрошення оболонки так і не з’являється. Машина працює та зайнята, але жодна з цих операцій не приносить користі.

Ось як це працює. Коли вільної пам’яті стає мало, ядро звільняє сторінки замість виділення нових. Найпростіше звільняти сторінки, пов’язані з файлами, а page cache містить виконуваний код усіх запущених процесів. Тому ядро витісняє текстові сторінки sshd, і наступна інструкція, яку виконує sshd, спричиняє page fault. Ядро має знову прочитати потрібні байти зі сховища. У результаті кожен процес чекає на диск замість виконання. Ті самі сторінки постійно витісняються та завантажуються назад. Це називається thrashing.

На VPS ситуацію погіршують два чинники порівняно з ноутбуком. Сховище часто підключене через мережу або спільно використовується, тому кожен page fault займає більше мілісекунд, ніж на локальному NVMe-пристрої. Крім того, ядро вимірює не час, а результат: доки reclaim повертає хоча б одну сторінку, навіть дуже повільно, ядро вважає, що робота триває, і не запускає out of memory (OOM) killer. Сервер може перебувати в такому стані багато хвилин, перш ніж буде завершено будь-який процес.

Цей процес можна спостерігати. Ядро експортує pressure stall information (PSI) у Linux 4.20 і новіших версіях:

cat /proc/pressure/memory
cat /proc/pressure/io
some avg10=63.72 avg60=41.02 avg300=12.33 total=13729481
full avg10=48.15 avg60=30.44 avg300=8.90 total=9114233

Важливим є рядок full. full avg10=48.15 означає, що протягом останніх десяти секунд у 48% часу кожне runnable-завдання на сервері було призупинене в очікуванні операцій із пам’яттю, тому нічого не виконувалося. На справному сервері значення full близьке до нуля. За значення понад 10 система здається повільною, а за 40 або більше виникає стан, який зазвичай описують як зависання.

Саме тому один лише ліміт нічого не гарантує. Unit, обмежений через MemoryHigh=, throttled замість завершення. Він залишається запущеним і повільним, а systemd його не перезапускає, оскільки з його погляду unit не завершився з помилкою. Якщо для unit із лімітом дозволено swap, операції читання та запису зараховуються цьому unit, але виконуються одним спільним пристроєм. Через це unit може підвищити значення /proc/pressure/io для кожного іншого сервісу на сервері. Ліміти визначають, хто несе витрати через нестачу ресурсів, але не створюють додаткової ємності.

Перевірте, що VPS використовує cgroup v2

stat -fc %T /sys/fs/cgroup

cgroup2fs — це уніфікована ієрархія, яка потрібна для всіх наведених нижче налаштувань. tmpfs означає, що система завантажила стару структуру v1, у якій MemoryHigh= і MemorySwapMax= не існують, а поведінка OOM для окремих unit відрізняється. Ubuntu 22.04 і новіші версії, а також Debian 11 і новіші версії, типово використовують v2. Старий образ або ядро, завантажене з параметром systemd.unified_cgroup_hierarchy=0, не використовує v2.

У cgroup v2 systemd типово вмикає облік пам’яті для кожного unit, тому потрібні значення вже доступні:

systemd-cgtop -m

Команда виводить cgroup, відсортовані за використанням пам’яті. Це найшвидший спосіб з’ясувати, «що споживає ресурси цього сервера», поки він ще відповідає. Якщо сервер новий, спочатку виконайте налаштування облікового запису та firewall з розділу перші десять хвилин на новому VPS.

MemoryHigh обмежує. MemoryMax завершує процес.

Різниця між цими двома параметрами пам’яті визначає наслідки збою.

  • MemoryHigh= — це м’яке обмеження. Коли використання перевищує його, ядро агресивніше звільняє пам’ять із цього cgroup і навмисно сповільнює подальше виділення пам’яті. Використання все одно може перевищити це значення, і жоден процес не завершується.
  • MemoryMax= — це жорстке обмеження. Якщо виділення пам’яті неможливо виконати в межах цього обмеження, OOM killer запускається всередині цього cgroup і завершує один із процесів цього unit.

Саме тому для всього, чому ви не довіряєте повністю, слід встановлювати MemoryMax=. Без обмеження нестача пам’яті стає проблемою всього сервера, а глобальний OOM killer вибирає процес за oom_score, що здебільшого означає найбільший процес. Найбільшим процесом зазвичай є база даних, а не скрипт, який спричинив витік пам’яті. Якщо встановити обмеження, процес буде завершено всередині unit, що спричинив проблему.

Встановіть обидва параметри, задавши MemoryHigh= приблизно на 20–30 відсотків нижче за MemoryMax=. Різниця між ними є зоною попередження: повільний витік перевищує High, і сервіс починає працювати повільніше, тоді як раптовий стрибок одразу перевищує Max і спричиняє завершення процесу.

Процентні значення обчислюються від установленого обсягу фізичної пам’яті, тому MemoryMax=25% на плані з 4 GB становить 1 GB і після зміни плану залишається чвертю доступної пам’яті сервера. MemorySwapMax=0 повністю забороняє цьому unit використовувати swap, перетворюючи тривале сповільнення на швидке й очевидне завершення процесу.

Для обмеження потрібна політика перезапуску, інакше після завершення процесу сервіс просто залишиться зупиненим.

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=5s

StartLimit* має бути в [Unit], а Restart= — у [Service]. Якщо розмістити будь-який із них у неправильному розділі, systemd його проігнорує. П’ять перезапусків за п’ять хвилин свідчать про витік, а не про короткочасний збій. Після цього systemd припиняє спроби й залишає unit у стані failed. Саме цей стан ви зможете виявити пізніше, замість прихованого циклу аварійних перезапусків.

Обмеження CPU за допомогою CPUQuota або спільне використання CPU за допомогою CPUWeight

CPUQuota= використовує заданий відсоток часу, доступного на одному CPU. CPUQuota=50% — це половина одного ядра. CPUQuota=200% еквівалентний двом ядрам, і unit може розподіляти це навантаження між довільною кількістю потоків. У тарифному плані з 2 vCPU CPUQuota=200% — це вся машина.

CPUWeight= зазвичай є кращим варіантом для більшості сервісів. Це відносна частка від 1 до 10000, а значення за замовчуванням у ядрі — 100. Обмеження діє лише тоді, коли процеси конкурують за CPU: під час навантаження завдання резервного копіювання з CPUWeight=20 поступається вебсерверу зі значенням 100, але використовує всю машину, коли вона простоює. Жорстке обмеження відкидає цей вільний ресурс.

Реалістично оцінюйте, що дає обмеження CPU. Процес, обмежений CPU, рідко повністю зупиняє Linux, оскільки планувальник продовжує виділяти процесорний час усім процесам. Машину зазвичай виводить з ладу нестача пам’яті. Використовуйте CPUQuota=, коли потрібна передбачувана верхня межа, наприклад для складання проєкту або агента, який інакше працював би на повній потужності протягом години. Визначення ресурсів для такого навантаження — окреме питання. Воно розглянуте в матеріалі скільки RAM і CPU потрібно VPS для coding agent.

Якщо CPU завантажений, хоча жоден із ваших процесів майже не використовує його, причина може бути на іншому боці гіпервізора. Це час CPU steal через надмірне навантаження сусіднього клієнта, і жодне встановлене вами обмеження не змінить ситуацію.

TasksMax зупиняє цикл fork

TasksMax= — це максимальна кількість процесів і потоків, які може містити unit. Потоки також враховуються, тому сервісу на Java або Go потрібно більше запасу, ніж може здатися зі списку процесів. Це найдешевший захист від скрипта, який запускає fork у циклі: fork завершується помилкою всередині unit, а не після вичерпання ідентифікаторів процесів на всьому сервері.

TasksMax=128

Коли unit досягає ліміту, kernel записує рядок із назвою cgroup:

cgroup: fork rejected by pids controller in /system.slice/myapp.service

Сама програма зазвичай повідомляє fork: retry: Resource temporarily unavailable. Перевірте, які значення manager застосовує за замовчуванням, за допомогою systemctl show -p DefaultTasksMax.

Обмеження одноразового завдання за допомогою systemd-run

Для цього не потрібен unit-файл. systemd-run створює тимчасовий unit для однієї команди.

sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh

--scope запускає команду у вашому терміналі після виведення Running scope as unit: run-r7c1a....scope. Вивід залишається на екрані, а обмеження зникають після завершення команди. Будь-яка властивість із systemd.resource-control працює після -p.

Для тривалого завдання приберіть --scope і вкажіть йому ім’я. Тоді воно працюватиме у фоновому режимі як тимчасовий сервіс, а журнали записуватимуться до journal:

sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -f

Ті самі параметри працюють із --user, коли ви не є root. Однак user manager має лише ті контролери, які йому делеговано, тому певну властивість може бути відхилено. Якщо це станеться, запустіть команду за допомогою sudo. Коли завдання отримує постійне розміщення, ці параметри без змін переносяться до справжнього unit: див. запуск скрипту як сервісу та таймера systemd.

Чесна відповідь на питання про swap

Swap змінює характер збою, а не запобігає йому.

Без swap витік пам’яті швидко досягає ліміту, і за кілька секунд завершується якийсь процес. Збій гучний, короткий, і його легко проаналізувати в журналі згодом. Із swap ядро записує неактивні анонімні сторінки на диск і виграє час. Якщо споживання пам’яті мало стабілізуватися, swap допомагає. Якщо процес неконтрольовано споживає пам’ять, swap перетворює п’ятисекундний збій на двадцятихвилинне зависання. Це гірше, оскільки після завершення процесу у вас залишається робоча оболонка, а під час активного swapping система перестає нормально відповідати.

swapon --show
free -h

Практичний варіант для невеликого VPS: створіть помірний swap-файл для сторінок, які виділяються один раз і більше не використовуються, а для unit-файлів, якими ви готові пожертвувати, встановіть MemorySwapMax=0. Важливі сервіси зберігають доступ до swap. Непередбачувані сервіси швидко досягають ліміту й перезапускаються.

Зменшення vm.swappiness — слабкий інструмент, і важливо розуміти чому. Воно лише змінює баланс між витісненням кешу сторінок і записуванням анонімних сторінок у swap. В обох випадках пізніше потрібне читання з диска. Змінюється те, які сторінки спричиняють thrashing, але не те, чи буде система thrashing.

Ранній OOM-демон завершує процеси до зависання

Ядро чекає, доки відновлення пам’яті повністю не завершиться помилкою. На невеликому VPS саме протягом цього очікування ви втрачаєте доступ до машини. Два демони в userspace усувають цю проблему: вони самостійно контролюють пам’ять і завершують процеси раніше.

earlyoom контролює доступну пам’ять і вільний swap та завершує процес із найвищим балом, коли будь-яке зі значень опускається нижче порогового.

sudo apt install earlyoom
systemctl status earlyoom

Пакет для Debian і Ubuntu запускає сервіс під час встановлення. Його параметри зберігаються в /etc/default/earlyoom:

EARLYOOM_ARGS="-m 5,2 -s 5,2 --avoid '^(sshd|systemd)$' --prefer '^(node|python3)$'"

-m PERCENT задає мінімальний обсяг доступної пам’яті, а -s PERCENT — мінімальний обсяг вільного swap. За замовчуванням обидва значення становлять 10 відсотків. Друге число в кожній парі є порогом для SIGKILL: earlyoom надсилає SIGTERM, коли значення стає нижчим за перший поріг, а потім SIGKILL — коли воно опускається нижче другого. За замовчуванням другий поріг становить половину першого. Застосуйте зміну за допомогою sudo systemctl restart earlyoom. Перегляньте journalctl -u earlyoom, щоб дізнатися, який процес було завершено і скільки пам’яті він утримував.

systemd-oomd — інший варіант. На його сторінці довідки він описаний як "системний сервіс, що використовує cgroups-v2 і pressure stall information (PSI) для моніторингу та виконання коригувальних дій до виникнення OOM у просторі ядра". Він працює з цілими cgroups, а не з окремими процесами, тому завершує unit, а не випадковий дочірній процес. Unit може явно підключитися за допомогою ManagedOOMMemoryPressure=kill або ManagedOOMSwap=kill, а пороги зберігаються в /etc/systemd/oomd.conf.

systemctl status systemd-oomd
oomctl

oomctl виводить список об’єктів, які він зараз контролює. На серверному образі цей список часто порожній, оскільки параметр потрібно вмикати окремо для кожного unit. Виберіть один демон і використовуйте лише його. Якщо запустити обидва, вони одночасно змагатимуться за вибір процесу для завершення, а причину завершення буде складніше встановити.

Який unit відповідав за це?

Почніть із kernel, оскільки він записує кожне виконане ним примусове завершення процесу.

journalctl -k --grep "Killed process" --since "2 hours ago"

Примусове завершення процесу глобальним OOM killer має такий вигляд:

Out of memory: Killed process 4127 (node) total-vm:2731084kB, anon-rss:1874232kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:4212kB oom_score_adj:0

anon-rss — це обсяг пам’яті, який процес займав у RAM на момент завершення; у цьому випадку приблизно 1.8 GB. Читайте назву в дужках критично. Це процес, якого kernel обрав жертвою. Kernel обирає найбільший процес, але саме він не завжди спричиняє нестачу пам’яті.

Примусове завершення процесу через ліміт cgroup має інший префікс. У звіті над ним указано cgroup, яка досягла власного ліміту:

Memory cgroup out of memory: Killed process 8811 (python3) total-vm:1044320kB, anon-rss:769112kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:1720kB oom_score_adj:0

Цей префікс містить більшу частину діагностичної інформації. Memory cgroup out of memory означає, що один unit досяг заданого вами MemoryMax=, а решта системи працювала нормально. Звичайний Out of memory означає, що в машині загалом закінчилася пам’ять. Отже, обмеження або не задані, або їхня сумарна величина надто велика.

Потім перевірте, що побачив systemd:

systemctl status myapp.service
journalctl -u myapp.service -n 50
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'oom-kill'.

systemctl status повідомляє те саме одним рядком у форматі Active: failed (Result: oom-kill).

Лічильники cgroup — третє джерело інформації й єдине, яке фіксує throttling. Throttling взагалі не створює запису в журналі:

cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
cat /sys/fs/cgroup/system.slice/myapp.service/memory.peak
low 0
high 4213
max 118
oom 12
oom_kill 12

high підраховує, скільки разів unit перевищував MemoryHigh= і переходив у режим throttling. max підраховує, як часто unit досягав жорсткого ліміту, а oom_kill — кількість фактично примусово завершених процесів. Велике значення high разом із oom_kill 0 означає описаний раніше непомітний випадок: сервіс працює, його робота надзвичайно сповільнена, але він не повідомив нікому про помилку. memory.peak (Linux 5.19 і новіші версії) містить максимальне використання пам’яті, якого досягла cgroup. Саме на це значення слід орієнтувати MemoryMax=. Обидва файли скидаються після перезапуску unit, оскільки systemd створює cgroup повторно.

У всіх цих перевірок є одна спільна передумова. Якщо /var/log/journal не існує, журнал зберігається в RAM, і після потрібного для відновлення системи перезавантаження всі записи буде втрачено.

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --list-boots

Якщо journalctl --list-boots показує значення, що охоплює більше, ніж поточне завантаження, історія вже зберігається. Тоді journalctl -k -b -1 може показати повідомлення kernel із завантаження, під час якого система вийшла з ладу.

Початкова конфігурація для невеликого VPS

На тарифі з 2 GB залиште 300–400 MB для ядра та кешу сторінок. Не встановлюйте обмеження так, щоб їхня сума дорівнювала повним 2 GB, оскільки кожен unit може досягти пікового споживання одночасно з іншими. Сервісу, який має найбільше значення, виділіть найбільшу частку ресурсів. Для всіх другорядних сервісів установіть жорсткі обмеження.

[Service]
MemoryHigh=256M
MemoryMax=384M
MemorySwapMax=0
CPUWeight=20
TasksMax=64
Restart=on-failure
RestartSec=5s

Можливість підключитися до сервера варта ще одного налаштування. OOMScoreAdjust=-500 у drop-in для ssh.service значно зменшує ймовірність того, що глобальний OOM killer вибере SSH daemon як процес для завершення. Це може визначити, чи вдасться виправити сервер, чи доведеться перезавантажувати його через панель керування. Це налаштування змінює лише вибір процесу ядром. Воно не скорочує тривалість зависання.

Контейнери працюють у власних cgroups, які створює container runtime, а не ваші unit-файли. Тому обмеження для docker.service не стає обмеженням для окремого контейнера. Еквіваленти MemoryMax= і CPUQuota= для окремих контейнерів описано в розділі налаштування обмежень пам’яті та CPU у Docker Compose.

FAQ

Чому мій VPS завис, а не завершив некерований процес?

Тому що ядро оцінює прогрес за тим, чи повертає reclaim сторінки, а не за тривалістю цієї операції. Коли пам’яті недостатньо, воно витісняє page cache, зокрема сторінки виконуваних програм, а потім зчитує їх назад під час виконання наступної інструкції. Усі процеси очікують на сховище, але жоден запит на виділення пам’яті формально не завершився помилкою, тому OOM killer не запускається. Перевірте /proc/pressure/memory, поки це відбувається: значення full avg10 понад 40 означає, що за останні десять секунд майже жодне завдання не отримало часу виконання. Користувацький демон на кшталт earlyoom завершує процеси до того, як система досягне такого стану.

У чому різниця між MemoryHigh і MemoryMax?

MemoryHigh= — це м’яке обмеження, яке сповільнює роботу. Ядро примусово звільняє пам’ять unit і сповільнює подальші виділення, але використання може перевищити це значення, і жоден процес не буде завершений. MemoryMax= — це жорстке обмеження: якщо запит на виділення пам’яті неможливо виконати в його межах, OOM killer запускається всередині власної cgroup цього unit. У результаті завершується процес, який спричинив проблему, а не найбільший процес у системі. Встановіть MemoryHigh= нижче за MemoryMax= і розглядайте проміжок між ними як зону попередження.

Як дізнатися, який сервіс завершив OOM killer?

Виконайте journalctl -k --grep "Killed process" --since "2 hours ago". Рядок, що починається з Memory cgroup out of memory, означає, що один unit досяг власного MemoryMax=, а звичайний Out of memory означає, що пам’ять закінчилася на всій машині. Потім виконайте journalctl -u <unit> -n 50 і знайдіть Failed with result 'oom-kill'. Якщо /var/log/journal відсутній на сервері, журнал зберігався в RAM і ці дані було втрачено під час перезавантаження. Створіть цей каталог до наступного інциденту.

Чи варто додавати swap до невеликого VPS?

Невеликий swap-файл допомагає для неактивних сторінок, які виділяються один раз і більше не використовуються. Для некерованого процесу він не допомагає: завершення процесу відкладається, а короткий збій перетворюється на тривале зависання, під час якого ви не можете ввійти в систему й усунути проблему. Використовуйте помірний обсяг swap і встановіть MemorySwapMax=0 для unit, роботу яких можна перервати. Такі unit досягнуть свого ліміту та швидко перезапустяться, а важливі сервіси й надалі матимуть доступ до swap.

Чи можна обмежити команду без створення unit-файлу?

Так. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh запускає команду у вашому терміналі всередині тимчасової області виконання з указаними обмеженнями. Після завершення команди ці обмеження зникають. Після -p доступна кожна властивість із systemd.resource-control, тому там також працюють MemorySwapMax=, TasksMax= і CPUWeight=. Приберіть --scope і додайте --unit=name, щоб запустити завдання у фоновому режимі та записувати його вивід до журналу.