Когда нужно мониторить десяток нод, а не сотню — поднимать полноценный observability-стек с Loki, Tempo, Mimir и тремя репликами Prometheus — оверкилл. У меня есть отдельная VPS с 1 vCPU и 2 GB RAM, на которой крутится Prometheus + Grafana + alertmanager и собирает метрики с 14 серверов. Работает второй год, ни разу не падала по OOM.

Стек

Всё в docker compose, ничего из родных пакетов. Хост-система — Debian 12, docker 27.3, compose v2.

services:
  prometheus:
    image: prom/prometheus:v2.55.1
    container_name: prometheus
    restart: unless-stopped
    user: "65534:65534"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./rules:/etc/prometheus/rules:ro
      - prom-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=45d'
      - '--storage.tsdb.retention.size=8GB'
      - '--web.enable-lifecycle'
      - '--storage.tsdb.wal-compression'
    ports:
      - "127.0.0.1:9090:9090"
    mem_limit: 900m

  grafana:
    image: grafana/grafana-oss:11.3.0
    container_name: grafana
    restart: unless-stopped
    volumes:
      - grafana-data:/var/lib/grafana
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GF_PASS}
      GF_USERS_ALLOW_SIGN_UP: "false"
      GF_AUTH_ANONYMOUS_ENABLED: "false"
    ports:
      - "127.0.0.1:3000:3000"
    mem_limit: 400m

  alertmanager:
    image: prom/alertmanager:v0.27.0
    container_name: alertmanager
    restart: unless-stopped
    volumes:
      - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
      - am-data:/alertmanager
    ports:
      - "127.0.0.1:9093:9093"
    mem_limit: 128m

volumes:
  prom-data:
  grafana-data:
  am-data:

mem_limit тут не для красоты — на 2 ГБ хосте без них Prometheus при перезаливе блоков спокойно сжирает 1.5 ГБ и убивает grafana по OOM.

Конфиг Prometheus

Главное — не пихать в один Prometheus сотни таргетов, и не лить туда метрики с интервалом 5 секунд. По дефолту я ставлю 30s и не страдаю.

global:
  scrape_interval: 30s
  evaluation_interval: 30s
  external_labels:
    site: lab1

rule_files:
  - /etc/prometheus/rules/*.yml

alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets:
          - 10.8.0.2:9100
          - 10.8.0.3:9100
          - 10.8.0.4:9100
        labels:
          env: prod
    relabel_configs:
      - source_labels: [__address__]
        regex: '([^:]+):.*'
        target_label: instance
        replacement: '$1'

  - job_name: 'nginx'
    metrics_path: /metrics
    static_configs:
      - targets:
          - 10.8.0.2:9113

Доступ к нодам — через WireGuard (10.8.0.0/24), наружу 9100 порт не торчит. Это, наверное, самое важное правило: никаких public-таргетов без TLS+basic-auth.

node_exporter без лишнего

На таргетах ставлю node_exporter с урезанным набором коллекторов — иначе сжирает CPU на старых VPS.

node_exporter \
  --collector.disable-defaults \
  --collector.cpu \
  --collector.diskstats \
  --collector.filesystem \
  --collector.loadavg \
  --collector.meminfo \
  --collector.netdev \
  --collector.netstat \
  --collector.systemd \
  --collector.uname \
  --collector.vmstat \
  --collector.time \
  --web.listen-address=10.8.0.2:9100

Без --collector.disable-defaults он по дефолту тянет ещё штук тридцать модулей, включая textfile, mdadm, bonding — большинство из которых на VPS бесполезны.

Алёрты, которые реально нужны

Не плодите алёрты на каждую метрику. У меня для 14 нод всего 9 правил, и из них в среднем срабатывает 1-2 в неделю.

groups:
- name: basic
  rules:
  - alert: NodeDown
    expr: up{job="node"} == 0
    for: 3m
    labels:
      severity: critical
    annotations:
      summary: "Нода {{ $labels.instance }} не отвечает"

  - alert: DiskFull
    expr: |
      (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} 
       / node_filesystem_size_bytes) * 100 < 10
    for: 10m
    labels:
      severity: warning

  - alert: HighLoad
    expr: node_load5 > (count by (instance)(node_cpu_seconds_total{mode="idle"})) * 1.5
    for: 15m

  - alert: OOMKillRecent
    expr: increase(node_vmstat_oom_kill[10m]) > 0
    labels:
      severity: critical

Для алёртов в Telegram использую самописный webhook на Go в 80 строк — alertmanager-bot мне показался жирноватым.

Grafana — минимум дашбордов

Не качайте 50 готовых дашбордов с grafana.com. Качайте Node Exporter Full (id 1860), удаляйте оттуда половину панелей, оставляйте то, что реально смотрите. Каждая лишняя панель — лишний PromQL-запрос при каждом рефреше, и через год Prometheus начнёт упираться в CPU на ровном месте.

Резервные копии

Снапшот Prometheus делается одной командой:

curl -XPOST http://127.0.0.1:9090/api/v1/admin/tsdb/snapshot

Снапшот лежит в data/snapshots/, заливаем в S3-совместимое хранилище через restic. Для grafana — просто rsync /var/lib/grafana/grafana.db. Дашборды я ещё держу в git как JSON, через grafana-cli или ручной экспорт.

Итог

Реальный observability на одной VPS — это нормально для парка до 20-30 нод. Когда упрётесь в 1.5-2 ГБ памяти под Prometheus — пора смотреть в сторону VictoriaMetrics или remote_write в централизованное хранилище. До этого момента — не усложняйте.