Процессы, сигналы и systemd
Что запущено, как это правильно остановить и как держать приложение живым с помощью systemd.
За 30 секунд
Каждая запущенная программа — это процесс с идентификатором, владельцем и родителем. Процессы смотрят через ps и top, а управляют ими сигналами. В современном Linux systemd запускает сервисы при загрузке, перезапускает их при падении и собирает их логи. Небольшой unit-файл — самый чистый способ держать Node- или Python-приложение запущенным на сервере.
Почему я это использую
- Долгоживущие задачи на своих серверах я держу запущенными через менеджеры процессов: supervisor для очередей Laravel и pm2 для Node-приложений, и оба работают поверх systemd.
- Когда сервер тормозит, первым делом я смотрю, какой процесс ест CPU или память, и начинается это с top и ps.
- Знание systemd и journalctl позволяет видеть, почему сервис не запустился, а не гадать.
Как это работает
01Процессы и PID
Процесс — это запущенный экземпляр программы. Ядро выдаёт каждому идентификатор процесса (PID), и у каждого процесса есть родитель (его PPID). На машине с systemd PID 1 — это systemd, предок всех остальных. Процесс работает от конкретного пользователя, имеет собственную память, открытые файлы и переменные окружения и завершается с кодом выхода: 0 означает успех, всё остальное — какой-то сбой. ps показывает снимок процессов, top и htop — живой отсортированный список. В top средняя нагрузка (load average) показывает, сколько процессов в среднем хотят CPU за 1, 5 и 15 минут; сравнивайте её с числом ядер CPU (nproc).
ps aux | head # all processes, with user, CPU, memory
ps aux --sort=-%mem | head -n 10 # biggest memory users
ps -ef --forest | less # parent/child tree
pgrep -a node # find processes by name
top # live view (P = sort CPU, M = memory, q = quit)
htop # friendlier top (apt install htop)
uptime; nproc # load average vs. number of cores
02Сигналы и kill
Сигналы — это короткие сообщения, которые ядро доставляет процессу. Несмотря на название, kill просто отправляет сигнал. SIGTERM (15, по умолчанию) вежливо просит процесс остановиться: хорошее приложение завершает текущие запросы, закрывает соединения с БД и выходит. SIGKILL (9) перехватить нельзя: ядро убирает процесс немедленно, без очистки, что может оставить недописанные файлы или блокировки, поэтому используйте его, только если TERM не сработал. SIGINT (2) — это то, что отправляет Ctrl+C. SIGHUP (1) традиционно означает «перечитай конфиг», и многие демоны, например nginx, его поддерживают. Хорошо написанное приложение обрабатывает SIGTERM, потому что именно его systemd, Docker и Kubernetes отправляют при каждом деплое или остановке.
kill 4242 # SIGTERM: please stop
kill -HUP 4242 # reload config (if the app supports it)
kill -9 4242 # SIGKILL: last resort, no cleanup
pkill -f "node server.js" # by command line
# In Node: shut down gracefully on SIGTERM
# process.on("SIGTERM", () => server.close(() => process.exit(0)));
03Передний план, фон и выживание после выхода
Команда, запущенная в терминале, работает на переднем плане и привязана к этой сессии. Ctrl+Z ставит её на паузу, bg продолжает её в фоне, fg возвращает обратно, а & в конце сразу запускает её в фоне. Когда вы закрываете SSH-сессию, shell отправляет своим задачам SIGHUP, и они обычно умирают. nohup или терминальный мультиплексор вроде tmux сохраняют процесс после выхода, и для разовой миграции или долгого копирования этого достаточно. Но ни один из них не перезапустит упавший процесс и не запустит его после перезагрузки. Для всего, что должно работать постоянно, используйте менеджер сервисов.
long-task & # start in background
jobs # list background jobs
fg %1 # bring job 1 back
nohup ./import.sh > import.log 2>&1 & # survive logout
tmux new -s work # detach with Ctrl+B then D
tmux attach -t work # reattach later
04Юниты systemd и systemctl
systemd — система инициализации и менеджер сервисов почти во всех современных дистрибутивах, включая Ubuntu, Debian и RHEL. Он управляет юнитами: .service запускает процесс, .timer запускает сервис по расписанию, .socket стартует сервис по требованию, а .target группирует юниты. Пакеты устанавливают свои юниты в /usr/lib/systemd/system (или /lib/systemd/system в Debian); ваши собственные юниты и переопределения кладутся в /etc/systemd/system, у которого приоритет выше. Управляют ими командой systemctl. Если сервис поддерживает reload, предпочитайте его restart: reload применяет новый конфиг, не обрывая соединения. После создания или правки unit-файла выполните systemctl daemon-reload, чтобы systemd перечитал его.
systemctl status nginx # state, PID, last log lines
sudo systemctl restart nginx # stop + start
sudo systemctl reload nginx # re-read config, keep connections
sudo systemctl enable --now myapp # start now and at every boot
sudo systemctl disable myapp # do not start at boot
systemctl list-units --type=service --state=failed
systemctl cat nginx # show the unit file in use
sudo systemctl edit nginx # safe override in /etc/systemd/system
sudo systemctl daemon-reload # after editing unit files
05Пишем юнит для Node-приложения
Unit-файл — это короткий конфиг в стиле INI. [Unit] описывает сервис и порядок запуска, [Service] — как его запускать, а [Install] — когда стартовать при загрузке. Пример ниже запускает Node-приложение под отдельным непривилегированным пользователем, загружает секреты из защищённого env-файла, перезапускает приложение при падении и добавляет немного дешёвой песочницы. Type=simple подходит приложениям, которые остаются на переднем плане, — а это именно то, что нужно: не позволяйте приложению самому уходить в демон. Пусть приложение слушает localhost, а перед ним для TLS стоит nginx. Сохраните это как /etc/systemd/system/myapp.service, затем выполните daemon-reload и enable --now.
[Unit]
Description=My Node app
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
WorkingDirectory=/srv/myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=multi-user.target
06journalctl: чтение логов сервисов
systemd собирает всё, что сервис выводит в stdout и stderr, в журнал с временными метками и метаданными, так что приложение может просто логировать в консоль. Читает журнал journalctl. Фильтруйте по юниту через -u, следите в реальном времени через -f, ограничивайте по времени через --since и показывайте только ошибки через -p err. Когда сервис не запускается, systemctl status показывает последние несколько строк, а journalctl -u name -b — всё с момента последней загрузки. В большинстве дистрибутивов журнал хранится на диске в /var/log/journal, и его размер можно ограничить — об этом в уроке про логи.
journalctl -u myapp -f # follow live
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -b -p err # errors since boot
journalctl -u nginx -n 50 --no-pager # last 50 lines
journalctl -k # kernel messages (e.g. OOM kills)
07Таймеры или cron
cron — классический планировщик: одна строка на задачу в crontab, просто и доступно везде. Таймеры systemd делают то же самое, но с большими возможностями. Юнит таймера запускает соответствующий юнит сервиса, поэтому задача получает логи в журнале, лимиты ресурсов, зависимости, а Persistent=true выполняет пропущенный запуск после того, как машина была выключена. Все таймеры и время их следующего запуска показывает systemctl list-timers. Для одной быстрой задачи cron вполне подходит и всем понятен. Для важных задач, где нужны логи, повторы и отсутствие наложения запусков, таймеры — лучший инструмент. Многие служебные задачи дистрибутива, например обновления apt и ротация логов, уже работают как таймеры.
# /etc/systemd/system/backup.timer (runs backup.service)
[Unit]
Description=Nightly backup
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
# Enable with: sudo systemctl enable --now backup.timer
# Check with: systemctl list-timers
Когда применять
- Запуск любого долгоживущего приложения (Node, Python, Go, воркеры) на VPS без контейнеров.
- Диагностика высокой нагрузки на CPU, утечек памяти или зависшего процесса.
- Планирование регулярных задач, которым нужны логи, лимиты или догоняющий запуск после простоя (таймеры).
- Плавные перезапуски при деплое: отправить SIGTERM, дать приложению завершить работу, запустить новую версию.
Когда не стоит
- Внутри Docker-контейнеров systemd обычно нет; процесс перезапускает контейнерный рантайм или Compose.
- Не держите продакшен-приложения живыми через nohup, screen или tmux: они не перезапускаются ни при падении, ни при загрузке.
- Не используйте kill -9 как обычный способ остановки: он пропускает всю очистку.
Альтернативы
- pm2
Популярный менеджер процессов для Node с кластеризацией и работой с логами; сам он запускается юнитом systemd.
- supervisor
Простой зрелый менеджер, часто используемый для очередей Laravel и нескольких процессов-воркеров.
- Docker Compose
Политики перезапуска держат контейнеры запущенными; сам демон Docker — это сервис systemd.
- cron
Самый простой планировщик для небольших задач, где логирование в журнал и зависимости не важны.
Лучшие практики
- Запускайте каждый сервис под своим непривилегированным пользователем, никогда не под root.
- Обрабатывайте SIGTERM в приложении, чтобы перезапуски и деплои не обрывали запросы и не портили данные.
- Используйте Restart=on-failure вместе с RestartSec, чтобы падающее в цикле приложение не долбило систему.
- Кладите секреты в EnvironmentFile с правами 600, а не прямо в юнит.
- Для переопределений используйте systemctl edit, а не правку unit-файлов из пакетов.
- Логируйте в stdout/stderr и позвольте journald собирать логи; ограничьте размер журнала.
- Проверяйте systemctl --failed после перезагрузок и обновлений.
Частые ошибки
- Правят unit-файл и забывают про systemctl daemon-reload, так что продолжает работать старая версия.
- Запускают приложение через node server.js & по SSH и удивляются, почему оно пропадает после выхода или перезагрузки.
- Используют kill -9 по умолчанию и оставляют устаревшие lock-файлы или недописанные данные.
- Позволяют приложению самому уйти в демон при Type=simple в юните, и systemd считает, что оно завершилось.
- Путают load average с процентом загрузки CPU и не сравнивают его с числом ядер.
Глубже
08Состояния процессов, зомби и настоящий смысл load
Столбец STAT в ps говорит, чем занят процесс. R — выполняется или готов к выполнению, S — спит, пока что-то не произойдёт (большинство процессов большую часть времени), D — непрерываемый сон, обычно ожидание диска или сетевой файловой системы, T — остановлен, Z — зомби. Зомби уже завершился; он лишь сохраняет запись, чтобы родитель мог прочитать код завершения через wait. Зомби не тратят CPU и память, но их большое число означает забагованного родителя, который никогда не собирает потомков; убивать зомби бесполезно, помогает исправление или рестарт родителя. Если родитель умирает первым, его потомков усыновляет PID 1 или subreaper. В Linux load average учитывает задачи и в R, и в D, поэтому высокий load при простаивающем CPU обычно означает процессы, застрявшие в D на медленном хранилище, а не избыток вычислений.
ps -eo pid,ppid,stat,wchan:20,cmd | awk '$3 ~ /D|Z/' # stuck or zombie
ps -o ppid= -p 12345 # parent of a zombie
cat /proc/loadavg # 1/5/15 min load, running/total tasks
cat /proc/pressure/cpu /proc/pressure/io # PSI: time spent stalled
cat /proc/12345/status | grep -E 'State|VmRSS|Threads'
ls -l /proc/12345/fd | wc -l # open file descriptors
09cgroups v2: ограничения для сервисов
systemd помещает каждый сервис в собственную control group (cgroup) — контейнер ядра для учёта и ограничений. Текущие выпуски Ubuntu, Debian и RHEL используют единую иерархию cgroup v2, которую systemd предоставляет в виде простых настроек юнита. MemoryHigh притормаживает сервис и отбирает у него память при превышении мягкого лимита, MemoryMax — жёсткий потолок, при котором OOM killer срабатывает только внутри этого сервиса, так что умирает воркер с утечкой, а не база данных рядом. CPUQuota ограничивает процессорное время, CPUWeight задаёт относительный приоритет при конкуренции, IOWeight делает то же для диска, а TasksMax ограничивает число процессов и потоков, что останавливает fork-бомбу. systemd-cgtop показывает потребление по сервисам в реальном времени. Лимиты работающего сервиса можно менять через systemctl set-property, который заодно записывает постоянный drop-in.
# /etc/systemd/system/worker.service.d/limits.conf
[Service]
MemoryHigh=400M
MemoryMax=512M
CPUQuota=150%
TasksMax=256
IOWeight=50
# Apply without editing files:
# sudo systemctl set-property worker.service MemoryMax=512M
# Watch usage live:
# systemd-cgtop
10Зависимости, типы запуска и политика рестартов
Две разные идеи часто путают. Wants= и Requires= говорят, какие юниты должны запускаться вместе; After= и Before= задают только порядок. Requires без After запускает оба одновременно, а Requires означает, что ваш юнит остановится, если остановится зависимость, поэтому Wants вместе с After — мягкий вариант по умолчанию. Type= сообщает systemd, когда запуск завершён: simple считает сервис запущенным сразу после fork, exec ждёт, пока бинарник действительно будет запущен, так что неверный путь проваливает старт, а notify ждёт, пока приложение сообщит о готовности через sd_notify. Restart=on-failure ограничен параметрами StartLimitIntervalSec и StartLimitBurst в [Unit]; после слишком частых рестартов в этом окне systemd сдаётся с сообщением «start request repeated too quickly». При остановке systemd отправляет KillSignal (SIGTERM), ждёт TimeoutStopSec, затем шлёт SIGKILL всей cgroup, так что ни один дочерний процесс не ускользнёт.
[Unit]
Description=Queue worker
Wants=redis-server.service
After=network-online.target redis-server.service
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=exec
User=worker
ExecStart=/usr/bin/node /srv/worker/index.js
Restart=on-failure
RestartSec=5
TimeoutStopSec=60
KillMode=mixed
11Харденинг сервиса и измерение результата
systemd может поместить сервис в песочницу несколькими строками, часто эффективнее, чем контейнер. ProtectSystem=strict монтирует для сервиса всю файловую систему только на чтение, а ReadWritePaths= открывает лишь то, куда ему нужно писать. ProtectHome=true скрывает /home, PrivateTmp даёт собственный /tmp, PrivateDevices скрывает железо, NoNewPrivileges блокирует повышение привилегий через setuid, а RestrictAddressFamilies ограничивает типы сокетов, которые он может открывать. Пустой CapabilityBoundingSet= отбрасывает все capabilities. systemd-analyze security name выводит оценку уязвимости со списком незаданных опций, что делает харденинг постепенным и измеримым. Начните с дешёвых и безопасных опций, перезапустите, проверьте журнал на ошибки и добавляйте дальше. Время загрузки и запуска тоже измеримо: systemd-analyze blame и critical-chain показывают, какие юниты замедляют загрузку.
# /etc/systemd/system/myapp.service.d/hardening.conf
[Service]
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp /var/log/myapp
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
NoNewPrivileges=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
CapabilityBoundingSet=
# Check the score: systemd-analyze security myapp.service
Реальные ситуации
Сервис застрял в цикле падений
Проблема
После деплоя systemctl status показывает приложение в состоянии failed с сообщением «start request repeated too quickly». Ручной перезапуск каждый раз падает за несколько секунд.
Как решить
Достигнут start limit, поэтому systemd перестал пытаться. Сначала прочитайте причину: journalctl -u myapp -b -n 100 показывает настоящую ошибку — часто это отсутствующая переменная окружения, неверный путь после переключения релиза, порт, всё ещё занятый старым процессом (ss -ltnp), или проблема с правами. Запустите строку ExecStart вручную от пользователя сервиса через sudo -u myapp, чтобы воспроизвести ошибку. Устраните причину, выполните daemon-reload, если юнит менялся, затем systemctl reset-failed myapp и запустите сервис. После этого сделайте так, чтобы деплой падал быстро: health check после рестарта и автоматический откат symlink, если сервис не стал active за отведённое время.
Воркер с утечкой памяти роняет базу данных
Проблема
Раз в несколько дней сервер перестаёт отвечать, и OOM killer убивает MySQL. Графики памяти показывают, что воркер очереди медленно растёт, пока не заканчивается RAM.
Как решить
Сначала изолируйте, потом исправляйте. Задайте воркеру лимит cgroup через MemoryHigh и MemoryMax в drop-in, чтобы OOM killer срабатывал внутри cgroup воркера, а Restart=on-failure поднимал его обратно, не трогая базу данных. Дополнительно защитите базу через OOMScoreAdjust=-500 в её юните. Большинство фреймворков очередей также умеют завершать воркер после определённого числа задач или порога памяти, позволяя менеджеру процессов запустить свежий. Затем найдите утечку: сравните память по типам задач, снимите heap snapshot в Node, ищите растущие кэши или незакрытые соединения. Проверьте journalctl -k на строки «Memory cgroup out of memory», чтобы убедиться, что лимит работает.
Деплои обрывают текущие запросы
Проблема
Каждый рестарт Node API во время деплоя даёт всплеск ошибок 502 в nginx и несколько прерванных фоновых задач.
Как решить
Приложение завершается сразу по SIGTERM или вообще не слушает его и его убивают. Обрабатывайте SIGTERM в коде: прекратите принимать новые соединения через server.close, дайте открытым запросам завершиться, доделайте или освободите текущую задачу, закройте пулы соединений с базой данных и выйдите с кодом 0. Задайте в юните TimeoutStopSec чуть больше самого долгого нормального запроса, чтобы systemd не переходил к SIGKILL. Для zero-downtime запустите два инстанса на разных портах или сокетах за upstream в nginx и перезапускайте их по одному, либо используйте менеджер процессов с graceful reload. Добавьте proxy_next_upstream в nginx, чтобы запрос к останавливающемуся инстансу повторялся на другом.
Шпаргалка
- Найти зомби и процессы в состоянии D
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z|D/'- CPU и память по сервисам в реальном времени
systemd-cgtop- Задать лимит памяти работающему сервису
sudo systemctl set-property myapp.service MemoryMax=512M- Все свойства юнита
systemctl show myapp -p MainPID,MemoryCurrent,NRestarts- Сбросить состояние failed после исправления
sudo systemctl reset-failed myapp- Проверить unit-файл на ошибки
systemd-analyze verify /etc/systemd/system/myapp.service- Оценка харденинга сервиса
systemd-analyze security myapp.service- Что замедляет загрузку
systemd-analyze blame; systemd-analyze critical-chain- Запустить разовую команду как transient-юнит
sudo systemd-run --unit=import -p MemoryMax=1G /srv/app/import.sh- Pressure stall info (CPU, память, I/O)
cat /proc/pressure/{cpu,memory,io}
В моих реальных проектах
- На своих серверах я запускаю очереди Laravel под supervisor, а Node-приложения под pm2, а cron запускает планировщик Laravel.
- На сервере POS SaaS долгоживущие сервисы и воркеры BullMQ работают в Docker Compose с health checks, по которым видно, что они живы.
- Перед любыми изменениями я диагностирую проблемы в продакшене по логам сервисов — сначала только чтение.
Вопросы и ответы
Q1Чем SIGTERM отличается от SIGKILL?
SIGTERM — это просьба завершиться. Процесс может перехватить его и корректно остановиться: перестать принимать новую работу, завершить текущие запросы, сбросить буферы, закрыть соединения с БД и выйти. Это сигнал kill по умолчанию, и именно его первым отправляют systemd, Docker и Kubernetes. SIGKILL нельзя перехватить, заблокировать или проигнорировать; ядро завершает процесс немедленно, поэтому никакой очистки не происходит, и можно получить повреждённые файлы, зависшие блокировки или потерянные задачи. Правильная процедура: TERM, подождать разумный таймаут, и только если процесс всё ещё жив — KILL. Именно так поступает systemd с TimeoutStopSec.
Q2Как держать Node.js-приложение запущенным на Linux-сервере?
Я бы не запускал его вручную по SSH. Самый простой надёжный вариант — юнит systemd: запуск под отдельным пользователем, WorkingDirectory и EnvironmentFile для секретов, ExecStart с бинарником node, Restart=on-failure и enable, чтобы приложение стартовало при загрузке. Логи идут в журнал. Альтернативы — pm2, который добавляет кластеризацию и собственную работу с логами и стартует при загрузке через юнит systemd, сгенерированный pm2 startup, или запуск в контейнере с политикой перезапуска. В любом случае я привязываю приложение к localhost, ставлю перед ним nginx для TLS и обрабатываю SIGTERM в коде, чтобы перезапуски проходили без сюрпризов.
Q3Сервер тормозит. Как понять, почему?
Начинаю с uptime и сравниваю load average с nproc. Затем top или htop с сортировкой по CPU, а потом по памяти, чтобы найти тяжёлый процесс. free -h показывает, закончилась ли память и используется ли swap, а journalctl -k или dmesg — не убивал ли процессы OOM killer. Если CPU загружен слабо, а нагрузка высокая, процессы, скорее всего, ждут диск, поэтому я смотрю iostat или значение wa в top, а также df -h на предмет заполненного диска. Наконец, смотрю на сторону приложения: медленные запросы, зависший воркер очереди или всплеск трафика, видимый в access-логе nginx.
Q4Чем systemctl restart отличается от reload?
restart останавливает сервис и запускает его снова, поэтому есть короткий момент без процесса, и существующие соединения обрываются. reload просит работающий процесс перечитать конфигурацию, обычно отправляя SIGHUP или выполняя настроенную команду ExecReload, и продолжает обслуживать запросы. nginx, например, запускает новые воркеры с новым конфигом и даёт старым завершить свои запросы, так что для пользователей reload незаметен. Не каждый сервис поддерживает reload; systemctl reload-or-restart обрабатывает оба случая. Перед reload nginx я всегда запускаю nginx -t, потому что сломанный конфиг провалит reload, и это лучше поймать заранее.
Q5Таймеры systemd или cron: что выбрать?
Для небольшой простой задачи cron вполне подходит, и его синтаксис знают все. Для важных задач я предпочитаю таймеры systemd. Задача запускается как обычный сервис, поэтому её вывод попадает в журнал с временными метками, у неё могут быть лимиты ресурсов и отдельный пользователь, она не запустит вторую копию, пока работает первая, а Persistent=true догоняет запуски, пропущенные, пока сервер был выключен. systemctl list-timers также показывает, когда каждая задача запускалась в последний раз и когда запустится следующий, чего cron не умеет. Цена — два маленьких файла вместо одной строки.
Q6Куда смотреть, когда сервис systemd не запускается?
systemctl status name показывает состояние, код выхода и последние строки лога, и часто этого достаточно. Для подробностей journalctl -u name -b показывает весь его вывод с момента загрузки, а -p err отфильтровывает ошибки. Частые причины — неверный путь в ExecStart, отсутствующая переменная окружения, уже занятый порт (проверьте через ss -ltnp), права на файлы для сервисного пользователя или синтаксическая ошибка в самом юните, которую может поймать systemd-analyze verify. Если я правил юнит, убеждаюсь, что выполнил daemon-reload. После исправления systemctl reset-failed сбрасывает состояние ошибки.
Вопросы уровня senior
S1Почему PID 1 должен быть контейнерный init или менеджер сервисов, а не ваше приложение, и что иначе идёт не так?
У PID 1 особые обязанности. Ядро не применяет к нему обработчики сигналов по умолчанию, поэтому SIGTERM, который приложение явно не обрабатывает, просто игнорируется, и контейнер ждёт таймаута, прежде чем его убьют. PID 1 также должен собирать осиротевших потомков; приложение, запускающее подпроцессы, например shell-обёртку или headless-браузер, накапливает зомби, если никогда не вызывает wait. На хосте PID 1 — это systemd, и он делает и то, и другое. В контейнерах либо используйте крошечный init вроде tini (docker run --init), либо убедитесь, что приложение обрабатывает сигналы и не порождает неуправляемых потомков. Также запускайте приложение в exec-форме в Dockerfile или через exec в entry-скрипте, чтобы сигналы получало оно, а не промежуточный shell.
S2Юнит systemd, pm2, supervisor или контейнеры: как выбрать менеджер процессов?
Все они перезапускают процессы; различия — в охвате и зависимостях. Простой юнит systemd не требует ничего дополнительного, даёт лимиты cgroup, песочницу и логи в журнале и является самым надёжным выбором для одного-двух сервисов на хост. pm2 добавляет удобства для Node, такие как cluster mode по всем ядрам, zero-downtime reload и процесс деплоя, ценой ещё одного демона со своими лог-файлами. supervisor прост и не зависит от языка, популярен для запуска нескольких процессов-воркеров очередей, но собственных лимитов cgroup не имеет. Контейнеры дают воспроизводимые образы и изоляцию, что окупается при большом числе сервисов или команд. Что бы вы ни выбрали, не громоздите менеджеры друг на друга без причины и чётко определите, какой слой отвечает за рестарты, логи и лимиты.
S3Как ограничить радиус поражения утечки памяти на общем сервере?
Исходите из того, что что-то обязательно потечёт, и заранее решите, кто должен умереть. Поместите каждый сервис в собственную cgroup с MemoryHigh как мягким лимитом, чтобы ядро сначала отбирало память и замедляло сервис, и MemoryMax как жёстким лимитом, чтобы OOM killer действовал только внутри этой cgroup. Понизьте OOM score критичных сервисов, например базы данных, через OOMScoreAdjust. Позвольте политике рестартов восстанавливать сервис с утечкой и настройте алерты на NRestarts или события OOM, чтобы утечка была видна, а не тихо «лечилась». Оставьте немного swap для поглощения пиков и присмотритесь к systemd-oomd, который может срабатывать раньше при нехватке памяти. Цена в том, что слишком низкие лимиты вызывают рестарты на легитимных пиках, поэтому выставляйте их по наблюдаемому потреблению плюс запас.
Глоссарий
- Процесс-зомби
- Процесс, который завершился, но родитель ещё не забрал его код завершения через wait.
- Непрерываемый сон (D)
- Состояние, в котором процесс ждёт I/O и не может быть прерван, даже через SIGKILL, пока I/O не завершится.
- Subreaper
- Процесс, усыновляющий осиротевших потомков вместо PID 1, как это делают пользовательские менеджеры systemd и init-процессы контейнеров.
- cgroup
- Control group: группировка процессов в ядре для учёта и ограничения CPU, памяти, I/O и числа процессов.
- Drop-in
- Небольшой .conf-файл в каталоге .d юнита, переопределяющий выбранные настройки без замены всего юнита.
- sd_notify
- Протокол, через который приложение сообщает systemd, что оно готово или всё ещё живо; используется с Type=notify и WatchdogSec.
- Start limit
- Ограничение systemd на число рестартов в окне времени. После его достижения юнит остаётся failed до сброса.
- PSI
- Pressure Stall Information: метрики ядра, показывающие, сколько времени задачи ждут CPU, памяти или I/O.
- Transient-юнит
- Юнит, создаваемый на лету через systemd-run, с лимитами и логированием, который исчезает после завершения.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.