Настройка мониторинга и оповещений сервера

Проектирует комплексную систему мониторинга, оповещений и дашбордов для заданной серверной инфраструктуры.

// промпт
Ты — опытный SRE/DevOps-инженер с практикой построения наблюдаемости (observability) в продакшене. Спроектируй полную систему мониторинга и оповещений для серверной среды ниже. ## Контекст - **Серверная Среда:** {{servernaia_sreda}} - **Стек Мониторинга:** {{stek_monitoringa}} - **Критичные Сервисы:** {{kriticnye_servisy}} - **Целевой SLA:** {{celevoi_sla}} - **Каналы Оповещений:** {{kanaly_opoveshhenii}} ## Что нужно спроектировать **1. Сбор метрик** - Системные метрики: CPU, память, диск (использование и IOPS), сеть. - Метрики сервисов и приложений, состояние процессов, разбор логов. - Подход к экспортёрам/агентам и интервал опроса. **2. Пороги и правила оповещений** - Для каждой ключевой метрики укажи warning- и critical-порог с обоснованием. - Различай симптомы и причины; опиши, как избежать ложных срабатываний и усталости от алертов (группировка, подавление, окна). - Процедура эскалации и дежурства (on-call). **3. Дашборды** - Перечень панелей для обзорного дашборда и дашборда по сервису. - Ключевые KPI и метрики для планирования мощностей (capacity planning). **4. Автоматическое реагирование** - Где уместны самовосстановление, авто-перезапуск или авто-масштабирование, а где нужен человек. - Краткий runbook на 1–2 типовых инцидента. ## Формат вывода - **Архитектура** — компоненты и поток данных от метрики до оповещения. - **Таблица алертов** — колонки: метрика, порог (warning/critical), серьёзность, действие. - **Конфиги** — примеры правил алертинга и образец скрипта проверки в блоках кода под целевую среду. - **Чек-лист внедрения** — пошаговый порядок развёртывания. Сначала задай мне до 3 уточняющих вопросов, если данных недостаточно. Помечай любые допущения. Используй конкретные значения и команды под указанную среду, а не общие фразы.
Заполните переменные

Пример запуска

Сгенерировано 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. Уточняющие вопросы и допущения

Вопросы. Ответы на них изменят часть порогов и конфигов:

  1. Где размещён стек мониторинга и есть ли точка вне Yandex Cloud? Речь об отдельной ВМ или о размещении внутри k8s. Если все проверки идут изнутри VPC, вы не заметите сбой на входе в облако (NLB, DNS, сертификат, маршрутизация).
  2. Что именно работает в k8s, а что на ВМ? Где запущены PayGate и воркеры BullMQ? Инструментирован ли Node.js через prom-client с гистограммами латентности?
  3. Как устроены 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

