· · 11 min leestijd Advanced

Thanos remote write: push-based metrics voor edge en multi-cluster

Schaal Prometheus met Thanos Receive via push-based remote write. Ideaal voor edge clusters, intermitterende connectiviteit en grote multi-cluster omgevingen.

Kleine installaties op schepen en vrachtwagens sturen tapes naar één verzamelaar, de hengels van een wezen raken in de knoop

TL;DR Thanos Receive draait de metricsstroom om. Elke edge-Prometheus pusht z’n samples via remote_write naar één centraal endpoint, buffert in z’n WAL als de link wegvalt, en ziet nooit een object-storage-credential. Hieronder de Receive StatefulSet, de hashring, de edge-config, twee manieren om het endpoint dicht te timmeren, en de hybride die ik draai met sidecars op de core clusters.

In mijn vorige post over Prometheus en Thanos zette ik de sidecar-architectuur op. Thanos Sidecar draait naast Prometheus, uploadt TSDB-blocks naar object storage en stelt de data via gRPC beschikbaar aan de Querier. Voor clusters in hetzelfde datacenter, met een dikke stabiele link naar de centrale infrastructuur, heb ik daar niks op aan te merken. Alles pullt, en alles kan bij alles.

Toen ging ik Prometheus op clusters aan de edge zetten.

Die clusters vallen uren van het netwerk zodra een site z’n uplink kwijt is. Sommige zitten achter firewalls die outbound HTTPS toelaten en verder niks. En ik ging van een handvol clusters naar een paar dozijn, met meer onderweg. Op elk daarvan zat het sidecar-model me in de weg.

De oplossing is stoppen met pullen en beginnen met pushen. Thanos heeft daar een component voor, Receive, en daar gaat deze post over.

Waar het sidecar-model vastloopt

De sidecar-opzet doet vier aannames. In het datacenter klopten ze alle vier.

  1. Thanos Sidecar draait op elke Prometheus-instance
  2. Elk cluster heeft directe toegang tot object storage om z’n eigen blocks te uploaden
  3. Het netwerk blijft lang genoeg up om die block-uploads af te ronden
  4. gRPC komt vanaf de centrale Querier terug bij elk cluster voor live queries

Aan de randen van je topologie kloppen ze niet meer. Een edge-site waar de internetlink komt en gaat. Een air-gapped site die alleen outbound mag praten, nooit inbound. Honderden kleine clusters waar één sidecar per stuk bij elkaar echte overhead wordt. Een multi-tenant platform waar je niet eens alle clusters bezit die data naar je pushen.

Aanname 2 beet als eerste. Object-storage-credentials uitdelen aan elke edge-site, ook sites die ik niet volledig beheer, is een blast radius waar ik geen zin in heb. Aanname 3 beet als tweede. Een fabriekssite die een middag z’n uplink kwijt is stopt met blocks uploaden, en inhalen is daarna nergens geregeld. Die blocks blijven staan tot iemand het merkt.

Dus ik wilde metrics van clusters die ik niet betrouwbaar kan bereiken, en ik wilde m’n storage-sleutels op geen van die clusters. Heeft Thanos daar een component voor? Ja.

Thanos Receive draait de richting om

Receive keert de pijlen om. In plaats van sidecars die blocks naar object storage pushen en een Querier die naar binnen reikt, pusht elke Prometheus z’n samples naar één centrale Receive, en Receive regelt al het storage-loodgieterswerk.

Centraal Cluster

Edge Clusters

Site C (Retail)

Site B (Magazijn)

Site A (Fabriek)

remote_write

remote_write

remote_write

Prometheus

Prometheus

Prometheus

Thanos Receive
(HA cluster)

Thanos Querier

Store Gateway

Compactor

Object Storage

Grafana

Wat de overstap goedkoop maakt is dat Prometheus remote_write al ingebouwd heeft. Je installeert niks nieuws aan de edge. Het is een standaard Prometheus-feature die samples naar elk compatibel endpoint streamt, en Receive implementeert dat endpoint. Aan de edge-kant is het een paar regels config.

