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 data via gRPC beschikbaar aan de Querier. Voor clusters die in hetzelfde datacenter staan met een dikke, stabiele link naar je centrale infrastructuur is dat heerlijk. Alles pullt. Alles praat met alles. Het leven is goed.

Toen begon ik Prometheus op clusters aan de edge te zetten, en werd het leven minder goed.

Die clusters vallen uren van het netwerk als een site zijn uplink verliest. Sommige zitten achter firewalls die alleen outbound HTTPS toelaten, punt uit. En ik ging van een handvol clusters naar een paar dozijn, met meer onderweg. Opeens vocht het sidecar model op elke site tegen me.

De oplossing is stoppen met pullen en beginnen met pushen. Thanos heeft daar precies een component voor: Receive.

Waar het sidecar model vastloopt

De sidecar setup maakt een paar aannames, en elke aanname is redelijk tot het dat niet meer is:

  1. Thanos Sidecar draait op elke Prometheus instance
  2. Elk cluster krijgt directe object storage toegang om zijn eigen blocks te uploaden
  3. Het netwerk blijft up lang genoeg om die block uploads af te ronden
  4. gRPC reikt terug vanaf de centrale Querier naar elk cluster voor live queries

Binnen één datacenter is hier niets mis mee. De problemen duiken op aan de randen van je topologie:

  • Edge locaties waar de internetlink komt en gaat
  • Air-gapped sites die alleen outbound kunnen praten, nooit inbound
  • Honderden kleine clusters waar één sidecar per stuk echte overhead wordt
  • Multi-tenant platforms waar je niet eens alle clusters bezit die data naar je pushen

Aanname 2 was de eerste die me beet. Object storage credentials uitdelen aan elke edge site, inclusief sites die ik niet volledig beheer, is een blast radius die ik niet wil. Aanname 3 was de tweede die me beet: een fabriekssite die een middag zijn uplink verliest stopt gewoon met blocks uploaden, en er is geen nette manier om in te halen.

Dus hoe verzamel je metrics van clusters die je niet betrouwbaar kunt bereiken, zonder storage credentials naar elk daarvan te sturen?

Thanos Receive draait de richting om

Receive keert de pijlen om. In plaats van sidecars die blocks naar object storage pushen en de Querier die naar binnen reikt, pusht elke Prometheus zijn samples naar één centrale Receive component, en die component bezit alle storage plumbing.

flowchart TD
    subgraph edge["Edge Clusters"]
        subgraph E1["Site A (Fabriek)"]
            P1["Prometheus"]
        end
        subgraph E2["Site B (Magazijn)"]
            P2["Prometheus"]
        end
        subgraph E3["Site C (Retail)"]
            P3["Prometheus"]
        end
    end

    subgraph central["Centraal Cluster"]
        R["Thanos Receive<br/>(HA cluster)"]
        Q["Thanos Querier"]
        SG["Store Gateway"]
        C["Compactor"]
        OS["Object Storage"]
    end

    P1 -->|"remote_write"| R
    P2 -->|"remote_write"| R
    P3 -->|"remote_write"| R

    R --> OS
    OS --> SG
    SG --> Q
    R --> Q
    OS --> C

    Q --> G["Grafana"]

En dit is het stuk dat de overstap goedkoop maakt: Prometheus heeft remote_write al ingebouwd. Je installeert niets nieuws aan de edge. Het is een standaard Prometheus feature die samples streamt naar elk compatibel endpoint, en Receive implementeert dat endpoint. De edge-kant hiervan is config, geen software.

Wat push je oplevert aan de edge

1. De edge deployment krimpt tot bijna niets

Geen sidecar. Geen storage credentials aan de edge. Geen gRPC poorten om inbound te exposen. Het edge cluster draait Prometheus met een remote_write URL en dat is het hele verhaal.

# 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 het hele zaakje. 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-hoek: de site die ik niet volledig beheer krijgt een write-only token naar één endpoint, geen sleutel tot mijn storage.

2. Het overleeft het verliezen van het netwerk

Verlies de link en Prometheus blijft scrapen, en buffert de niet-verzonden samples lokaal. Breng de link terug en het werkt de achterstand weg. De Write-Ahead Log (WAL) doet dit zonder dat je iets 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 terugkwam. Niemand raakte iets aan. De WAL had de samples al die tijd vastgehouden.

3. Outbound-only werkt mét de firewall, niet ertegen

Edge sites hebben meestal paranoïde firewalls, en eerlijk gezegd horen ze dat ook te hebben. Inbound connecties worden geblokkeerd. VPNs zijn een onderhoudsbelasting die ik liever niet over tientallen sites betaal. Outbound HTTPS naar één bekende hostname is daarentegen vrijwel altijd prima. Push past zich aan de vorm van deze netwerken aan in plaats van te eisen dat ze voor mij buigen.

4. Minder draaiend aan de edge

