# Правила алертов: инфраструктура и здоровье самого мониторинга. # # Принцип отбора — тот же, что у задачи #3078: сюда попадает только то, что уже # ломалось молча. Правила «на всякий случай» не заводим: лишний алерт, который # никто не разбирает, обесценивает остальные. groups: # ── Здоровье самого наблюдателя ───────────────────────────────────────────── # Пустая панель неотличима от «всё хорошо». Эти правила закрывают именно это. - name: monitoring-self interval: 60s rules: # Всегда горит. Существует ради того, чтобы его ОТСУТСТВИЕ было заметно: # раз в 12 часов приходит подтверждение, что цепочка правило → Alertmanager # → Telegram → человек цела. Молчащий канал — самый частый способ узнать # об аварии последним (в проекте МЕРЫ наружу не ушло ни одного сообщения # с 30 мая, и это выяснилось случайно). - alert: Watchdog expr: vector(1) labels: severity: none annotations: summary: "Сторож мониторинга" # Агент замолчал. На Poincare это единственный источник всех метрик: # если он умер, графики просто перестанут обновляться, оставаясь зелёными. - alert: HostAgentDown expr: up{job="node"} == 0 or absent(up{job="node", host="apps"}) for: 5m labels: severity: critical annotations: summary: "Агент метрик не отвечает" description: "Хост {{ $labels.host }}: node-exporter недоступен более 5 минут. Метрики этого хоста больше не поступают." # Приёмник перестал принимать push. Симптом со стороны центра. - alert: RemoteWriteStalled expr: | absent_over_time(up{job="node", host="apps"}[15m]) for: 5m labels: severity: critical annotations: summary: "С продуктового хоста 15 минут не приходят метрики" description: "Либо лёг агент на Poincare, либо оборван канал до metrics.gendsgn.ru, либо приёмник не принимает remote-write." # ── Хост ──────────────────────────────────────────────────────────────────── - name: host interval: 60s rules: # Диск. На Beget уже был случай, когда занято 79 % и никто не смотрел; # порог 85 % даёт запас на реакцию, а не сообщает о свершившемся факте. - alert: DiskSpaceLow expr: | (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) > 0.85 for: 15m labels: severity: warning annotations: summary: "Диск занят больше 85 %" description: "{{ $labels.host }} {{ $labels.mountpoint }}: занято {{ $value | humanizePercentage }}." - alert: DiskSpaceCritical expr: | (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) > 0.93 for: 5m labels: severity: critical annotations: summary: "Диск почти кончился" description: "{{ $labels.host }} {{ $labels.mountpoint }}: занято {{ $value | humanizePercentage }}. Postgres при заполнении диска останавливается." # Прогноз важнее порога: он ловит утечку до того, как она упрётся в стену. - alert: DiskWillFillIn24h expr: | predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[6h], 24*3600) < 0 for: 30m labels: severity: warning annotations: summary: "По текущему темпу диск кончится за сутки" description: "{{ $labels.host }} {{ $labels.mountpoint }}: экстраполяция по последним 6 часам." - alert: MemoryPressure expr: | (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.90 for: 15m labels: severity: warning annotations: summary: "Памяти доступно меньше 10 %" description: "{{ $labels.host }}: свободной памяти {{ $value | humanizePercentage }} от общей." # Своп сам по себе не беда, но резкий рост означает, что что-то перестало # помещаться. На Beget своп в 1,78 ГБ был симптомом сосуществования прода # и инфраструктуры — и рассосался ровно в момент переезда. - alert: SwapGrowing expr: | node_memory_SwapTotal_bytes > 0 and (1 - node_memory_SwapFree_bytes / node_memory_SwapTotal_bytes) > 0.50 for: 30m labels: severity: warning annotations: summary: "Своп занят больше половины" description: "{{ $labels.host }}: обычно означает, что рабочий набор перестал помещаться в память." # ── Контейнеры ────────────────────────────────────────────────────────────── - name: containers interval: 60s rules: # Цикл перезапусков. Контейнер в restart-loop снаружи выглядит «поднятым», # а деплой при этом зелёный — именно так чуть не уехал в прод пустой пароль # у infra-postgres (#3061). - alert: ContainerRestartLoop expr: | changes(container_start_time_seconds{name!=""}[30m]) > 3 for: 5m labels: severity: warning annotations: summary: "Контейнер перезапускается по кругу" description: "{{ $labels.host }} / {{ $labels.name }}: больше трёх стартов за полчаса." # Подошёл к своему mem_limit — следующий шаг OOM-kill. - alert: ContainerNearMemoryLimit expr: | container_spec_memory_limit_bytes{name!=""} > 0 and container_memory_working_set_bytes{name!=""} / container_spec_memory_limit_bytes{name!=""} > 0.90 for: 15m labels: severity: warning annotations: summary: "Контейнер у своего потолка памяти" description: "{{ $labels.host }} / {{ $labels.name }}: {{ $value | humanizePercentage }} от mem_limit. Дальше OOM-kill." # ── Postgres ──────────────────────────────────────────────────────────────── - name: postgres interval: 60s rules: # Забытая транзакция держит горизонт vacuum и отравляет весь кластер. # Разбор #2607: осиротевшие запросы висели 46 часов. - alert: PostgresLongTransaction expr: pg_activity_horizon_oldest_xact_age_s > 3600 for: 10m labels: severity: warning annotations: summary: "Транзакция открыта больше часа" description: "{{ $labels.host }} / {{ $labels.db }}: {{ $value | humanizeDuration }}. Пока она жива, vacuum не может убрать мёртвые строки во ВСЕЙ базе." - alert: PostgresLongTransactionCritical expr: pg_activity_horizon_oldest_xact_age_s > 21600 for: 10m labels: severity: critical annotations: summary: "Транзакция открыта больше шести часов" description: "{{ $labels.host }} / {{ $labels.db }}: {{ $value | humanizeDuration }}. Это уже влияет на размер базы." - alert: PostgresIdleInTransaction expr: pg_activity_horizon_idle_in_transaction > 3 for: 15m labels: severity: warning annotations: summary: "Соединения висят в открытой транзакции" description: "{{ $labels.host }} / {{ $labels.db }}: {{ $value }} шт. Обычно это незакрытая сессия в коде." # Раздутие. Не мгновенный сигнал, а тренд — но именно его отсутствие # позволило 91 день не замечать 198 апдейтов на строку. - alert: PostgresLowHotUpdateRatio expr: | rate(pg_table_write_amplification_tup_upd[6h]) > 0.5 and rate(pg_table_write_amplification_tup_hot_upd[6h]) / rate(pg_table_write_amplification_tup_upd[6h]) < 0.2 for: 6h labels: severity: warning annotations: summary: "Обновления идут мимо HOT" description: "{{ $labels.host }} / {{ $labels.table }}: доля HOT {{ $value | humanizePercentage }}. Каждый такой апдейт переписывает строку во все индексы и заново тостит длинные поля — так набегает раздутие." - alert: PostgresDeadTuplesHigh expr: | pg_table_write_amplification_dead_tup > 1000000 and pg_table_write_amplification_dead_tup / (pg_table_write_amplification_live_tup + 1) > 0.5 for: 1h labels: severity: warning annotations: summary: "Мёртвых строк больше половины от живых" description: "{{ $labels.host }} / {{ $labels.table }}: {{ $value }} мёртвых. Autovacuum не справляется либо заблокирован долгой транзакцией." # WAL. Замер 20.08: 7 ГБ/сутки при четырёх пользовательских расчётах. - alert: PostgresWalRateHigh expr: rate(pg_wal_bytes_wal_bytes_total[1h]) > 104857600 / 3600 for: 2h labels: severity: warning annotations: summary: "WAL пишется быстрее 100 МБ/час" description: "{{ $labels.host }} / {{ $labels.db }}: {{ $value | humanize1024 }}B/с. Стоит сверить с реальной пользовательской нагрузкой — расхождение означает лишние записи."