Wat push je oplevert aan de edge

1. De edge-deployment krimpt tot bijna niks

Geen sidecar, geen storage-credentials aan de edge, geen gRPC-poorten inbound open. Het edge-cluster draait Prometheus met een remote_write-URL.

# De HELE edge configuratie
global:
  external_labels:
    cluster: factory-site-a
    region: europe-west

remote_write:
  - url: https://thanos-receive.central.example.com/api/v1/receive
    bearer_token_file: /etc/prometheus/token

Dat is de hele edge-config. Het edge-cluster heeft geen idee dat object storage bestaat, laat staan welke bucket of welke provider. Het scrapet lokaal en stuurt samples upstream. Dit is meteen de sovereignty-kant. Een site die ik niet volledig beheer krijgt een write-only token naar één endpoint, en geen sleutel van m’n storage.

2. Het overleeft een wegvallend netwerk

Trek de link eruit en Prometheus blijft scrapen, met de niet-verzonden samples lokaal in de buffer. Komt de link terug, dan werkt het de achterstand weg. De Write-Ahead Log (WAL) doet dat zonder dat jij er iets voor hoeft te bedraden.

remote_write:
  - url: https://thanos-receive.central.example.com/api/v1/receive
    queue_config:
      capacity: 100000        # Buffer grootte
      max_shards: 50          # Parallelle send workers
      min_shards: 1
      max_samples_per_send: 5000
      batch_send_deadline: 30s
      min_backoff: 1s
      max_backoff: 5m         # Exponential backoff bij failure

Ik heb een fabriekscluster vier uur donker zien staan en daarna stilletjes het hele gat zien backfillen zodra de uplink terug was. Niemand heeft iets aangeraakt. De WAL had de samples al die tijd vastgehouden.

3. Outbound-only werkt mét de firewall

Edge-sites hebben meestal paranoïde firewalls, en terecht. Inbound wordt geblokkeerd. VPN’s zijn een onderhoudsbelasting die ik liever niet over tientallen sites betaal. Outbound HTTPS naar één bekende hostname is daarentegen vrijwel altijd prima. Push past op die netwerken zoals ze zijn, dus ik hoef met niemand te discussiëren over een open poort.

4. Minder draaiend aan de edge

De sidecar laten vallen scheelt geheugen op de edge-nodes, één container minder om te patchen en in de gaten te houden, geen gRPC-verbindingen om in leven te houden, en minder manieren waarop het stuk kan. Op een refurbished mini-pc op een afgelegen site telt elk stukje daarvan.

Het opzetten

De Receive-deployment op het centrale cluster

Receive is stateful. Het schrijft inkomende samples eerst naar lokale disk en uploadt blocks daarna op z’n eigen schema naar object storage, dus het heeft persistent volumes nodig. Draai het als StatefulSet:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: thanos-receive
  namespace: monitoring
spec:
  serviceName: thanos-receive
  replicas: 3
  selector:
    matchLabels:
      app: thanos-receive
  template:
    metadata:
      labels:
        app: thanos-receive
    spec:
      containers:
        - name: thanos-receive
          image: quay.io/thanos/thanos:v0.34.0
          args:
            - receive
            - --http-address=0.0.0.0:10902
            - --grpc-address=0.0.0.0:10901
            - --remote-write.address=0.0.0.0:19291
            - --tsdb.path=/var/thanos/receive
            - --objstore.config-file=/etc/thanos/objstore.yaml
            - --label=receive_replica="$(POD_NAME)"
            - --tsdb.retention=6h
            # Hashring voor HA distributie
            - --receive.hashrings-file=/etc/thanos/hashring.json
            - --receive.local-endpoint=$(POD_NAME).thanos-receive.monitoring.svc:10901
          ports:
            - name: http
              containerPort: 10902
            - name: grpc
              containerPort: 10901
            - name: remote-write
              containerPort: 19291
          volumeMounts:
            - name: data
              mountPath: /var/thanos/receive
            - name: objstore-config
              mountPath: /etc/thanos
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
      volumes:
        - name: objstore-config
          secret:
            secretName: thanos-objstore-secret
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        storageClassName: longhorn
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 50Gi