De sidecar laten vallen betekent minder geheugen op edge nodes, één container minder om te patchen en te bewaken, geen gRPC connecties om in leven te houden, en minder manieren waarop dingen kapot kunnen. Op een refurbished mini PC op een afgelegen site telt elk stukje daarvan.

Het opzetten

Centraal cluster: de Receive deployment

Receive is stateful. Het schrijft inkomende samples eerst naar lokale disk en uploadt dan blocks naar object storage op zijn eigen schema, 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 om te bepalen 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"
    ]
  }
]

Sla dit op 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. Dat is wat voorkomt dat je gedupliceerde of gesplitste data krijgt wanneer meerdere replicas tegelijk writes aannemen.

Receive blootstellen aan de buitenwereld

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

Edge cluster: Prometheus naar Receive wijzen

Op elk edge cluster is dit alle Prometheus operator config die je nodig hebt om te beginnen met 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 leerde ik op de vervelende manier: zet je externalLabels vanaf het begin goed. Ze zijn hoe je ziet van welke site een metric komt zodra alles centraal is gepoold. Vergeet ze en je staart naar een Grafana panel zonder te kunnen zeggen of een piek de fabriek of het magazijn is.

Het dichttimmeren

Een Receive endpoint open op het internet zonder auth is een gratis metrics-injectie service voor iedereen die het vindt. Lever dat niet op. Twee opties die ik heb 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 goed genoeg is

Voor sites waar één gedeeld geheim te broos voelt, geeft mutual TLS elk cluster zijn 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

Als verschillende teams of klanten in dezelfde Receive pushen, kun je hun data gescheiden houden met een tenant header:

# 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

Data wordt opgeslagen met het tenant label, wat isolatie in queries mogelijk maakt:

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

Receive labelt elke sample met zijn tenant label, zodat één Receive cluster meerdere geïsoleerde tenants kan bedienen zonder dat hun data in elkaars queries doorbloedt.

De hele reden dat ik dit pad opging was het edge connectiviteitsprobleem, dus het is de moeite waard om even stil te staan bij hoe graceful de degradation echt is.

De WAL buffer tunen

Prometheus parkeert niet-verzonden samples in zijn Write-Ahead Log en speelt ze opnieuw af als het endpoint terugkomt. De buffergrootte is aan jou om te tunen voor 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 scraped, dekt een 100k sample buffer ruwweg 3-4 uur metrics. Hoeveel dat in jouw geval is hangt af van je scrape interval en hoeveel targets elke site heeft, dus dimensioneer het tegen jouw werkelijke worst-case outage in plaats van de mijne.

De remote write bewaken vanaf de edge

Op het edge cluster, alert wanneer remote write begint te falen zodat je weet dat de buffer vol begint te lopen:

- 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 edge-side alert vuurt alleen als de edge je nog kan bereiken. Het geval dat me echt zorgen baart is een site die volledig donker gaat, dus vang die centraal op:

- 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 kiezen

Receive is geen vervanging voor de sidecar, het is het juiste gereedschap voor een ander netwerk. Zo verhouden de twee zich:

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

Gebruik Sidecar wanneer:

  • Alle clusters stabiele connectiviteit hebben
  • Je minimale centrale infrastructuur wilt
  • Real-time queries over alle clusters belangrijk zijn

Gebruik Receive wanneer:

  • Edge clusters intermitterende connectiviteit hebben
  • Je schaalt naar veel (50+) clusters
  • Edge resources beperkt zijn
  • Alleen outbound firewall regels gelden

Je hoeft niet globaal te kiezen. De gezondste setup die ik heb gedraaid is hybride: sidecar op de core, stabiele clusters, Receive voor alles aan de edge, beide voeden dezelfde Querier en dezelfde object storage. De Querier kan het niets schelen hoe de data is aangekomen.

Hoe ik dit echt draai

Die hybride is wat bij mij in productie staat:

# 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 ongeacht welk pad de data nam om er te komen.

Wat dit eigenlijk oploste

Het oorspronkelijke probleem was simpel: ik kon clusters die ik niet betrouwbaar kon bereiken niet zien, en ik wilde geen storage credentials geven aan sites die ik niet volledig vertrouw. Receive beantwoordt allebei. Edge sites rijden connectiviteitsverlies uit op hun lokale WAL, ze houden niets meer vast 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. Het simpele ding naar de edge pushen en het ingewikkelde ding centraal houden betekent dat elke edge site iets is waar ik van begin tot eind over kan redeneren, en de firewall regels matchen wat het security team toch al had gewild. Als een site donker gaat is dat verwacht gedrag met een gedefinieerde recovery, geen incident.

Pull is het juiste antwoord wanneer alles connected blijft. Push is het juiste antwoord wanneer het netwerk andere plannen heeft. Draai beide, en je monitoring stopt met afhangen van perfecte omstandigheden die je toch nooit ging krijgen.