Skip to content
Alerte care nu știu în ce cloud rulează
← ← Înapoi la Idei Cloud

Alerte care nu știu în ce cloud rulează

Fiecare cloud îți vinde alarme. CloudWatch Alarms, reguli de alertă Azure Monitor, politici de alertare Cloud Monitoring: fiecare e preconfigurată, fiecare e la câteva click-uri și fiecare e definită într-un format pe care îl înțelege doar propriul cloud. Dacă rulezi în mai mult de unul, ajungi cu trei sisteme de alertare, trei rutări de on-call, trei definiții pentru „API-ul e nesănătos” și, mai devreme sau mai târziu, trei praguri diferite pentru același lucru, pentru că cineva a reglat unul și le-a uitat pe celelalte.

Am argumentat că logurile și trace-urile ar trebui emise de aplicație și stocate undeva unde cloud-ul nu le deține. Alertele sunt a treia piesă, și sunt cea care contează la 02:40, pentru că o alertă e lucrul care decide dacă ești treaz. Iată cum le definim o singură dată, din metrici pe care le emite aplicația, ca „rata de erori peste 1 %” să însemne același lucru pe EKS, GKE și AKS, iar mutarea unui serviciu între ele să nu schimbe ce te sună.

Alertează pe ce vede utilizatorul, nu pe ce vede cloud-ul

Alarmele native ale cloud-ului sunt despre resursele cloud-ului: CPU pe o instanță, numărul de 5xx pe un load balancer, throttle-uri pe o tabelă. Sunt utile și sunt lucrul greșit pe care să sune pager-ul, pentru că un utilizator nu simte CPU. Un utilizator simte un request lent sau eșuat. Așa că alertele care sună sunt pe trei numere per serviciu, măsurate din punctul de vedere al aplicației:

  • Rata: request-uri pe secundă, ca o cădere la zero să fie vizibilă.
  • Erorile: fracțiunea de request-uri care au eșuat.
  • Durata: latența la percentila 95 sau 99.

Vin din același pipeline OpenTelemetry care poartă trace-urile. Conectorul spanmetrics al collector-ului transformă fiecare span de server într-o histogramă de durate și un contor de apeluri, etichetate cu service.name, ruta HTTP, codul de status și, pentru că resourcedetection a rulat înainte, cloud.provider. Nicio instrumentare nouă în aplicație. Span-urile pe care le emiți deja devin metricile pe care alertezi.

connectors:
  spanmetrics:
    histogram:
      explicit: { buckets: [50ms, 100ms, 250ms, 500ms, 1s, 2s, 5s] }
    dimensions:
      - name: http.route
      - name: http.response.status_code
    exemplars: { enabled: true }
    resource_metrics_key_attributes: [service.name, cloud.provider, cloud.region]

service:
  pipelines:
    traces:  { receivers: [otlp], processors: [resourcedetection, batch], exporters: [otlphttp/tempo, spanmetrics] }
    metrics: { receivers: [spanmetrics, otlp], processors: [batch], exporters: [prometheusremotewrite/mimir] }

Metricile merg în Mimir (sau Prometheus simplu, sau VictoriaMetrics; arhitecturii nu-i pasă). Exemplarele sunt pornite, deci un punct de pe graficul de latență trimite la un trace real care l-a produs.

aplicațiiEKS · GKE · AKSspan-uri prin OTLPfără cod de metrici collectorresourcedetection →conectorul spanmetricsrată · erori · durată Mimirlabel-urile includcloud_providerexemplare → Tempo Grafana alertingreguli ca și codburn rate pe SLO-urio regulă, toate cloudurile pagero singură rotație alarme native: CPU RDS, erori NAT, ACU Aurora, limite de cotărămân în fiecare cloud · trimise la același pager cu prioritate mică

Regulile, scrise o singură dată

Regulile de alertă Grafana trăiesc într-un fișier YAML din repository și sunt provizionate la fiecare deploy, la fel ca dashboard-urile. O regulă per SLO, nu una per serviciu per cloud. Query-ul face desfacerea:

