· · 10 min leestijd Intermediate

Prometheus en Thanos: metrics op schaal

Schaal Prometheus met Thanos voor lange-termijn opslag, globale queries en hoge beschikbaarheid. Praktische gids voor productie deployments.

Een toren tapes tot het plafond, zij haakt een tape van een jaar oud onderuit, een wezen laat een kruiwagen vol vallen

TL;DR Prometheus alleen bewaart een week of twee aan metrics op lokale schijf en kan geen vraag over meerdere clusters heen beantwoorden. Thanos zet een sidecar naast elke Prometheus, stuurt de blocks naar object storage en zet er een querier voor die alles terugleest via dezelfde PromQL. Hieronder de installatie, de vier componenten, de bedrading voor HA en multi-cluster, en de getallen die ik in productie draai.

De eerste keer dat iemand me vroeg “was dit vorige maand trager dan nu?”, had ik geen antwoord. M’n Prometheus onthield twee weken. De data die ik nodig had was allang van de lokale schijf af en weg. Dat gat is de reden dat deze post bestaat.

Prometheus is de standaard voor Kubernetes-metrics, en ik zou hem zo weer kiezen. Het werkt, precies tot het moment dat je langetermijnopslag nodig hebt, of één blik over meer dan één cluster, of hoge beschikbaarheid die een node overleeft. Dan houdt het op, en hard ook.

Thanos breidt Prometheus uit in plaats van het te vervangen. Je houdt de setup die je al snapt, schroeft er een paar componenten op, en je retentie wordt begrensd door je bucket in plaats van je schijf, met één query over al je clusters erbij. Dat “houd wat je kent” is wat mij over de streep trok. Ik laat liever een systeem groeien dat ik al kan doorgronden dan dat ik het inruil voor iets dat alles belooft en niks uitlegt.

Waar één Prometheus ophoudt

Een enkele Prometheus heeft vier harde grenzen. Het is één node, zonder ingebouwde clustering of HA. Het schrijft naar lokale schijf, dus de retentie is wat erop past. Het ziet één cluster, en over twee clusters heen queryen kan gewoon niet. En het downsamplet nooit, dus een metric van vorig jaar kost evenveel ruimte als eentje van vanochtend.

Voor één klein cluster met twee weken retentie doet dat geen pijn. Voor een multi-cluster-productiesetup met compliance-eisen is elk van die vier een blokker. Je loopt ertegenaan. De enige vraag is welke het eerst.

Wat Thanos toevoegt

Thanos wikkelt een handvol componenten om de Prometheus die je al hebt:

Cluster B

Cluster A

Prometheus + Sidecar

Prometheus + Sidecar

Object Storage
(S3/MinIO/GCS)

Querier

Store Gateway

Compactor

Grafana

De sidecar draait naast Prometheus en uploadt z’n blocks naar object storage. De store gateway serveert die historie weer terug uit de bucket. De querier beantwoordt PromQL door de sidecars en de store gateway te bevragen en samen te voegen wat er terugkomt. De compactor downsamplet en dedupliceert wat er in de bucket ligt.

Vier bewegende delen, elk met één taak die ik in m’n hoofd kan houden. Zo’n ontwerp vertrouw ik een stuk meer dan één binary die alles doet.

Installeren met de Bitnami-chart

Begin met de Bitnami Helm chart en zet iets draaiend:

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

helm install thanos bitnami/thanos \
  --namespace monitoring \
  --create-namespace \
  --set objstoreConfig="$(cat thanos-objstore.yaml)"

De object-store-config in thanos-objstore.yaml wijst Thanos naar je bucket:

type: s3
config:
  bucket: thanos-metrics
  endpoint: minio.storage:9000
  access_key: ${MINIO_ACCESS_KEY}
  secret_key: ${MINIO_SECRET_KEY}
  insecure: true  # Voor MinIO zonder TLS

Dat is het skelet. Er wordt nog niks in geüpload, en dat is de volgende stap.

Prometheus met de sidecar

Vertel Prometheus dat de Thanos-sidecar ernaast moet draaien:

apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: prometheus
  namespace: monitoring
