Команда остановки сервиса выполняется девяносто секунд, потом терминал наконец отпускает, а в логе появляется сухая строка про то, что остановка не уложилась в таймаут и процесс пришлось убить силой. Перезагрузка сервера превращается в пытку ожиданием, деплой буксует, а на первый взгляд непонятно, что вообще происходит, ведь сам процесс давно отвечал нормально. За этой картиной почти всегда стоит связка из двух настроек unit-файла, которые администраторы либо не трогают, либо трогают неправильно.
Речь про TimeoutStopSec и KillMode. Первая задаёт, сколько systemd готов ждать корректного завершения, прежде чем перейти к насилию. Вторая определяет, кого именно при остановке считать частью сервиса и кого убивать. Ошибка в любой из них приводит либо к зависанию остановки на полторы минуты, либо к тихому накоплению осиротевших процессов, которые формально не принадлежат уже ни одному сервису, но продолжают есть память и держать порты. Разберём механику по шагам, потому что без понимания внутренней логики systemd эти симптомы выглядят случайными.
Что на самом деле происходит между командой stop и убийством процесса
Распространённое заблуждение состоит в том, что таймаут это пауза перед отправкой сигнала завершения. На деле порядок обратный, и это принципиально для диагностики. Сигнал мягкого завершения уходит процессу немедленно, сразу после команды остановки. Таймаут отсчитывается уже после этого сигнала. Если за отведённое время процесс не завершился, тогда systemd отправляет жёсткий сигнал убийства. Сигнал мягкого завершения выдаётся немедленно после команды остановки, и только если это по какой-то причине не завершает процесс, по истечении таймаута процессу посылается сигнал принудительного убийства.
Полная последовательность шагов выглядит так. Сначала всем процессам в зоне ответственности сервиса уходит сигнал завершения, по умолчанию это SIGTERM, а сразу за ним SIGCONT, чтобы даже приостановленные задачи смогли корректно завершиться. Затем systemd ждёт. Дальше развилка по условиям: либо завершился главный процесс, либо прошло время TimeoutStopSec, либо выполнено какое-то из условий конкретного режима убийства. После наступления развилки запрос на завершение повторяется уже сигналом SIGKILL, который проигнорировать невозможно.
Практический вывод из этой механики один и важный. Если остановка стабильно занимает ровно столько секунд, сколько стоит в TimeoutStopSec, это не значит, что процесс долго умирает. Это значит, что процесс вообще не реагирует на сигнал завершения и доживает до принудительного убийства. Лечить нужно не таймаут, а причину игнорирования сигнала. Это разворачивает всю диагностику в правильную сторону с самого начала.
Как читать лог остановки и поймать застрявшую фазу
Диагностика начинается не с правки конфигурации, а с чтения того, что systemd сам рассказывает о фазах остановки. По умолчанию он немногословен, поэтому сначала смотрим базовую картину по конкретному юниту.
# Статус и последние записи по юниту
systemctl status myapp.service
# Хвост лога именно этого юнита в реальном времени
journalctl -u myapp.service -f
# Последние строки вокруг попытки остановки
journalctl -u myapp.service -n 50 --no-pager
В логе нужно искать характерные переходы состояний. Строка про смену состояния на stop-sigterm означает, что сервису отправлен мягкий сигнал и начался отсчёт таймаута. Если следом через ровно заданное число секунд появляется строка о том, что остановка не уложилась во время и сервис убивается, диагноз почти поставлен: процесс не реагирует на SIGTERM. Ещё показательнее переход в состояние final-sigterm с последующим таймаутом, это говорит, что в зоне сервиса остались процессы уже после ухода главного.
Штатной детализации часто не хватает, и тогда включают подробный режим логирования самого systemd. Он показывает выполнение правил ExecStop и момент, когда менеджер переходит к зачистке контрольной группы.
# Поднять детализацию логов systemd на лету, без перезагрузки
sudo systemd-analyze set-log-level debug
# Воспроизвести проблему
sudo systemctl stop myapp.service
# Вернуть обычный уровень логирования
sudo systemd-analyze set-log-level info
Здесь есть важный нюанс, который экономит часы. Подробный лог показывает, что выполняются ExecStop и ExecStart, и показывает переход к убийству контрольной группы, но он не сообщает, какие именно процессы были убиты. Эту часть приходится добирать другими инструментами, и именно она обычно содержит разгадку.
Контрольная группа против дерева процессов, корень всех бед
Чтобы понять, почему остановка зависает или почему остаются сироты, нужно знать, как systemd вообще определяет границы сервиса. Современный менеджер не отслеживает дерево процессов по родительским связям. Он опирается на контрольную группу, в которую помещаются все процессы юнита. Пока в этой группе есть хоть один живой процесс, сервис не считается до конца остановленным, и menеджер либо ждёт, либо переходит к зачистке группы.
Посмотреть реальный состав группы сервиса в момент проблемы важнее, чем гадать.
# Дерево всех процессов по контрольным группам
systemd-cgls
# Только процессы конкретного сервиса
systemctl status myapp.service
# Кто реально жив в системе под этим именем
ps auxfww | grep myapp
Классический сбивающий с толку случай выглядит так. В выводе systemd-cgls сервис уже исчез, процессов под ним не видно, а команда остановки всё равно продолжает висеть до самого таймаута. Это указывает, что systemd ждёт уведомления о завершении, которое по каким-то причинам не приходит вовремя, хотя процессы фактически мертвы. Зеркальная ситуация противоположна: сервис формально остановлен и помечен как failed, но в выводе ps процессы живёхоньки. Это уже не зависание, это сироты, и причину нужно искать в режиме убийства.
Почему KillMode=process выглядит безобидно и плодит сирот
Здесь кроется самая коварная из настроек. Режим убийства по умолчанию это control-group, и он означает буквально следующее: при остановке сигнал получают все процессы контрольной группы сервиса, включая всех потомков, которых породил главный процесс. Группа зачищается целиком, сирот не остаётся.
Режим process меняет правила радикально. При нём сигнал завершения адресуется только главному процессу сервиса. Всё, что главный процесс успел породить, дочерние воркеры, форкнутые обработчики, запущенные внешние утилиты, остаётся вне зоны поражения. Когда главный процесс умирает, его потомки не получают ни SIGTERM, ни SIGKILL. С точки зрения systemd сервис остановлен, ресурсов он больше не потребляет, на самом же деле в системе живут отвязанные процессы, которые сменили родителя на init и продолжают работать. Не рекомендуется выставлять KillMode в process или тем более none, поскольку это позволяет процессам ускользать от управления жизненным циклом и оставаться запущенными, в то время как сервис уже считается остановленным и предполагается не потребляющим ресурсов.
Опасность этого режима двойная и обе стороны бьют по проду. Первая сторона это утечка ресурсов. Сироты держат память, файловые дескрипторы и сетевые порты. Через несколько циклов перезапуска сервиса на машине накапливается выводок старых процессов, новый экземпляр не может занять порт, и начинается необъяснимая на первый взгляд деградация. Вторая сторона касается перезапуска. Если режим оставляет процессы от прошлого запуска внутри контрольной группы, сервис в режиме control-group или mixed может вообще не перезапуститься, потому что группа не считается пустой.
Случай гибридного режима и ловушка с ExecStopPost
Между двумя крайностями есть промежуточный режим mixed, и его выбирают, рассчитывая получить лучшее от обоих. В нём мягкий сигнал получает только главный процесс, но если после его смерти в группе остаются процессы, по таймауту им прилетает жёсткий сигнал убийства уже по всей группе. На бумаге это разумный компромисс. На практике у него есть известные острые углы, о которые спотыкаются.
Первый угол это долгоживущие потомки, которые сами по себе игнорируют завершение главного процесса. Если порождённый процесс не реагирует на смерть родителя и не получает прямого сигнала до самого таймаута, остановка снова растягивается на полный TimeoutStopSec, теперь уже из-за фазы final-sigterm. Второй угол тоньше и встречается в боевых конфигурациях с пост-обработкой. При сочетании гибридного режима с заданным ExecStopPost наблюдалось, что нужный сигнал убийства не доставлялся дочерним процессам, и они оставались жить после остановки сервиса. То есть добавление безобидного на вид постскрипта меняло поведение зачистки и порождало тех самых сирот.
Отдельная ловушка связана с поведением systemd при сносе контрольной группы, когда внутри неё застрял неубиваемый процесс. В логах это видно как невозможность уничтожить группу с пометкой про занятость устройства или ресурса. Менеджер может пометить операцию остановки как завершённую, при этом честно записав, что процесс продолжает работать после остановки юнита. Формально задача закрыта, фактически сирота жив.
Как чинить, не плодя новых проблем
Лечение зависит от того, что показала диагностика, и здесь важно не хвататься за первое попавшееся решение. Если остановка упирается ровно в таймаут, корень почти всегда в том, что приложение не обрабатывает сигнал завершения. Правильное лечение это научить приложение реагировать на SIGTERM корректным завершением, а не маскировать симптом. Сокращение таймаута лишь ускоряет переход к насильственному убийству, но не делает остановку чистой.
Когда корректно завершить процесс приложение в принципе не умеет и исправить его код нельзя, осознанным компромиссом бывает разумное конечное значение таймаута, чтобы не ждать стандартные полторы минуты впустую.
# /etc/systemd/system/myapp.service.d/override.conf
[Service]
# Конечное разумное время вместо дефолтных 90 секунд
TimeoutStopSec=20
# Явный сигнал, если приложение слушает не SIGTERM
KillSignal=SIGTERM
Отдельно стоит предупредить про значение infinity у таймаута остановки. Оно выглядит безопасным, мол, подождём сколько надо, но на части систем приводит к обратному эффекту: менеджер убивает сервис мгновенно, оставляя дочерние процессы осиротевшими. Поэтому в боевых unit-файлах задают вменяемое конечное число, а не бесконечность и не ноль наугад.
Если же диагностика показала именно сирот, решение почти всегда в отказе от режима process в пользу control-group или хотя бы mixed, чтобы зачистка покрывала всё дерево потомков.
# /etc/systemd/system/myapp.service.d/override.conf
[Service]
# Зачищать всю контрольную группу, не оставляя потомков
KillMode=control-group
TimeoutStopSec=20
# Гарантированно добивать остаток группы после таймаута
SendSIGKILL=yes
После любой правки override обязательна перечитка конфигурации и контрольный цикл остановки с проверкой, что сирот не осталось.
# Применить изменения override-файла
sudo systemctl daemon-reload
# Контрольная остановка
sudo systemctl stop myapp.service
# Проверка, что ничего не пережило остановку
ps auxfww | grep myapp
systemd-cgls | grep myapp
Что из этого складывается в практику
Если свести всё к рабочему правилу, оно короткое. Зависание остановки ровно на длину таймаута это почти всегда не медленное завершение, а полное игнорирование сигнала, и чинить надо реакцию приложения, а не цифру таймаута. Оставшиеся после остановки процессы это почти всегда последствие режима process, который выглядит аккуратным, а на деле выпускает потомков из-под управления. Режим по умолчанию control-group существует не просто так, и менять его на process стоит только с полным пониманием, что все потомки сервиса теперь придётся убирать чем-то ещё.
Главный вывод не про конкретные директивы, а про модель мышления. systemd не видит дерева процессов, он видит контрольную группу, и почти все загадочные сбои остановки объясняются расхождением между тем, что администратор считает сервисом, и тем, что им считает менеджер. Стоит держать в голове эту разницу, читать фазы остановки в логе, а не гадать, и проверять реальный состав группы вместо предположений, и зависания при остановке перестают быть загадкой и превращаются в короткую предсказуемую процедуру разбора.