Поток данных от метрики до оповещения:

  1. Prometheus опрашивает экспортёры (pull) и получает метрики k8s через remote_write от агента в кластере.
  2. Loki ruler превращает логи Nginx в метрики SLI и отправляет их в Prometheus через remote_write.
  3. Prometheus считает recording rules (SLI, burn rate, capacity).
  4. Alert rules с for и keep_firing_for отправляют сработавшие алерты в Alertmanager.
  5. Alertmanager группирует, подавляет, учитывает окна тишины и маршрутизирует в PagerDuty, Telegram или webhook.
  6. Отдельный скрипт раз в сутки собирает историю из серии 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 Принципы

  1. Будят человека только симптомы: пользователь не может купить или заплатить, либо это случится в ближайшие часы. Сюда относятся burn rate SLO, внешняя проверка, латентность, платежи и надвигающиеся необратимые события (диск, XID wraparound, сертификаты, потеря HA БД).

  2. Причины (CPU, память, лаг) идут в warning → Telegram. Их разбирают в рабочее время. В critical причина поднимается, только если без вмешательства через минуты-часы наступит отказ.

  3. 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)

  1. SLO доступности за 30 дней (stat) и оставшийся бюджет ошибок в % и минутах (gauge).
  2. Burn rate 1h / 6h (time series) с линиями 14.4× и 6×.
  3. RPS и доля 5xx по Nginx, с разбивкой по app-узлам.
  4. p50/p95/p99 латентности API с линией 300 мс.
  5. Платежи: успешные, отказы, техошибки в минуту; p99 обработки с линией 2 с; конверсия «заказ → оплата».
  6. Внешние проверки: статус по двум локациям, время ответа, дней до истечения сертификата.
  7. Состояние компонентов (state timeline): лидер Patroni, master Redis, узлы k8s, shopflow-api на каждом app-узле.
  8. Очереди BullMQ: wait, active, failed.
  9. Активные алерты (Alertmanager datasource) и аннотации деплоев из CI.
  10. 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-*)»

  1. Определить роль узла и что растёт:
    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
    
  2. Если растёт 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 вручную.
  3. Если растёт 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, онлайн
    
    Повторите на всех узлах кластера, чтобы реплики не отстали.
  4. Если свободно меньше 2 % на лидере: проверьте patronictl list. Если реплика в строю и лаг < 1 MB, сделайте patronictl switchover на реплику с большим запасом места, затем расширяйте диск бывшего лидера.
  5. После инцидента: аннотация в Grafana; задача на capacity-алерт «меньше 60 дней до 80 %», если он не сработал заранее.

Runbook RB-PAY: «Платежи: техошибки > 5 % или нет успешных платежей 20 мин»

  1. Отделить нашу проблему от проблемы Ю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'
    
  2. Коды 5xx или таймауты от ЮKassa:
    • проверить статус-страницу ЮKassa и написать в поддержку (номер договора в vault);
    • включить в витрине баннер «оплата временно недоступна» через feature flag, чтобы не копить брошенные корзины;
    • SEV1: уведомить бизнес.
  3. Коды 401/403: проверьте ротацию секретов (kubectl -n payments get secret paygate-yookassa -o yaml, дата изменения). Также проверьте, не изменился ли egress IP NAT-шлюза, если в ЮKassa настроен allowlist.
  4. Ошибки наши (исключения, БД, Redis): проверьте алерты PG и Redis и последний деплой PayGate (kubectl -n payments rollout history deploy/paygate). Откат выполняется через kubectl -n payments rollout undo deploy/paygate с подтверждением IC.
  5. Вебхуки не приходят (растёт возраст pending): найдите POST /payments/webhook в логах Nginx через Loki:
    {job="nginx_access"} | json | request_uri=~"/payments/webhook.*"
    
    Проверьте allowlist IP ЮKassa в security group и Nginx. Когда связь восстановится, ЮKassa повторит доставку, но для «зависших» платежей запустите сверку GET /v3/payments/{id} скриптом reconcile.
  6. После восстановления: сверка списка платежей за интервал инцидента с Ю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: ожидается W HostSystemdUnitFailed (или автоматический рестарт), пейджа нет.
  • patronictl switchover --candidate pg-02: ожидается W PatroniFailoverHappened, приложение работает.
  • 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.

Похожие промпты

IT и Администрирование

Ревью и рефакторинг PowerShell-скрипта

Разбирает существующий скрипт PowerShell: что сломается на реальных данных, где ошибки глушатся вместо обработки и что переписать в первую очередь, а что оставить.

IT и Администрирование

Скрипт PowerShell под задачу

Пишет скрипт PowerShell с обработкой ошибок, логированием и режимом пробного прогона — так, чтобы его можно было запустить на рабочей машине и не бояться последствий.

IT и Администрирование

Диагностика проблем VPN-подключения

Методично диагностирует проблемы VPN: соединение не устанавливается, скорость падает, утекает DNS — по одному шагу, с командами под вашу ОС.

IT и Администрирование

Помощник по настройке WireGuard и OpenVPN

Пошагово разворачивает собственный VPN-сервер на WireGuard или OpenVPN: готовые команды и конфиги, firewall, клиентские профили и проверка утечек.