Сервис перезапускается без единой строки в логе, в Kubernetes висит «exit code 137», под нагрузкой сыпется «Too many open files», а диск полон, хотя логи только что удалили. Все четыре истории — не про код, а про то, как Linux управляет процессом. Сетевую часть — порты, соединения, DNS — разбирает сетевая диагностика, здесь всё остальное.
Остановка всегда идёт в два шага: вежливая просьба, потом принудительное завершение, если процесс не уложился.
Сигналы: как процесс останавливаютспросят на собеседовании
Процесс нельзя «выключить» снаружи напрямую — ему посылают сигнал, короткое уведомление от ядра. Для остановки их два. 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 с лимитом.
Что почитать дальше
- Сетевая диагностика — порты, соединения и состояния TCP с той же машины.
- Корректная остановка — как обработать
SIGTERMи дописать запросы на своём языке. - Запуск контейнеров — PID 1, лимиты памяти и диагностика упавшего контейнера.