Три часа ночи, алерт: API отвечает 500. Рука сама набирает systemctl restart, и часто это даже помогает: утечка или зависший поток сбрасываются, и до утра можно спать.

Но есть инциденты, где рестарт ничего не чинит. А иногда добивает то, что ещё работало, и заодно стирает улики, по которым можно было найти причину. Ниже пять таких из Prod is Down.

1. Битый конфиг: рестарт убивает последний живой процесс

Как выглядит. После деплоя база перестала отвечать, API отдаёт 500. systemctl status postgresql — failed. Ещё один systemctl restart — снова failed, и так сколько угодно раз.

Что сделал рестарт. PostgreSQL читает конфигурацию целиком при старте. Пока процесс работал, битая строка в новом drop‑in никому не мешала: он жил со старыми настройками. Рестарт после деплоя заставил перечитать всё, и одна строка вроде shared_buffers = '512XB' уронила весь кластер. Каждый следующий рестарт падает на той же строке.

Так же ведут себя nginx, haproxy и sshd. Работающий процесс со старым конфигом — последнее, что у вас осталось. С sshd особенно больно: рестарт с битым sshd_config — и на сервер больше не зайти.

Как найти. Не гадать, а прочитать, что пишет сам PostgreSQL: ошибка назовёт параметр и файл.

systemctl status postgresql
journalctl -u postgresql -n 30 --no-pager
# LOG:  invalid value for parameter "shared_buffers": "512XB"
# FATAL:  configuration file "/etc/postgresql/15/main/conf.d/95-tuning.conf" contains errors

Как починить. Откатить именно сломанную строку и не трогать соседние файлы. В кейсе рядом лежит 98-backup.conf с архивацией WAL, и тот, кто «для надёжности» чистит весь conf.d, теряет возможность восстановить базу на момент времени.

sudo sed -i 's/^shared_buffers/# &/' /etc/postgresql/15/main/conf.d/95-tuning.conf
sudo systemctl restart postgresql
pg_isready -h 127.0.0.1 && curl -i http://127.0.0.1:8000/health

Чтобы не повторилось. Проверять конфиг до рестарта, пока процесс ещё жив. У PostgreSQL для этого есть представление pg_file_settings: оно читает файлы заново и показывает ошибки, ничего не применяя.

SELECT sourcefile, sourceline, name, error
FROM pg_file_settings WHERE error IS NOT NULL;

У остальных свои проверки: nginx -t, sshd -t, haproxy -c -f, apachectl configtest, systemd-analyze verify. И там, где можно, предпочитать reload: nginx с битым конфигом просто не применит его и продолжит работать на старом. Параметры, которые требуют рестарта (shared_buffers, shared_preload_libraries, wal_level), стоит выкатывать отдельно от приложения и сначала на реплику.

2. OOM Killer: перезапущенный сервис убьют снова

Как выглядит. Критичные сервисы падают сами по себе, память на дашборде — 99%, в журнале ядра Out of memory: Killed process ... (api-server). Перезапускаешь API — через пару минут его снова нет.

Что сделает рестарт. OOM killer убивает не виновника, а процесс с наибольшим oom_score. Часто это как раз большой и полезный API, а утечка живёт в соседнем сервисе, который утром выкатили без лимита памяти. Перезапущенный API снова занимает свою память, сосед продолжает течь, и ядро выбирает ту же жертву.

Убить виновника тоже не выход: в кейсе его поднимает watchdog, в жизни — systemd с Restart=always. killпомогает ровно на то время, пока утечка набирает память заново.

Как найти. Кто ест память и кого убивало ядро:

free -m
ps aux --sort=-%mem | head
journalctl -k | grep -i 'killed process'   # или dmesg -T | grep -i oom
cat /proc/$(pgrep -f api-server)/oom_score

Если в списке убитых один сервис, а первым в ps другой — вы нашли и жертву, и виновника.

Как починить. Ограничить виновника, а не только убить его. С лимитом при утечке OOM killer сработает внутри его cgroup и убьёт только его, а API продолжит работать. Рестарт здесь тоже есть, но после лимита, а не вместо него:

sudo mkdir -p /etc/systemd/system/analytics_worker.service.d
printf '[Service]\nMemoryMax=200M\n' | \
  sudo tee /etc/systemd/system/analytics_worker.service.d/limits.conf
sudo systemctl daemon-reload && sudo systemctl restart analytics_worker

Добавить swap — соблазнительный вариант, но он лишь растягивает агонию: сервер начинает тормозить вместо того, чтобы упасть. А саму утечку отдать разработчикам с данными: график памяти, время до OOM. Лимит — ремень безопасности, а не исправление.

