From 77ee26014d6f8f7aba056f75aa0cb49c72c260c8 Mon Sep 17 00:00:00 2001 From: Max Ronzhin Date: Tue, 28 Jul 2026 00:21:01 +0300 Subject: [PATCH] =?UTF-8?q?feat(monitoring):=20=D0=BC=D0=B5=D1=82=D1=80?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=20CPU/RAM/=D0=B4=D0=B8=D1=81=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=85=D0=BE=D1=81=D1=82=D0=B0=20=D0=B8=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дашборд «Пайплайны пост-обработки» покрывал только прикладную логику — нехватка памяти/CPU на сервере была видна только косвенно, по латентности API. Добавлены node-exporter (метрики хоста) и cAdvisor (метрики по контейнерам, профиль monitoring) — оба без публикации портов наружу, Prometheus ходит к ним по внутренней сети compose. Новый дашборд host.json («Хост и контейнеры») и три алерта (HostMemoryLow/HostDiskLow/HostCpuHigh) с порогами под сервер 1gb (8 ГБ RAM, 4 CPU, 50 ГБ диска). --- deploy/docker-compose.yml | 67 +++++ deploy/monitoring/alerts.yml | 55 ++++ .../monitoring/grafana/dashboards/host.json | 245 ++++++++++++++++++ deploy/monitoring/prometheus.yml | 21 +- 4 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 deploy/monitoring/grafana/dashboards/host.json diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 7da150f..efaa3f1 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -704,6 +704,73 @@ services: profiles: ["monitoring"] logging: *default-logging + node-exporter: + # Метрики железа хоста (CPU, память, диск, сеть, load average) — то, + # чего нет ни в одном из приложенческих экспортеров выше. Без + # `network_mode: host` (не нужен: читаем /proc,/sys,/ хоста через + # bind-mount, а Prometheus достаёт их по имени сервиса во внутренней + # сети compose — так безопаснее, не расширяет сетевой доступ контейнера). + image: prom/node-exporter:v1.8.2 + restart: unless-stopped + pid: host + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--path.rootfs=/rootfs' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + # Не публикуем порт наружу вообще (не 127.0.0.1:9100, а совсем без + # ports) — Prometheus ходит к нему по внутренней сети compose + # (`node-exporter:9100`), публикация на хост для этого не нужна. + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9100/metrics >/dev/null || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 10s + profiles: ["monitoring"] + logging: *default-logging + + cadvisor: + # Метрики по контейнерам (CPU/память каждого) — отвечает на вопрос + # «какой из контейнеров ест ресурсы», в отличие от node-exporter + # (только хост целиком). Дороже node-exporter по CPU/RAM (собственные + # ~60-120 МБ RSS + периодический обход cgroups) — на сервере 8 ГБ/4 CPU + # с ~13 контейнерами это приемлемо, но не бесплатно; `--docker_only` + # и `--disable_metrics` ниже отключают часть коллекторов сверх дефолта + # cAdvisor (который уже отключает cpu_topology/hugetlb/memory_numa/ + # process/sched/tcp/udp и т.п.) — здесь дополнительно per-core CPU, + # perf_event и дисковые метрики (диск и так покрыт node-exporter'ом, + # дублировать через cAdvisor незачем). Список допустимых значений — + # `cadvisor -help` (в версии образа меняется, не все имена стабильны + # между релизами — при апгрейде образа сверяться с `-help`). + image: gcr.io/cadvisor/cadvisor:v0.49.2 + restart: unless-stopped + volumes: + - /:/rootfs:ro + - /var/run:/var/run:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + # Только чтение — cAdvisor не должен иметь возможность управлять + # контейнерами, ему нужен только список и статистика. + - /var/run/docker.sock:/var/run/docker.sock:ro + command: + - '--docker_only=true' + - '--disable_metrics=percpu,perf_event,diskIO,disk,app' + # Без ports вообще — тот же принцип, что и у node-exporter: Prometheus + # достаёт метрики по имени сервиса (`cadvisor:8080`) внутри сети compose. + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:8080/healthz >/dev/null || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 15s + profiles: ["monitoring"] + logging: *default-logging + grafana: image: grafana/grafana:13.1.0 restart: unless-stopped diff --git a/deploy/monitoring/alerts.yml b/deploy/monitoring/alerts.yml index f6231fc..46f8ba2 100644 --- a/deploy/monitoring/alerts.yml +++ b/deploy/monitoring/alerts.yml @@ -60,3 +60,58 @@ groups: # суммаризация встанет (задачи будут копиться в очереди summarize, # см. также алерт QueueGrowing). Проверить # `docker compose ps llm` / `llm-gpu` и `docker compose logs llm`. + + # Железо хоста (job `node` — node-exporter). Пороги подобраны под + # конкретный сервер 1gb: 8 ГБ RAM, 4 CPU, 50 ГБ диска — если сервер + # сменится, пересчитать. + - name: vidconf-host + rules: + # MemAvailable — уже честная оценка Linux с учётом того, что легко + # освобождаемый page cache/buffers не в счёт (в отличие от naive + # used = total - free). 10% от 8 ГБ ≈ 800 МБ — `for: 10m`, чтобы не + # дёргать на кратковременный всплеск (например, разовый всплеск + # transcribe/summarize), но успеть среагировать до OOM killer. + - alert: HostMemoryLow + expr: (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 < 10 + for: 10m + labels: + severity: warning + annotations: + summary: "Мало свободной памяти на хосте" + description: >- + Свободно {{ $value | printf "%.1f" }}% RAM дольше 10 минут + (порог 10% ≈ 800 МБ из 8 ГБ). Смотреть, какой контейнер ест + память — дашборд «Хост и контейнеры», топ по памяти (cAdvisor). + + # 50 ГБ диска — 10% ≈ 5 ГБ. `for: 15m` (не мгновенно): диск не растёт + # так же резко, как память, ложные срабатывания на всплеск не грозят, + # но и разовая проверка на границе некритична — 15 минут отсекает шум. + - alert: HostDiskLow + expr: (node_filesystem_avail_bytes{mountpoint="/",fstype!="tmpfs"} / node_filesystem_size_bytes{mountpoint="/",fstype!="tmpfs"}) * 100 < 10 + for: 15m + labels: + severity: warning + annotations: + summary: "Мало места на диске хоста" + description: >- + Свободно {{ $value | printf "%.1f" }}% диска дольше 15 минут + (порог 10% ≈ 5 ГБ из 50 ГБ). Частые причины на этом проекте — + записи транскрибации (`recordings`), логи docker, образы/слои + после пересборки — проверить `docker system df`. + + # 4 CPU. Порог 90% и `for: 15m` — сознательно строже по времени, чем + # у памяти/диска: кратковременные пики от пайплайна пост-обработки + # (транскрибация/суммаризация) — это ожидаемая, не аварийная нагрузка, + # алерт должен ловить именно устойчивую перегрузку, а не обычный всплеск. + - alert: HostCpuHigh + expr: 100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90 + for: 15m + labels: + severity: warning + annotations: + summary: "Устойчиво высокая загрузка CPU хоста" + description: >- + Загрузка CPU {{ $value | printf "%.1f" }}% дольше 15 минут + подряд (порог 90% из 4 ядер). Смотреть топ контейнеров по CPU + (дашборд «Хост и контейнеры», cAdvisor) и латентность API — + возможно, не хватает уровня AI/ресурсов под нагрузку. diff --git a/deploy/monitoring/grafana/dashboards/host.json b/deploy/monitoring/grafana/dashboards/host.json new file mode 100644 index 0000000..b76c709 --- /dev/null +++ b/deploy/monitoring/grafana/dashboards/host.json @@ -0,0 +1,245 @@ +{ + "title": "Хост и контейнеры", + "description": "CPU/RAM/диск/сеть хоста (node-exporter) и разбивка по контейнерам (cAdvisor) — профиль compose monitoring. Пороги на панелях согласованы с deploy/monitoring/alerts.yml (группа vidconf-host).", + "uid": "vidconf-host", + "editable": false, + "timezone": "browser", + "schemaVersion": 39, + "version": 1, + "time": { "from": "now-6h", "to": "now" }, + "refresh": "30s", + "tags": ["vidconf", "host"], + "panels": [ + { + "id": 1, + "title": "Загрузка CPU", + "description": "100 - idle, усреднено по всем ядрам. Порог алерта HostCpuHigh — 90% дольше 15 минут, см. alerts.yml.", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "unit": "percent", "max": 100, "min": 0, "custom": { "drawStyle": "line", "fillOpacity": 10 } }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)", + "legendFormat": "CPU busy", + "refId": "A" + } + ] + }, + { + "id": 2, + "title": "Load average", + "description": "node_load1/5/15 — на 4-ядерном сервере устойчивое значение выше ~4 означает очередь на CPU.", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 5 } }, + "overrides": [] + }, + "targets": [ + { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "node_load1", "legendFormat": "1 мин", "refId": "A" }, + { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "node_load5", "legendFormat": "5 мин", "refId": "B" }, + { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "node_load15", "legendFormat": "15 мин", "refId": "C" } + ] + }, + { + "id": 3, + "title": "Использование RAM", + "description": "(1 - MemAvailable/MemTotal) — MemAvailable уже учитывает легко освобождаемый page cache/buffers. Порог алерта HostMemoryLow — доступно <10% дольше 10 минут, см. alerts.yml.", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "unit": "percent", "max": 100, "min": 0, "custom": { "drawStyle": "line", "fillOpacity": 10 } }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100", + "legendFormat": "RAM used", + "refId": "A" + } + ] + }, + { + "id": 4, + "title": "Своп (использовано)", + "description": "SwapTotal - SwapFree. На этом сервере swap не заведён под мониторинг отдельным алертом — рост от нуля сам по себе сигнал, что памяти уже не хватает (см. HostMemoryLow).", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "unit": "decbytes", "custom": { "drawStyle": "line", "fillOpacity": 10 } }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes", + "legendFormat": "swap used", + "refId": "A" + } + ] + }, + { + "id": 5, + "title": "Свободно на диске (/)", + "description": "node_filesystem_avail_bytes для корня. Порог алерта HostDiskLow — <10% (≈5 ГБ из 50 ГБ) дольше 15 минут.", + "type": "stat", + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 16 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "unit": "decbytes", + "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 5368709120 }] } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "node_filesystem_avail_bytes{mountpoint=\"/\",fstype!=\"tmpfs\"}", + "refId": "A" + } + ] + }, + { + "id": 6, + "title": "RAM сейчас, %", + "description": "Текущий снимок панели «Использование RAM». Порог совпадает с HostMemoryLow (used > 90%).", + "type": "stat", + "gridPos": { "h": 4, "w": 6, "x": 6, "y": 16 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 90 }] } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100", + "refId": "A" + } + ] + }, + { + "id": 7, + "title": "Диск сейчас, %", + "description": "Текущий снимок использования корневого раздела. Порог совпадает с HostDiskLow (used > 90%).", + "type": "stat", + "gridPos": { "h": 4, "w": 6, "x": 12, "y": 16 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 90 }] } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "(1 - node_filesystem_avail_bytes{mountpoint=\"/\",fstype!=\"tmpfs\"} / node_filesystem_size_bytes{mountpoint=\"/\",fstype!=\"tmpfs\"}) * 100", + "refId": "A" + } + ] + }, + { + "id": 8, + "title": "CPU сейчас, %", + "description": "Текущий снимок загрузки CPU. Порог совпадает с HostCpuHigh (>90%).", + "type": "stat", + "gridPos": { "h": 4, "w": 6, "x": 18, "y": 16 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 90 }] } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)", + "refId": "A" + } + ] + }, + { + "id": 9, + "title": "Сетевой трафик", + "description": "rx/tx по физическим интерфейсам хоста (исключены lo/veth/docker/br — это внутренние интерфейсы контейнеров, а не реальный сетевой трафик сервера).", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "unit": "Bps", "custom": { "drawStyle": "line", "fillOpacity": 5 } }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "rate(node_network_receive_bytes_total{device!~\"lo|veth.*|docker.*|br-.*\"}[5m])", + "legendFormat": "rx {{device}}", + "refId": "A" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "rate(node_network_transmit_bytes_total{device!~\"lo|veth.*|docker.*|br-.*\"}[5m])", + "legendFormat": "tx {{device}}", + "refId": "B" + } + ] + }, + { + "id": 10, + "title": "Топ контейнеров по CPU", + "description": "Источник — cAdvisor (job cadvisor). Отвечает на вопрос «кто из контейнеров грузит CPU» при срабатывании HostCpuHigh.", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "unit": "percentunit", "custom": { "drawStyle": "line", "fillOpacity": 10, "stacking": { "mode": "normal" } } }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "topk(5, sum by (name) (rate(container_cpu_usage_seconds_total{name!=\"\"}[5m])))", + "legendFormat": "{{name}}", + "refId": "A" + } + ] + }, + { + "id": 11, + "title": "Топ контейнеров по памяти", + "description": "container_memory_working_set_bytes — это то, что реально учитывает cgroup OOM killer (в отличие от RSS), источник — cAdvisor.", + "type": "timeseries", + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 28 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { "unit": "decbytes", "custom": { "drawStyle": "line", "fillOpacity": 10, "stacking": { "mode": "normal" } } }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "topk(5, container_memory_working_set_bytes{name!=\"\"})", + "legendFormat": "{{name}}", + "refId": "A" + } + ] + } + ] +} diff --git a/deploy/monitoring/prometheus.yml b/deploy/monitoring/prometheus.yml index 15bf565..bfd48e7 100644 --- a/deploy/monitoring/prometheus.yml +++ b/deploy/monitoring/prometheus.yml @@ -1,6 +1,7 @@ # Конфигурация Prometheus (devops) — сбор метрик backend, -# PostgreSQL, Redis и локального LLM-сервера. Поднимается compose-профилем -# `monitoring` (deploy/docker-compose.yml, сервис `prometheus`). +# PostgreSQL, Redis, железа хоста, контейнеров и локального LLM-сервера. +# Поднимается compose-профилем `monitoring` (deploy/docker-compose.yml, +# сервис `prometheus`). # # Имена метрик backend (`vidconf_http_request_duration_seconds`, # `vidconf_pipeline_sessions`, `vidconf_celery_queue_depth`) — КОНТРАКТ с @@ -33,6 +34,22 @@ scrape_configs: static_configs: - targets: ["redis-exporter:9121"] + # Хост целиком: CPU, память, диск, сеть, load average (профиль monitoring + # — сервис node-exporter). Единственный источник, который покажет + # нехватку памяти/CPU на сервере, если она не проявится как рост + # латентности API (см. дашборд host.json). + - job_name: node + static_configs: + - targets: ["node-exporter:9100"] + + # Метрики по каждому контейнеру (CPU/память отдельно у backend, worker, + # postgres и т.д. — профиль monitoring, сервис cadvisor). Отвечает на + # вопрос «какой из контейнеров ест ресурсы», в отличие от job `node` + # (только хост целиком). + - job_name: cadvisor + static_configs: + - targets: ["cadvisor:8080"] + # Локальный LLM-сервер (llama.cpp, LLAMA_ARG_ENDPOINT_METRICS=1). Адрес # `llm:8080` разрешается ОДНИМ из двух compose-сервисов в зависимости от # выбранного при установке пресета — `llm` (CPU, профиль `llm`, уровни