- PVSM.RU - https://www.pvsm.ru -
Представьте: вы открываете Grafana в три часа ночи по алерту. На экране — 30 дашбордов, сотни графиков, и везде мигает жёлтым. CPU 60%, connections 50%, replication lag 500ms, bloat растёт, dead tuples накапливаются. Ни один показатель не пробил красную черту — но что-то явно идёт не так. База тормозит. Пользователи жалуются. А вы стоите перед этим океаном данных и пытаетесь понять: это уже пожар или ещё можно ждать до утра?
Это не гипотетическая история. Именно в такие моменты становится ясно: мониторинг PostgreSQL сломан на уровне концепции. Не потому что метрик мало — их избыток. Проблема в том, что нет единого ответа на вопрос «база здорова?».
У человека есть пульс и давление — два числа, по которым врач за секунду оценивает состояние пациента. У самолёта есть искусственный горизонт. У сайта — Apdex. А у PostgreSQL… 150 метрик в pg_stat_* и ощущение, что вы смотрите на доску с приборами без стрелок.
В этой статье — как мы пришли к идее Health Score, как устроена формула под капотом, и почему это не просто «ещё один дашборд».
Есть такой эффект: чем больше информации на экране, тем меньше внимания каждый элемент получает. В авиации это называют «стеклянной кабиной» — когда пилот смотрит на дисплей, но не видит важного показателя среди десятков других. В мониторинге это называют dashboard fatigue.
Типичный стек мониторинга PostgreSQL выглядит так:
# Что обычно разворачивают
postgres_exporter → Prometheus → Grafana
# Что получают в итоге
- Dashboard: Overview (20 графиков)
- Dashboard: Connections (15 графиков)
- Dashboard: Replication (12 графиков)
- Dashboard: Vacuum & Bloat (18 графиков)
- Dashboard: Query Performance (25 графиков)
- Dashboard: Locks (10 графиков)
# ... и ещё несколько
Через три месяца DBA открывает только первый дашборд. Через полгода — только когда что-то сломалось. Алерты выставлены по пороговым значениям, но пороги либо слишком консервативные (ложные срабатывания каждый день), либо слишком мягкие (алерт приходит, когда база уже умирает).
Вот реальный сценарий:
-- Смотрим состояние соединений
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
-- state | count
-- --------------------+-------
-- active | 12
-- idle | 45
-- idle in transaction| 38
-- (null) | 5
По отдельности: connections заняты на 50% от max_connections — норма. Но 38 соединений в состоянии idle in transaction — это бомба замедленного действия. Они держат блокировки, мешают autovacuum, накапливают bloat. Ни один метрик-алерт не сработал, потому что каждый смотрел на своё число.
Другой пример — классическая комбинация, которая медленно убивает базу:
|
Метрика |
Значение |
Алерт? |
|---|---|---|
|
CPU |
60% |
Нет (порог 80%) |
|
Connections |
50% |
Нет (порог 90%) |
|
Idle in transaction |
40% от active |
Нет (не мониторят) |
|
Table bloat |
30% |
Нет (порог 50%) |
|
Last vacuum |
3 дня назад |
Нет (порог 7 дней) |
|
Cache hit ratio |
94% |
Нет (порог 90%) |
Каждый показатель — жёлтый. Ни один не красный. Но в совокупности — это серьёзная проблема, которая через день-два выльется в инцидент.
В APM давно решили эту задачу. Apdex (Application Performance Index) — число от 0 до 1, которое агрегирует время ответа приложения. Не нужно смотреть на p50/p95/p99 и думать, хорошо ли это — Apdex сразу говорит «0.94: всё хорошо» или «0.62: проблема».
В медицине есть Early Warning Score (EWS) — система, которая объединяет пульс, давление, температуру, частоту дыхания и уровень кислорода в один балл. Врач в приёмном покое за три секунды понимает: пациент стабилен или нужна реанимация.
Нам нужно то же самое для PostgreSQL. Единое число, которое:
Учитывает несколько категорий метрик с разными весами
Реагирует на опасные комбинации, а не только на выход отдельных метрик за порог
Позволяет сравнивать состояние базы во времени («вчера было 87, сегодня 71 — что случилось?»)
Понятно без экспертизы — дежурный DevOps без глубоких знаний PostgreSQL может принять правильное решение
В основе — взвешенная сумма штрафов по категориям:
Health Score = 100 - Σ(penalty_i × weight_i)
Каждая категория имеет свой вес и набор метрик, из которых вычисляется штраф от 0 до 100. Итоговый Health Score — от 0 (база мертва) до 100 (идеальное состояние).
Мы специально не делаем формулу точной до последнего знака публично — иначе DBA начнут «играть» в неё, отключая метрики для улучшения числа. Но общая структура такая:
SELECT
count(*) FILTER (WHERE state = 'active') AS active_connections,
count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_transaction,
count(*) FILTER (WHERE state = 'idle in transaction (aborted)') AS idle_in_transaction_aborted,
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_connections,
now() - min(xact_start) AS longest_transaction_age
FROM pg_stat_activity
WHERE pid != pg_backend_pid();
Штраф в этой категории растёт нелинейно: первые 60% заполнения connection pool дают небольшой штраф, после 80% — экспоненциальный рост. Отдельно штрафуем за idle in transaction старше 30 секунд — это почти всегда симптом проблемы в приложении.
Самый важный блок по весу — то, что пользователи чувствуют напрямую:
-- Cache hit ratio
SELECT
round(
100.0 * sum(heap_blks_hit) /
nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0),
2
) AS cache_hit_ratio
FROM pg_statio_user_tables;
-- Slow queries (из pg_stat_statements)
SELECT
count(*) AS slow_queries_count,
round(avg(mean_exec_time)::numeric, 2) AS avg_exec_ms
FROM pg_stat_statements
WHERE mean_exec_time > 1000 -- медленнее 1 секунды
AND calls > 10;
Cache hit ratio ниже 95% — первый жёлтый флаг. Ниже 90% — серьёзный штраф. Среднее время запроса растёт? Штраф пропорционален отклонению от базового уровня (храним историю, умеем считать аномалии).
-- Dead tuples ratio по таблицам
SELECT
schemaname,
relname,
n_dead_tup,
n_live_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_stat_user_tables
WHERE n_live_tup + n_dead_tup > 10000
ORDER BY dead_ratio DESC
LIMIT 20;
Bloat оцениваем через pgstattuple (где доступен) или через эвристику по соотношению relpages к реальному количеству данных. Disk usage берём через pg_database_size() и сравниваем с лимитом файловой системы.
SELECT
application_name,
state,
sent_lsn,
replay_lsn,
(sent_lsn - replay_lsn) AS lag_bytes,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
Если реплик нет — категория не штрафует (не актуально). Если replication lag > 1MB или > 5 секунд — начинается штраф. Если реплика отвалилась совсем — максимальный штраф в этой категории.
-- Возраст XID (критично для wraparound)
SELECT
datname,
age(datfrozenxid) AS xid_age,
2147483648 - age(datfrozenxid) AS xids_until_wraparound
FROM pg_database
ORDER BY xid_age DESC;
-- Последний vacuum по таблицам
SELECT
schemaname,
relname,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze,
now() - greatest(last_vacuum, last_autovacuum) AS vacuum_age
FROM pg_stat_user_tables
WHERE n_live_tup > 10000
ORDER BY vacuum_age DESC NULLS FIRST
LIMIT 20;
XID age близкий к 2 миллиардам — это экстренная ситуация (база уйдёт в read-only из-за wraparound). Начинаем агрессивно штрафовать с 1.5 миллиарда. Vacuum, который не работал больше недели на активных таблицах — жёлтый флаг.
Мы перебирали разные варианты весов, прежде чем остановиться на текущих. Несколько принципов:
Performance (0.25) — самый высокий вес, потому что именно это чувствуют пользователи. Медленные запросы и плохой cache hit ratio — это прямой пользовательский опыт. Всё остальное — риски, которые пока ещё не ударили по пользователям.
Connections и Maintenance (по 0.20) — второй уровень. Переполнение connection pool убивает базу быстро. Запущенный bloat и остановившийся vacuum убивают медленно, но верно.
Replication (0.15) — важно, но не всегда применимо. Если реплик нет — этот вес перераспределяется на остальные категории.
Мы также рассматривали отдельные категории для Locks и WAL, но решили не дробить сильнее — чем проще модель, тем лучше она воспринимается.
Score 95+ — можно спать спокойно. Возможно, есть мелкие замечания уровня INFO, но ничего критичного.
Score 70–94 — есть одна-две жёлтые зоны. Например, bloat на нескольких таблицах растёт быстрее обычного, или replication lag периодически достигает 500ms. Требует внимания в ближайшие дни, не сейчас.
Score 40–69 — несколько метрик в плохом состоянии одновременно. Типичный пример: много dead tuples + connection pool на 85% + vacuum не работал 3 дня. Нужно разобраться сегодня.
Score < 40 — срочно. Возможно, сочетание: emergency autovacuum, replication сломана, disk 90%+.
Health Score без объяснений — это как лампочка «Check Engine» в машине. Число загорелось, но что делать?
Поэтому рядом с числом имеет смысл выводить список конкретных проблем с приоритетами и рекомендациями:
Health Score: 61 / 100
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ HIGH | Table `public.orders` — 15.2M dead tuples
| Last autovacuum: 4 days, 3 hours ago
| Dead ratio: 42%
|
| Рекомендация: Запустите VACUUM ANALYZE вручную.
| Если проблема повторяется — проверьте настройки
| autovacuum для этой таблицы:
| ALTER TABLE orders SET (
| autovacuum_vacuum_scale_factor = 0.01,
| autovacuum_vacuum_cost_delay = 2
| );
⚠️ MEDIUM | Replication lag: 820ms (replica: standby-01)
| Avg за последний час: 650ms
|
| Рекомендация: Проверьте нагрузку на реплику.
| Возможные причины: тяжёлые SELECT-запросы на реплике,
| wal_receiver_timeout, сетевые задержки.
ℹ️ LOW | Cache hit ratio: 94.2% (рекомендуется > 99%)
| Таблицы с наибольшим количеством disk reads:
| public.events: 18k reads/min
| public.logs: 12k reads/min
|
| Рекомендация: Увеличьте shared_buffers.
| Текущее значение: 128MB
| Рекомендуемое: 25% от RAM (для 16GB → 4GB)
Каждое правило — это не просто порог, а знание. Когда мы видим высокий dead tuples ratio + давний vacuum, мы знаем: нужен VACUUM, и конкретно понимаем, почему autovacuum мог пропустить таблицу.
-- Проверка, почему autovacuum пропускает таблицу
SELECT
relname,
n_dead_tup,
n_live_tup,
n_live_tup * current_setting('autovacuum_vacuum_scale_factor')::float
+ current_setting('autovacuum_vacuum_threshold')::float AS autovacuum_threshold,
n_dead_tup > (
n_live_tup * current_setting('autovacuum_vacuum_scale_factor')::float
+ current_setting('autovacuum_vacuum_threshold')::float
) AS vacuum_due
FROM pg_stat_user_tables
WHERE relname = 'orders';
Если autovacuum_vacuum_scale_factor = 0.2 (дефолт) и таблица orders содержит 50 миллионов строк — autovacuum запустится только когда мёртвых строк станет 10 миллионов. Это нормально для маленьких таблиц и катастрофично для больших.
Одна из сложностей — что «нормально» очень зависит от типа нагрузки. OLTP-база с тысячами коротких транзакций и аналитическая база с редкими тяжёлыми запросами имеют принципиально разный профиль метрик.
Решение — через базовый уровень (baseline): собираем данные первые 7 дней и выстраиваем индивидуальный профиль для каждой базы. Аномалия определяется не абсолютным порогом, а отклонением от исторического паттерна. Если ваша база обычно работает с cache hit ratio 97% и вдруг опустилась до 93% — это замечается. Если у вас аналитика и cache hit ratio исторически 82% — не штрафуем за «норму».
Отдельная ценность — временной ряд. Можно посмотреть, что происходило с базой в прошлом:
«Вчера в 14:00 Score упал с 89 до 67. Что случилось?» — открываем детали, видим: в 14:00 запустился деплой, резко выросло количество active connections, появились slow queries. Всё сходится.
«Score держится на 74 уже неделю» — значит, есть хроническая проблема, к которой привыкли, но не решили. Самое опасное состояние.
Health Score — это не замена мониторингу метрик. Grafana с её 30 дашбордами никуда не девается, и когда нужно разобраться в проблеме глубоко — детальные графики незаменимы.
Но Health Score — это первый экран, который открывает дежурный инженер. Одно число, один взгляд, одно решение: «сейчас разбираемся» или «можно идти спать».
В ближайших планах — Health Score для конкретных баз данных внутри кластера (сейчас считается на уровне инстанса), и интеграция с pg_stat_statements для более точного штрафа за медленные запросы. Также думаем над тем, чтобы пользователи могли настраивать веса под свои приоритеты — например, для read replica, где replication health важнее performance.
Если у вас есть опыт мониторинга PostgreSQL и идеи, какие метрики критически важны в вашей практике — пишите в комментариях. Всегда интересно, как другие DBA смотрят на проблему.
Автор: Leg1onary
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/postgresql/448007
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/1016288/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1016288
Нажмите здесь для печати.