Чтобы не повторилось.

  • Лимит памяти у каждого сервиса: MemoryMax= в systemd, resources.limits.memory в Kubernetes. Утечка одного не должна ронять соседей.

  • Защита критичных процессов: OOMScoreAdjust=-500 у API и базы, чтобы ядро выбирало жертву правильно.

  • Алерт на сами события OOM (node_vmstat_oom_kill в Prometheus) и на рост памяти процесса, а не только на падение сервиса.

3. Retry storm: после рестарта все клиенты приходят разом

Как выглядит. Ночью API сначала замедляется, потом сыплет ошибками. Соединений к PostgreSQL всё больше, вместе с ними растут CPU и память API, лог ест диск, а через пару минут падает сама база. На дашборде всё красное сразу, и непонятно, что причина, а что следствие.

Что сделает рестарт. Недавно таймаут подключения к базе подняли до 300 секунд. Запросы не падают быстро, а висят и копятся. Рестарт API сбрасывает висящие соединения, и через минуту всё повторяется.

Хуже всего — момент, когда базу всё‑таки поднимают. Перезапущенный API с бесконечными мгновенными повторами (DB_MAX_RETRIES=0, DB_RETRY_BACKOFF_SECONDS=0) и все его клиенты приходят разом и снова кладут едва поднявшуюся базу. Это и есть retry storm: короткий сбой превращается в DDoS собственной базы, а каждый рестарт запускает его заново.

Как найти. Отделить следствия от причины. Соединения, CPU, память и диск растут из‑за одного: запросы висят. Значит, смотреть нужно на таймауты и повторы:

ss -tan state syn-sent | wc -l                 # подключения, зависшие на старте
sudo -u postgres psql -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY state"
systemctl status api                           # откуда читается конфиг
grep -E 'TIMEOUT|RETRIES|BACKOFF' /opt/api/config.ini

Как починить. По звеньям и с проверкой после каждого. Короткий таймаут — единицы секунд: быстрая ошибка лучше висящего запроса. Повторов — не больше трёх и с паузой. И только потом рестарт:

sed -i 's/^DB_CONNECT_TIMEOUT=.*/DB_CONNECT_TIMEOUT=5/;
        s/^DB_MAX_RETRIES=.*/DB_MAX_RETRIES=3/;
        s/^DB_RETRY_BACKOFF_SECONDS=.*/DB_RETRY_BACKOFF_SECONDS=2/' /opt/api/config.ini
systemctl restart api

В коде правильный повтор выглядит так: пауза растёт экспоненциально и получает случайный разброс (jitter), чтобы тысяча клиентов не пришла в одну и ту же секунду.

for attempt in range(3):
    try:
        return connect(timeout=5)
    except OperationalError:
        time.sleep(min(30, 2 ** attempt) + random.uniform(0, 1))
raise DatabaseUnavailable()

Чтобы не повторилось.

  • Таймауты и retry‑политики проходят ревью, как код: у каждого значения есть обоснование.

  • Circuit breaker и пул соединений с лимитом (PgBouncer): при проблемах с базой приложение быстро отказывает, а не душит её.

  • Алерты на число соединений к базе и на latency API: каскад виден задолго до падения базы.

  • Нагрузочный тест «что будет, если база ответит за 10 секунд?» — лучше узнать на стенде, чем ночью на проде.

4. CrashLoopBackOff: Kubernetes уже перезапускает за вас

Как выглядит. Пятница, вечер, после обновления конфигурации. kubectl get pods показывает CrashLoopBackOff, счётчик RESTARTS растёт, сервис недоступен.

Что сделает рестарт. Ничего нового: Kubernetes и так перезапускает контейнер, с растущей паузой до пяти минут. Именно эта пауза и называется BackOff. kubectl delete pod или kubectl rollout restart создадут новые поды с той же конфигурацией, и они упадут так же.

У rollout restart есть и побочный эффект. Он меняет шаблон пода (аннотация restartedAt) и создаёт новую ревизию. После него простой rollout undo вернёт не последнюю рабочую ревизию, а ту же сломанную — придётся искать номер в истории.

Как найти. Главное — лог предыдущего, упавшего запуска: текущий часто ещё пуст.

kubectl get pods
kubectl describe pod <pod>              # Last State, Exit Code, события
kubectl logs <pod> --previous           # FATAL: Configuration error ...
kubectl rollout history deployment/api

Как починить. Сначала понять, как именно сломали конфигурацию. Если плохим rollout, откатить его:

