Настройка мониторинга и оповещений сервера
Проектирует комплексную систему мониторинга, оповещений и дашбордов для заданной серверной инфраструктуры.
Пример запуска
Сгенерировано Claude Opus 5.5, 24 сентября 2026 г.
Значения переменных
- Серверная среда
- 12 виртуальных машин Ubuntu 22.04 LTS в Yandex Cloud: 3 узла Kubernetes (k8s v1.28, kubeadm), 3 узла PostgreSQL 15 (Patroni-кластер с одним лидером и двумя репликами), 2 узла Redis 7 (sentinel), 4 узла бэкенд-приложений (Node.js 20 + Nginx как reverse proxy). Сеть — приватный VPC, доступ по SSH через bastion-хост.
- Стек мониторинга
- Prometheus 2.51 + Alertmanager 0.27, Grafana 10, node_exporter и cadvisor для метрик хостов/контейнеров, Loki 2.9 + Promtail для логов, Blackbox exporter для внешних проверок доступности API.
- Критичные сервисы
- API интернет-магазина «ShopFlow» (Node.js REST API на порту 3000), PostgreSQL (основная БД заказов и пользователей), Redis (кэш сессий и очередь задач через BullMQ), Nginx (входной прокси и балансировка), сервис оплаты «PayGate» (интеграция с внешним платёжным шлюзом ЮKassa).
- Целевой sla
- 99.9% аптайма в месяц (допустимый даунтайм ≈ 43 минуты/месяц), время отклика API p95 < 300 мс, обработка платежей p99 < 2 с.
- Каналы оповещений
- Telegram-бот в группу дежурной команды для warning-алертов, звонок через PagerDuty (эскалация на дежурного инженера) для critical-алертов, e-mail-дайджест раз в сутки на sre-team@shopflow.example для отчёта по всем инцидентам.
Система мониторинга и оповещений для ShopFlow (Yandex Cloud, 12 ВМ)
0. Уточняющие вопросы и допущения
Вопросы. Ответы на них изменят часть порогов и конфигов:
- Где размещён стек мониторинга и есть ли точка вне Yandex Cloud? Речь об отдельной ВМ или о размещении внутри k8s. Если все проверки идут изнутри VPC, вы не заметите сбой на входе в облако (NLB, DNS, сертификат, маршрутизация).
- Что именно работает в k8s, а что на ВМ? Где запущены PayGate и воркеры BullMQ? Инструментирован ли Node.js через
prom-clientс гистограммами латентности? - Как устроены PostgreSQL и Redis? Репликация в Patroni синхронная или асинхронная, где работает DCS (etcd), делаются ли бэкапы (WAL-G или pgBackRest)? Один и тот же Redis обслуживает и сессии, и BullMQ?
Чтобы не блокировать работу, ниже приведён полный проект на явных допущениях. После ваших ответов я скорректирую отмеченные пункты.
Допущения. В тексте они помечены [A1]–[A8]:
| # | Допущение |
|---|---|
| A1 | Стек мониторинга работает на отдельной ВМ mon-01 (4 vCPU / 16 GB / 200 GB network-ssd, ru-central1-a, 10.10.0.10) внутри того же VPC. Prometheus опрашивает цели напрямую, без bastion. |
| A2 | ShopFlow API работает на app-01..04 как systemd-юнит shopflow-api.service, за локальным Nginx; перед Nginx стоит Yandex Network Load Balancer. PayGate и воркеры BullMQ работают в k8s (namespace payments). |
| A3 | Приложения экспортируют метрики через prom-client на отдельном порту 9464 (не на 3000), доступном только из VPC. |
| A4 | Patroni 3.x использует etcd на тех же трёх PG-узлах, репликация асинхронная, maximum_lag_on_failover = 1 MB (значение по умолчанию). pgbouncer нет. Бэкапы делает WAL-G в Object Storage. |
| A5 | Redis redis-01/02 работает в режиме master + replica, sentinel запущен на обоих узлах. Сессии и BullMQ живут в одном инстансе. |
| A6 | Внутренние DNS-имена имеют вид <host>.ru-central1.internal, часовой пояс команды — Europe/Moscow. |
| A7 | В дежурной ротации минимум 4 инженера, PagerDuty и Telegram-бот уже заведены. |
| A8 | Loki хранит чанки в Yandex Object Storage (storage.yandexcloud.net). |
Архитектурные риски, найденные при анализе
Эти риски сильнее влияют на SLA, чем любые алерты:
- Sentinel на двух узлах не даёт надёжного failover. При кворуме 2 и падении узла вместе с его sentinel оставшийся sentinel не наберёт большинство. Нужен третий sentinel, например на
mon-01илиapp-01. - Сессии и BullMQ в одном Redis несовместимы по политике вытеснения. Кэшу сессий подходит
allkeys-lru, а для BullMQ обязательнаnoeviction, иначе задачи (включая платёжные) молча теряются. Рекомендую разделить на два инстанса или хотя бы выставитьnoevictionи следить за памятью. Ниже есть проверка этого параметра. - Сертификаты kubeadm живут 1 год. Если кластер ставили в 2025 году, срок, возможно, уже подходит. Проверка включена в скрипт ниже.
- Параметры ВМ в Yandex Cloud. Убедитесь, что продовые ВМ не прерываемые (
preemptible=false) и имеютcore_fraction=100. При 20/50 % вы получите CPU steal. Для него есть отдельный алерт. - Patroni не промоутит отстающие реплики. Реплика с лагом больше
maximum_lag_on_failoverне будет повышена. Если обе реплики отстают, автоматический failover не сработает. Отсюда критичный алерт на эту ситуацию.
1. Архитектура
┌────────────── вне Yandex Cloud ──────────────┐
│ VPS: blackbox_exporter #2 (HTTPS+basic auth) │
│ healthchecks.io ← heartbeat Watchdog │
└───────────────┬──────────────────────────────┘
│ scrape 30s ▲ ping 1m
┌──────────────── VPC 10.10.0.0/16 ────────┼─────────────────────┼─────────────────┐
│ ▼ │ │
│ app-01..04: node_exporter:9100, shopflow-api:9464/metrics, │ │
│ nginx-exporter:9113, promtail:9080 ──┐ │ │
│ pg-01..03: node_exporter, postgres_exporter:9187,│ pull 15s │ │
│ patroni:8008/metrics, promtail ───────┤ │ │
│ redis-01/02:node_exporter, redis_exporter:9121 ├──► mon-01 ─┴─────────────┐ │
│ (redis + sentinel), promtail ─────────┤ Prometheus :9090 │ │
│ k8s-01..03: Prometheus (agent mode) + kube-state- │ ├ rules → Alertmanager │ │
│ metrics + kubelet/cAdvisor ──remote_write──► :9093 │ │
│ promtail DaemonSet ────────────────── logs ─► Loki :3100 │ │
│ bastion: node_exporter │ └ ruler: recording │ │
│ │ rules → remote_write │
│ Blackbox #1 :9115 ──┘ в Prometheus │ │
│ Grafana :3000 (доступ через bastion/SSO) ◄─────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
Alertmanager ─► severity=critical ─► PagerDuty (звонок) + Telegram
─► severity=warning ─► Telegram (группа дежурных)
─► Watchdog ─► healthchecks.io (dead man's switch → PagerDuty)
cron 09:00 MSK: digest.py ─► Prometheus API (ALERTS, SLO) ─► e-mail sre-team@shopflow.example
Поток данных от метрики до оповещения:
- Prometheus опрашивает экспортёры (pull) и получает метрики k8s через
remote_writeот агента в кластере. - Loki ruler превращает логи Nginx в метрики SLI и отправляет их в Prometheus через
remote_write. - Prometheus считает recording rules (SLI, burn rate, capacity).
- Alert rules с
forиkeep_firing_forотправляют сработавшие алерты в Alertmanager. - Alertmanager группирует, подавляет, учитывает окна тишины и маршрутизирует в PagerDuty, Telegram или webhook.
- Отдельный скрипт раз в сутки собирает историю из серии
ALERTSи отправляет её на e-mail.
Почему агент Prometheus внутри k8s. kubeadm-кластер с kubelet, cAdvisor и kube-state-metrics проще опрашивать изнутри: service discovery и RBAC работают штатно. Центральный Prometheus при этом остаётся единственной точкой правил и алертов.
2. Сбор метрик
2.1 Экспортёры, порты, интервалы
| Источник | Экспортёр / агент | Порт | Интервал | Хосты |
|---|---|---|---|---|
| ОС (CPU, RAM, диск, IOPS, сеть, systemd) | node_exporter 1.8.x | 9100 | 15s | все 12 + bastion + mon |
| Кастомные проверки (сертификаты, конфиги) | textfile collector + systemd timer | — | 5m (генерация) | по ролям |
| Контейнеры | cAdvisor, встроенный в kubelet (/metrics/cadvisor) |
10250 | 15s | k8s |
| Объекты k8s | kube-state-metrics 2.x | 8080 | 15s | k8s |
| etcd, apiserver | встроенные /metrics |
2381 / 6443 | 15s | k8s |
| PostgreSQL | postgres_exporter 0.15 + custom queries | 9187 | 30s (тяжёлые запросы) | pg-* |
| Patroni | встроенный /metrics |
8008 | 15s | pg-* |
| Redis и Sentinel | redis_exporter (oliver006) | 9121 | 15s | redis-* |
| Nginx | nginx-prometheus-exporter (stub_status) + логи JSON → Loki | 9113 | 15s | app-* |
| ShopFlow API, PayGate | prom-client | 9464 | 15s | app-*, k8s |
| Внешняя доступность | blackbox_exporter (внутри VPC и снаружи) | 9115 | 30s | mon-01, VPS |
| Логи | Promtail 2.9 → Loki | 9080 | потоково | все |
Почему scrape_interval 15s и evaluation_interval 15s. Бюджет даунтайма 43 минуты в месяц. При 15-секундном опросе и for: 2m обнаружение укладывается примерно в 2,5 минуты. Интервал 5s удвоил бы объём TSDB без выигрыша для SLO.
Оценка объёма. Около 150–250 тысяч серий. При 15s это примерно 1,5–2 GB в сутки, то есть около 60 GB за 30 дней локальной retention (--storage.tsdb.retention.time=30d). Долгосрочное хранение (Thanos или Mimir в Object Storage) — вторая фаза. Для capacity planning хватит recording rules с агрегатами.
2.2 Системные метрики: что именно смотрим
-
CPU:
rate(node_cpu_seconds_total)по режимамuser/system/iowait/steal; load average на ядро. -
Память:
MemAvailable/MemTotal, swap in/out,node_vmstat_oom_kill. -
Диск:
- заполнение
node_filesystem_avail_bytesдля/и/var/lib/postgresql; - прогноз
predict_linear; - инод-ы;
- IOPS
rate(node_disk_reads/writes_completed_total); - латентность через отношение
write_time_secondsкwrites_completed; io_time(util).
Лимиты IOPS и пропускной способности в Yandex Cloud зависят от типа и размера диска. Внесите их в дашборд по документации для ваших дисков.
- заполнение
-
Сеть: байты и пакеты,
errs/drop,node_nf_conntrack_entries/limit(актуально на app-узлах с Nginx), TCP retransmits (node_netstat_Tcp_RetransSegs). -
Время:
node_timex_offset_seconds. etcd и Patroni чувствительны к расхождению часов. -
Процессы:
node_systemd_unit_stateдляshopflow-api|nginx|patroni|redis-server|redis-sentinel|etcd|promtail.
2.3 Метрики приложений (добавить в код, если их ещё нет)
ShopFlow API (Node.js):
collectDefaultMetrics(): event loop lag, heap, GC,process_start_time_seconds.http_request_duration_seconds{method,route,status_code}— гистограмма.routeдолжен быть шаблоном (/api/v1/orders/:id), иначе будет взрыв кардинальности. Бакеты:[0.025,0.05,0.1,0.2,0.3,0.5,0.75,1,2,3,5,10]. Бакеты 0.3 и 2 обязательны, это границы SLO.shopflow_orders_created_total,shopflow_db_pool_{total,idle,waiting},shopflow_redis_errors_total.- Если приложение работает в cluster mode или под pm2, используйте
AggregatorRegistry, иначе/metricsвернёт данные одного воркера.
PayGate:
paygate_payments_total{result="succeeded|canceled|declined|error"}. Отказы банка (declined) отделены от технических ошибок (error: 5xx, таймауты ЮKassa, наши исключения).paygate_payment_processing_duration_seconds— гистограмма, SLO p99 < 2 с.paygate_yookassa_request_duration_seconds{endpoint,code},paygate_webhooks_received_total{event}.paygate_payments_pending_oldest_age_seconds— возраст самого старого платежа без финального статуса.
BullMQ:
- redis_exporter с флагом
--check-single-keysотдаёт длинуbull:<queue>:waitиbull:<queue>:failed. - Воркеры дополнительно экспортируют
bullmq_jobs_completed_totalиbullmq_job_duration_seconds.
2.4 Логи (Loki + Promtail)
| Источник | Путь | Метки Loki (низкая кардинальность) | Retention |
|---|---|---|---|
| Nginx access (JSON) | /var/log/nginx/shopflow_access.json |
job, host |
14d |
| Nginx error | /var/log/nginx/error.log |
job, host |
14d |
| systemd journal (api, patroni, redis, sentinel, etcd, kernel) | journald | job, host, unit |
14d |
| PostgreSQL | /var/log/postgresql/*.log |
job, host |
30d |
| Поды k8s | /var/log/pods (DaemonSet) |
namespace, app, container |
14d |
Правила для меток и хранения:
request_id,user_idиstatusне делаем метками, их разбирают в запросах (| json).- IP клиентов в логах — персональные данные (152-ФЗ). Держите retention не больше требуемого политикой.
Метрики из логов. Loki ruler вычисляет SLI доступности по логам Nginx: 5xx и все запросы. Логи Nginx — самая близкая к пользователю точка: они видят 502/504, которые приложение не зафиксирует, если процесс упал.
3. Пороги, правила оповещений, эскалация
3.1 Принципы
-
Будят человека только симптомы: пользователь не может купить или заплатить, либо это случится в ближайшие часы. Сюда относятся burn rate SLO, внешняя проверка, латентность, платежи и надвигающиеся необратимые события (диск, XID wraparound, сертификаты, потеря HA БД).
-
Причины (CPU, память, лаг) идут в
warning→ Telegram. Их разбирают в рабочее время. Вcriticalпричина поднимается, только если без вмешательства через минуты-часы наступит отказ. -
SLO 99,9 % контролируется multi-window multi-burn-rate алертами (по Google SRE Workbook), а не порогом «5xx больше X %»:
Окно (длинное / короткое) Burn rate Порог доли ошибок Съедено бюджета Серьёзность 1h / 5m 14.4× 1.44 % 2 % за 1 ч critical 6h / 30m 6× 0.6 % 5 % за 6 ч critical 1d / 2h 3× 0.3 % 10 % за сутки warning 3d / 6h 1× 0.1 % 10 % за 3 дня warning Короткое окно даёт быстрый сброс после восстановления. Дополнительное условие «RPS > 1» исключает ложные срабатывания ночью, когда 1 ошибка из 20 запросов даёт 5 %.
3.2 Таблица алертов
Обозначения: C = critical → PagerDuty и Telegram, W = warning → Telegram, I = info → только в дайджест. Все for указаны в таблице. Для флапающих метрик добавлен keep_firing_for: 5m.
| Метрика / алерт | Warning | Critical | Серьёзность | Действие |
|---|---|---|---|---|
| СИМПТОМЫ | ||||
| Burn rate бюджета доступности API (5xx на Nginx) | 3× (1d & 2h) или 1× (3d & 6h) | 14.4× (1h & 5m) или 6× (6h & 30m) | C / W | RB-API: найти источник 5xx (upstream, БД, Redis), откат последнего деплоя |
Внешняя проверка https://api.shopflow.example/health/deep |
провал из 1 локации 3m | провал из обеих локаций 2m | C | Проверить NLB, DNS, сертификат, Nginx на всех app-узлах |
Латентность API p95 (без /health) |
> 300 мс 10m | > 1 с 5m | W / C | Проверить медленные запросы в PG, event loop lag, пул соединений |
| Обработка платежа p99 | > 2 с 10m | > 5 с 5m | W / C | Сравнить с латентностью ЮKassa и нашей БД |
Доля техошибок платежей (result="error") |
> 2 % 10m | > 5 % 5m | W / C | RB-PAY |
| Нет успешных платежей при наличии заказов | — | 0 успешных за 20m и > 10 заказов | C | RB-PAY |
| Самый старый платёж в ожидании (вебхук не пришёл) | > 15 мин | > 60 мин | W / C | Проверить приём вебхуков и IP-allowlist ЮKassa |
| TLS-сертификат API (blackbox) | < 21 дня | < 7 дней | W / C | Продлить (certbot или Certificate Manager) |
| ХОСТЫ | ||||
| CPU utilization | > 85 % 15m | — | W | Проверить топ процессов; если это рост нагрузки — capacity |
| CPU steal | > 10 % 10m | — | W | Проверить core_fraction, перенести ВМ или сменить платформу |
| iowait | > 20 % 10m | — | W | Смотреть IOPS против лимита диска |
| MemAvailable | < 10 % 10m | < 5 % 5m | W / C | Найти утечку (heap Node, work_mem PG) |
| OOM kill | ≥ 1 за 5m | — | W | Найти убитый процесс в журнале: journalctl -k | grep -i oom |
| Заполнение ФС | > 80 % 15m | > 90 % 5m | W / C | Очистить или расширить диск (RB-PGDISK для PG) |
| Прогноз заполнения ФС | заполнится за 24h | заполнится за 4h | W / C | То же, с приоритетом |
| Свободные inode | < 10 % | < 5 % | W / C | Найти каталоги с мелкими файлами |
| Латентность записи на диск | > 20 мс 10m (SSD) | — | W | Упор в IOPS: увеличить диск или сменить тип |
| conntrack (app-узлы) | > 75 % | > 90 % 5m | W / C | Поднять nf_conntrack_max, искать шторм соединений |
| Ошибки и дропы на интерфейсе | > 0 за 10m | — | W | Сеть или MTU |
| Смещение часов | > 50 мс 5m | — | W | chronyc tracking |
Хост недоступен (up{job="node"}==0) |
1 узел 2m | ≥ 2 app- или k8s-узла 2m | W / C | Консоль Yandex Cloud, serial console |
| Упавший systemd-юнит | 1 юнит 5m | — | W | systemctl status |
| POSTGRESQL / PATRONI | ||||
| Нет лидера Patroni | — | 1m | C | RB-PG: patronictl list, etcd |
| Больше одного лидера (split-brain) | — | 30s | C | Остановить запись немедленно, человек |
| Произошёл failover (сменился timeline) | факт | — | I / W | Разбор причины, проверка приложений |
| Живых реплик | < 2 за 5m | 0 за 5m | W / C | Восстановить реплику (patronictl reinit) |
| Лаг реплики (байты) | > 1 MB (maximum_lag_on_failover) 5m |
обе реплики > 1 MB 10m | W / C | Реплика не годится для failover: проверить IO и сеть |
Соединения / max_connections |
> 80 % 5m | > 90 % 5m | W / C | Утечка пула; ввести pgbouncer |
Длинная транзакция или idle in transaction |
> 5 мин | > 30 мин | W / C | pg_terminate_backend после согласования |
Возраст XID (age(datfrozenxid)) |
> 1.0e9 | > 1.5e9 | W / C | VACUUM FREEZE по графику |
| Deadlocks | > 0 за 10m | — | W | Разбор в логах PG |
| Возраст последнего бэкапа WAL-G [A4] | > 26 ч | > 50 ч | W / C | Проверить cron и Object Storage |
| Ошибки архивации WAL | рост за 15m | непрерывно 30m | W / C | Иначе pg_wal заполнит диск |
| REDIS / SENTINEL / BULLMQ | ||||
| Нет master или больше одного master | — | 1m | C | RB-Redis, redis-cli -p 26379 sentinel masters |
| Sentinel ok_sentinels | < 3 | < 2 (кворум) | W / C | Восстановить sentinel |
| Память / maxmemory | > 80 % 10m | > 90 % 5m | W / C | Сессии: TTL; очередь: разбор backlog |
| Вытеснение ключей | > 0 (кэш) | > 0 на инстансе с BullMQ | W / C | Потеря задач: проверить noeviction |
Политика вытеснения ≠ noeviction на инстансе с очередью |
1 | — | W | Исправить конфиг |
| Разрыв связи реплики с master | 2m | — | W | Сеть, repl-backlog-size |
| Отказы в соединениях (rejected) | > 0 | — | W | Поднять maxclients |
| Ошибка сохранения RDB/AOF | 1 | — | W | Диск, права |
Очередь BullMQ wait |
> 1000 10m | > 5000 10m (или payments > 200) |
W / C | Масштабировать воркеры (KEDA), искать зависший воркер |
Рост failed в BullMQ |
> 50 за 15m | — | W | Логи воркеров |
| NGINX / NODE.JS | ||||
| «no live upstreams» в логах | > 0 за 5m | — | W | Симптом покрыт burn rate; причина — упавший shopflow-api |
Активные соединения / worker_connections |
> 80 % | — | W | Поднять лимиты |
| Event loop lag p99 | > 100 мс 5m | — | W | Профилирование, CPU-bound код |
| Рестарты процесса | > 2 за 15m | — | W | Логи падений |
| Heap used / heap limit | > 85 % 15m | — | W | Утечка памяти |
| KUBERNETES | ||||
| Node NotReady | 1 узел 5m | ≥ 2 узла 2m | W / C | kubelet, etcd |
| CrashLoop (restarts > 3 за 15m) | да | PayGate: доступно 0 реплик 2m | W / C | kubectl logs --previous |
| Нет лидера etcd | — | 1m | C | etcdctl endpoint status |
| etcd WAL fsync p99 | > 100 мс 10m | > 500 мс 10m | W / C | Диск под etcd |
| Сертификаты kubeadm | < 30 дней | < 7 дней | W / C | kubeadm certs renew all |
| Заполнение PVC | > 80 % | > 90 % | W / C | Расширить PVC |
| САМ МОНИТОРИНГ | ||||
| Нет сигнала Watchdog (heartbeat) | — | нет пинга 5 мин (healthchecks.io) | C | Мониторинг «ослеп»: проверить mon-01 |
| Ошибки отправки в Alertmanager | — | > 0 за 10m | C | Токен бота или ключ PagerDuty |
Target down (up==0, не node) |
5m | — | W | Экспортёр, security group |
| Ошибки вычисления правил Prometheus | > 0 | — | W | promtool check rules |
Устаревший файл textfile (infra-checks.prom) |
старше 15m | — | W | Таймер проверок |
| Ошибки ingestion Loki или drops Promtail | > 0 10m | — | W | Лимиты Loki |
3.3 Как избежать ложных срабатываний и усталости от алертов
| Механизм | Реализация |
|---|---|
| Выдержка | for: 2–15m по таблице; для прогнозов нужны и predict_linear, и порог заполнения > 80 %, чтобы логротация не давала шум |
| Гистерезис | keep_firing_for: 5m (есть в Prometheus 2.42+) для латентности, памяти, CPU: алерт не «мигает» на границе |
| Группировка | group_by: [alertname, service], group_wait: 30s, group_interval: 5m, repeat_interval: 4h (W) и 1h (C). PagerDuty сам ведёт инцидент, повторы дедуплицируются по dedup_key |
| Подавление (inhibit) | HostDown гасит все алерты этого instance; PatroniNoLeader гасит причинные алерты service=postgresql; ExternalProbeDown гасит burn rate и латентность; critical гасит warning с тем же alertname+instance |
| Минимальный трафик | Burn-rate алерты срабатывают только при RPS > 1 |
| Окна | time_intervals с location: Europe/Moscow: окно планового обслуживания (вт 02:00–04:00) глушит причинные warning; некритичные warning (urgency=low) ночью приходят утром, если ещё активны |
| Деплой | Перед деплоем CI ставит silence только на причинные алерты (рестарты процесса) на 15 минут через amtool silence add. Симптомы при деплое не глушатся никогда |
| Гигиена | У каждого C-алерта есть runbook_url, иначе PR не проходит (promtool unit test + линтер). Ежемесячно разбираем алерты без действий: удалить или перенастроить |
| Цель | Не больше 2 инцидентов с пейджем за 12-часовую смену, доля ложных пейджей меньше 10 % |
3.4 Эскалация и дежурство
| Уровень | Критерий | Канал | Реакция |
|---|---|---|---|
| SEV1 | Нет продаж или платежей, API недоступен, burn 14.4× | PagerDuty high urgency (звонок) | ack ≤ 5 мин; без ack через 10 мин — secondary; через 20 мин — техлид; через 30 мин — CTO. Назначается Incident Commander, статус бизнесу каждые 30 мин |
| SEV2 | Потеря резервирования (0 реплик PG, нет кворума sentinel), burn 6×, диск < 4h | PagerDuty high urgency | ack ≤ 15 мин, эскалация через 20 мин |
| SEV3 | Warning | Telegram | Реакция в рабочее время (10–19 MSK) эмодзи-меткой «👀 взял». Warning, который висит больше 24 ч, превращается в задачу в трекере |
Ротация:
- Недельная, primary и secondary; передача смены в понедельник в 12:00 MSK с разбором алертов прошедшей недели.
- В PagerDuty: Escalation Policy «ShopFlow-SRE» из трёх уровней.
- Override на праздники составляется заранее.
Postmortem:
- Blameless-разбор обязателен для SEV1/SEV2 и для любого инцидента, съевшего больше 25 % месячного бюджета ошибок. Срок — 5 рабочих дней.
Политика бюджета ошибок:
- Если бюджет исчерпан, релизы фич замораживаются, пока не выполнены задачи надёжности из postmortem.
4. Дашборды (Grafana 10, папка ShopFlow, provisioning из git)
4.1 «ShopFlow — Overview» (главный экран дежурного и NOC)
- SLO доступности за 30 дней (stat) и оставшийся бюджет ошибок в % и минутах (gauge).
- Burn rate 1h / 6h (time series) с линиями 14.4× и 6×.
- RPS и доля 5xx по Nginx, с разбивкой по app-узлам.
- p50/p95/p99 латентности API с линией 300 мс.
- Платежи: успешные, отказы, техошибки в минуту; p99 обработки с линией 2 с; конверсия «заказ → оплата».
- Внешние проверки: статус по двум локациям, время ответа, дней до истечения сертификата.
- Состояние компонентов (state timeline): лидер Patroni, master Redis, узлы k8s,
shopflow-apiна каждом app-узле. - Очереди BullMQ: wait, active, failed.
- Активные алерты (Alertmanager datasource) и аннотации деплоев из CI.
- Heatmap CPU и памяти по 12 хостам.
4.2 Дашборды по сервисам
| Дашборд | Ключевые панели |
|---|---|
| API (Node.js) | RPS, ошибки и латентность по route (top-10 медленных), event loop lag, heap и GC, пул БД (idle/waiting), рестарты, ссылка в Loki на логи с фильтром по request_id |
| PayGate | Платежи по result, p50/p99 обработки, латентность и коды ответа ЮKassa по endpoint, вебхуки в минуту, возраст самого старого pending, реплики пода |
| PostgreSQL / Patroni | Роли узлов, timeline, TPS (commit/rollback), соединения по state, лаг реплик (байты и секунды), cache hit ratio, deadlocks, длинные транзакции, возраст XID, размер БД и топ таблиц, скорость генерации WAL, возраст бэкапа. Основа — Grafana ID 9628, доработанный |
| Redis / BullMQ | Роли, ops/s, память против maxmemory, evicted и expired, hit ratio сессий, клиенты, лаг реплики, кворум sentinel, очереди по имени (wait/active/failed/delayed), throughput и длительность задач. Основа — ID 763 |
| Nginx | Соединения, RPS по статусам, upstream response time против request time, 499/502/504, ошибки upstream из логов |
| Kubernetes | Узлы, CPU и память requests/limits против usage, рестарты подов, etcd (лидер, fsync, размер БД), дни до истечения сертификатов |
| Hosts | Node Exporter Full (ID 1860) с переменной role |
| Capacity | См. ниже |
4.3 KPI для планирования мощностей
| KPI | Запрос / метод | Триггер для планирования |
|---|---|---|
| Пиковый RPS (p99 за сутки) и тренд за 4 недели | max_over_time(shopflow:nginx_requests:rps5m[1d]) |
Рост > 20 % в месяц |
| Запас app-слоя по схеме N+1 | Пиковый CPU на узел | Пик > 60 %: при потере 1 из 4 узлов нагрузка ×4/3 даст около 80 % |
| CPU-секунды на запрос | rate(cpu)/rps |
Рост → регресс производительности |
| Рост БД, ГБ в неделю, и дни до заполнения диска | predict_linear(...[7d], 30d) |
Меньше 60 дней до 80 % |
Соединения PG, пик против max_connections |
— | Пик > 70 % → pgbouncer |
| Генерация WAL, ГБ в сутки | — | Размер архива и стоимость Object Storage |
| Память Redis, пик и тренд | — | Больше 70 % maxmemory |
| Throughput BullMQ против времени обработки | — | Время ожидания в очереди растёт |
| Лимиты IOPS и пропускной способности дисков | Пиковые IOPS / лимит | Больше 70 % → расширить или сменить тип |
| Requests/limits k8s против allocatable | kube-state-metrics | Больше 75 % allocatable |
5. Автоматическое реагирование
| Ситуация | Автоматически? | Как и с какими ограничениями |
|---|---|---|
Падение процесса shopflow-api или nginx |
Да | systemd Restart=on-failure, RestartSec=5, StartLimitBurst=5 за 5 мин. Дальше юнит в failed → алерт человеку |
| Под PayGate или воркер завис | Да | liveness и readiness probes; PodDisruptionBudget minAvailable: 1 |
| Рост очереди BullMQ | Да | KEDA (redis list scaler по bull:<queue>:wait), min 2, max 10 воркеров |
| Нагрузка на PayGate | Да | HPA по CPU (70 %), min 2, max 6 |
| Отказ лидера PG | Да (Patroni) | Только если реплика в пределах maximum_lag_on_failover. Человек проверяет последствия |
| Отказ master Redis | Да (Sentinel) | Только после добавления третьего sentinel |
| Рост app-слоя | Полуавтомат | Сейчас это обычные ВМ. Рекомендую перевести в Instance Group с автомасштабированием по CPU. До этого масштабирует человек по capacity-алерту |
| Диск заполняется журналами или логами | Да, безопасное | journalctl --vacuum-size=1G, logrotate. Ограничения ниже |
pg_wal, данные БД, слоты репликации, файлы Redis |
Нет | Удалять вручную нельзя: возможна потеря данных |
| Split-brain PG или Redis | Нет | Нужно решение человека о том, чьи данные истинны |
| Сбой платежей и сверка с ЮKassa | Нет | Финансовые последствия, возможна связь с ЮKassa |
| Откат релиза | Полуавтомат | CI предлагает откат при burn 14.4× в течение 30 мин после деплоя, подтверждает дежурный |
| Продление сертификатов kubeadm | Нет (плановая работа) | Требуется рестарт static pods control plane |
Ограничения для очистки диска. Автоочистка затрагивает только /var/log/journal и *.gz старше 7 дней. Не более 1 запуска в час. Каждый запуск записывается в журнал и сообщается в Telegram.
Общие правила самовосстановления:
- ограничение частоты;
- идемпотентность;
- каждое действие видно в Telegram и в дайджесте;
- автоматика не должна скрывать хроническую проблему: например, частота рестартов — отдельный алерт.
Runbook RB-PGDISK: «PostgreSQL: диск заканчивается (HostDiskWillFillIn4h / HostDiskUsageCritical на pg-*)»
- Определить роль узла и что растёт:
ssh -J bastion pg-01.ru-central1.internal sudo -u postgres patronictl -c /etc/patroni/config.yml list sudo du -xsh /var/lib/postgresql/15/main/{base,pg_wal} /var/log/postgresql - Если растёт
pg_wal, это самая частая причина. Проверить слоты и архивацию:SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots ORDER BY 3 DESC; SELECT archived_count, failed_count, last_failed_wal, last_failed_time FROM pg_stat_archiver;- Неактивный слот держит WAL. Убедитесь, что это не слот живой реплики Patroni: Patroni управляет своими слотами сам. Если это лишний слот (например, от старого CDC), выполните
SELECT pg_drop_replication_slot('<slot>');. Удаление слота делается только с подтверждением второго инженера. - Архивация падает (
failed_countрастёт). Посмотритеjournalctl -u patroni | grep -i wal-g, проверьте доступ к Object Storage и ключи. После починки WAL уйдёт в архив сам. - Никогда не удаляйте файлы из
pg_walвручную.
- Неактивный слот держит WAL. Убедитесь, что это не слот живой реплики Patroni: Patroni управляет своими слотами сам. Если это лишний слот (например, от старого CDC), выполните
- Если растёт
base(данные или bloat), время покупается расширением диска:
Повторите на всех узлах кластера, чтобы реплики не отстали.yc compute disk list --folder-id <folder> # найти диск pg-01-data yc compute disk resize --name pg-01-data --size 400 # проверьте `yc compute disk resize --help` sudo growpart /dev/vdb 1 2>/dev/null; sudo resize2fs /dev/vdb1 # ext4, онлайн - Если свободно меньше 2 % на лидере: проверьте
patronictl list. Если реплика в строю и лаг < 1 MB, сделайтеpatronictl switchoverна реплику с большим запасом места, затем расширяйте диск бывшего лидера. - После инцидента: аннотация в Grafana; задача на capacity-алерт «меньше 60 дней до 80 %», если он не сработал заранее.
Runbook RB-PAY: «Платежи: техошибки > 5 % или нет успешных платежей 20 мин»
- Отделить нашу проблему от проблемы ЮKassa. Grafana → PayGate: панель «ЮKassa latency/codes».
kubectl -n payments get pods -l app=paygate -o wide kubectl -n payments logs deploy/paygate --since=15m | grep -Ei 'error|timeout|ECONN' | tail -50 # доступность ЮKassa с узла k8s (ответ 401 без ключа = сеть и TLS в порядке) kubectl -n payments exec deploy/paygate -- sh -c \ 'curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" https://api.yookassa.ru/v3/payments' - Коды 5xx или таймауты от ЮKassa:
- проверить статус-страницу ЮKassa и написать в поддержку (номер договора в vault);
- включить в витрине баннер «оплата временно недоступна» через feature flag, чтобы не копить брошенные корзины;
- SEV1: уведомить бизнес.
- Коды 401/403: проверьте ротацию секретов (
kubectl -n payments get secret paygate-yookassa -o yaml, дата изменения). Также проверьте, не изменился ли egress IP NAT-шлюза, если в ЮKassa настроен allowlist. - Ошибки наши (исключения, БД, Redis): проверьте алерты PG и Redis и последний деплой PayGate (
kubectl -n payments rollout history deploy/paygate). Откат выполняется черезkubectl -n payments rollout undo deploy/paygateс подтверждением IC. - Вебхуки не приходят (растёт возраст pending): найдите
POST /payments/webhookв логах Nginx через Loki:
Проверьте allowlist IP ЮKassa в security group и Nginx. Когда связь восстановится, ЮKassa повторит доставку, но для «зависших» платежей запустите сверку{job="nginx_access"} | json | request_uri=~"/payments/webhook.*"GET /v3/payments/{id}скриптом reconcile. - После восстановления: сверка списка платежей за интервал инцидента с ЮKassa (выгрузка из личного кабинета), затем postmortem.
6. Конфиги
6.1 /etc/prometheus/prometheus.yml
Prometheus запускается с флагами --web.enable-remote-write-receiver --storage.tsdb.retention.time=30d.
global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 15s
external_labels: { env: prod, region: ru-central1 }
rule_files: ["/etc/prometheus/rules/*.yml"]
alerting:
alertmanagers:
- static_configs: [{ targets: ["10.10.0.10:9093"] }]
# Общий relabel: instance = имя хоста без порта, чтобы inhibit по instance
# работал между разными экспортёрами одного хоста.
x-instance-relabel: &instance_relabel
- source_labels: [__address__]
regex: '([^:]+)(?::\d+)?'
target_label: instance
replacement: '$1'
scrape_configs:
- job_name: node
file_sd_configs: [{ files: ["/etc/prometheus/targets/node.yml"] }]
relabel_configs: *instance_relabel
- job_name: postgres
scrape_interval: 30s
static_configs:
- targets: [pg-01.ru-central1.internal:9187, pg-02.ru-central1.internal:9187, pg-03.ru-central1.internal:9187]
labels: { service: postgresql, cluster: shopflow-pg }
relabel_configs: *instance_relabel
- job_name: patroni
static_configs:
- targets: [pg-01.ru-central1.internal:8008, pg-02.ru-central1.internal:8008, pg-03.ru-central1.internal:8008]
labels: { service: postgresql, cluster: shopflow-pg }
relabel_configs: *instance_relabel
- job_name: redis
static_configs:
- targets: [redis-01.ru-central1.internal:9121, redis-02.ru-central1.internal:9121]
labels: { service: redis }
relabel_configs: *instance_relabel
- job_name: nginx
static_configs:
- targets: [app-01.ru-central1.internal:9113, app-02.ru-central1.internal:9113,
app-03.ru-central1.internal:9113, app-04.ru-central1.internal:9113]
labels: { service: nginx }
relabel_configs: *instance_relabel
- job_name: shopflow-api
static_configs:
- targets: [app-01.ru-central1.internal:9464, app-02.ru-central1.internal:9464,
app-03.ru-central1.internal:9464, app-04.ru-central1.internal:9464]
labels: { service: shopflow-api }
relabel_configs: *instance_relabel
- job_name: blackbox-http
metrics_path: /probe
params: { module: [http_2xx_api] }
scrape_interval: 30s
static_configs:
- targets: [https://api.shopflow.example/health/deep]
labels: { probe_location: yc-internal, service: shopflow-api }
relabel_configs: &bb_relabel
- { source_labels: [__address__], target_label: __param_target }
- { source_labels: [__param_target], target_label: instance }
- { target_label: __address__, replacement: "127.0.0.1:9115" }
- job_name: blackbox-http-external # VPS вне Yandex Cloud
metrics_path: /probe
scheme: https
basic_auth: { username: prom, password_file: /etc/prometheus/secrets/bb_ext }
params: { module: [http_2xx_api] }
scrape_interval: 30s
static_configs:
- targets: [https://api.shopflow.example/health/deep]
labels: { probe_location: external, service: shopflow-api }
relabel_configs:
- { source_labels: [__address__], target_label: __param_target }
- { source_labels: [__param_target], target_label: instance }
- { target_label: __address__, replacement: "bb-ext.shopflow.example:443" }
- job_name: blackbox-tcp-yookassa # связность с ЮKassa (TLS handshake)
metrics_path: /probe
params: { module: [tcp_tls] }
scrape_interval: 60s
static_configs:
- targets: ["api.yookassa.ru:443"]
labels: { service: paygate }
relabel_configs: *bb_relabel
Пример /etc/prometheus/targets/node.yml:
- targets: [app-01.ru-central1.internal:9100, app-02.ru-central1.internal:9100,
app-03.ru-central1.internal:9100, app-04.ru-central1.internal:9100]
labels: { role: app }
- targets: [pg-01.ru-central1.internal:9100, pg-02.ru-central1.internal:9100, pg-03.ru-central1.internal:9100]
labels: { role: postgres }
- targets: [redis-01.ru-central1.internal:9100, redis-02.ru-central1.internal:9100]
labels: { role: redis }
- targets: [k8s-01.ru-central1.internal:9100, k8s-02.ru-central1.internal:9100, k8s-03.ru-central1.internal:9100]
labels: { role: k8s }
Если ваша версия Prometheus отвергнет ключ x-instance-relabel на верхнем уровне конфига, продублируйте блок relabel_configs в каждой задаче.
/etc/blackbox_exporter/config.yml:
modules:
http_2xx_api:
prober: http
timeout: 5s
http:
method: GET
valid_status_codes: [200]
fail_if_not_ssl: true
fail_if_body_not_matches_regexp: ['"status"\s*:\s*"ok"']
preferred_ip_protocol: ip4
tcp_tls:
prober: tcp
timeout: 5s
tcp: { tls: true }
6.2 Loki: SLI из логов Nginx
nginx.conf, секция http:
log_format json_sf escape=json '{"time":"$time_iso8601","request_id":"$request_id",'
'"method":"$request_method","request_uri":"$request_uri","status":"$status",'
'"request_time":"$request_time","upstream_response_time":"$upstream_response_time",'
'"upstream_addr":"$upstream_addr","body_bytes_sent":"$body_bytes_sent"}';
access_log /var/log/nginx/shopflow_access.json json_sf;
server { # для nginx-prometheus-exporter
listen 127.0.0.1:8080;
location /stub_status { stub_status; }
}
Фрагмент Promtail, запуск с -config.expand-env=true:
scrape_configs:
- job_name: nginx_access
static_configs:
- targets: [localhost]
labels: { job: nginx_access, host: "${HOSTNAME}", __path__: /var/log/nginx/shopflow_access.json }
pipeline_stages:
- json: { expressions: { time: time } }
- timestamp: { source: time, format: RFC3339 }
- job_name: nginx_error
static_configs:
- targets: [localhost]
labels: { job: nginx_error, host: "${HOSTNAME}", __path__: /var/log/nginx/error.log }
- job_name: journal
journal: { max_age: 12h, labels: { job: systemd-journal, host: "${HOSTNAME}" } }
relabel_configs:
- { source_labels: [__journal__systemd_unit], target_label: unit }
Фрагмент конфига Loki для ruler. В 2.9 ключ remote_write.client помечен устаревшим, а в новых версиях используется карта clients. Сверьтесь с loki -print-config-stderr.
ruler:
storage: { type: local, local: { directory: /loki/rules } }
rule_path: /loki/rules-temp
alertmanager_url: http://10.10.0.10:9093
enable_api: true
wal: { dir: /loki/ruler-wal }
remote_write:
enabled: true
client: { url: http://10.10.0.10:9090/api/v1/write }
/loki/rules/fake/shopflow.yaml (при auth_enabled: false тенант называется fake):
groups:
- name: shopflow-nginx-sli
interval: 1m
rules:
- record: shopflow:nginx_requests:rate1m
expr: |
sum by (status) (
rate({job="nginx_access"} | json status="status", request_uri="request_uri"
| request_uri !~ "/health.*" [1m]))
- alert: NginxNoLiveUpstreams
expr: sum by (host) (count_over_time({job="nginx_error"} |= "no live upstreams" [5m])) > 0
labels: { severity: warning, service: nginx }
annotations: { summary: "Nginx на {{ $labels.host }}: нет живых upstream" }
- alert: PostgresPanicOrFatalBurst
expr: sum by (host) (count_over_time({job="postgres"} |~ "PANIC|FATAL" [5m])) > 20
labels: { severity: warning, service: postgresql }
annotations: { summary: "Всплеск FATAL/PANIC в логах PostgreSQL на {{ $labels.host }}" }
6.3 Recording rules и правила алертинга
/etc/prometheus/rules/shopflow.yml:
groups:
# ---------- SLI / SLO ----------
- name: shopflow-slo-recording
interval: 1m
rules:
- record: shopflow:nginx_requests:rps5m
expr: sum(avg_over_time(shopflow:nginx_requests:rate1m[5m]))
# Шаблон повторяется для окон 5m 30m 1h 2h 6h 1d 3d 30d
- record: shopflow:sli_errors:ratio_rate5m
expr: sum(sum_over_time(shopflow:nginx_requests:rate1m{status=~"5.."}[5m])) / sum(sum_over_time(shopflow:nginx_requests:rate1m[5m]))
- record: shopflow:sli_errors:ratio_rate30m
expr: sum(sum_over_time(shopflow:nginx_requests:rate1m{status=~"5.."}[30m])) / sum(sum_over_time(shopflow:nginx_requests:rate1m[30m]))
- record: shopflow:sli_errors:ratio_rate1h
expr: sum(sum_over_time(shopflow:nginx_requests:rate1m{status=~"5.."}[1h])) / sum(sum_over_time(shopflow:nginx_requests:rate1m[1h]))
- record: shopflow:sli_errors:ratio_rate2h
expr: sum(sum_over_time(shopflow:nginx_requests:rate1m{status=~"5.."}[2h])) / sum(sum_over_time(shopflow:nginx_requests:rate1m[2h]))
- record: shopflow:sli_errors:ratio_rate6h
expr: sum(sum_over_time(shopflow:nginx_requests:rate1m{status=~"5.."}[6h])) / sum(sum_over_time(shopflow:nginx_requests:rate1m[6h]))
- record: shopflow:sli_errors:ratio_rate1d
expr: sum(sum_over_time(shopflow:nginx_requests:rate1m{status=~"5.."}[1d])) / sum(sum_over_time(shopflow:nginx_requests:rate1m[1d]))
- record: shopflow:sli_errors:ratio_rate3d
expr: sum(sum_over_time(shopflow:nginx_requests:rate1m{status=~"5.."}[3d])) / sum(sum_over_time(shopflow:nginx_requests:rate1m[3d]))
- record: shopflow:error_budget:remaining_ratio30d
expr: 1 - (sum(sum_over_time(shopflow:nginx_requests:rate1m{status=~"5.."}[30d])) / sum(sum_over_time(shopflow:nginx_requests:rate1m[30d]))) / 0.001
- record: shopflow:api_latency_seconds:p95_5m
expr: histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{job="shopflow-api",route!~"/health.*"}[5m])))
- record: paygate:processing_seconds:p99_5m
expr: histogram_quantile(0.99, sum by (le) (rate(paygate_payment_processing_duration_seconds_bucket[5m])))
- name: shopflow-symptoms
rules:
- alert: ShopFlowAPIErrorBudgetBurnFast
expr: |
(
(shopflow:sli_errors:ratio_rate1h > 14.4*0.001 and shopflow:sli_errors:ratio_rate5m > 14.4*0.001)
or
(shopflow:sli_errors:ratio_rate6h > 6*0.001 and shopflow:sli_errors:ratio_rate30m > 6*0.001)
) and on() shopflow:nginx_requests:rps5m > 1
for: 2m
labels: { severity: critical, service: shopflow-api }
annotations:
summary: "API ShopFlow быстро сжигает бюджет ошибок (5xx 5m: {{ $value | humanizePercentage }})"
runbook_url: https://wiki.shopflow.example/runbooks/RB-API
- alert: ShopFlowAPIErrorBudgetBurnSlow
expr: |
(
(shopflow:sli_errors:ratio_rate1d > 3*0.001 and shopflow:sli_errors:ratio_rate2h > 3*0.001)
or
(shopflow:sli_errors:ratio_rate3d > 0.001 and shopflow:sli_errors:ratio_rate6h > 0.001)
) and on() shopflow:nginx_requests:rps5m > 1
for: 15m
labels: { severity: warning, service: shopflow-api }
annotations:
summary: "Медленное сжигание бюджета ошибок API"
runbook_url: https://wiki.shopflow.example/runbooks/RB-API
- alert: ShopFlowAPIExternalProbeDown
expr: count(probe_success{job=~"blackbox-http.*", service="shopflow-api"} == 0) == 2
for: 2m
labels: { severity: critical, service: shopflow-api }
annotations:
summary: "API недоступен из обеих локаций проверки"
runbook_url: https://wiki.shopflow.example/runbooks/RB-API
- alert: ShopFlowAPIProbeFailingSingleLocation
expr: probe_success{job=~"blackbox-http.*", service="shopflow-api"} == 0
for: 3m
labels: { severity: warning, service: shopflow-api }
annotations: { summary: "Проверка API падает из локации {{ $labels.probe_location }}" }
- alert: ShopFlowAPILatencyP95High
expr: shopflow:api_latency_seconds:p95_5m > 0.3
for: 10m
keep_firing_for: 5m
labels: { severity: warning, service: shopflow-api }
annotations: { summary: "p95 API = {{ $value | humanizeDuration }} (SLO 300 мс)" }
- alert: ShopFlowAPILatencyP95Critical
expr: shopflow:api_latency_seconds:p95_5m > 1
for: 5m
labels: { severity: critical, service: shopflow-api }
annotations:
summary: "p95 API = {{ $value | humanizeDuration }}"
runbook_url: https://wiki.shopflow.example/runbooks/RB-API
- alert: PayGateProcessingP99High
expr: paygate:processing_seconds:p99_5m > 2
for: 10m
keep_firing_for: 5m
labels: { severity: warning, service: paygate }
annotations: { summary: "p99 обработки платежа {{ $value | humanizeDuration }} (SLO 2 с)" }
- alert: PayGateTechnicalErrorsHigh
expr: |
sum(rate(paygate_payments_total{result="error"}[5m]))
/ sum(rate(paygate_payments_total[5m])) > 0.05
and on() sum(rate(paygate_payments_total[5m])) > 0.05
for: 5m
labels: { severity: critical, service: paygate }
annotations:
summary: "Техошибки платежей {{ $value | humanizePercentage }}"
runbook_url: https://wiki.shopflow.example/runbooks/RB-PAY
- alert: PayGateNoSuccessfulPayments
expr: |
(sum(increase(paygate_payments_total{result="succeeded"}[20m])) or vector(0)) == 0
and on() sum(increase(shopflow_orders_created_total[20m])) > 10
for: 1m
labels: { severity: critical, service: paygate }
annotations:
summary: "20 минут нет успешных платежей при наличии заказов"
runbook_url: https://wiki.shopflow.example/runbooks/RB-PAY
- alert: TLSCertExpiringCritical
expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 7
labels: { severity: critical, service: shopflow-api }
annotations: { summary: "Сертификат {{ $labels.instance }} истекает через {{ $value | printf \"%.0f\" }} дн." }
# ---------- Хосты ----------
- name: hosts
rules:
- alert: HostDown
expr: up{job="node"} == 0
for: 2m
labels: { severity: warning }
annotations: { summary: "Хост {{ $labels.instance }} ({{ $labels.role }}) недоступен" }
- alert: AppTierDegraded
expr: count(up{job="node", role="app"} == 0) >= 2
for: 2m
labels: { severity: critical, service: shopflow-api }
annotations: { summary: "Недоступно {{ $value }} из 4 app-узлов" }
- alert: HostMemoryCritical
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.05
for: 5m
labels: { severity: critical }
annotations: { summary: "{{ $labels.instance }}: доступно < 5% RAM" }
- alert: HostCpuStealHigh
expr: avg by (instance) (rate(node_cpu_seconds_total{mode="steal"}[5m])) > 0.10
for: 10m
labels: { severity: warning }
annotations: { summary: "{{ $labels.instance }}: CPU steal {{ $value | humanizePercentage }} — проверьте core_fraction" }
- alert: HostDiskWillFillIn4h
expr: |
predict_linear(node_filesystem_avail_bytes{fstype=~"ext4|xfs"}[6h], 4*3600) < 0
and node_filesystem_avail_bytes{fstype=~"ext4|xfs"} / node_filesystem_size_bytes{fstype=~"ext4|xfs"} < 0.20
and node_filesystem_readonly{fstype=~"ext4|xfs"} == 0
for: 15m
labels: { severity: critical }
annotations:
summary: "{{ $labels.instance }}:{{ $labels.mountpoint }} заполнится < 4 ч"
runbook_url: https://wiki.shopflow.example/runbooks/RB-PGDISK
- alert: HostDiskUsageCritical
expr: 1 - node_filesystem_avail_bytes{fstype=~"ext4|xfs"} / node_filesystem_size_bytes{fstype=~"ext4|xfs"} > 0.90
for: 5m
labels: { severity: critical }
annotations:
summary: "{{ $labels.instance }}:{{ $labels.mountpoint }} занято {{ $value | humanizePercentage }}"
runbook_url: https://wiki.shopflow.example/runbooks/RB-PGDISK
- alert: HostDiskWriteLatencyHigh
expr: |
rate(node_disk_write_time_seconds_total[5m]) / rate(node_disk_writes_completed_total[5m]) > 0.02
and rate(node_disk_writes_completed_total[5m]) > 10
for: 10m
labels: { severity: warning }
annotations: { summary: "{{ $labels.instance }}/{{ $labels.device }}: латентность записи {{ $value | humanizeDuration }}" }
- alert: HostConntrackHigh
expr: node_nf_conntrack_entries / node_nf_conntrack_entries_limit > 0.75
for: 5m
labels: { severity: warning }
- alert: HostSystemdUnitFailed
expr: node_systemd_unit_state{state="failed"} == 1
for: 5m
labels: { severity: warning }
annotations: { summary: "{{ $labels.instance }}: юнит {{ $labels.name }} в failed" }
# ---------- PostgreSQL / Patroni ----------
- name: postgresql
rules:
- alert: PatroniNoLeader
expr: (sum(patroni_primary{cluster="shopflow-pg"}) or vector(0)) < 1
for: 1m
labels: { severity: critical, service: postgresql }
annotations:
summary: "В кластере Patroni нет лидера — запись в БД невозможна"
runbook_url: https://wiki.shopflow.example/runbooks/RB-PG
- alert: PatroniMultipleLeaders
expr: sum(patroni_primary{cluster="shopflow-pg"}) > 1
for: 30s
labels: { severity: critical, service: postgresql }
annotations: { summary: "SPLIT-BRAIN: {{ $value }} лидера Patroni" }
- alert: PatroniFailoverHappened
expr: changes(max(patroni_postgres_timeline{cluster="shopflow-pg"})[10m:]) > 0
labels: { severity: warning, service: postgresql }
annotations: { summary: "Сменился timeline PostgreSQL — был failover/switchover" }
- alert: PostgresNoFailoverCandidate
expr: (count(shopflow_pg_replication_lag_bytes < 1048576) or vector(0)) == 0
for: 10m
labels: { severity: critical, service: postgresql }
annotations:
summary: "Нет реплики с лагом < maximum_lag_on_failover — автоматический failover не сработает"
runbook_url: https://wiki.shopflow.example/runbooks/RB-PG
- alert: PostgresReplicaLagging
expr: shopflow_pg_replication_lag_bytes > 1048576
for: 5m
labels: { severity: warning, service: postgresql }
annotations: { summary: "Реплика {{ $labels.application_name }} отстаёт на {{ $value | humanize1024 }}B" }
- alert: PostgresConnectionsHigh
expr: sum by (instance) (pg_stat_activity_count) / on(instance) pg_settings_max_connections > 0.9
for: 5m
labels: { severity: critical, service: postgresql }
- alert: PostgresXidWraparoundRisk
expr: max by (instance, datname) (shopflow_pg_xid_age) > 1.5e9
labels: { severity: critical, service: postgresql }
- alert: PostgresBackupTooOld
expr: time() - walg_last_backup_timestamp_seconds > 50*3600
labels: { severity: critical, service: postgresql }
# ---------- Redis / BullMQ ----------
- name: redis
rules:
- alert: RedisNoMaster
expr: (count(redis_instance_info{role="master"}) or vector(0)) < 1
for: 1m
labels: { severity: critical, service: redis }
- alert: RedisMultipleMasters
expr: count(redis_instance_info{role="master"}) > 1
for: 1m
labels: { severity: critical, service: redis }
- alert: RedisMemoryCritical
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.9 and redis_memory_max_bytes > 0
for: 5m
labels: { severity: critical, service: redis }
- alert: RedisEvictionsWithQueue
expr: increase(redis_evicted_keys_total[5m]) > 0
labels: { severity: critical, service: redis }
annotations: { summary: "Redis вытесняет ключи — возможна потеря задач BullMQ" }
- alert: RedisSentinelQuorumAtRisk
expr: min(redis_sentinel_master_ok_sentinels) < 2
for: 2m
labels: { severity: critical, service: redis }
- alert: RedisEvictionPolicyNotNoeviction
expr: redis_maxmemory_policy_info{policy!="noeviction"} == 1
labels: { severity: warning, service: redis }
- alert: BullMQPaymentsBacklog
expr: redis_key_size{key="bull:payments:wait"} > 200
for: 10m
labels: { severity: critical, service: paygate }
# ---------- Kubernetes (через агент в кластере) ----------
- name: kubernetes
rules:
- alert: PayGateUnavailable
expr: (kube_deployment_status_replicas_available{namespace="payments", deployment="paygate"} or vector(0)) < 1
for: 2m
labels: { severity: critical, service: paygate }
- alert: K8sNodesNotReady
expr: count(kube_node_status_condition{condition="Ready", status="true"} == 0) >= 2
for: 2m
labels: { severity: critical, service: kubernetes }
- alert: K8sCertExpiringSoon
expr: (min by (instance, cert) (k8s_cert_expiry_timestamp_seconds) - time()) / 86400 < 30
labels: { severity: warning, service: kubernetes }
annotations: { summary: "{{ $labels.instance }}: {{ $labels.cert }} истекает через {{ $value | printf \"%.0f\" }} дн." }
- alert: K8sCertExpiringCritical
expr: (min by (instance, cert) (k8s_cert_expiry_timestamp_seconds) - time()) / 86400 < 7
labels: { severity: critical, service: kubernetes }
# ---------- Сам мониторинг ----------
- name: meta
rules:
- alert: Watchdog
expr: vector(1)
labels: { severity: none }
annotations: { summary: "Heartbeat: цепочка Prometheus → Alertmanager жива" }
- alert: AlertmanagerNotificationsFailing
expr: rate(alertmanager_notifications_failed_total[5m]) > 0
for: 10m
labels: { severity: critical }
annotations: { summary: "Alertmanager не может отправить в {{ $labels.integration }}" }
- alert: InfraChecksStale
expr: time() - node_textfile_mtime_seconds{file="infra-checks.prom"} > 900
labels: { severity: warning }
Откуда метрики в этих правилах:
patroni_primaryпоявился в Patroni 3.x. Сверьтесь сcurl -s pg-01:8008/metrics | grep -E '^patroni_(primary|master|postgres_timeline)'.walg_last_backup_timestamp_secondsзаписывает wrapper бэкапа в textfile collector [A4].
Кастомные запросы postgres_exporter, /etc/postgres_exporter/queries.yaml, передаются через --extend.query-path. Флаг помечен устаревшим, но в 0.15 работает. Альтернатива — sql_exporter.
shopflow_pg_replication:
query: |
SELECT application_name, client_addr::text AS client_addr,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes,
COALESCE(EXTRACT(epoch FROM replay_lag), 0) AS lag_seconds
FROM pg_stat_replication WHERE NOT pg_is_in_recovery()
metrics:
- application_name: { usage: LABEL }
- client_addr: { usage: LABEL }
- lag_bytes: { usage: GAUGE, description: "Отставание реплики в байтах" }
- lag_seconds: { usage: GAUGE, description: "replay_lag в секундах" }
shopflow_pg_xid:
query: "SELECT datname, age(datfrozenxid) AS age FROM pg_database WHERE datallowconn"
metrics:
- datname: { usage: LABEL }
- age: { usage: GAUGE }
shopflow_pg_idle_in_tx:
query: |
SELECT COALESCE(MAX(EXTRACT(epoch FROM now() - state_change)), 0) AS max_seconds
FROM pg_stat_activity WHERE state = 'idle in transaction'
metrics:
- max_seconds: { usage: GAUGE }
Флаги redis_exporter на redis-01/02:
redis_exporter --redis.addr=redis://127.0.0.1:6379 --redis.password-file=/etc/redis_exporter/pass.json \
--check-single-keys='db0=bull:payments:wait,db0=bull:payments:failed,db0=bull:emails:wait,db0=bull:emails:failed'
# Sentinel опрашивается через multi-target: /scrape?target=redis://127.0.0.1:26379
6.4 /etc/alertmanager/alertmanager.yml (0.27)
global:
resolve_timeout: 5m
templates: ["/etc/alertmanager/templates/*.tmpl"]
time_intervals:
- name: night-msk
time_intervals:
- times: [{ start_time: "22:00", end_time: "24:00" }, { start_time: "00:00", end_time: "09:00" }]
location: Europe/Moscow
- name: maintenance-window
time_intervals:
- weekdays: [tuesday]
times: [{ start_time: "02:00", end_time: "04:00" }]
location: Europe/Moscow
route:
receiver: telegram
group_by: [alertname, service]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers: [alertname="Watchdog"]
receiver: deadmans-switch
group_wait: 0s
group_interval: 1m
repeat_interval: 1m
- matchers: [severity="critical"]
receiver: pagerduty
repeat_interval: 1h
continue: true # дублировать critical в Telegram для контекста
- matchers: [severity="critical"]
receiver: telegram
- matchers: [severity="warning", urgency="low"]
receiver: telegram
mute_time_intervals: [night-msk]
- matchers: [severity="warning"]
receiver: telegram
mute_time_intervals: [maintenance-window]
- matchers: [severity=~"info|none"]
receiver: "null"
inhibit_rules:
- source_matchers: [severity="critical"]
target_matchers: [severity="warning"]
equal: [alertname, instance]
- source_matchers: [alertname="HostDown"]
target_matchers: [alertname!="HostDown"]
equal: [instance]
- source_matchers: [alertname="PatroniNoLeader"]
target_matchers: [service="postgresql", alertname!~"PatroniNoLeader|PatroniMultipleLeaders"]
- source_matchers: [alertname="ShopFlowAPIExternalProbeDown"]
target_matchers: [alertname=~"ShopFlowAPILatency.*|ShopFlowAPIErrorBudgetBurn.*|ShopFlowAPIProbeFailingSingleLocation"]
- source_matchers: [alertname="RedisNoMaster"]
target_matchers: [service="redis", alertname!="RedisNoMaster"]
receivers:
- name: pagerduty
pagerduty_configs:
- routing_key_file: /etc/alertmanager/secrets/pd_routing_key # chmod 600
severity: critical
description: '{{ template "shopflow.title" . }}'
send_resolved: true
details:
firing: '{{ .Alerts.Firing | len }}'
runbook: '{{ (index .Alerts 0).Annotations.runbook_url }}'
summary: '{{ (index .Alerts 0).Annotations.summary }}'
- name: telegram
telegram_configs:
- bot_token_file: /etc/alertmanager/secrets/tg_bot_token
chat_id: -1001234567890 # ID группы дежурных
parse_mode: HTML
send_resolved: true
message: '{{ template "telegram.shopflow" . }}'
- name: deadmans-switch
webhook_configs:
- url: https://hc-ping.com/<UUID> # в healthchecks.io: период 1m, grace 5m, интеграция PagerDuty
send_resolved: false
- name: "null"
/etc/alertmanager/templates/shopflow.tmpl:
{{ define "shopflow.title" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} ({{ .CommonLabels.service }}){{ end }}
{{ define "telegram.shopflow" }}{{ if eq .Status "firing" }}🔥{{ else }}✅{{ end }} <b>{{ template "shopflow.title" . }}</b>
severity: <b>{{ .CommonLabels.severity }}</b>
{{ range .Alerts }}• <code>{{ .Labels.instance }}</code> {{ .Annotations.summary | html }}
{{ if .Annotations.runbook_url }} 📖 <a href="{{ .Annotations.runbook_url }}">runbook</a>
{{ end }}{{ end }}{{ end }}
6.5 Скрипт проверки: /usr/local/bin/infra-checks.sh (textfile collector)
#!/usr/bin/env bash
# Кастомные проверки для node_exporter textfile collector.
# Роли определяются по наличию файлов: k8s-узлы — сертификаты kubeadm, redis-узлы — политика maxmemory.
set -euo pipefail
OUT_DIR=/var/lib/node_exporter/textfile
OUT="${OUT_DIR}/infra-checks.prom"
TMP="$(mktemp "${OUT_DIR}/.infra-checks.XXXXXX")"
trap 'rm -f "$TMP"' EXIT
emit() { printf '%s\n' "$*" >> "$TMP"; }
cert_end_ts() { # stdin: PEM-сертификат → unix time окончания
local end
end="$(openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)" || return 1
[[ -n "$end" ]] && date -d "$end" +%s
}
# 1. Сертификаты kubeadm (срок по умолчанию 1 год) и kubelet
if [[ -d /etc/kubernetes/pki ]]; then
emit '# HELP k8s_cert_expiry_timestamp_seconds Время окончания действия сертификата k8s'
emit '# TYPE k8s_cert_expiry_timestamp_seconds gauge'
while IFS= read -r crt; do
if ts="$(cert_end_ts < "$crt")"; then
emit "k8s_cert_expiry_timestamp_seconds{cert=\"${crt#/etc/kubernetes/}\"} ${ts}"
fi
done < <(find /etc/kubernetes/pki -name '*.crt' -type f | sort)
for conf in admin.conf controller-manager.conf scheduler.conf; do
f="/etc/kubernetes/${conf}"
[[ -f "$f" ]] || continue
data="$(awk '/client-certificate-data:/ {print $2; exit}' "$f")"
[[ -n "$data" ]] || continue
if ts="$(base64 -d <<<"$data" | cert_end_ts)"; then
emit "k8s_cert_expiry_timestamp_seconds{cert=\"${conf}\"} ${ts}"
fi
done
kubelet_pem=/var/lib/kubelet/pki/kubelet-client-current.pem
if [[ -f "$kubelet_pem" ]] && ts="$(cert_end_ts < "$kubelet_pem")"; then
emit "k8s_cert_expiry_timestamp_seconds{cert=\"kubelet-client-current.pem\"} ${ts}"
fi
fi
# 2. Политика вытеснения Redis (для BullMQ обязательна noeviction)
if systemctl is-active --quiet redis-server 2>/dev/null; then
# пароль лежит в /etc/redis/cli.env как REDISCLI_AUTH=..., права 600
[[ -f /etc/redis/cli.env ]] && set -a && . /etc/redis/cli.env && set +a
policy="$(redis-cli --no-auth-warning CONFIG GET maxmemory-policy 2>/dev/null | sed -n 2p || true)"
emit '# HELP redis_maxmemory_policy_info Текущая maxmemory-policy'
emit '# TYPE redis_maxmemory_policy_info gauge'
emit "redis_maxmemory_policy_info{policy=\"${policy:-unknown}\"} 1"
fi
# 3. Служебная метрика успешного прогона
emit '# TYPE infra_checks_last_run_timestamp_seconds gauge'
emit "infra_checks_last_run_timestamp_seconds $(date +%s)"
chmod 644 "$TMP"
mv -f "$TMP" "$OUT" # атомарная замена: node_exporter не прочитает полуфайл
trap - EXIT
Юниты systemd:
# /etc/systemd/system/infra-checks.service
[Unit]
Description=Infra checks for node_exporter textfile collector
[Service]
Type=oneshot
ExecStart=/usr/local/bin/infra-checks.sh
Nice=10
# /etc/systemd/system/infra-checks.timer
[Unit]
Description=Run infra checks every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target
Флаги node_exporter (в /etc/default/node_exporter или в unit-файле):
--collector.textfile.directory=/var/lib/node_exporter/textfile \
--collector.systemd \
--collector.systemd.unit-include='(shopflow-api|nginx|patroni|etcd|redis-server|redis-sentinel|promtail|kubelet|containerd)\.service' \
--collector.processes \
--web.listen-address=:9100
6.6 Ежедневный e-mail-дайджест: /usr/local/bin/alert-digest.py
Alertmanager не умеет суточные дайджесты. Поэтому история строится по встроенной серии Prometheus ALERTS: число сэмплов, умноженное на evaluation_interval, даёт длительность в минутах.
#!/usr/bin/env python3
"""Суточный дайджест алертов и SLO → sre-team@shopflow.example."""
import json
import os
import smtplib
import urllib.parse
import urllib.request
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from email.mime.text import MIMEText
from html import escape
PROM = os.environ.get("PROM_URL", "http://127.0.0.1:9090")
EVAL_SEC = 15
TO = "sre-team@shopflow.example"
MSK = timezone(timedelta(hours=3))
def query(expr: str) -> list:
url = f"{PROM}/api/v1/query?" + urllib.parse.urlencode({"query": expr})
with urllib.request.urlopen(url, timeout=30) as resp:
body = json.load(resp)
if body.get("status") != "success":
raise RuntimeError(f"Prometheus error: {body}")
return body["data"]["result"]
def scalar(expr: str):
res = query(expr)
return float(res[0]["value"][1]) if res else None
agg = defaultdict(lambda: {"minutes": 0.0, "instances": set()})
for s in query('count_over_time(ALERTS{alertstate="firing", alertname!="Watchdog"}[24h])'):
lbl = s["metric"]
key = (lbl.get("severity", "none"), lbl["alertname"], lbl.get("service", "-"))
agg[key]["minutes"] += float(s["value"][1]) * EVAL_SEC / 60
agg[key]["instances"].add(lbl.get("instance", "-"))
budget = scalar("shopflow:error_budget:remaining_ratio30d")
avail_1d = scalar("1 - shopflow:sli_errors:ratio_rate1d")
p95_max = scalar("max_over_time(shopflow:api_latency_seconds:p95_5m[24h])")
order = {"critical": 0, "warning": 1, "info": 2, "none": 3}
rows = "".join(
f"<tr><td>{escape(sev)}</td><td>{escape(name)}</td><td>{escape(svc)}</td>"
f"<td>{v['minutes']:.0f}</td><td>{escape(', '.join(sorted(v['instances'])))}</td></tr>"
for (sev, name, svc), v in sorted(agg.items(), key=lambda kv: (order.get(kv[0][0], 9), -kv[1]["minutes"]))
) or "<tr><td colspan=5>Алертов за сутки не было 🎉</td></tr>"
def fmt(x, f):
return f.format(x) if x is not None else "n/a"
now = datetime.now(MSK)
html = f"""<h2>ShopFlow: дайджест за 24 ч до {now:%Y-%m-%d %H:%M} MSK</h2>
<ul>
<li>Доступность за сутки: <b>{fmt(avail_1d and avail_1d * 100, '{:.3f}')}%</b> (SLO 99.9%)</li>
<li>Остаток бюджета ошибок (30d rolling): <b>{fmt(budget and budget * 100, '{:.1f}')}%</b></li>
<li>Максимум p95 API: <b>{fmt(p95_max and p95_max * 1000, '{:.0f}')} мс</b> (SLO 300 мс)</li>
</ul>
<table border=1 cellpadding=4 cellspacing=0>
<tr><th>Severity</th><th>Alert</th><th>Service</th><th>Алерто-минуты</th><th>Instances</th></tr>{rows}</table>
<p>Алерто-минуты суммируются по всем instance. Детали инцидентов — в PagerDuty.</p>"""
msg = MIMEText(html, "html", "utf-8")
msg["Subject"] = f"[ShopFlow SRE] Дайджест алертов {now:%Y-%m-%d}"
msg["From"] = os.environ["SMTP_FROM"]
msg["To"] = TO
with smtplib.SMTP(os.environ["SMTP_HOST"], int(os.environ.get("SMTP_PORT", "587")), timeout=30) as smtp:
smtp.starttls()
smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
smtp.send_message(msg)
Остаток бюджета считается по скользящим 30 дням, а не по календарному месяцу. Для отчёта за календарный месяц запросите тот же ряд на 1-е число следующего месяца.
Запуск по таймеру: OnCalendar=*-*-* 09:00:00 Europe/Moscow, EnvironmentFile=/etc/alert-digest.env (права 600).
7. Чек-лист внедрения
Фаза 0. Подготовка (день 1)
- Ответить на 3 вопроса из раздела 0 и скорректировать допущения [A1]–[A8].
- Создать ВМ
mon-01:yc compute instance create --name mon-01 --zone ru-central1-a --cores 4 --memory 16 \ --create-boot-disk image-family=ubuntu-2204-lts,size=30,type=network-ssd \ --create-disk name=mon-01-data,size=200,type=network-ssd \ --network-interface subnet-name=<subnet>,ipv4-address=10.10.0.10 --preemptible=false - Security group для экспортёров: входящий трафик только от
10.10.0.10/32на порты 9100, 9113, 9121, 9187, 9464, 8008, 9080, а для Loki — 3100 от всех узлов VPC:yc vpc security-group update-rules <sg-id> \ --add-rule "direction=ingress,port=9100,protocol=tcp,v4-cidrs=[10.10.0.10/32]" - Проверить
core_fraction=100иpreemptible=falseу всех 12 ВМ:yc compute instance list --format json | jq '.[] | {name, resources, scheduling_policy}'.
Фаза 1. Сбор данных (дни 2–4)
- Развернуть экспортёры через Ansible:
ansible-galaxy collection install prometheus.prometheus, ролиnode_exporter,blackbox_exporter,prometheus,alertmanager. Не используйте пакетprometheus-node-exporterиз Ubuntu 22.04: он устарел (1.3.x). - Установить postgres_exporter (пользователь с ролью
pg_monitor), redis_exporter и nginx-prometheus-exporter;curl -s <host>:<port>/metrics | headсmon-01. - В k8s: kube-state-metrics и Prometheus в режиме агента (
--enable-feature=agent) сremote_writeнаhttp://10.10.0.10:9090/api/v1/write. - Добавить инструментирование
prom-clientв ShopFlow API и PayGate: гистограммы с бакетами 0.3 и 2, шаблонныйroute. - Loki (чанки в Object Storage) и Promtail на всех хостах, JSON-лог в Nginx,
nginx -t && systemctl reload nginx. - Проверить таргеты:
curl -s localhost:9090/api/v1/targets | jq '.data.activeTargets[] | select(.health!="up") | .labels'должен вернуть пустой результат.
Фаза 2. Правила и маршрутизация (дни 5–7)
- Положить правила в git и проверить в CI:
promtool check config prometheus.yml,promtool check rules rules/*.yml,promtool test rules tests/*.yml(unit-тесты на burn rate и inhibit),amtool check-config alertmanager.yml. - Проверить маршруты:
amtool config routes test --config.file=alertmanager.yml severity=critical alertname=PatroniNoLeader # → pagerduty amtool config routes test --config.file=alertmanager.yml severity=warning alertname=HostCpuStealHigh # → telegram - Отправить тестовый алерт в оба канала:
amtool alert add TestAlert severity=critical service=test --annotation=summary="Тест пейджинга" \ --alertmanager.url=http://10.10.0.10:9093 - Настроить healthchecks.io: Watchdog с периодом 1m и grace 5m, интеграция с PagerDuty. Проверить, остановив Alertmanager на 6 минут.
- Включить
infra-checks.timerна k8s- и redis-узлах (systemctl enable --now infra-checks.timer) и дайджест наmon-01. - PagerDuty: сервис «ShopFlow Production», Escalation Policy на 3 уровня, расписание на 4+ человек.
Фаза 3. Дашборды и runbooks (дни 8–10)
- Provisioning Grafana из git: Overview, API, PayGate, PostgreSQL, Redis/BullMQ, Nginx, Kubernetes, Hosts (1860), Capacity. Доступ только через SSO или bastion.
- Runbooks RB-API, RB-PAY, RB-PG, RB-PGDISK, RB-Redis в вики. Ссылка
runbook_urlобязательна в каждом C-алерте. - Аннотации деплоев: CI отправляет
POST /api/annotationsв Grafana.
Фаза 4. Исправление архитектурных рисков (параллельно)
- Третий Redis Sentinel (на
mon-01илиapp-01),quorum 2. - Разделить Redis на сессии (
allkeys-lru) и BullMQ (noeviction), либо выставитьnoevictionдля общего инстанса. -
kubeadm certs check-expirationна каждом control-plane узле; при сроке меньше 60 дней — плановыйkubeadm certs renew all. - Второй blackbox вне Yandex Cloud; во второй фазе —
mon-02вru-central1-bс кластером Alertmanager (порт 9094).
Фаза 5. Проверка боем, game day (день 11–12, в окно обслуживания)
-
sudo systemctl stop shopflow-apiнаapp-01: ожидается WHostSystemdUnitFailed(или автоматический рестарт), пейджа нет. -
patronictl switchover --candidate pg-02: ожидается WPatroniFailoverHappened, приложение работает. -
redis-cli -p 26379 SENTINEL FAILOVER mymaster: master меняется, BullMQ продолжает работу. - Вызвать 5xx на staging-копии и проверить, что burn-rate алерт доходит до PagerDuty за 2–5 минут.
- Заполнить тестовый диск (
fallocate -l) и проверить прогнозный алерт.
Фаза 6. Эксплуатация (постоянно)
- Еженедельный разбор алертов на передаче смены.
- Ежемесячно: отчёт по SLO и бюджету, чистка шумных правил, пересмотр порогов capacity.
- Ежеквартально: game day и проверка восстановления из бэкапа WAL-G.
Похожие промпты
Ревью и рефакторинг PowerShell-скрипта
Разбирает существующий скрипт PowerShell: что сломается на реальных данных, где ошибки глушатся вместо обработки и что переписать в первую очередь, а что оставить.
Скрипт PowerShell под задачу
Пишет скрипт PowerShell с обработкой ошибок, логированием и режимом пробного прогона — так, чтобы его можно было запустить на рабочей машине и не бояться последствий.
Диагностика проблем VPN-подключения
Методично диагностирует проблемы VPN: соединение не устанавливается, скорость падает, утекает DNS — по одному шагу, с командами под вашу ОС.
Помощник по настройке WireGuard и OpenVPN
Пошагово разворачивает собственный VPN-сервер на WireGuard или OpenVPN: готовые команды и конфиги, firewall, клиентские профили и проверка утечек.