Если вы регулярно запускаете «тяжелые» разовые команды — бэкапы, импорты, сборки фронтенда или видео-транскодирование — то рано или поздно сталкиваетесь с проблемой: процесс внезапно съедает все ядра и память, а сервисы на этом же сервере начинают «задыхаться». Классические утилиты nice/ionice помогают лишь частично: они не гарантируют жестких лимитов и не интегрируются с мониторингом systemd. В современном Linux на cgroup v2 элегантным решением становится systemd-run — создание transient units с мгновенными ограничениями CPUQuota= и MemoryMax= без написания файлов в /etc/systemd/system.
Что такое systemd-run и transient units
systemd-run динамически создает временные (transient) юниты в systemd. Это могут быть сервисы или «области» (scope), которые живут ровно столько, сколько идет команда, а затем исчезают. Плюс — мгновенно включается вся инфраструктура systemd: журналирование, учет ресурсов, границы по CPU/RAM/Tasks, а при желании — таймеры, срезы (slices) и политики OOM.
Transient units — это способ получить преимущества systemd (cgroups, логирование, изоляция) для разовых и интерактивных задач без постоянных unit-файлов. Отлично подходит для DevOps-практик и CI/CD.
Два ключевых режима:
--service(значение по умолчанию) — создается transient-сервис сExecStartравным вашей команде. Умеет «one-shot» поведение черезType=oneshot, поддерживает зависимости и перезапуски.--scope— не сервис, а «контейнер» для уже запущенного процесса с вашим TTY. Идеально для интерактивных команд: вы видите stdout/stderr сразу в консоли, а cgroup-лимиты работают как на полноценный сервис.
CPU: как работает CPUQuota и что с многопроцессорными системами
Свойство CPUQuota= задает потолок CPU в процентах от одного ядра. Примеры:
CPUQuota=50%— половина одного CPU в сумме по всем ядрам.CPUQuota=100%— ровно одно полноценное ядро.CPUQuota=250%— два с половиной ядра суммарно (на 4 vCPU это около 62.5% общего CPU).
Важно понимать: quota — это интегральный потолок «сколько CPU-времени суммарно» потребляет группа процессов в cgroup. Это не пиннинг на конкретные ядра и не приоритет. Если вам нужно распределить долю CPU относительно других сервисов, используйте CPUWeight= (от 1 до 10000). А если требуется закрепить выполнение на определенных ядрах — пригодятся CPUAffinity= или AllowedCPUs=.
Резюме по CPU-параметрам для transient units:
CPUQuota=— жесткий лимит «сколько CPU максимум».CPUWeight=— относительная «важность» при конкуренции (работает при отсутствии жесткого потолка).AllowedCPUs=— перечисление разрешенных CPU (например,0-1,3).CPUAffinity=— битовая маска/список для пиннинга процессов юнита.
Память: MemoryMax, MemoryHigh, MemorySwapMax и OOM
MemoryMax= — жесткий потолок RSS+cache для cgroup. Превышение ведет к OOMKill внутри cgroup (ядро завершит «виновных» процессов). Значение 0 означает «без лимита» (не рекомендуется на многоарендных серверах).
MemoryHigh= — мягкий потолок: ядро будет дросселировать задачи при превышении, но не обязательно завершать. Полезно, чтобы не допустить «выбросов», не рискуя мгновенным убийством процесса.
MemorySwapMax= — лимит на swap для cgroup. Значение 0 — запрет свопа для юнита; infinity — без лимита. На небольших инстансах это отличный способ предотвратить «болото» из свопинга.
Поведение при OOM можно уточнить через OOMPolicy= (например, continue, stop, kill) и параметры взаимодействия с systemd-oomd (если он активен в дистрибутиве). Помните, что systemd-oomd ориентируется на PSI и может завершить юнит еще до срабатывания «жесткого» OOM ядра. Для чувствительных задач стоит явно выставлять ManagedOOMPreference= и смежные опции по политике.

