guard/docs/research/observability.md
loki5512344 cf9608ce5d
Initial commit: Rampart v0.2.0
Multi-layer DDoS protection for Minecraft servers.

- rampart-core: Edge node with XDP/eBPF + Rust L7 filtering
- rampart-manager: REST API with JWT auth, Redis sync
- rampart-cli: CLI tool for operators
- velocity-plugin: Domain check, HMAC verify, server registry, load balancer
- paper-plugin: Auto-registration, heartbeat, HMAC verify
- dashboard: React + Vite web UI for management
2026-07-20 20:53:32 +02:00

9.7 KiB
Raw Blame History

Observability - Метрики, Трейсинг, Логи

Актуально: v0.3+


Стек

Метрики:   Prometheus → VictoriaMetrics (долгосрочное хранение)
Трейсинг:  OpenTelemetry → Grafana Tempo
Логи:      трейсинг → Loki
Дашборды:  Grafana
Атаки:     ClickHouse (аналитика за месяцы)
Профайлинг: Parca (continuous)
Debug:     tokio-console (async tasks)

Что собираем с каждого компонента

Edge нода (Rust) → порт 9090

rampart_connections_total{node, result}   - total/blocked/allowed
rampart_active_connections{node}
rampart_bytes_proxied_total{node, dir}    - in/out
rampart_handshake_parse_errors_total{node, reason}
rampart_rate_limit_hits_total{node}
rampart_blacklist_size{node}
rampart_xdp_drops_total{node, reason}    - если XDP включён

# Гистограммы (важны для P99)
rampart_handshake_duration_seconds{node}
rampart_proxy_latency_seconds{node}
rampart_hmac_verify_duration_seconds{node}

Velocity нода (Java) → порт 9091

velocity_players_online
velocity_domain_check_failures_total{reason}
velocity_hmac_check_failures_total
velocity_server_registry_size{type}       - hub/survival/skyblock
velocity_balancer_decisions_total{strategy, server_type}
velocity_redis_latency_seconds            - гистограмма

Game сервер (Paper агент) → порт 9092

paper_tps{server, interval}              - 1m/5m/15m
paper_mspt{server}                       - мс на тик
paper_players_online{server}
paper_chunks_loaded{server}
paper_entities_total{server}
paper_memory_used_bytes{server}
paper_memory_max_bytes{server}
paper_gc_pause_seconds{server}           - GC паузы

Prometheus конфиг с авто-дискавери

# prometheus.yml
scrape_configs:

  - job_name: 'rampart-edge'
    static_configs:
      - targets: ['10.0.100.1:9090', '10.0.100.2:9090']

  - job_name: 'rampart-velocity'
    static_configs:
      - targets: ['10.0.0.2:9091', '10.0.0.3:9091']

  # Game серверы - авто-дискавери (Manager генерирует файл из Redis)
  - job_name: 'paper-servers'
    file_sd_configs:
      - files: ['/etc/prometheus/game_servers.json']
        refresh_interval: 30s

  - job_name: 'haproxy'
    static_configs:
      - targets: ['10.0.0.1:8404']

Авто-генерация game_servers.json

// Manager генерирует файл каждые 30 сек
async fn generate_sd_file(redis: &Redis) {
    let servers: Vec<serde_json::Value> = redis
        .hgetall("rampart:servers").await
        .values()
        .map(|raw| {
            let s: ServerEntry = serde_json::from_str(raw).unwrap();
            serde_json::json!({
                "targets": [format!("{}:9092", s.ip)],
                "labels": { "server": s.name, "type": s.server_type }
            })
        })
        .collect();

    std::fs::write(
        "/etc/prometheus/game_servers.json",
        serde_json::to_string_pretty(&servers).unwrap()
    ).unwrap();
}

Alerting правила

# alerts.yml
groups:
  - name: rampart-critical
    rules:

      - alert: DDoSAttack
        expr: |
          rate(rampart_connections_total{result="blocked"}[1m])
          / rate(rampart_connections_total[1m]) > 0.8
        for: 30s
        annotations:
          summary: "DDoS атака на {{ $labels.node }}"

      - alert: EdgeNodeDown
        expr: up{job="rampart-edge"} == 0
        for: 10s
        annotations:
          summary: "Edge нода {{ $labels.instance }} недоступна"

      - alert: VelocityNodeDown
        expr: up{job="rampart-velocity"} == 0
        for: 15s

      - alert: LowTPS
        expr: paper_tps{interval="1m"} < 15
        for: 2m
        annotations:
          summary: "Низкий TPS на {{ $labels.server }}: {{ $value }}"

      - alert: HighMSPT
        expr: paper_mspt > 45
        for: 1m
        annotations:
          summary: "Высокий MSPT: {{ $value }}ms на {{ $labels.server }}"

Алерт в Discord

# alertmanager.yml
receivers:
  - name: discord
    webhook_configs:
      - url: "${DISCORD_WEBHOOK}"
        send_resolved: true
        http_config:
          headers:
            Content-Type: application/json
        title: '{{ .GroupLabels.alertname }}'
        text: |
          {{ range .Alerts }}
          **{{ .Annotations.summary }}**
          {{ end }}

