3. SLO e Alerta de Burn Rate
Um dashboard precisa de alguém olhando para ele. Um alerta olha por você — e um alerta ruim te acorda às 3 da manhã por nada, até você aprender a ignorá-lo. Este passo transforma uma promessa ao usuário em um alerta que dispara quando, e somente quando, a promessa está em risco. A teoria está na aula de alertas e na aula de SLO.
Da promessa aos números
| Termo | catalog do BoxOffice |
|---|---|
| SLI (indicador) | fração das requisições ao catalog, sem os health checks, não respondidas com 5xx |
| SLO (objetivo) | 99,5% em 28 dias corridos (a janela padrão do curso: quatro semanas inteiras) |
| Orçamento de erros | 0,5% das requisições podem falhar: a 100 req/s, 100 × 86 400 × 28 × 0,005 ≈ 1,21 milhão de requisições com falha, ou cerca de 202 minutos de queda total a cada 28 dias |
| Burn rate (taxa de queima) | quão rápido o orçamento está sendo gasto, relativo a "exatamente no orçamento": fração de erros / 0,005 |
Uma taxa de queima de 1 gasta o orçamento inteiro em exatamente 28 dias. Uma taxa de 14,4 gasta cerca de 2,1% dele em uma hora (14,4 × 1 h / 672 h ≈ 2,1%) e todo ele em menos de dois dias. (Os limiares 14,4 e 6 vêm do SRE workbook do Google, que os derivou para uma janela de 30 dias: 2% e 5% do orçamento; com 28 dias eles significam 2,1% e 5,4% — perto o bastante para manter os números padrão.)
Por que excluir /actuator/* do SLI?
O Docker, o Kubernetes e a borda chamam os endpoints de saúde a cada poucos segundos, e essas chamadas quase nunca falham. Contá-las infla a taxa de sucesso com requisições que nenhum usuário fez — o SLI precisa medir o que os fãs vivem. O mesmo raciocínio exclui dos SLIs de produção a carga sintética que você mesmo gera, a menos que ela seja marcada e filtrada.
Por que "taxa de erros > 1%" é um alerta ruim
| Alerta | Problema |
|---|---|
erros > 1% por 5 min | dispara num soluço de 5 minutos que custou cerca de 0,01% do orçamento (ruído), e fica calado com uma taxa de erros de 0,8% que queima o orçamento inteiro em 17,5 dias (perdido) |
erros > 0 | aciona o tempo todo |
p99 > 1 s em uma instância | mede uma máquina, não usuários |
A resposta do SRE workbook do Google é o alerta de múltiplas janelas e múltiplas taxas de queima: acionar quando o orçamento queima rápido em uma janela longa (é um problema real, não um soluço) e em uma janela curta (ainda está acontecendo — o alerta para logo depois da recuperação).
| Severidade | Taxa de queima | Janela longa | Janela curta | Orçamento gasto quando dispara |
|---|---|---|---|---|
| página | 14,4 | 1 h | 5 min | ≈ 2,1% |
| página | 6 | 6 h | 30 min | ≈ 5,4% |
| ticket | 1 | 3 dias | 6 h | ≈ 10,7% |
Este lab provisiona a primeira regra como página e a segunda como ticket (um sinal mais lento e mais barato para uma equipe menor); a terceira fica como exercício.
As regras, como código
O Grafana carrega regras de alerta de arquivos YAML na sua pasta de provisionamento (montada no passo 1 a partir de infra/grafana/alerting/). Cada regra roda as consultas A e B — a taxa de queima nas duas janelas — e uma expressão matemática C, que é a condição.
# Multiwindow, multi-burn-rate alerts for the catalog availability SLO (Google SRE workbook, ch. 5).
# SLI: share of catalog requests (health checks excluded) NOT answered with 5xx. SLO: 99.5% / 28 days.
apiVersion: 1
groups:
- orgId: 1
name: catalog-slo
folder: BoxOffice
interval: 30s
rules:
- uid: catalog-availability-fast-burn
title: "catalog availability SLO: fast burn"
condition: C
data:
- refId: A # burn rate over the long window (1 h)
relativeTimeRange: { from: 3600, to: 0 }
datasourceUid: prometheus
model:
refId: A
instant: true
expr: |
(
sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*", http_response_status_code=~"5.."}[1h]))
/ sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*"}[1h]))
) / 0.005
- refId: B # burn rate over the short window (5 min): stops paging soon after recovery
relativeTimeRange: { from: 300, to: 0 }
datasourceUid: prometheus
model:
refId: B
instant: true
expr: |
(
sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*", http_response_status_code=~"5.."}[5m]))
/ sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*"}[5m]))
) / 0.005
- refId: C
datasourceUid: __expr__
model:
refId: C
type: math
expression: "$A > 14.4 && $B > 14.4"
for: 1m
noDataState: OK
execErrState: Error
labels:
severity: page
slo: catalog-availability
annotations:
summary: "catalog is burning its 28-day error budget 14.4x too fast"
description: 'Burn rate {{ printf "%.1f" $values.A.Value }} (1 h) and {{ printf "%.1f" $values.B.Value }} (5 min): at this pace about 2% of the 28-day budget is gone in one hour.'
- uid: catalog-availability-slow-burn
title: "catalog availability SLO: slow burn"
condition: C
data:
- refId: A # 6 h window
relativeTimeRange: { from: 21600, to: 0 }
datasourceUid: prometheus
model:
refId: A
instant: true
expr: |
(
sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*", http_response_status_code=~"5.."}[6h]))
/ sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*"}[6h]))
) / 0.005
- refId: B # 30 min window
relativeTimeRange: { from: 1800, to: 0 }
datasourceUid: prometheus
model:
refId: B
instant: true
expr: |
(
sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*", http_response_status_code=~"5.."}[30m]))
/ sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*"}[30m]))
) / 0.005
- refId: C
datasourceUid: __expr__
model:
refId: C
type: math
expression: "$A > 6 && $B > 6"
for: 5m
noDataState: OK
execErrState: Error
labels:
severity: ticket
slo: catalog-availability
annotations:
summary: "catalog is burning its error budget 6x too fast"
description: 'Burn rate {{ printf "%.1f" $values.A.Value }} (6 h) and {{ printf "%.1f" $values.B.Value }} (30 min): about 5% of the 28-day budget in six hours.'
Reinicie o otel-lgtm e abra Alerting → Alert rules: as duas regras aparecem na pasta BoxOffice como Normal.
Dispare-o
Gere tráfego constante e então quebre o banco de que o catalog depende:
Inicie o db assim que a regra disparar e deixe o k6 rodando: com tráfego, a taxa de queima de 5 minutos cai abaixo de 14,4 em um ou dois minutos e a página volta a Normal. Sem tráfego, a fração é 0 / 0 (NaN) e o Grafana mantém o alerta no último estado.
Por que o gateway sonda /actuator/health/readiness
O SLI é medido pelo próprio catalog, então o catalog precisa continuar recebendo as requisições enquanto o db está fora. Isso vale porque o balanceador do gateway (lab 4) sonda /actuator/health/readiness, não /actuator/health: a saúde completa inclui o db e o redis, então, com o db parado, ele marcaria todas as réplicas como down em poucos segundos e responderia 503 ele mesmo — os fãs receberiam erros, o catalog não veria nenhuma requisição e o alerta nunca dispararia. A readiness diz se esta instância pode receber tráfego: tirar todas as réplicas por causa de uma dependência que todas compartilham não ajuda ninguém.
Preveja: quanto tempo até a regra de queima rápida disparar?
Durante a queda toda requisição ao catalog falha, então a taxa de queima curta (5 min) salta para cerca de 1 / 0,005 = 200 em um minuto. A janela longa é a lenta: a fração de erros da última hora precisa passar de 14,4 × 0,5% = 7,2%. Com tráfego constante e uma hora de histórico saudável, isso leva 0,072 × 60 min ≈ 4,3 minutos de queda; some o for: 1m e até 30 s de intervalo de avaliação: ≈ 5–6 minutos até a página. (Se a stack está de pé há menos de uma hora, a janela tem menos histórico saudável e o alerta dispara antes; o teste de estresse do passo 2 há menos de uma hora soma histórico saudável e o atrasa.)
Esse é o compromisso dos alertas de taxa de queima: uma queda total leva minutos para acionar; uma taxa de erros persistente de 4% (taxa de queima 8) é pega pela regra de 6 horas quando a fração de 6 horas passa de 6 × 0,5% = 3%, depois de cerca de 6 h × 3/4 = 4,5 horas; e um soluço de 30 segundos nunca aciona ninguém.
Numa execução de teste deste lab (com o teste de estresse do passo 2 ainda na janela de 1 hora), a regra ficou Pending cerca de 6,5 minutos e Firing cerca de 440 s depois de o db parar, com a descrição "Burn rate 17.0 (1 h) and 200.0 (5 min)". O ticket de queima lenta também disparou, cerca de um minuto depois.
Registre nos seus resultados a hora em que parou o db, a hora em que a regra ficou Pending e Firing, e as taxas de queima mostradas na descrição do alerta.
Armadilhas
- Sem tráfego, sem SLI. A divisão por zero não produz dados;
noDataState: OKmantém uma noite tranquila tranquila, mas um serviço que não recebe tráfego porque a borda caiu parece "saudável". Combine alertas de SLO com uma sonda sintética ou com um alerta de tráfego caindo a zero. - Alerte por sintomas, investigue causas. CPU, heap e pool são para o dashboard; a página é sobre usuários. Um disco cheio que ainda não afeta usuários é um ticket, não uma página.
- Toda página precisa de um link de runbook (um
annotations.runbook_url) dizendo o que checar primeiro — veja a aula de resposta a incidentes.
Exercício: um SLO de latência
Escreva uma terceira regra para a promessa "95% dos GET /catalog/events/{id} terminam em menos de 250 ms". Dica: o SLI é a razão entre a taxa do bucket le="0.25" e a taxa de _count dessa rota; o orçamento é 5%. Dispare-a adicionando latência (no lab 9 você vai usar o Toxiproxy exatamente para isso).