Zum Hauptinhalt springen
  1. Blog/

Grafana Alloy vs. node_exporter + cadvisor: Monitoring für mehrere Raspberry Pis

Michael Bäcker (aka BakermanLP)
Autor
Michael Bäcker (aka BakermanLP)
Ich bin Baujahr 1972, lebe in Schwabach und spiele für mein Leben gerne mit Computern und Freunden.

Bei mir laufen mittlerweile vier Raspberry Pis im Dauerbetrieb. Sie tragen einen Reverse Proxy, Pi-hole, ein bisschen DynDNS-Kram und ein paar kleinere Container. Damit ich nicht bei jedem “läuft der eigentlich noch?” erst eine SSH-Session aufmachen muss, sollten alle vier zentral in Grafana auftauchen: CPU, RAM, Netzwerk, und dazu noch, was die einzelnen Docker-Container so treiben.

Die Zielarchitektur stand schnell fest. Auf meinem Server tower laufen bereits VictoriaMetrics und Grafana für mein Energie-Monitoring, da sollten die Pi-Daten einfach mit reinlaufen. Jeder Pi bekommt also einen kleinen Agenten, der lokale Metriken sammelt und per remote_write an VictoriaMetrics schickt.

Blieb nur noch die Frage: mit welchem Agenten? Auf einem Pi hatte ich schon Grafana Alloy im Einsatz, weil es alles in einem Container erledigt: Host-Metriken, Container-Metriken, Versand, alles aus einer Config heraus. Bequem, aber mir kam der Ressourcenverbrauch für das, was so ein Pi eigentlich leisten soll, unverhältnismäßig hoch vor. Die naheliegende Alternative ist die klassische Prometheus-Welt: node_exporter für den Host, cadvisor für die Container und ein schlanker Prometheus im Agent-Modus, der beides nur weiterreicht.

Statt das aus dem Bauch heraus zu entscheiden, habe ich einen echten A/B-Test gefahren: zwei baugleiche Raspberry Pi 4B mit 8 GB RAM, auf dem einen Alloy, auf dem anderen die Drei-Container-Kombination, beide mit identischer Datenabdeckung und demselben Scrape-Intervall. Nach über 76 Stunden Laufzeit stand das Ergebnis fest, und ich habe die Konfiguration danach auf alle vier Pis ausgerollt.

Unten stehen beide Configs komplett, dazu die Zahlen, die am Ende den Ausschlag gegeben haben.

Variante 1: Grafana Alloy
#

Alloy bündelt alles in einem Container. Die docker-compose.yml sieht entsprechend übersichtlich aus:

docker-compose.yml
#

services:
  alloy:
    image: grafana/alloy:latest
    container_name: alloy
    restart: unless-stopped
    network_mode: host
    pid: host
    privileged: true
    env_file: .env
    volumes:
      - ./alloy/config.alloy:/etc/alloy/config.alloy:ro
      - ./data/alloy:/var/lib/alloy
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /sys:/sys:ro
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /run/containerd/containerd.sock:/run/containerd/containerd.sock
      - /var/lib/docker:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    command:
      - run
      - /etc/alloy/config.alloy

Dazu eine .env mit der Ziel-URL:

# URL der zentralen VictoriaMetrics-Instanz (inkl. /api/v1/write)
VICTORIAMETRICS_URL=http://192.168.1.40:8428/api/v1/write

Die eigentliche Logik steckt in config.alloy. Alloy nutzt dafür eine eigene, deklarative Konfigurationssprache statt YAML:

config.alloy
#

// ── System-Metriken ──────────────────────────────────────────
prometheus.exporter.unix "default" {
  procfs_path = "/host/proc"
  sysfs_path  = "/host/sys"
  rootfs_path = "/rootfs"

  filesystem {
    fs_types_exclude     = "^(autofs|binfmt_misc|bpf|cgroup2?|configfs|debugfs|devpts|devtmpfs|tmpfs|fusectl|hugetlbfs|iso9660|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$"
    mount_points_exclude = "^/(dev|proc|run/credentials/.+|sys|var/lib/docker/.+)($|/)"
    mount_timeout        = "5s"
  }

  netclass {
    ignored_devices = "^(veth.*|br-.*|docker.*|[a-f0-9]{15})$"
  }

  netdev {
    device_exclude = "^(veth.*|br-.*|docker.*|[a-f0-9]{15})$"
  }
}

prometheus.scrape "unix" {
  targets         = prometheus.exporter.unix.default.targets
  forward_to      = [prometheus.remote_write.vm.receiver]
  scrape_interval = "15s"
}

// ── Docker Container-Metriken (built-in cAdvisor) ────────────
prometheus.exporter.cadvisor "default" {
  docker_host = "unix:///var/run/docker.sock"
  docker_only = true
}

discovery.relabel "cadvisor" {
  targets = prometheus.exporter.cadvisor.default.targets

  rule {
    target_label = "instance"
    replacement  = constants.hostname
  }
}

