Кейс из практики: диагностика утечки соединений с PostgreSQL через метрики Prometheus, баг в асинхронных коллбэках при форке воркера Minion.
Как я нашёл утечку соединений PostgreSQL через обычный график в Grafana
Система падала примерно раз в шесть-восемь часов. Не полностью, но достаточно заметно: часть запросов начинала висеть, потом сервис переставал принимать новые соединения, потом его перезапускали, и всё повторялось заново. Классическая картина для продакшена, где что-то утекает, а никто пока не знает, что именно.
Логи ничего не говорили. Ошибки были общие, в духе «не удалось получить соединение с базой», что в общем-то и так понятно, когда соединений не осталось. Полезной информации в этом примерно столько же, сколько в сообщении «что-то сломалось».
Я открыл Prometheus, потому что там уже были метрики по количеству активных соединений с PostgreSQL, собранные стандартным экспортером. И увидел ровно то, чего не хотел увидеть: график не скакал туда-сюда, как это обычно бывает под нагрузкой, а полз вверх линейно. Почти идеальная прямая. Количество соединений росло со временем, а не с нагрузкой, и это уже не про трафик, это про утечку.
Дальше была довольно муторная часть, потому что линейный рост сам по себе не говорит, откуда именно течёт. Система была написана на Perl поверх Minion, это очередь фоновых задач с воркерами, которые периодически форкаются заново. У каждого воркера свой пул соединений с базой, и в теории при форке новый процесс должен был либо унаследовать чистое состояние, либо корректно переоткрыть соединения.
На практике внутри асинхронных коллбэков оставались ссылки на объекты соединений из родительского процесса. Форк в Unix копирует память процесса, включая файловые дескрипторы, и если коллбэк держал где-то в замыкании старый хендл соединения, этот хендл формально существовал и после форка, просто уже ни для чего не использовался и никогда не закрывался. Новый воркер открывал свои соединения как положено, а старые просто повисали мёртвым грузом с той стороны PostgreSQL, которая честно считала их активными, пока не истечёт таймаут, которого в конфигурации по сути не было.
Фикс был не длинным: явное закрытие соединений перед форком и правильная очистка замыканий в коллбэках, которые раньше неявно тащили за собой лишние ссылки. Самым полезным оказался не сам фикс, а то, что я настроил алертинг именно на эту метрику, число активных соединений к базе, с порогом на линейный рост, а не только на абсолютное значение. После фикса прошло четырнадцать месяцев без единого инцидента такого рода, и это не потому что баг был единственным возможным источником подобных проблем, а потому что теперь есть сигнал, который сработает раньше, чем база откажет принимать новые соединения.
Мораль тут довольно скучная, но от этого не менее верная. Когда система падает с равными интервалами, а не хаотично, это почти всегда намёк на что-то, что накапливается, а не на что-то, что происходит случайно. И метрики, которые вы уже собираете для совсем других целей, часто оказываются единственным способом увидеть это накопление до того, как оно превратится в инцидент.
Похожие баги в бизнес-логике и инфраструктуре я нахожу при ручном аудите кода, не только через метрики в проде.