# alerting/rules.yaml
groups:
  - name: api-slo
    interval: 1m
    rules:
      - alert: ApiErrorBudgetBurn
        for: 2m
        labels: { severity: page, team: platform }
        annotations:
          summary: 'api error budget burning {{ $labels.cloud_provider }} · {{ $value | humanizePercentage }}'
          runbook: https://runbooks.internal/api-errors
        expr: |
          (
            sum by (cloud_provider) (rate(calls_total{service_name="api", http_response_status_code=~"5.."}[5m]))
            /
            sum by (cloud_provider) (rate(calls_total{service_name="api"}[5m]))
          ) > (14.4 * 0.001)
          and
          (
            sum by (cloud_provider) (rate(calls_total{service_name="api", http_response_status_code=~"5.."}[1h]))
            /
            sum by (cloud_provider) (rate(calls_total{service_name="api"}[1h]))
          ) > (14.4 * 0.001)

E o alertă multi-window de burn rate pentru un SLO de disponibilitate de 99,9 %: sună când rata de erori din ultimele cinci minute și din ultima oră depășesc amândouă de 14,4 ori bugetul de erori, ceea ce corespunde consumării întregului buget pe 30 de zile în cam două zile. sum by (cloud_provider) e toată povestea multi-cloud. O regulă, o expresie, și se declanșează separat pentru aws, gcp și azure, cu cloud-ul în titlul alertei, dacă doar unul dintre ele arde. Mutarea API-ului de pe EKS pe GKE schimbă care valoare de label se declanșează. Nu schimbă regula.

O a doua regulă cu o fereastră mai lungă (6 ore și 3 zile, prag 1×) prinde arderile lente ca un tichet, nu ca o paginare. Latența primește același tratament pe histogram_quantile(0.99, sum by (le, cloud_provider) (rate(duration_bucket{...}[5m]))).

rata de erori api pe cloud · o regulă, trei valori de label SLO 0,1 % 14,4× · paginare aws gcp azure 02:40 · paginare: gcp 14:00–19:00 · ardere lentă: tichet, azure 00:0024:00

Ce rămâne nativ și cum se alătură

Alarmele proprii ale cloud-ului nu dispar; își schimbă rolul. CPU pe RDS, utilizarea ACU pe Aurora, erorile de alocare de porturi pe NAT gateway, throttle-urile DynamoDB, limitele de cotă ale serviciilor: sunt proprietăți ale infrastructurii pe care o deține cloud-ul, iar monitorizarea cloud-ului le vede prima și cel mai bine. Ținem alarmele alea native, definite în același CDK care definește resursa, și le rutăm către același pager ca prioritate mică. Informează; nu trezesc pe nimeni. Dacă un NAT începe să piardă pachete, alerta de rată de erori a API-ului sună oricum în două minute, iar alarma NAT stă acolo în canalul de incident explicând de ce.

Inversiunea asta e tot rostul. Simptomele sună. Cauzele adnotează. Alertele de simptom sunt neutre față de cloud pentru că aplicația a emis datele; alertele de cauză sunt specifice cloud-ului pentru că cloud-ul a emis datele. Niciuna nu se preface a fi cealaltă.

Trei lucruri care ne-au luat mai mult decât ar fi trebuit

Cardinalitatea. spanmetrics cu http.route ca dimensiune e în regulă. Cu http.target (calea brută, cu ID-uri) generează o serie de timp per comandă și Mimir cade. Folosește șablonul rutei, niciodată calea.

Liniște în timpul deploy-urilor. Un rolling deploy produce o rafală de connection reset-uri câteva secunde. for: 2m de pe regula de paginare o absoarbe. Fără el, fiecare deploy a sunat pe cineva în prima săptămână.

Pager-ul trebuie să fie un singur lucru. Aveam CloudWatch mergând într-o unealtă de on-call și Grafana în alta, pentru că fuseseră configurate în momente diferite. Două rotații, două aplicații pe telefon și o noapte în care persoana de gardă pentru una nu era de gardă pentru cealaltă. Totul se rutează acum prin contact point-urile Grafana către o singură rotație. Alarmele native ajung acolo printr-un topic SNS → webhook, ceea ce a luat o oră de configurat și ar fi trebuit să fie din prima zi.

Versiunea scurtă

Alertează pe cele trei numere pe care le simte utilizatorul, derivate din span-uri pe care le emiți deja, stocate într-un backend de metrici pe care îl deții, evaluate de reguli pe care le ții în git cu un sum by (cloud_provider) în ele. Ține alarmele cloud-ului pentru lucrurile cloud-ului, cu prioritate mică, în același pager. Când un serviciu se mută între clouduri, se schimbă o valoare de label și nimeni nu editează o alertă.

Vrei fișierul de reguli pentru propriile servicii, reglat pe SLO-uri reale, nu pe numere rotunde? Hai să vorbim.