prometheus.scrape "cadvisor" {
  targets         = discovery.relabel.cadvisor.output
  forward_to      = [prometheus.remote_write.vm.receiver]
  scrape_interval = "15s"
  job_name        = "cadvisor"
}

// ── Remote Write zu VictoriaMetrics ─────────────────────────
prometheus.remote_write "vm" {
  endpoint {
    url = env("VICTORIAMETRICS_URL")
  }

  external_labels = {
    instance = constants.hostname,
  }
}

constants.hostname setzt das instance-Label automatisch auf den echten Hostnamen des jeweiligen Pi. Diese Datei läuft also auf jedem Host unverändert, ganz ohne Anpassung.

Variante 2: node_exporter + cadvisor + Prometheus im Agent-Modus
#

Hier braucht es drei Container statt einem, dafür ist jeder für sich genommen deutlich schlanker.

docker-compose.yml
#

services:
  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    restart: unless-stopped
    network_mode: host
    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=^/(dev|proc|run/credentials/.+|sys|var/lib/docker/.+)($$|/)'
      - '--collector.filesystem.fs-types-exclude=^(autofs|binfmt_misc|bpf|cgroup2?|configfs|debugfs|devpts|devtmpfs|tmpfs|fusectl|hugetlbfs|iso9660|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$$'
      - '--collector.netdev.device-exclude=^(veth.*|br-.*|docker.*|[a-f0-9]{15})$$'
      - '--collector.netclass.ignored-devices=^(veth.*|br-.*|docker.*|[a-f0-9]{15})$$'

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    restart: unless-stopped
    network_mode: host
    privileged: true
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    devices:
      - /dev/kmsg
    command:
      - '-housekeeping_interval=15s'
      - '-docker_only=true'
      - '-disable_root_cgroup_stats=true'

  prometheus-agent:
    image: prom/prometheus:latest
    container_name: prometheus-agent
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--enable-feature=agent'

Zwei Anpassungen bei cadvisor lohnen sich gegenüber der Standard-Config. Erstens -housekeeping_interval=15s statt der Voreinstellung von 1 Sekunde: Gescraped wird ohnehin nur alle 15 Sekunden, alles darunter ist verschwendete CPU-Zeit fürs Lesen von cgroup-Stats. Zweitens -docker_only=true zusammen mit -disable_root_cgroup_stats=true - sonst erfasst cadvisor zusätzlich sämtliche System-Cgroups und Systemd-Services, die man in so einem Setup gar nicht sehen will.

prometheus.yml
#

global:
  scrape_interval: 15s
  external_labels:
    instance: pi-three

scrape_configs:
  - job_name: 'integrations/unix'
    static_configs:
      - targets: ['localhost:9100']
    relabel_configs:
      - target_label: instance
        replacement: pi-three

  - job_name: 'cadvisor'
    static_configs:
      - targets: ['localhost:8080']
    relabel_configs:
      - target_label: instance
        replacement: pi-three

remote_write:
  - url: http://192.168.1.40:8428/api/v1/write

Der job_name integrations/unix ist kein Zufall, sondern bewusst an Alloys Namensgebung angeglichen. Sonst zeigen fertige Community-Dashboards wie “Node Exporter Full” plötzlich leere Panels, weil die Job-Variable im Dashboard noch auf den alten Namen zeigt. Der instance-Wert (hier pi-three) ist der einzige Teil, der sich von Pi zu Pi unterscheidet, gleich dreimal in dieser Datei, jeweils von Hand pro Host gesetzt.

Der Vergleich: CPU und RAM
#

Damit der Vergleich fair ist, mussten am Ende auf beiden Seiten exakt dieselben Metriken ankommen: keine zusätzlichen Labels, keine fehlenden Collectoren. Erst danach habe ich beide Setups parallel über gut 76 Stunden mitlaufen lassen, gemessen per PromQL direkt gegen VictoriaMetrics.

Metrik Alloy node_exporter + cadvisor + Prometheus-Agent Verhältnis
CPU Ø 0,0962 Cores 0,0436 Cores Alloy ~2,21×
CPU Peak 0,0999 Cores 0,0561 Cores Alloy ~1,78×
RAM Ø 179,0 MiB 119,6 MiB Alloy ~1,50×
RAM Peak 189,9 MiB 142,3 MiB Alloy ~1,34×

Der CPU-Unterschied ist am deutlichsten: Alloy braucht im Schnitt gut das Doppelte. Bei RAM ist der Abstand kleiner, aber immer noch spürbar - gut 50 % im Mittel. Und das bei identischer Datenabdeckung, derselben Scrape-Frequenz und ohne dass eine Seite irgendwelche Abkürzungen genommen hat.

Auf einem Raspberry Pi 4, der nebenbei noch andere Container stemmen soll, ist das kein Rundungsfehler. Drei schlanke Container schlagen bei mir also einen bequemen. Ich habe die Alternative danach auf alle vier Pis im Cluster ausgerollt. Einziger Wermutstropfen: Bei drei Containern statt einem muss man auch drei Update-Zyklen im Auge behalten. Für den Ressourcen-Gewinn nehme ich das gerne in Kauf.