spec:
  replicas: 2  # HA paar
  retention: 2h  # Korte lokale retentie, Thanos handelt lange-termijn

  # Thanos sidecar configuratie
  thanos:
    baseImage: quay.io/thanos/thanos
    version: v0.34.0
    objectStorageConfig:
      key: thanos.yaml
      name: thanos-objstore-secret

  # Externe labels voor deduplicatie
  externalLabels:
    cluster: production
    replica: $(POD_NAME)

  # Laat Thanos sidecar Prometheus data benaderen
  storage:
    volumeClaimTemplate:
      spec:
        storageClassName: longhorn
        resources:
          requests:
            storage: 50Gi

De sidecar doet drie dingen:

  1. Stelt Prometheus-data beschikbaar aan de Thanos-querier via gRPC
  2. Uploadt voltooide TSDB-blocks naar object storage
  3. Beantwoordt Store-API-queries voor recente data

Kijk naar die retention: 2h. De lokale schijf houdt nu alleen het meest recente stukje vast. De historie staat in object storage, en de sidecar blijft die aanvullen.

De drie componenten die blocks teruglezen

Nu de sidecar blocks verstuurt, heb je de onderdelen nodig die ze weer teruglezen.

Querier

apiVersion: apps/v1
kind: Deployment
metadata:
  name: thanos-querier
  namespace: monitoring
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: thanos-query
          image: quay.io/thanos/thanos:v0.34.0
          args:
            - query
            - --http-address=0.0.0.0:9090
            - --grpc-address=0.0.0.0:10901
            # Verbind met sidecars
            - --store=dnssrv+_grpc._tcp.prometheus-operated.monitoring.svc
            # Verbind met store gateway
            - --store=dnssrv+_grpc._tcp.thanos-store.monitoring.svc
            # Deduplicatie
            - --query.replica-label=replica
          ports:
            - name: http
              containerPort: 9090
            - name: grpc
              containerPort: 10901

Store gateway

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: thanos-store
  namespace: monitoring
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: thanos-store
          image: quay.io/thanos/thanos:v0.34.0
          args:
            - store
            - --http-address=0.0.0.0:10902
            - --grpc-address=0.0.0.0:10901
            - --data-dir=/var/thanos/store
            - --objstore.config-file=/etc/thanos/objstore.yaml
          volumeMounts:
            - name: objstore-config
              mountPath: /etc/thanos
            - name: data
              mountPath: /var/thanos/store
      volumes:
        - name: objstore-config
          secret:
            secretName: thanos-objstore-secret
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        storageClassName: longhorn
        resources:
          requests:
            storage: 10Gi

Compactor

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: thanos-compactor
  namespace: monitoring
spec:
  replicas: 1  # Slechts één compactor!
  template:
    spec:
      containers:
        - name: thanos-compact
          image: quay.io/thanos/thanos:v0.34.0
          args:
            - compact
            - --http-address=0.0.0.0:10902
            - --data-dir=/var/thanos/compact
            - --objstore.config-file=/etc/thanos/objstore.yaml
            - --retention.resolution-raw=30d
            - --retention.resolution-5m=90d
            - --retention.resolution-1h=1y
            - --wait
          volumeMounts:
            - name: objstore-config
              mountPath: /etc/thanos
            - name: data
              mountPath: /var/thanos/compact

Die replicas: 1 op de compactor is geen typefout. Draai er twee en ze vechten om dezelfde blocks en maken je data kapot. Op die regel staat bij mij een dikke comment, zodat de toekomstige ik niet slim gaat doen.

De retentievlaggen zijn waar de kosten omlaag gaan. Raw data blijft 30 dagen op volle resolutie, de 5m-reeksen blijven 90 dagen en de 1h-reeksen 1 jaar. Oudere data neemt minder ruimte in omdat er per uur minder van is. Je ruilt resolutie voor historie, en voor alles ouder dan een paar weken is dat precies de ruil die ik wil.

GitOps-deployment

