Три часа ночи, алерт: 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
Так что, не перезапускать?
Перезапускать можно, но в правильный момент. Во всех пяти случаях рестарт в итоге есть, только стоит он последним шагом, а не первым. Рестарт — это способ применить починку, а не сама починка.
Порядок, который работает почти всегда:
Сохранить улики:
journalctl -u, лог предыдущего запуска, снимок памяти или потоков (py-spy dump,jstack). После рестарта их уже не будет.Ответить на вопрос «что изменилось»: деплой, конфиг, новый сервис по соседству, рост нагрузки.
Починить причину и проверить починку до применения:
nginx -t,pg_file_settings,logrotate -d.Перезапустить — и проверить сервис так, как его видит пользователь, а не только
systemctl status.
А ваш самый запомнившийся рестарт, после которого стало только хуже? Расскажите в комментариях.
Vindicar
В ленте:
Вы вообще читаете что постите, или это прямая кишка "нейронка-хабр"?
Судя по 6(!) статьям за три дня с инвайта, скорее второе.