feat(monitoring): алерты на недоступность БД и исчерпание пулов + дашборд
DatabaseUnavailable (vidconf_db_up == 0, for: 30s, critical) и DbConnectionPoolNearExhaustion/RedisConnectionPoolNearExhaustion (занято > 80% дольше минуты, warning) — сигнал оператору, не автолечение: healthcheck backend'а по решению оператора остаётся мягким, рестарт при недоступной БД оборвал бы WS у всех, кто в конференциях. Дашборд Grafana «БД и пулы соединений» — занятость пулов на графике, следующий нагрузочный тест будут смотреть глазами.
This commit is contained in:
@@ -61,6 +61,89 @@ groups:
|
|||||||
# см. также алерт QueueGrowing). Проверить
|
# см. также алерт QueueGrowing). Проверить
|
||||||
# `docker compose ps llm` / `llm-gpu` и `docker compose logs llm`.
|
# `docker compose ps llm` / `llm-gpu` и `docker compose logs llm`.
|
||||||
|
|
||||||
|
# Состояние БД и пулов соединений (сессия 33, разбор инцидента 07.08 —
|
||||||
|
# `.forcc/session-results/32-loadtest-07-08-debug.md`). `/api/health`
|
||||||
|
# отдавал 200 с `db: false` во время отказа — Prometheus его не скрейпит и
|
||||||
|
# не умеет разобрать JSON-тело, поэтому оба сигнала строятся на метриках
|
||||||
|
# `backend/api/metrics.py`, которые читаются вне основного пула.
|
||||||
|
- name: vidconf-db
|
||||||
|
rules:
|
||||||
|
# `vidconf_db_up` — отдельное соединение вне основного пула
|
||||||
|
# (`core/db.py::check_db_up`), поэтому 0 означает именно «БД не
|
||||||
|
# отвечает», а не «пул занят» (для второго см. DbConnectionPoolNearExhaustion
|
||||||
|
# ниже — раздельные метрики нарочно, см. «Главное требование» промпта
|
||||||
|
# сессии 33). `for: 30s` — два цикла скрейпа (`scrape_interval: 15s`),
|
||||||
|
# чтобы не среагировать на одиночный неудачный `connect()` (сеть/GC-пауза),
|
||||||
|
# но не тянуть с сигналом дольше: это самый критичный алерт в проекте.
|
||||||
|
- alert: DatabaseUnavailable
|
||||||
|
expr: vidconf_db_up == 0
|
||||||
|
for: 30s
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "БД недоступна"
|
||||||
|
description: >-
|
||||||
|
vidconf_db_up == 0 дольше 30 секунд — backend не может открыть
|
||||||
|
отдельное (вне основного пула) соединение с Postgres. НЕ значит
|
||||||
|
автоматически «нужен рестарт контейнера» — по решению оператора
|
||||||
|
от 09.08 healthcheck backend'а остаётся мягким (не хардфейлится
|
||||||
|
на недоступной БД — рестарт-петля в разгар инцидента оборвала бы
|
||||||
|
WS у всех, кто в конференциях), это сигнал оператору, не
|
||||||
|
автолечение. Смотреть
|
||||||
|
`docker compose ps postgres`, `docker compose logs postgres`,
|
||||||
|
`pg_isready`.
|
||||||
|
|
||||||
|
# Раннее предупреждение — тот самый сигнал, которого не хватило
|
||||||
|
# 07.08: пул заполнялся постепенно (idle in transaction 3→8→16→26→35→
|
||||||
|
# 39→40 участников комнаты, см. session 32), а `up{job="backend"}`
|
||||||
|
# ничего не показывал, потому что backend отвечал исправно вплоть до
|
||||||
|
# самого потолка. Порог 80% — предложение из промпта сессии 33,
|
||||||
|
# `for: 1m` — фильтр от секундных всплесков (короткий пик параллельных
|
||||||
|
# запросов рассасывается за секунды, устойчивый рост участников
|
||||||
|
# комнаты — нет). На нагрузочном тесте 07.08 от пересечения 80% до
|
||||||
|
# исчерпания пула прошло по грубой оценке меньше двух минут — порог
|
||||||
|
# НЕ даёт большого запаса и это осознанный компромисс, а не идеал:
|
||||||
|
# цель — успеть до 500-х у пользователей, а не за много минут
|
||||||
|
# заранее. Перепроверить оба числа на следующем нагрузочном тесте
|
||||||
|
# (см. .forcc/session-results/33-db-health-alert.md) и подстроить,
|
||||||
|
# если реальный запас окажется у́же ожидаемого.
|
||||||
|
- alert: DbConnectionPoolNearExhaustion
|
||||||
|
expr: >-
|
||||||
|
(vidconf_db_pool_checked_out
|
||||||
|
/ (vidconf_db_pool_size + vidconf_db_pool_max_overflow)) * 100 > 80
|
||||||
|
for: 1m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Основной пул соединений с БД близок к исчерпанию"
|
||||||
|
description: >-
|
||||||
|
Занято {{ $value | printf "%.0f" }}% основного пула БД дольше
|
||||||
|
минуты (порог 80%). Частая причина в этом проекте — долгоживущие
|
||||||
|
WS-подключения комнат (`api/chat.py`) держат соединение на
|
||||||
|
каждого сидящего в конференции; смотреть
|
||||||
|
`vidconf_db_pool_checked_out` и число открытых WS чата в логах,
|
||||||
|
не только текущую HTTP-нагрузку.
|
||||||
|
|
||||||
|
# Тот же класс отказа, что у пула БД (см. выше), только пул Redis —
|
||||||
|
# закрыт в 0.0.31 (`451c18e`) заданием явного max_connections, но без
|
||||||
|
# метрики занятости прошлый потолок нашёлся только руками на
|
||||||
|
# нагрузочном тесте. Бонус к задаче сессии 33 («потолки в этом
|
||||||
|
# проекте стоят лесенкой»), не отдельно запрошен промптом — пороги
|
||||||
|
# взяты по аналогии с пулом БД, не проверялись отдельным нагрузочным
|
||||||
|
# тестом именно на Redis.
|
||||||
|
- alert: RedisConnectionPoolNearExhaustion
|
||||||
|
expr: (vidconf_redis_pool_in_use / vidconf_redis_pool_max_connections) * 100 > 80
|
||||||
|
for: 1m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Пул соединений Redis близок к исчерпанию"
|
||||||
|
description: >-
|
||||||
|
Занято {{ $value | printf "%.0f" }}% пула Redis дольше минуты
|
||||||
|
(порог 80%). Каждое WS-подключение комнаты держит собственную
|
||||||
|
pub/sub-подписку из этого же пула — смотреть число открытых WS
|
||||||
|
чата, не только команды Celery/кэша.
|
||||||
|
|
||||||
# Железо хоста (job `node` — node-exporter). Пороги подобраны под
|
# Железо хоста (job `node` — node-exporter). Пороги подобраны под
|
||||||
# конкретный сервер 1gb: 8 ГБ RAM, 4 CPU, 50 ГБ диска — если сервер
|
# конкретный сервер 1gb: 8 ГБ RAM, 4 CPU, 50 ГБ диска — если сервер
|
||||||
# сменится, пересчитать.
|
# сменится, пересчитать.
|
||||||
|
|||||||
173
deploy/monitoring/grafana/dashboards/db-pool.json
Normal file
173
deploy/monitoring/grafana/dashboards/db-pool.json
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"title": "БД и пулы соединений",
|
||||||
|
"description": "Доступность БД (vidconf_db_up) и занятость основных пулов (SQLAlchemy/БД, Redis) — метрики читаются вне самих пулов, доступны и при их исчерпании (сессия 33, разбор инцидента 07.08 — .forcc/session-results/32-loadtest-07-08-debug.md). Пороги алертов см. deploy/monitoring/alerts.yml (группа vidconf-db).",
|
||||||
|
"uid": "vidconf-db-pool",
|
||||||
|
"editable": false,
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"time": { "from": "now-1h", "to": "now" },
|
||||||
|
"refresh": "10s",
|
||||||
|
"tags": ["vidconf", "db", "pool"],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "БД доступна",
|
||||||
|
"description": "vidconf_db_up — отдельное соединение вне основного пула (core/db.py::check_db_up). Алерт DatabaseUnavailable, for: 30s.",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"mappings": [
|
||||||
|
{ "type": "value", "options": { "0": { "text": "DOWN", "color": "red" }, "1": { "text": "UP", "color": "green" } } }
|
||||||
|
],
|
||||||
|
"thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] }
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "vidconf_db_up", "refId": "A" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "Занятость пула БД сейчас, %",
|
||||||
|
"description": "vidconf_db_pool_checked_out / (vidconf_db_pool_size + vidconf_db_pool_max_overflow) * 100. Порог алерта DbConnectionPoolNearExhaustion — 80% дольше минуты.",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "percent",
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "orange", "value": 60 }, { "color": "red", "value": 80 }] }
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"expr": "(vidconf_db_pool_checked_out / (vidconf_db_pool_size + vidconf_db_pool_max_overflow)) * 100",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Занятость пула Redis сейчас, %",
|
||||||
|
"description": "vidconf_redis_pool_in_use / vidconf_redis_pool_max_connections * 100. Порог алерта RedisConnectionPoolNearExhaustion — 80% дольше минуты.",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "percent",
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "orange", "value": 60 }, { "color": "red", "value": 80 }] }
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"expr": "(vidconf_redis_pool_in_use / vidconf_redis_pool_max_connections) * 100",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Активных алертов группы vidconf-db",
|
||||||
|
"description": "ALERTS{alertname=~\"DatabaseUnavailable|.*PoolNearExhaustion\", alertstate=\"firing\"} — снимок того, что прямо сейчас видит Alertmanager/страница Alerts.",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }] }
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"expr": "count(ALERTS{alertname=~\"DatabaseUnavailable|.*PoolNearExhaustion\", alertstate=\"firing\"}) OR on() vector(0)",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "Занятость пула БД (соединений)",
|
||||||
|
"description": "vidconf_db_pool_checked_out на фоне вместимости (size + max_overflow) — эта картина должна расти под нагрузочным тестом до срабатывания алерта. Ранний сигнал: в инциденте 07.08 занятость росла постепенно по мере входа участников в комнату, а не рывком от общей HTTP-нагрузки.",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10 } },
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "vidconf_db_pool_checked_out", "legendFormat": "занято", "refId": "A" },
|
||||||
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "vidconf_db_pool_size + vidconf_db_pool_max_overflow", "legendFormat": "вместимость (size+overflow)", "refId": "B" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"title": "Занятость пула Redis (соединений)",
|
||||||
|
"description": "vidconf_redis_pool_in_use на фоне vidconf_redis_pool_max_connections. Второй потолок того же класса, что у БД (закрыт в 0.0.31, redis-py 8 сменил дефолт max_connections на 100).",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10 } },
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "vidconf_redis_pool_in_use", "legendFormat": "занято", "refId": "A" },
|
||||||
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "vidconf_redis_pool_max_connections", "legendFormat": "вместимость", "refId": "B" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"title": "БД доступна во времени",
|
||||||
|
"description": "vidconf_db_up как временной ряд — удобно видеть провал целиком (начало/длительность отказа), не только текущее состояние.",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 6, "w": 12, "x": 0, "y": 12 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"custom": { "drawStyle": "line", "fillOpacity": 20, "lineInterpolation": "stepAfter" },
|
||||||
|
"mappings": [
|
||||||
|
{ "type": "value", "options": { "0": { "text": "DOWN" }, "1": { "text": "UP" } } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "vidconf_db_up", "legendFormat": "db_up", "refId": "A" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"title": "Сеансы failed / очереди Celery (для сверки с общей нагрузкой)",
|
||||||
|
"description": "Тот же контекст, что на дашборде «Пайплайны пост-обработки» — здесь рядом с пулами, чтобы не переключаться между дашбордами при разборе инцидента.",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 6, "w": 12, "x": 12, "y": 12 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "custom": { "drawStyle": "line", "fillOpacity": 5 } },
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "vidconf_pipeline_sessions{status=\"failed\"}", "legendFormat": "сеансов failed", "refId": "A" },
|
||||||
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum(vidconf_celery_queue_depth)", "legendFormat": "глубина очередей (сумма)", "refId": "B" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user