Режимы запуска: когда --scope, а когда --service
Используйте --scope, если:
- вам нужна интерактивность (вы видите вывод в своей TTY, можно вводить пароли и т.п.);
- вы запускаете разовую команду с лимитами и хотите вернуться к шеллу после завершения;
- достаточно простых ограничений ресурсов и журналирования.
Используйте --service, если:
- нужен «one-shot» сервис с
Type=oneshot, контролем зависимостей, политиками перезапуска; - вы хотите именованный юнит для мониторинга/логов (
--unit=), и чтобы он оставался в истории (--collectили без него); - планируете подвязать таймеры (
systemd-run --on-active=,--on-unit-active=, и т.д.).
Быстрый старт: примеры команд
Интерактивный запуск с лимитами через scope
systemd-run --scope -p CPUQuota=100% -p MemoryMax=1G bash -lc 'tar -cf /backups/site.tar /var/www/site'
Здесь мы ограничили задачу одним CPU и 1 Гбайтом памяти. После завершения вернемся в shell, а лог попадет в journald под именем transient-unit.
One-shot transient service с именем и ожиданием завершения
systemd-run --unit=backup-db --wait --quiet -p Type=oneshot -p CPUQuota=80% -p MemoryMax=2G bash -lc 'pg_dump -F c appdb > /srv/backup/appdb.dump'
--wait вернет коду выхода процесса в оболочку (удобно для CI). --quiet приглушает вспомогательный вывод. Имя юнита backup-db.service упростит поиск логов в journalctl -u backup-db.
Пиннинг к ядрам и «легкая» конкуренция
systemd-run --scope -p AllowedCPUs=2-3 -p CPUWeight=200 bash -lc 'make -j4 build'
Сборка будет использовать только CPU 2 и 3, а CPUWeight подскажет планировщику относительную «важность» этой группы по сравнению с другими cgroups.
Куда складываются логи и как следить за ресурсами
Все, что запущено через systemd-run, автоматически уходит в journald. Для именованных юнитов проверяйте:
journalctl -u backup-db -e
Для «безымянных» scope можно смотреть последние записи по PID, UID или глобально по времени. Нагрузка по cgroups видна в systemd-cgtop. Это удобно, когда нужно проверить, сработали ли CPUQuota и MemoryMax в условиях конкуренции.
Разница между MemoryMax= и «оно умерло от OOM»
Если процесс завершился с OOM, важно понять «чей OOM»: ядра внутри cgroup (жесткий лимит MemoryMax) или системный/oomd из-за глобального давления. Признаки:
- При срабатывании
MemoryMaxжурнал содержит записи о «killed process in cgroup»; exit code обычно 9 или 137 (SIGKILL). - Если сработал
systemd-oomd, в журнале появятся строчки от него с PSI-контекстом.
Для критичных задач добавляйте мягкий MemoryHigh рядом с MemoryMax — это позволит ядру заранее дросселировать и снизить риск мгновенного убийства.
Срезы (slices), группировка и бюджет ресурсов
Если у вас несколько периодических задач, имеет смысл складывать их в общий slice и задать лимиты на уровне среза, а отдельным запускам — дополнительные, более строгие:
systemd-run --scope -p Slice=batch.slice -p CPUQuota=200% -p MemoryMax=3G bash -lc 'rclone sync /data s3:bucket/data'
Срезы удобны тем, что их лимиты наследуются всеми дочерними юнитами. Например, можно ограничить весь batch.slice двумя CPU и 4 Гбайтами памяти, а внутри запускать разные systemd-run с частными квотами. Подробнее про срезы в проде — в материале Как организовать slices и cgroups для PHP-FPM.