De hashring houdt replicas eerlijk

Draai je meer dan één Receive-replica, dan heb je een hashring nodig die bepaalt welke replica welke inkomende series bezit:

[
  {
    "endpoints": [
      "thanos-receive-0.thanos-receive.monitoring.svc:10901",
      "thanos-receive-1.thanos-receive.monitoring.svc:10901",
      "thanos-receive-2.thanos-receive.monitoring.svc:10901"
    ]
  }
]

Zet dit in een ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: thanos-receive-hashring
  namespace: monitoring
data:
  hashring.json: |
    [
      {
        "endpoints": [
          "thanos-receive-0.thanos-receive.monitoring.svc:10901",
          "thanos-receive-1.thanos-receive.monitoring.svc:10901",
          "thanos-receive-2.thanos-receive.monitoring.svc:10901"
        ]
      }
    ]

De hashring pint elke time series aan een vaste Receive-instance, zodat samples voor één series altijd op dezelfde replica landen. Zonder hashring krijg je met meerdere replicas die tegelijk writes aannemen gedupliceerde of gesplitste data.

Receive naar buiten openzetten

Edge-clusters moeten Receive van buiten het cluster bereiken, dus zet het achter een load balancer en een Ingress:

apiVersion: v1
kind: Service
metadata:
  name: thanos-receive-lb
  namespace: monitoring
spec:
  type: LoadBalancer
  ports:
    - name: remote-write
      port: 443
      targetPort: 19291
  selector:
    app: thanos-receive
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: thanos-receive
  namespace: monitoring
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
    nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
spec:
  tls:
    - hosts:
        - thanos-receive.example.com
      secretName: thanos-receive-tls
  rules:
    - host: thanos-receive.example.com
      http:
        paths:
          - path: /api/v1/receive
            pathType: Prefix
            backend:
              service:
                name: thanos-receive
                port:
                  number: 19291

De edge-Prometheus naar Receive wijzen

Op elk edge-cluster is dit alle Prometheus-operator-config die je nodig hebt om te gaan pushen:

apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: prometheus
  namespace: monitoring
spec:
  replicas: 1
  retention: 24h  # Lokale retentie voor edge queries

  externalLabels:
    cluster: factory-site-a
    region: europe-west
    environment: production

  remoteWrite:
    - url: https://thanos-receive.example.com/api/v1/receive
      bearerTokenSecret:
        name: thanos-remote-write-token
        key: token
      queueConfig:
        capacity: 50000
        maxShards: 20
        minShards: 1
        maxSamplesPerSend: 2000
        batchSendDeadline: 30s
        minBackoff: 1s
        maxBackoff: 5m
      writeRelabelConfigs:
        # Drop high-cardinality metrics die je centraal niet nodig hebt
        - sourceLabels: [__name__]
          regex: 'go_.*'
          action: drop

Eén ding heb ik op de vervelende manier geleerd. Zet je externalLabels vanaf het begin goed, want daaraan zie je van welke site een metric komt zodra alles centraal bij elkaar zit. Vergeet ze en je staart naar een Grafana-panel zonder te kunnen zeggen of een piek de fabriek is of het magazijn. Dat staren heb ik gedaan.

Het dichttimmeren

Een Receive-endpoint dat open op het internet staat zonder auth is een gratis metrics-injectiedienst voor iedereen die hem vindt. Dat rol je zo niet uit. Ik heb twee opties gebruikt.

Bearer token, als simpel genoeg is

Genereer een token, geef het aan de edge-clusters en laat Receive het controleren:

# Genereer token
openssl rand -hex 32 > receive-token.txt