Push vs Pull

Проблема: Edge ноды - дешёвые VDS по всему миру, часто за NAT, с динамическими IP. Prometheus pull (scrape) не сработает если нода за NAT или firewall.

Решение:

Edge ноды → vmagent (push через remote_write) или OTel Collector
  Причина: edge за NAT, динамические IP, firewall блокирует входящие

Manager / Velocity / HAProxy → Prometheus pull (статичные IP внутри WG сети)

Схема

                    ┌──────────────┐
                    │  VictoriaMetrics  │
                    │  (remote_write)  │
                    └───────┬──────┘
                            │
            ┌───────────────┼───────────────┐
            ▼               ▼               ▼
      vmagent          Prometheus      Prometheus
    (edge-eu-1)      (manager)       (velocity)
    push              pull            pull

Конфиг vmagent для edge ноды

# /etc/vmagent.yml
remote_write:
  - url: "https://victoria.rampart.internal/api/v1/write"

scrape_configs:
  - job_name: 'rampart-edge'
    static_configs:
      - targets: ['127.0.0.1:9090']  # localhost - не требует доступа извне

OpenTelemetry - distributed tracing

// src/telemetry.rs

use opentelemetry_otlp::WithExportConfig;
use tracing_opentelemetry::OpenTelemetryLayer;

pub fn init(service: &str, otlp_endpoint: &str) {
    let tracer = opentelemetry_otlp::new_pipeline()
        .tracing()
        .with_exporter(
            opentelemetry_otlp::new_exporter()
                .tonic()
                .with_endpoint(otlp_endpoint)
        )
        .install_batch(opentelemetry_sdk::runtime::Tokio)
        .unwrap();

    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::from_default_env())
        .with(OpenTelemetryLayer::new(tracer))
        .init();
}

// Использование - автоматически создаёт spans
#[tracing::instrument(skip(stream, config))]
pub async fn handle_connection(stream: TcpStream, config: Arc<Config>) {
    let handshake = parse_handshake(&stream).await;  // child span
    filter_request(&handshake).await;                // child span
    proxy_to_backend(stream).await;                  // child span
}

ClickHouse - attack log

Почему не PostgreSQL

SELECT count() WHERE country='CN' AND ts > now()-24h

PostgreSQL: ~2 сек на 100M строк
ClickHouse: ~50 мс на 100M строк
Сжатие:     PostgreSQL ~3:1, ClickHouse ~10:1

Схема

CREATE TABLE rampart.blocked (
    ts          DateTime CODEC(Delta, ZSTD),
    edge        LowCardinality(String),
    src_ip      IPv4,
    src_asn     UInt32,
    src_country LowCardinality(FixedString(2)),
    reason      LowCardinality(String),
    proto_ver   Int32,
    hostname    String CODEC(ZSTD)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (ts, edge, src_ip)
TTL ts + INTERVAL 90 DAY;

-- Materialized View для агрегатов (не пересчитываем каждый раз)
CREATE MATERIALIZED VIEW rampart.blocked_by_country_mv
ENGINE = SummingMergeTree()
ORDER BY (toDate(ts), src_country)
AS SELECT toDate(ts) as date, src_country, count() as hits
   FROM rampart.blocked GROUP BY date, src_country;

Батч запись из Rust

// Не пишем на каждый пакет - накапливаем и сбрасываем раз в секунду
pub struct ClickHouseWriter {
    client: clickhouse::Client,
    buffer: Mutex<Vec<BlockedEvent>>,
}

impl ClickHouseWriter {
    pub async fn flush(&self) {
        let records = { self.buffer.lock().await.drain(..).collect::<Vec<_>>() };
        if records.is_empty() { return; }

        let mut insert = self.client.insert("rampart.blocked").unwrap();
        for r in &records { insert.write(r).await.unwrap(); }
        insert.end().await.unwrap();
    }
}

Parca - continuous profiling

# docker-compose.yml дополнение
  parca:
    image: ghcr.io/parca-dev/parca:latest
    ports:
      - "7070:7070"
    volumes:
      - ./parca.yaml:/etc/parca/parca.yaml

# parca.yaml
object_storage:
  bucket:
    type: FILESYSTEM
    config:
      directory: /tmp/parca

scrape_configs:
  - job_name: 'rampart-edge'
    scrape_interval: 10s
    targets:
      - targets: ['10.0.100.1:7071']  # pprof endpoint
// Включаем pprof endpoint в edge ноде
use pprof::ProfilerGuard;
// GET /debug/pprof/profile → CPU flame graph
// GET /debug/pprof/heap    → heap allocation graph

tokio-console - debug async tasks

# Запуск edge с поддержкой tokio-console
TOKIO_CONSOLE_BIND=10.0.100.1:6669 \
RUST_LOG=tokio=trace \
./rampart-edge

# Подключение (на своей машине)
tokio-console http://10.0.100.1:6669
# Видишь все async tasks, их состояние, сколько они poll'ятся