Пользовательские юниты: --user контур
systemd-run --user запускает transient units в пользовательском менеджере systemd. Это полезно на CI-агентах или разработческих машинах, где нет root-доступа. Ограничения CPU/RAM будут действовать в рамках пользовательского cgroup-дерева. Пример:
systemd-run --user --scope -p CPUQuota=150% -p MemoryMax=2G bash -lc 'npm ci && npm run build'
В user-контуре часть свойств может быть недоступна, если админ ограничил возможности через policy; проверяйте сообщения об ошибках от systemd-run.
Таймеры и отложенный старт без unit-файла
systemd-run умеет transient timers. Это удобно для одноразового отложенного запуска «с лимитами», без редактирования файлов:
systemd-run --unit=cleanup-once --on-active=10min -p Type=oneshot -p CPUQuota=50% -p MemoryMax=512M bash -lc 'find /tmp -type f -mtime +1 -delete'
Через 10 минут запустится разовая уборка с ограничениями по CPU и памяти. Логи — в journald, имя юнита известно, отследить просто. Если вы привыкли к cron, посмотрите сравнение подходов в статье Cron против systemd-timers: плюсы и минусы.
Чек-лист перед запуском «тяжелой» команды
- Определите потолок CPU:
CPUQuotaдля жесткой шапки илиCPUWeightдля «справедливой» конкуренции. - Задайте память:
MemoryMaxи, при необходимости,MemoryHigh; подумайте оMemorySwapMax. - Решите режим:
--scopeдля интерактива,--serviceдля one-shot с мониторингом состояния. - Дайте понятное имя юниту через
--unit=— проще будет смотреть логи и метрики. - При необходимости ограничьте CPU-набор:
AllowedCPUsилиCPUAffinity. - Сгруппируйте схожие задачи в
batch.sliceи задайте срезу общий бюджет. - Для CI используйте
--waitи проверяйте код выхода.
Практические паттерны для DevOps
Бэкап БД ночью с мягким дросселированием
systemd-run --unit=nightly-pg-backup --wait -p Type=oneshot -p CPUQuota=60% -p MemoryHigh=1.5G -p MemoryMax=2G bash -lc 'pg_dump -F c app > /srv/backup/app-$(date +%F).dump'
MemoryHigh позволит ядру притормаживать pg_dump при всплесках page cache, а MemoryMax спасет остальную систему от безлимитного роста потребления памяти.
Ограничение build-пайплайна фронтенда
systemd-run --scope -p CPUQuota=150% -p MemoryMax=2G bash -lc 'corepack pnpm i && corepack pnpm build'
Сборка не «съест» весь CPU сервера и не вытеснит PHP-FPM или БД.
Немедленная перезапаковка медиа с пиннингом
systemd-run --scope -p AllowedCPUs=1-2 -p MemoryMax=1G bash -lc 'ffmpeg -i in.mp4 -c:v libx264 -preset veryfast -c:a aac out.mp4'
Пиннинг на 1–2 CPU сохраняет отзывчивость «системных» ядер для веба и БД.
Типичные ошибки и способы избежать
- Неправильное понимание процентов в
CPUQuota. 100% — это одно полноценное ядро. Если у вас 8 vCPU и вы хотите «не более половины всей мощности», ставьте 400%, а не 50%. - Слишком жесткий
MemoryMaxбезMemoryHigh. Резкие всплески page cache могут привести к OOMKill. Добавьте «мягкий потолок». - Запуск без имени юнита. Потом сложно искать логи. Привычка к
--unit=окупается. - Забыли про своп. Если своп медленный, ограничьте
MemorySwapMaxили запретите своп для этой задачи. - Путают
--scopeи--service. Для интерактива — scope; для one-shot с таймерами и зависимостями — service.
Наблюдение и отладка
systemd-cgtop— покажет потребление CPU и памяти по cgroups; удобно для проверки эффектовCPUQuota/MemoryMax.journalctl -u <unit>— логи конкретного transient unit.- Проверяйте результат через код выхода:
systemd-run --waitвернет код процесса в оболочку. - Для долгих задач полезно добавить
RuntimeMaxSec=, чтобы не забыть «висящий» процесс.
Совместимость и нюансы платформ
Современные дистрибутивы используют cgroup v2, где политика CPU/Memory работает предсказуемо и единообразно. На старых системах с cgroup v1 некоторые свойства могут вести себя иначе или требовать дополнительных настроек. В виртуализированных окружениях убедитесь, что гипервизор корректно прокинул лимиты CPU/Memory и видимость CPU для гостя; это влияет на восприятие квот и веса.
В контейнерах возможны ограничения от runtime (namespace/cgroup) поверх systemd. Если systemd внутри контейнера не является PID 1 или запускается в ограниченном режиме, часть свойств может быть недоступна. В таких случаях либо переносите лимиты на уровень оркестратора, либо запускайте задачи снаружи контейнера — на хосте, где systemd управляет cgroups. На классическом shared-хостинге systemd обычно недоступен; если нужен полный контроль ресурсов и фоновые задания, используйте VDS.
Итог
systemd-run — быстрый и аккуратный способ навесить лимиты на CPU и память для разовых команд и «one-shot» задач. Он соединяет удобство shell-команды с управляемостью systemd: ограничение ресурсов, понятные логи, группировка по slices и интеграция с таймерами. Для DevOps это инструмент «под рукой», который помогает держать сервер отзывчивым даже в пиковые моменты — без риска «уронить» прод действием одной-единственной команды.


