Процессы и сигналы

Что происходит при остановке контейнера и почему приложение теряет запросы.

Процесс

У каждого процесса есть PID, родитель, владелец и состояние. Смотреть — ps, top, htop:

ps -ef | head
ps -o pid,ppid,stat,cmd -p 1

Колонка STAT важнее, чем кажется:

Код Состояние
R выполняется или готов
S спит, ждёт события
D непрерываемый сон — ждёт диск или сеть
Z зомби: завершился, родитель не забрал код возврата
T остановлен

Процесс в состоянии D не убивается даже SIGKILL. Если процесс «не убивается» — почти всегда он в D и ждёт недоступное хранилище.

Сигналы

Сигнал — способ сказать процессу, что что-то произошло. В работе важны три:

Сигнал Номер Что значит
SIGTERM 15 «завершись» — можно перехватить
SIGKILL 9 немедленное убийство ядром, перехватить нельзя
SIGHUP 1 обычно «перечитай конфиг»
kill 1234        # SIGTERM
kill -9 1234     # SIGKILL

Привычка сразу писать kill -9 — плохая: процесс не успевает закрыть соединения, дописать файлы и снять блокировки.

Как это связано с контейнерами

docker stop и Kubernetes при удалении пода делают одно и то же:

  1. Посылают SIGTERM главному процессу.
  2. Ждут (по умолчанию 10 секунд у Docker, 30 у Kubernetes).
  3. Если процесс ещё жив — SIGKILL.

Отсюда правило: приложение должно обрабатывать SIGTERM — перестать принимать новые запросы, дождаться текущих, закрыть соединения с базой. Иначе при каждом деплое часть запросов обрывается.

PID 1 и почему это ловушка

Главный процесс контейнера получает PID 1, а у него особое поведение: ядро не применяет к нему обработчики сигналов по умолчанию. Если процесс не установил свой обработчик SIGTERM явно, сигнал просто игнорируется — и контейнер всегда останавливается через SIGKILL спустя таймаут.

Вторая ловушка — запуск через shell:

CMD npm start          # shell-форма: PID 1 это /bin/sh
CMD ["npm", "start"]   # exec-форма: PID 1 это сам процесс

В shell-форме сигнал получает sh, который не передаёт его дочернему процессу. Приложение не узнает об остановке вообще. Используйте exec-форму.

Проверить

docker run --rm -d --name t nginx
docker stop t     # смотрим, сколько заняло

Быстрая остановка — сигнал обработан. Ровно десять секунд — почти наверняка ждали таймаут и убили принудительно.