kubectl rollout undo deployment/api            # или --to-revision=N
kubectl wait --for=condition=Ready pod -l app=api --timeout=60s

Если ConfigMap правили руками через kubectl edit, откатывать нечего: у ConfigMap нет ревизий. Нужно вернуть правильное значение через kubectl apply из git или kubectl patch. А вот уже после этого rollout restart уместен: переменные из ConfigMap под читает только при старте.

Чтобы не повторилось.

  • Конфигурация только через git и CI (Argo CD, Flux), а ручной kubectl edit на проде закрыт через RBAC.

  • Неизменяемые ConfigMap с хешем в имени (configMapGenerator в kustomize): новая конфигурация — это новый rollout, и для неё работает rollout undo.

  • readinessProbe и RollingUpdate с maxUnavailable: 0: плохая ревизия не заменит рабочие поды, rollout просто застрянет.

  • Алерт на рост kube_pod_container_status_restarts_total и на поды не в Ready.

5. Диск забит логом: rm не помогает, а рестарт помогает на полчаса

Как выглядит. Приложение пишет No space left on device, сервис не может записать даже heartbeat, df -h /var/log показывает 100%.

Что сделают rm и рестарт. Первым делом удаляют самый большой лог. Место не освобождается: процесс держит файл открытым, и rm убрал только имя. Тогда перезапускают сервис — и место наконец возвращается. Всё выглядит починенным.

Но причина никуда не делась: на проде включён debug, ротации нет, и через полчаса диск снова полон. А вместе с файлом уплыл лог, по которому можно было понять, что произошло. В спешке легко снести заодно и auth.log — аудит, который трогать нельзя.

Как найти.

df -h /var/log
sudo du -sh /var/log/* | sort -rh | head
sudo lsof +L1            # удалённые файлы, которые ещё держит какой-то процесс

Если df и du расходятся на гигабайты, почти наверняка кто‑то уже сделал rm открытого файла, и lsof +L1покажет кто и что.

Как починить. Очистить файл, не удаляя его, — процесс продолжит писать в тот же дескриптор с начала. Если файл уже удалён, его можно очистить через /proc без всякого рестарта:

sudo truncate -s 0 /var/log/app.log
# уже удалённый: PID и номер дескриптора — из вывода lsof +L1
sudo truncate -s 0 /proc/<PID>/fd/<FD>

Потом выключить debug и включить ротацию. copytruncate нужен для приложений, которые не умеют заново открывать лог по сигналу:

/var/log/app.log {
    daily
    rotate 7
    compress
    copytruncate
    missingok
    notifempty
}

Проверить конфиг без изменений на диске можно так: sudo logrotate -d /etc/logrotate.d/app.

Чтобы не повторилось.

  • Ротация для каждого лога при создании сервиса, а не после инцидента: logrotate или journald с SystemMaxUse=.

  • Отдельный раздел или лимит под логи, чтобы они не забивали корень и данные.

  • Debug на проде только временно, а лучше точечно: для одного запроса или пользователя.

  • Алерт с прогнозом «диск кончится через 4 часа» полезнее, чем «диск кончился»:

predict_linear(node_filesystem_avail_bytes{mountpoint="/var/log"}[6h], 4 * 3600) < 0

Так что, не перезапускать?

Перезапускать можно, но в правильный момент. Во всех пяти случаях рестарт в итоге есть, только стоит он последним шагом, а не первым. Рестарт — это способ применить починку, а не сама починка.

Порядок, который работает почти всегда:

  1. Сохранить улики: journalctl -u, лог предыдущего запуска, снимок памяти или потоков (py-spy dump, jstack). После рестарта их уже не будет.

  2. Ответить на вопрос «что изменилось»: деплой, конфиг, новый сервис по соседству, рост нагрузки.

  3. Починить причину и проверить починку до применения: nginx -t, pg_file_settings, logrotate -d.

  4. Перезапустить — и проверить сервис так, как его видит пользователь, а не только systemctl status.

А ваш самый запомнившийся рестарт, после которого стало только хуже? Расскажите в комментариях.

Комментарии (1)


  1. Vindicar
    27.10.2026 17:19

    В ленте:

    «Просто перезапусти»: 5 инцидентов, где рестарт делает только хуже · systemctl restart не поможет: 5 причин, почему прод не поднимается · Рестарт — не починка: 5 инцидентов, которые он добивает.

    Вы вообще читаете что постите, или это прямая кишка "нейронка-хабр"?

    Судя по 6(!) статьям за три дня с инвайта, скорее второе.