Pular para conteúdo

2. Dashboard RED

Dados que você não consegue ler em dez segundos durante um incidente não são observabilidade, são armazenamento. Este passo constrói um dashboard para todos os serviços do BoxOffice, provisionado como código, organizado em torno de dois métodos da aula de sinais:

Método Para Sinais Pergunta que responde
RED (Tom Wilkie) serviços movidos a requisições Rate (taxa), Errors (erros), Duration (duração) "Meus usuários estão sendo bem atendidos?"
USE (Brendan Gregg) recursos Utilization (utilização), Saturation (saturação), Errors (erros) "Algum recurso é o gargalo?"

O RED diz que algo está errado para os usuários; o USE diz onde a capacidade acabou. A linha de cima do dashboard é RED por serviço; a de baixo é USE para as JVMs e seus pools de conexão.

Dashboards como código

O Grafana carrega dashboards de arquivos listados por um provedor. Os dois arquivos ficam no seu repositório, então o dashboard é revisado, versionado e idêntico em todos os notebooks:

infra/grafana/dashboards.yaml
apiVersion: 1

providers:
  - name: BoxOffice
    folder: BoxOffice
    type: file
    options:
      path: /otel-lgtm/boxoffice-dashboards

Você salvou esse provedor no passo 1; os volumes dele o montam na pasta de provisionamento do Grafana e infra/grafana/dashboards/ no caminho para o qual ele aponta. Salve o dashboard abaixo como infra/grafana/dashboards/boxoffice-red.json e reinicie: docker compose restart otel-lgtm. Ele aparece em Dashboards → BoxOffice → BoxOffice — RED.