Niks hiervan klik ik met de hand in elkaar. Het gaat door ArgoCD net als de rest, zodat de clusterstate in Git staat waar ik hem kan lezen, reviewen en terugdraaien:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: thanos
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://charts.bitnami.com/bitnami
    chart: thanos
    targetRevision: 12.20.0
    helm:
      values: |
        objstoreConfig: |-
          type: s3
          config:
            bucket: thanos-metrics
            endpoint: minio.storage:9000
            insecure: true

        query:
          enabled: true
          replicaCount: 2
          stores:
            - dnssrv+_grpc._tcp.prometheus-operated.monitoring.svc

        storegateway:
          enabled: true
          replicaCount: 2
          persistence:
            size: 20Gi

        compactor:
          enabled: true
          retentionResolutionRaw: 30d
          retentionResolution5m: 90d
          retentionResolution1h: 1y
          persistence:
            size: 50Gi

        ruler:
          enabled: false  # Gebruik Prometheus rules in plaats daarvan

        receive:
          enabled: false  # Gebruiken sidecar mode

  destination:
    server: https://kubernetes.default.svc
    namespace: monitoring

Twee Prometheus-replica’s, één set grafieken

Twee Prometheus-replica’s die dezelfde targets scrapen hebben alleen zin als iets hun resultaten weer aan elkaar naait. Thanos is dat iets.

# Draai twee Prometheus instances
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
spec:
  replicas: 2
  externalLabels:
    replica: $(POD_NAME)  # Verschillend voor elke replica

Beide instances scrapen alles. De querier dedupliceert op het replica-label, zodat je niet elke lijn dubbel ziet:

# Querier configuratie
args:
  - --query.replica-label=replica
  - --query.replica-label=prometheus_replica

Valt er een node weg, of gewoon één Prometheus-pod, dan tekenen de dashboards verder vanaf de replica die over is. Daar draai je die tweede voor.

Eén query over al je clusters

Wijs meerdere clusters naar dezelfde Thanos-deployment en geef elk z’n eigen cluster-label. De eerste:

externalLabels:
  cluster: production-eu
  replica: $(POD_NAME)

En de tweede:

externalLabels:
  cluster: production-us
  replica: $(POD_NAME)

Nu aggregeert de querier over allebei:

# Totaal requests over alle clusters
sum(rate(http_requests_total[5m]))

# Requests per cluster
sum by (cluster) (rate(http_requests_total[5m]))

Eén PromQL-query, elk cluster, één antwoord. Geen drie Grafana-tabbladen meer open, één per cluster, terwijl je de getallen in je hoofd optelt.

Grafana hoeft het niet te weten

Grafana hoeft niet te weten dat Thanos bestaat. Wijs het naar de querier en het denkt dat het tegen een gewone Prometheus praat:

apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-datasources
data:
  thanos.yaml: |
    apiVersion: 1
    datasources:
      - name: Thanos
        type: prometheus
        url: http://thanos-query.monitoring:9090
        access: proxy
        isDefault: true
        jsonData:
          timeInterval: "15s"

Elk dashboard dat je al gebouwd hebt blijft werken. Aan de Grafana-kant is die ene URL de hele verandering.

Recording rules voor de dure queries

Sommige queries zijn duur genoeg dat je ze niet bij elke refresh van een dashboard wilt draaien. Reken ze één keer per minuut uit en lees het resultaat:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: recording-rules
spec:
  groups:
    - name: aggregations
      interval: 1m
      rules:
        # Pre-aggregeer request rate per service
        - record: service:http_requests:rate5m
          expr: sum by (service) (rate(http_requests_total[5m]))

        # Pre-aggregeer error rate
        - record: service:http_errors:rate5m
          expr: sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))

        # Pre-compute availability
        - record: service:availability:ratio
          expr: |
            1 - (
              service:http_errors:rate5m /
              service:http_requests:rate5m
            )

Dashboards queryen daarna de goedkope service:*-reeksen in plaats van bij elke load door de raw data te malen.

Alerting blijft bij Prometheus

Houd alerting dicht bij de data. Draai Alertmanager tegen Prometheus, niet tegen Thanos:

apiVersion: monitoring.coreos.com/v1
kind: Prometheus
spec:
  alerting:
    alertmanagers:
      - namespace: monitoring
        name: alertmanager
        port: web
  ruleSelector:
    matchLabels:
      role: alert-rules

Thanos Ruler bestaat en werkt. Het is ook wéér een component om te snappen en wéér iets dat stuk kan zijn terwijl je al iets anders aan het debuggen bent. Voor de meeste setups is Prometheus-alerting ruim genoeg, en ik heb liever minder onderdelen die me kunnen verrassen.

Thanos zelf in de gaten houden