# Maak secret in centraal cluster
kubectl create secret generic thanos-receive-auth \
  --from-file=token=receive-token.txt \
  -n monitoring

# Maak secret in edge cluster
kubectl create secret generic thanos-remote-write-token \
  --from-file=token=receive-token.txt \
  -n monitoring

Configureer Receive om tokens te valideren:

# Voeg toe aan Receive container args
- --receive.tenant-header=THANOS-TENANT
- --receive.default-tenant-id=anonymous

mTLS, als een gedeeld token niet genoeg is

Voor sites waar één gedeeld geheim te broos voelt, geeft mutual TLS elk cluster z’n eigen identiteit:

# Receive args
- --grpc-server-tls-cert=/etc/tls/server.crt
- --grpc-server-tls-key=/etc/tls/server.key
- --grpc-server-tls-client-ca=/etc/tls/ca.crt

# Edge Prometheus remoteWrite
remoteWrite:
  - url: https://thanos-receive.example.com/api/v1/receive
    tlsConfig:
      certFile: /etc/prometheus/tls/client.crt
      keyFile: /etc/prometheus/tls/client.key
      caFile: /etc/prometheus/tls/ca.crt

Tenants uit elkaar houden

Pushen verschillende teams of klanten naar dezelfde Receive, dan houdt een tenant-header hun data gescheiden:

# Edge cluster A
remoteWrite:
  - url: https://thanos-receive.example.com/api/v1/receive
    headers:
      THANOS-TENANT: tenant-a

# Edge cluster B
remoteWrite:
  - url: https://thanos-receive.example.com/api/v1/receive
    headers:
      THANOS-TENANT: tenant-b

Receive slaat de data op met het tenant-label, dus een query kan één tenant selecteren:

# Query alleen tenant-a data
http_requests_total{tenant_id="tenant-a"}

Elke sample draagt z’n tenant-label, zodat één Receive-cluster meerdere tenants kan bedienen zonder dat hun data in elkaars queries doorlekt.

De hele reden dat ik dit pad opging was het connectiviteitsprobleem aan de edge, dus dit is het stuk waar het mij om gaat. Hoe graceful is de degradation? Graceful genoeg dat ik me over de edge-links geen zorgen meer maak.

De WAL-buffer tunen

Prometheus parkeert niet-verzonden samples in z’n Write-Ahead Log en speelt ze opnieuw af als het endpoint terug is. De buffergrootte tune je zelf op hoe erg je ergste outage wordt:

remoteWrite:
  - url: https://thanos-receive.example.com/api/v1/receive
    queueConfig:
      capacity: 100000       # Samples om te bufferen
      maxShards: 30          # Parallelle senders bij inhalen
      maxSamplesPerSend: 5000
      batchSendDeadline: 60s
      minBackoff: 1s
      maxBackoff: 10m        # Niet hameren als het down is

Voor een typisch edge-cluster dat elke 30s scrapet dekt een buffer van 100k samples ruwweg 3-4 uur metrics. Hoeveel het bij jou is hangt af van je scrape-interval en het aantal targets per site, dus dimensioneer op jouw eigen worst-case outage en niet op de mijne.

De remote write bewaken vanaf de edge

Alert op het edge-cluster zodra remote write begint te falen, dan weet je dat de buffer volloopt:

- alert: PrometheusRemoteWriteFailing
  expr: |
    rate(prometheus_remote_storage_failed_samples_total[5m]) > 0
  for: 15m
  labels:
    severity: warning
  annotations:
    summary: "Prometheus remote write faalt"
    description: "Edge cluster {{ $labels.cluster }} faalt met verzenden metrics"

Vanuit het centrum waken voor stille edges

De alert aan de edge-kant vuurt alleen als de edge jou nog kan bereiken. Het geval waar ik bang voor ben is een site die helemaal donker gaat, dus die vang je centraal:

