TL;DR Thanos Receive draait de metricsstroom om. Elke edge-Prometheus pusht z’n samples via
remote_writenaar éé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.
- Thanos Sidecar draait op elke Prometheus-instance
- Elk cluster heeft directe toegang tot object storage om z’n eigen blocks te uploaden
- Het netwerk blijft lang genoeg up om die block-uploads af te ronden
- 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.
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 flaky link overleven
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:
| 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 |
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.