Thanos stelt z’n eigen Prometheus-metrics beschikbaar, en dat is maar goed ook, want het ding dat je systemen bewaakt is zelf een systeem dat stuk gaat. Deze houd ik in de gaten:

# Sidecar upload succes
thanos_shipper_uploads_total
thanos_shipper_upload_failures_total

# Store gateway performance
thanos_bucket_store_series_fetch_duration_seconds
thanos_bucket_store_block_loads_total

# Compactor health
thanos_compact_group_compactions_total
thanos_compact_group_compaction_failures_total

# Querier performance
thanos_query_gate_duration_seconds

En alert zodra uploads beginnen te falen, want een stille sidecar betekent dat je historie kwijtraakt zonder dat iemand het merkt:

- alert: ThanosSidecarUploadFailing
  expr: increase(thanos_shipper_upload_failures_total[1h]) > 0
  for: 15m
  labels:
    severity: warning
  annotations:
    summary: "Thanos sidecar faalt bij uploaden van blocks"

Wat de opslag kost

Hier verdient downsampling zichzelf terug:

ResolutieData per dag1 jaar kosten (S3)
Raw (15s)~100MB/target~$4/target
5m downsample~3MB/target~$0.12/target
1h downsample~0.5MB/target~$0.02/target

Bewaar raw data voor altijd en de storagerekening groeit elke maand een beetje, tot iemand ernaar vraagt. Downsample, en oude data kost bijna niks.

Voor self-hosted object storage doet MinIO het werk en blijft de data op hardware die van mij is:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: minio
spec:
  template:
    spec:
      containers:
        - name: minio
          image: minio/minio:latest
          args:
            - server
            - /data
            - --console-address
            - ":9001"
          env:
            - name: MINIO_ROOT_USER
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: root-user
            - name: MINIO_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: root-password

Wat ik draai, alles bij elkaar

Zet de stukken bij elkaar en dit is ongeveer wat er in productie draait:

# Prometheus met sidecar
prometheus:
  replicas: 2
  retention: 6h  # Zeer kort, Thanos handelt lange-termijn
  thanos:
    objectStorageConfig:
      name: thanos-objstore
  externalLabels:
    cluster: production
    environment: prod

# Thanos componenten
thanos:
  query:
    replicaCount: 2
    stores:
      - dnssrv+_grpc._tcp.prometheus-operated.monitoring.svc
      - dnssrv+_grpc._tcp.thanos-store.monitoring.svc

  storegateway:
    replicaCount: 2
    persistence:
      size: 50Gi

  compactor:
    retentionResolutionRaw: 14d
    retentionResolution5m: 60d
    retentionResolution1h: 365d
    persistence:
      size: 100Gi

# Object storage
minio:
  replicas: 4
  persistence:
    size: 500Gi

Elk van die getallen heeft een reden. De lokale retentie is 6h omdat de sidecar vaak uploadt, dus hamsteren op lokale schijf heeft geen zin. De raw retentie is 14d, en dat dekt elke debugsessie die ik waarschijnlijk ga hebben. De 1h-reeksen blijven 1 jaar, genoeg trendhistorie voor capacity planning. En de bucket is self-hosted MinIO , dus de metrics blijven op m’n eigen hardware, zonder cloudrekening en zonder derde partij die ze vasthoudt.

Dezelfde chart als de installatie bovenaan. De values zijn wat er veranderd is.

Waar de metrics voor zijn

Metrics zijn hoe ik m’n systemen begrijp in plaats van erover te gokken. De vragen zijn bij elk incident en elke planningssessie dezelfde:

  • Is deze service gezond?
  • Wat veranderde er vlak voordat het misging?
  • Halen we onze SLO’s ?
  • Waar is optimalisatie de moeite waard?

Verlies je langetermijnmetrics, dan heeft “vergeleken met wanneer?” geen antwoord meer. Verlies je queries over clusters heen, dan zie je altijd maar één stuk tegelijk. Prometheus plus Thanos geeft je retentie in jaren, één blik over al je clusters en een HA-paar, achter dezelfde Prometheus-interface die je al kunt lezen.

Die vraag van het begin van deze post, “was dit vorige maand trager?”, heeft nu een antwoord. Eén query tegen de querier, en de data is er omdat de sidecar hem al die tijd heeft geüpload.