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:
- Thanos Sidecar draait op elke Prometheus instance
- Elk cluster krijgt directe object storage toegang om zijn eigen blocks te uploaden
- Het netwerk blijft up lang genoeg om die block uploads af te ronden
- 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 flaky link overleven
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:
| Aspect | Sidecar | Receive |
|---|---|---|
| Edge/offline ondersteuning | Slecht | Uitstekend |
| Real-time queries | Ja (via gRPC) | Ja (via Receive gRPC) |
| Resources aan edge | Hoger | Lager |
| Object storage toegang | Vereist aan edge | Alleen centraal |
| Netwerk richting | Outbound + inbound | Alleen outbound |
| Complexiteit aan edge | Hoger | Lager |
| Block upload latency | 2h (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.
