← назад к разделу

Сервис перезапускается без единой строки в логе, в Kubernetes висит «exit code 137», под нагрузкой сыпется «Too many open files», а диск полон, хотя логи только что удалили. Все четыре истории — не про код, а про то, как Linux управляет процессом. Сетевую часть — порты, соединения, DNS — разбирает сетевая диагностика, здесь всё остальное.

SIGTERM«пожалуйста, закройся» ждём 10–30 спроцесс дорабатывает SIGKILLубит, код 137 успел — код 0 docker stop: 10 с, Kubernetes: 30 с перехватить нельзя

Остановка всегда идёт в два шага: вежливая просьба, потом принудительное завершение, если процесс не уложился.

Сигналы: как процесс останавливаютспросят на собеседовании

Процесс нельзя «выключить» снаружи напрямую — ему посылают сигнал, короткое уведомление от ядра. Для остановки их два. SIGTERM — просьба завершиться: процесс может его перехватить, дописать ответы, закрыть соединения с базой и выйти сам. SIGKILL перехватить нельзя: ядро убивает процесс мгновенно, незаписанное теряется.

docker stop и Kubernetes действуют одинаково: сначала SIGTERM, потом ждут — Docker 10 секунд, Kubernetes 30 секунд по умолчанию, — и если процесс жив, посылают SIGKILL. По коду выхода видно, что произошло: процесс, убитый сигналом, завершается с кодом 128 плюс номер сигнала. SIGTERM — 15, отсюда 143, SIGKILL — 9, отсюда 137.

Пример на Python, потому что его проще всего запустить; в Java, Go и Node устроено так же, а как писать обработчик на своём языке, разобрано в разделе про корректную остановку:

живой пример

import signal
import subprocess
import sys

graceful = """
import signal, sys, time
def stop(signum, frame):
    print("  получил SIGTERM, закрываю соединения", flush=True)
    sys.exit(0)
signal.signal(signal.SIGTERM, stop)
print("  работаю", flush=True)
time.sleep(30)
"""
careless = """
import time
print("  работаю", flush=True)
time.sleep(30)
"""

for name, code, sig in [
    ("с обработчиком, SIGTERM", graceful, signal.SIGTERM),
    ("без обработчика, SIGTERM", careless, signal.SIGTERM),
    ("с обработчиком, SIGKILL", graceful, signal.SIGKILL),
]:
    print(name)
    proc = subprocess.Popen([sys.executable, "-c", code], stdout=subprocess.PIPE, text=True)
    print(proc.stdout.readline().rstrip())
    proc.send_signal(sig)
    out, _ = proc.communicate()
    if out:
        print(out.rstrip())
    shell_code = 128 - proc.returncode if proc.returncode < 0 else proc.returncode
    print(f"  код выхода в shell: {shell_code}")
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Грабля контейнеров: процесс с номером 1 ядро защищает, и сигнал без обработчика ему не страшен. Приложение без своего обработчика, запущенное первым процессом, игнорирует SIGTERM и через таймаут всё равно получает SIGKILL с кодом 137. Почему так и как это лечат, разобрано в статье про запуск контейнеров.

Файловые дескрипторы и Too many open filesспросят на собеседовании

Для ядра открытый файл, сетевое соединение и канал между процессами — одно и то же: файловый дескриптор, номер в таблице процесса. Число дескрипторов ограничено, мягкий предел часто 1024. Каждое соединение с базой, каждый входящий запрос, каждый открытый лог тратит один номер. Когда номера кончаются, любая попытка открыть файл или принять соединение падает с ошибкой EMFILE, в логах — «Too many open files».

Причин две. Либо предел честно мал для нагрузки, и его поднимают через ulimit -n или настройки сервиса. Либо дескрипторы утекают: файл или соединение открыли и не закрыли, и их число растёт, пока не упрётся в предел. Утечку видно по ls /proc/<pid>/fd | wc -l, которое только растёт, а lsof -p <pid> покажет, что именно открыто. Как это выглядит изнутри, при пределе в 64:

живой пример

import errno
import resource
import tempfile

soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
resource.setrlimit(resource.RLIMIT_NOFILE, (64, hard))

leaked = []
try:
    while True:
        leaked.append(tempfile.TemporaryFile())
except OSError as error:
    print(f"открыто файлов: {len(leaked)}, дальше ошибка {errno.errorcode[error.errno]}")

for f in leaked:
    f.close()

with tempfile.TemporaryFile() as f:
    f.write("with закрывает файл сам".encode())
print("после закрытия файл снова открывается")
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Открыто меньше 64: первые номера уже заняты стандартным вводом, выводом и ошибками. Лечит утечку не подъём предела, а закрытие ресурса там, где его открыли: with в Python, try-with-resources в Java, defer в Go, finally в Node.

Диск: место, inode и удалённые файлыспросят на собеседовании

«No space left on device» бывает при свободных гигабайтах. Файловая система хранит описание каждого файла в отдельной записи — inode, и их число задаётся при создании диска. Миллионы мелких файлов, например кеш или сессии, исчерпают inode раньше, чем место. Поэтому смотрят обе команды: df -h показывает место, df -i — inode.

Вторая частая история: лог удалили, а место не освободилось. Пока процесс держит файл открытым, ядро не освобождает данные — пропало только имя. Такие файлы показывает lsof +L1 с пометкой (deleted). Место вернётся, когда процесс закроет файл или перезапустится. Поэтому большие логи не удаляют, а обнуляют командой truncate -s 0 или ротируют с сигналом процессу переоткрыть файл.

Память и OOM killerспросят на собеседовании

Когда памяти не хватает всей системе или контейнеру с лимитом, ядро не ждёт: OOM killer выбирает процесс, обычно самый большой, и убивает его через SIGKILL. В логах приложения ничего не будет — процесс не успел ничего записать. Признаки: код 137, у пода в Kubernetes причина OOMKilled, в dmesg строки про Out of memory: Killed process.

Сколько процесс занимает на самом деле, показывает RSS — резидентная память в ps и top. Частая причина OOM в контейнерах — приложение рассчитывает память по всей машине, а не по лимиту контейнера. JVM с 10-й версии сама берёт лимит контейнера за основу размера кучи. Go лимит не учитывает: сборщику мусора его сообщают переменной GOMEMLIMIT. Node задаёт предел кучи флагом --max-old-space-size. И везде к куче добавляются стеки потоков и буферы, поэтому предел ставят с запасом ниже лимита пода.

Коротко

  • SIGTERM — просьба завершиться, её можно перехватить; SIGKILL — немедленное убийство без обработчика.
  • Код выхода 128 + номер сигнала: 143 — SIGTERM, 137 — SIGKILL (часто OOM killer).
  • docker stop ждёт 10 с, Kubernetes — 30 с, потом SIGKILL.
  • Файлы и сокеты — файловые дескрипторы; предел ulimit -n, утечку видно в ls /proc/<pid>/fd и lsof -p.
  • df -h — место, df -i — inode; удалённый, но открытый файл держит место, его покажет lsof +L1.
  • OOM killer убивает молча: ищите OOMKilled, dmesg и сравнивайте RSS с лимитом.

Что почитать дальше