infra/grafana/dashboards/boxoffice-red.json (clique para expandir)
infra/grafana/dashboards/boxoffice-red.json
{
  "uid": "boxoffice-red",
  "title": "BoxOffice — RED",
  "tags": [
    "boxoffice"
  ],
  "timezone": "browser",
  "schemaVersion": 39,
  "refresh": "10s",
  "time": {
    "from": "now-15m",
    "to": "now"
  },
  "templating": {
    "list": [
      {
        "name": "service",
        "label": "service",
        "type": "query",
        "datasource": {
          "type": "prometheus",
          "uid": "prometheus"
        },
        "query": {
          "query": "label_values(http_server_request_duration_seconds_count, service_name)",
          "refId": "service"
        },
        "definition": "label_values(http_server_request_duration_seconds_count, service_name)",
        "multi": true,
        "includeAll": true,
        "current": {
          "text": "All",
          "value": "$__all"
        },
        "refresh": 2
      }
    ]
  },
  "panels": [
    {
      "id": 1,
      "type": "row",
      "title": "RED: Rate, Errors, Duration",
      "gridPos": {
        "x": 0,
        "y": 0,
        "w": 24,
        "h": 1
      },
      "collapsed": false,
      "panels": []
    },
    {
      "id": 2,
      "type": "timeseries",
      "title": "Rate (requests/s)",
      "description": "R: how much work arrives",
      "datasource": {
        "type": "prometheus",
        "uid": "prometheus"
      },
      "gridPos": {
        "x": 0,
        "y": 1,
        "w": 8,
        "h": 8
      },
      "fieldConfig": {
        "defaults": {
          "unit": "reqps"
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "sum by (service_name) (rate(http_server_request_duration_seconds_count{service_name=~\"$service\"}[$__rate_interval]))",
          "legendFormat": "{{service_name}}"
        }
      ]
    },
    {
      "id": 3,
      "type": "timeseries",
      "title": "Errors (% of requests answered 5xx)",
      "description": "E: the share of that work that fails",
      "datasource": {
        "type": "prometheus",
        "uid": "prometheus"
      },
      "gridPos": {
        "x": 8,
        "y": 1,
        "w": 8,
        "h": 8
      },
      "fieldConfig": {
        "defaults": {
          "unit": "percent"
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "100 * sum by (service_name) (rate(http_server_request_duration_seconds_count{service_name=~\"$service\", http_response_status_code=~\"5..\"}[$__rate_interval])) / sum by (service_name) (rate(http_server_request_duration_seconds_count{service_name=~\"$service\"}[$__rate_interval]))",
          "legendFormat": "{{service_name}}"
        }
      ]
    },
    {
      "id": 4,
      "type": "timeseries",
      "title": "Duration p95 (s)",
      "description": "D: how long the slowest 5% wait",
      "datasource": {
        "type": "prometheus",
        "uid": "prometheus"
      },
      "gridPos": {
        "x": 16,
        "y": 1,
        "w": 8,
        "h": 8
      },
      "fieldConfig": {
        "defaults": {
          "unit": "s"
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "histogram_quantile(0.95, sum by (service_name, le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[$__rate_interval])))",
          "legendFormat": "{{service_name}}"
        }
      ]
    },
    {
      "id": 5,
      "type": "timeseries",
      "title": "Duration p50 / p95 / p99 (all selected services)",
      "description": "The gap between p50 and p99 is the tail",
      "datasource": {
        "type": "prometheus",
        "uid": "prometheus"
      },
      "gridPos": {
        "x": 0,
        "y": 9,
        "w": 12,
        "h": 8
      },
      "fieldConfig": {
        "defaults": {
          "unit": "s"
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "histogram_quantile(0.5, sum by (le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[$__rate_interval])))",
          "legendFormat": "p50"
        },
        {
          "refId": "B",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "histogram_quantile(0.95, sum by (le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[$__rate_interval])))",
          "legendFormat": "p95"
        },
        {
          "refId": "C",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "histogram_quantile(0.99, sum by (le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[$__rate_interval])))",
          "legendFormat": "p99"
        }
      ]
    },
    {
      "id": 6,
      "type": "table",
      "title": "Slowest routes (p95 over 5 min)",
      "datasource": {
        "type": "prometheus",
        "uid": "prometheus"
      },
      "gridPos": {
        "x": 12,
        "y": 9,
        "w": 12,
        "h": 8
      },
      "fieldConfig": {
        "defaults": {
          "unit": "s"
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "instant": true,
          "format": "table",
          "expr": "topk(10, histogram_quantile(0.95, sum by (service_name, http_request_method, http_route, le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[5m]))))"
        }
      ],
      "transformations": [
        {
          "id": "organize",
          "options": {
            "excludeByName": {
              "Time": true
            }
          }
        }
      ]
    },
    {
      "id": 7,
      "type": "row",
      "title": "USE: Utilization, Saturation (JVM and pools)",
      "gridPos": {
        "x": 0,
        "y": 17,
        "w": 24,
        "h": 1
      },
      "collapsed": false,
      "panels": []
    },
    {
      "id": 8,
      "type": "timeseries",
      "title": "CPU utilization (of the container's CPUs)",
      "description": "",
      "datasource": {
        "type": "prometheus",
        "uid": "prometheus"
      },
      "gridPos": {
        "x": 0,
        "y": 18,
        "w": 8,
        "h": 8
      },
      "fieldConfig": {
        "defaults": {
          "unit": "percentunit"
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "avg by (service_name) (jvm_cpu_recent_utilization_ratio{service_name=~\"$service\"})",
          "legendFormat": "{{service_name}}"
        }
      ]
    },
    {
      "id": 9,
      "type": "timeseries",
      "title": "Heap used",
      "description": "",
      "datasource": {
        "type": "prometheus",
        "uid": "prometheus"
      },
      "gridPos": {
        "x": 8,
        "y": 18,
        "w": 8,
        "h": 8
      },
      "fieldConfig": {
        "defaults": {
          "unit": "bytes"
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "sum by (service_name) (jvm_memory_used_bytes{service_name=~\"$service\", jvm_memory_type=\"heap\"})",
          "legendFormat": "{{service_name}}"
        }
      ]
    },
    {
      "id": 10,
      "type": "timeseries",
      "title": "DB connections in use / waiting (saturation)",
      "description": "Waiting > 0 means requests queue for a connection: the pool is saturated",
      "datasource": {
        "type": "prometheus",
        "uid": "prometheus"
      },
      "gridPos": {
        "x": 16,
        "y": 18,
        "w": 8,
        "h": 8
      },
      "fieldConfig": {
        "defaults": {
          "unit": "short"
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "sum by (service_name) (db_client_connections_usage{service_name=~\"$service\", state=\"used\"})",
          "legendFormat": "{{service_name}} used"
        },
        {
          "refId": "B",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "sum by (service_name) (db_client_connections_max{service_name=~\"$service\"})",
          "legendFormat": "{{service_name}} max"
        },
        {
          "refId": "C",
          "datasource": {
            "type": "prometheus",
            "uid": "prometheus"
          },
          "expr": "sum by (service_name) (db_client_connections_pending_requests{service_name=~\"$service\"})",
          "legendFormat": "{{service_name}} waiting"
        }
      ]
    }
  ]
}