- alert: EdgeClusterMissing
  expr: |
    absent_over_time(up{job="prometheus", cluster=~"edge-.*"}[1h])
  for: 30m
  labels:
    severity: critical
  annotations:
    summary: "Edge cluster rapporteert niet"
    description: "Geen metrics van {{ $labels.cluster }} voor 1 uur"

Sidecar of Receive, per cluster

Receive en de sidecar lossen verschillende netwerken op, en de tabel laat zien waar ze verschillen:

AspectSidecarReceive
Edge/offline ondersteuningSlechtUitstekend
Real-time queriesJa (via gRPC)Ja (via Receive gRPC)
Resources aan edgeHogerLager
Object storage toegangVereist aan edgeAlleen centraal
Netwerk richtingOutbound + inboundAlleen outbound
Complexiteit aan edgeHogerLager
Block upload latency2h (block grootte)Real-time

Sidecar past als al je clusters stabiele connectiviteit hebben, je minimale centrale infrastructuur wilt en real-time queries over alle clusters belangrijk zijn. Receive past als edge-clusters intermitterende connectiviteit hebben, je naar veel (50+) clusters schaalt, de edge-resources beperkt zijn of de firewall alleen outbound toelaat.

Je hoeft niet globaal te kiezen. De gezondste opzet die ik gedraaid heb is hybride, met sidecars op de core clusters die een stabiele link hebben, Receive voor alles aan de edge, en allebei voeden ze dezelfde Querier en dezelfde object storage. De Querier kan het niks schelen hoe de data binnenkwam.

Hoe ik dit draai

Die hybride staat bij mij in productie:

# Centraal infrastructuur cluster: Sidecar
# (stabiele connectiviteit, heeft real-time queries nodig)
prometheus:
  thanos:
    enabled: true
    objectStorageConfig:
      name: thanos-objstore

# Edge manufacturing sites: Remote Write
# (intermitterende connectiviteit, resource beperkt)
prometheus:
  remoteWrite:
    - url: https://thanos-receive.central:443/api/v1/receive
      bearerTokenSecret:
        name: edge-token
      queueConfig:
        capacity: 150000  # 6+ uur buffer
        maxBackoff: 15m

# Alles voedt dezelfde Thanos Query
thanos:
  query:
    stores:
      - dnssrv+_grpc._tcp.prometheus-operated.monitoring.svc  # Sidecars
      - dnssrv+_grpc._tcp.thanos-receive.monitoring.svc        # Receive
      - dnssrv+_grpc._tcp.thanos-store.monitoring.svc          # Historisch

Edge-sites verliezen nog steeds uren connectiviteit. Ze reconnecten, de metrics backfillen vanzelf, en de centrale Grafana-dashboards tonen het volledige beeld, welk pad de data ook nam.

Wat dit oploste

Het oorspronkelijke probleem was dat ik clusters die ik niet betrouwbaar kon bereiken niet zag, en dat ik geen storage-credentials wilde geven aan sites die ik niet volledig vertrouw. Receive lost allebei op. Edge-sites zitten connectiviteitsverlies uit op hun lokale WAL, ze hebben niks meer in handen dan een write-only token naar één endpoint, en alle storage-complexiteit blijft op één plek die ik wél beheer.

De diepere reden dat ik dit fijn vind sluit aan bij hoe ik over infrastructuur in het algemeen denk. Duw het simpele ding naar de edge en houd het ingewikkelde ding centraal, en elke edge-site wordt iets waar ik van begin tot eind over kan redeneren. De firewallregels zijn wat het securityteam toch al gevraagd zou hebben. Gaat een site nu donker, dan loopt de WAL vol, vuurt de centrale alert na een uur, en vult het gat zich als de link terug is. Niemand opent er een incident voor.

Pull werkt als het netwerk blijft staan, push werkt als dat niet zo is, dus ik draai allebei. Die vier uur fabrieksuitval van eerder in deze post heeft me overtuigd. Niemand heeft iets aangeraakt, en in de grafiek zit geen gat.