Lendo os painéis

Cada painel é uma consulta PromQL sobre o histograma que o agente exporta. Aprenda as quatro formas; todo dashboard RED que você vai ver na vida é uma variação delas.

Painel Forma da consulta Leia como
Rate sum by (service_name) (rate(…_count[$__rate_interval])) requisições por segundo, por serviço
Errors rate(…_count{status=~"5.."}) / rate(…_count) a fração de requisições que falham (não a contagem)
Duration p95 histogram_quantile(0.95, sum by (service_name, le) (rate(…_bucket[…]))) 95% das requisições foram mais rápidas que isto
Rotas mais lentas topk(10, histogram_quantile(0.95, sum by (…, http_route, le) …)) onde olhar primeiro
Conexões do banco db_client_connections_usage{state="used"} vs _max, _pending_requests o pool está saturado quando waiting > 0

Três hábitos fazem a diferença:

  • Agregue antes do quantil. O histogram_quantile precisa receber a sum by (le) das taxas dos buckets: dá para somar buckets entre instâncias, não dá para tirar a média de percentis (a média de três p95 não é o p95 de nada).
  • Erros como fração. 50 erros por segundo é um desastre a 60 req/s e ruído a 60 000 req/s.
  • Percentis vindos de buckets são estimativas. O histogram_quantile interpola dentro de um bucket; com os limites padrão do agente (…, 0,25, 0,5, 0,75, 1, 2,5 s…), um p99 de "0,6 s" quer dizer "algo entre 0,5 e 0,75 s".

Exemplars: do pico ao trace que o causou

O Prometheus do otel-lgtm guarda exemplars: alguns trace ids de amostra presos aos buckets do histograma. No painel Duration, ative Exemplars nas opções da consulta; os losangos no gráfico são requisições reais, e clicar em um abre o seu trace no Tempo. É o caminho mais curto de "o p99 subiu" para "este comando SQL está lento".

Exercício: veja um gargalo aparecer

  1. Rode o teste de estresse em escada do lab 2 contra a borda — ele sobe a taxa de chegada em degraus até PEAK e depois desce: k6 run -e BASE_URL=http://localhost -e PEAK=600 load/stress.js.
  2. Observe Duration p95, CPU utilization e DB connections lado a lado. A curva de latência dobra onde o primeiro recurso satura. Quando waiting sai do zero, as requisições fazem fila por uma conexão (Lei de Little: com o pool cheio, concorrência extra vira tempo de espera); quando a CPU de um serviço chega perto de 100% da CPU do seu contêiner, elas fazem fila pelo processador. Numa execução de teste deste lab o gateway (limitado a uma CPU) saturou primeiro: cerca de 95% de CPU a cerca de 320 req/s, p95 acima de 2 s e 504 depois do seu timeout de 5 s, enquanto o pool do catalog nunca teve uma requisição esperando.
  3. Abra um exemplar no topo da curva: o trace mostra para onde foi o tempo — um grande intervalo antes do span SQL é tempo esperando uma conexão, não executando SQL; um span longo do gateway em volta de um span curto do catalog é tempo na fila do gateway.

Escreva nos seus resultados qual recurso saturou primeiro e a taxa de requisições em que isso aconteceu.

Próximo: SLO e alerta de burn rate