Artigos Técnicos11 de julho de 202618 min de leitura

Observabilidade self-hosted sem lock-in: a stack LGTM + Pyroscope no Kubernetes com alta disponibilidade sobre S3

Wallacy Santos Ferreira

Nuvem Online

Banner - Observabilidade self-hosted sem lock-in: a stack LGTM + Pyroscope no Kubernetes com alta disponibilidade sobre S3

TL;DR: A stack LGTM (Loki, Grafana, Tempo, Mimir) mais o Pyroscope cobre os quatro sinais da observabilidade — métricas, logs, traces e profiles — usando os charts oficiais da Grafana, portáveis e sem lock-in. A chave da alta disponibilidade é object storage S3-compatível: ele desacopla estado do compute, deixando cada serviço stateless e descartável, com custo e retenção sob controle. O Grafana roda efêmero, provisionado por sidecar via ConfigMap, e o Alloy alimenta tudo com um agente só.

Por que observabilidade self-hosted, e por que agora

Observabilidade deixou de ser luxo de time grande e virou pré-requisito de governança. Não se audita o que não se enxerga — e isso vale tanto para uma fila de pagamentos que travou às 3 da manhã quanto para o comportamento de um agente de IA em produção. O problema é que a resposta fácil — contratar um SaaS de observabilidade e mandar tudo para lá — cobra o preço em dois lugares: lock-in (seu dado de telemetria e seus dashboards viram reféns do fornecedor) e conta imprevisível (a fatura cresce com a cardinalidade, exatamente quando você mais precisa observar).

A alternativa madura é self-hosted sob padrões abertos: a mesma stack roda em qualquer cluster, em qualquer nuvem ou on-premises, e o custo é uma decisão de arquitetura sua — não uma surpresa no fim do mês. Este guia mostra como montar essa stack de ponta a ponta com os charts oficiais da Grafana, colocando object storage S3-compatível como a fundação da alta disponibilidade, e explicando o papel de cada peça. É o mesmo desenho que rodamos em produção aqui na Nuvem Online — e as evidências ao longo do texto vêm do nosso próprio Grafana.

O que é a stack LGTM (+ Pyroscope), e por que ela é anti-lock-in?

LGTM é o acrônimo dos quatro projetos open source da Grafana Labs que, juntos, cobrem os sinais da observabilidade moderna:

Componente Sinal O que faz
Mimir Métricas Backend Prometheus de longo prazo, horizontalmente escalável
Loki Logs Agregação de logs indexada por labels, barata em cardinalidade
Tempo Traces Backend de tracing distribuído, ingestão via OTLP
Grafana Visualização A camada de consulta, dashboards e alertas
Pyroscope Profiles Continuous profiling — o "quarto sinal", CPU e memória por função

O que torna esse conjunto anti-lock-in não é ideologia — é engenharia. Todos falam protocolos abertos (a API do Prometheus para métricas, OTLP para traces), todos são mantidos por uma comunidade sob a Grafana Labs, e todos rodam com os charts Helm oficiais (grafana/mimir-distributed, grafana/loki, grafana/tempo-distributed, grafana/pyroscope, grafana/grafana). Você não depende de um registry proprietário, de um operator fechado nem de um formato de dado que só um fornecedor lê. A stack inteira é portável por construção.

Arquitetura da stack LGTM + Pyroscope: aplicações e cluster enviam telemetria ao coletor único Alloy, que distribui métricas ao Mimir, logs ao Loki, traces ao Tempo e profiles ao Pyroscope; todos persistem em object storage S3-compatível, e o Grafana consulta os quatro backends

O fluxo é o do diagrama: as aplicações e o cluster produzem telemetria, o Alloy coleta tudo, os quatro backends processam e persistem no S3, e o Grafana lê dos quatro. Guarde essa imagem — cada seção a seguir detalha uma parte dela.

Por que object storage S3 é a fundação da alta disponibilidade?

Esta é a decisão de arquitetura que separa "subi o Grafana com um Prometheus do lado" de "tenho uma plataforma de observabilidade de verdade". A ideia central é separar compute de storage.

Num desenho ingênuo, cada instância de Prometheus/Loki guarda os dados no próprio disco. Isso amarra o dado à réplica: se o pod morre, o dado vai junto; se você quer HA, precisa duplicar o disco; se quer retenção longa, precisa de discos gigantes e caros. Na stack LGTM, o modelo é outro: os componentes escrevem blocks (métricas), chunks (logs), traces e profiles num object storage S3-compatível, e ficam eles próprios stateless — guardam localmente, no máximo, um WAL e caches efêmeros.

Como o S3 desacopla compute de estado: o write path e o read path são stateless e descartáveis, com apenas WAL/cache em disco local, enquanto o object storage S3-compatível guarda blocks, chunks, traces e profiles com durabilidade e replicação delegadas ao provedor

As consequências práticas são diretas:

  • Alta disponibilidade real. Réplicas stateless são descartáveis: um pod cai, o scheduler sobe outro, e ele lê os mesmos dados do bucket. Rollout de versão não perde nada.
  • Durabilidade delegada. Um object storage entrega tipicamente 11 noves de durabilidade e replicação interna — você não reinventa isso em PV.
  • Custo sob controle. Object storage é barato e elástico. Guardar 30, 90, 365 dias de métricas vira uma linha de retenção, não uma corrida de disco.
  • Portabilidade. "S3-compatível" é um contrato, não um fornecedor. Roda igual em MinIO (in-cluster), Ceph RGW (on-premises), Backblaze B2, OCI Object Storage ou AWS S3. Trocar o backend é trocar o endpoint.

Um detalhe que costuma passar batido: credencial de object storage é segredo, e segredo não mora no values.yaml. A forma correta é injetar via Secret do Kubernetes, expandindo variáveis de ambiente na config:

# grafana/mimir-distributed — S3 com credenciais via Secret (NUNCA inline)
mimir:
  structuredConfig:
    common:
      storage:
        backend: s3
        s3:
          endpoint: <S3_ENDPOINT>          # MinIO, Ceph, Backblaze, OCI, AWS...
          region: <REGION>
          access_key_id: ${AWS_ACCESS_KEY_ID}       # expandido do ambiente
          secret_access_key: ${AWS_SECRET_ACCESS_KEY}
global:
  extraEnvFrom:
    - secretRef:
        name: mimir-s3-credentials   # chaves: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
extraArgs:
  config.expand-env: "true"

O mesmo princípio — endpoint no values, credencial no Secret — se repete em Loki, Tempo e Pyroscope.

Como cada serviço funciona — e como implantá-lo

Mimir: métricas de longo prazo

O Mimir é o backend de métricas: recebe o remote_write do Prometheus/Alloy e serve a API Prometheus para consulta. No modo distributed (microsserviços), cada função é um workload que escala sozinho: distributor recebe as amostras, ingester as agrupa em blocks, store-gateway e querier servem as consultas, compactor compacta os blocks no S3, e o ruler avalia recording e alerting rules. Um desenho típico de produção roda 3 distributors, 3 ingesters, 2 queriers, 3 store-gateways e 2 rulers — e todos os blocks TSDB, o estado do ruler e do alertmanager vão para o S3, em buckets (ou prefixos) separados por finalidade.

Para a alta disponibilidade das séries, há duas estratégias — e vale conhecer as duas:

  • Ring clássico com replication_factor: 3 e zone-aware replication. Cada série é escrita em 3 ingesters, distribuídos por zonas lógicas com anti-affinity. Você pode reiniciar uma zona inteira sem perder quórum de escrita. É o caminho consolidado e o default para a maioria.
  • Ingest storage via Kafka. No modelo mais novo, os distributors publicam num log de eventos (Kafka) e os ingesters consomem de partições. A durabilidade da ingestão passa a ser do Kafka, e o desacoplamento write/read fica ainda mais nítido. É mais peça para operar — adote quando a escala justificar.
# grafana/mimir-distributed — HA clássica sobre S3
mimir:
  structuredConfig:
    ingester:
      ring:
        replication_factor: 3
    blocks_storage:
      s3: { bucket_name: <bucket-metrics-blocks> }
    ruler_storage:
      s3: { bucket_name: <bucket-metrics-ruler> }
ingester:
  replicas: 3
  zoneAwareReplication:
    enabled: true
distributor: { replicas: 3 }
store_gateway: { replicas: 3 }
helm repo add grafana https://grafana.github.io/helm-charts
helm upgrade --install mimir grafana/mimir-distributed -n mimir --create-namespace -f mimir-values.yaml

Depois de coletar, o resultado aparece no Grafana como um datasource Prometheus comum. Métricas de cluster com 24 horas de retenção, servidas pelo Mimir:

Painel "Cluster CPU Utilization" no Grafana mostrando 24 horas de uso de CPU do cluster servidas pelo Mimir, com baseline em torno de 13% e picos até 24%

Loki: logs indexados por labels

O Loki trata logs como o Prometheus trata métricas: indexa por labels, não pelo conteúdo. Isso o torna barato — você não paga por full-text index de tudo. O chart oferece três modos de deployment, e escolher o certo é metade do trabalho:

  • SingleBinary — todos os componentes num processo. Com réplicas e S3 já entrega HA para volumes pequenos e médios.
  • SimpleScalable — separa write (distributor + ingester), read (querier + query-frontend) e backend (compactor + ruler + index-gateway); cada caminho escala independente.
  • Distributed — cada microsserviço exposto individualmente, para volumes muito altos.

Comece no menor que atende. Em ambos os casos, replication_factor: 3 mais o S3 compartilhado é o que dá HA: as três réplicas de write apontam para o mesmo bucket.

# grafana/loki — SingleBinary x3 sobre S3
deploymentMode: SingleBinary
singleBinary:
  replicas: 3
loki:
  storage:
    type: s3
    bucketNames: { chunks: <bucket-logs>, ruler: <bucket-logs>, admin: <bucket-logs> }
    s3:
      endpoint: <S3_ENDPOINT>
      region: <REGION>
      s3ForcePathStyle: true
  schemaConfig:
    configs:
      - from: "2024-01-01"
        store: tsdb
        object_store: s3
        schema: v13
        index: { prefix: loki_index_, period: 24h }
  commonConfig:
    replication_factor: 3
  limits_config:
    retention_period: 672h   # 28 dias

No Explore do Grafana, o volume de logs por nível mostra a agregação funcionando — sem precisar abrir uma única linha para ver a saúde do fluxo:

Explore do Loki no Grafana com o painel "Volume de logs" mostrando a distribuição por nível em uma hora: info 5,14 mil, warning 976, error 122

Tempo: tracing distribuído

O Tempo é o backend de traces. No modo distributed, ele recebe spans via OTLP (gRPC e HTTP), monta os traces e os persiste no S3 — sem precisar de um índice caro, porque a busca é por trace ID e por metadados. O ganho operacional é enorme: um trace amarra a jornada de uma request atravessando vários serviços, e correlaciona direto com logs (Loki) e métricas (Mimir) dentro do Grafana.

# grafana/tempo-distributed — traces sobre S3, ingestão OTLP
storage:
  trace:
    backend: s3
    s3:
      endpoint: <S3_ENDPOINT>
      bucket: <bucket-traces>
distributor:
  receivers:
    otlp:
      protocols:
        grpc: { }
        http: { }
querier: { replicas: 3 }

Como o Tempo só ingere por OTLP, o Alloy (ou o OpenTelemetry Collector) é quem entrega os spans a ele — voltamos a isso na seção do Alloy.

Pyroscope: continuous profiling

O Pyroscope é o quarto sinal, o menos difundido e o que mais surpreende quando entra: continuous profiling. Em vez de perfilar sob demanda numa crise, ele coleta profiles de CPU e memória continuamente, por serviço e por função, e guarda tudo no S3. Quando um serviço começa a queimar CPU, você não adivinha: abre o flame graph e vê exatamente qual função é responsável.

# grafana/pyroscope — profiles sobre S3, multi-tenant
pyroscope:
  structuredConfig:
    storage:
      backend: s3
      s3:
        endpoint: <S3_ENDPOINT>
        bucket_name: <bucket-profiles>
        region: <REGION>
    limits:
      max_profile_size_bytes: 4194304

O flame graph abaixo é de um profile de CPU real, com bilhões de amostras acumuladas — cada bloco é um frame de pilha, e a largura é o tempo de CPU gasto ali:

Flame graph de CPU no Grafana via Pyroscope, com 433 bilhões de amostras acumuladas, mostrando os frames de pilha de um serviço e a proporção de tempo de CPU em cada função

Por que rodar o Grafana sem persistência?

Aqui vai a decisão que mais gera dúvida — e que, quando você entende, nunca mais volta atrás: o Grafana roda efêmero, sem PVC.

A intuição diz "mas e meus dashboards?". A resposta é que o estado que importa não vive no Grafana — vive em ConfigMaps versionados em Git, carregados na inicialização por um sidecar. O sidecar observa os ConfigMaps do cluster que tenham um label específico (grafana_dashboard para painéis, grafana_datasource para fontes de dados) e injeta o conteúdo no Grafana automaticamente. Com persistence.enabled: false, o pod vira descartável e idêntico a cada restart.

# grafana/grafana — efêmero + provisionamento por sidecar
persistence:
  enabled: false
sidecar:
  dashboards:
    enabled: true
    label: grafana_dashboard
    searchNamespace: ALL
  datasources:
    enabled: true
    label: grafana_datasource
    searchNamespace: ALL
grafana.ini:
  auth.generic_oauth:
    enabled: true        # SSO via OIDC (Keycloak, Authentik, etc.) — IdP substituível

Um datasource, então, é só um ConfigMap com o label certo:

apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-datasources
  labels:
    grafana_datasource: "1"
data:
  datasources.yaml: |
    apiVersion: 1
    datasources:
      - name: Prometheus
        type: prometheus
        url: http://mimir-query-frontend.<ns>:8080/prometheus
        isDefault: true
      - name: Loki
        type: loki
        url: http://loki.<ns>:3100
      - name: Tempo
        type: tempo
        url: http://tempo-query-frontend.<ns>:3200
      - name: Pyroscope
        type: grafana-pyroscope-datasource
        url: http://pyroscope-query-frontend.<ns>:4040    

Os ganhos são concretos:

  • Sem PVC RWO travando upgrade. Um dos motivos clássicos de um pod de Grafana ficar preso em Pending num upgrade é o volume ReadWriteOnce que não desprende do nó antigo. Sem persistência, o problema deixa de existir.
  • Réplicas idênticas. Você pode subir N Grafanas atrás de um load balancer sem estado divergente entre eles.
  • GitOps de verdade. Todo painel e todo datasource é um objeto declarativo, revisável em merge request, reproduzível em qualquer cluster.

É esse mecanismo que provisiona as fontes de dados que você vê aqui — Mimir (como Prometheus), Loki, Tempo, Pyroscope e o Alertmanager, todos apontando para os Services internos do cluster:

Tela de Fontes de dados do Grafana mostrando cinco datasources provisionados automaticamente — Alertmanager, Loki, Prometheus (Mimir), Pyroscope e Tempo — cada um apontando para o Service interno do respectivo backend

Nenhum desses datasources foi criado na mão: todos nasceram de ConfigMaps carregados pelo sidecar. O SSO fica na frente via OAuth/OIDC — o que mantém o IdP substituível, mais um ponto de portabilidade.

Como o Alloy coleta tudo com um agente só?

Repare que até aqui falamos dos backends, mas não de quem os alimenta. Esse é o papel do Grafana Alloy — um coletor único que substitui, na prática, o Prometheus de scraping, o Promtail e o OpenTelemetry Collector, todos de uma vez.

A forma mais direta de implantá-lo em Kubernetes é pelo meta-chart grafana/k8s-monitoring, que orquestra várias instâncias do Alloy por função: alloy-metrics (raspa métricas), alloy-logs (DaemonSet lendo logs de pod), alloy-profiles (profiling via eBPF), alloy-receiver (recebe OTLP de aplicações) e alloy-singleton (sincroniza as regras). Você declara os destinos e liga as features; o chart cuida do resto.

# grafana/k8s-monitoring — um agente, quatro sinais
cluster:
  name: <cluster-name>
destinations:
  - name: mimir
    type: prometheus
    url: http://mimir-distributor.<ns>:8080/api/v1/push
  - name: loki
    type: loki
    url: http://loki.<ns>:3100/loki/api/v1/push
  - name: tempo
    type: otlp
    url: http://tempo-distributor.<ns>:4318
    metrics: { enabled: false }
    logs: { enabled: false }
    traces: { enabled: true }
  - name: pyroscope
    type: pyroscope
    url: http://pyroscope-distributor.<ns>:4040
clusterMetrics: { enabled: true }        # cAdvisor, kube-state-metrics, node-exporter
clusterEvents: { enabled: true }
podLogs: { enabled: true }
applicationObservability:                # recebe traces OTLP das apps
  enabled: true
  receivers: { otlp: { grpc: { enabled: true }, http: { enabled: true } } }
profiling: { enabled: true }             # eBPF + pprof
prometheusOperatorObjects:
  enabled: true                          # descobre ServiceMonitor/PodMonitor existentes

Dois pontos que valem ouro na operação:

  • Descoberta nativa do ecossistema Prometheus. Ligando prometheusOperatorObjects, o Alloy passa a honrar os ServiceMonitor e PodMonitor que outros charts já criam — você reaproveita o que a comunidade já configurou em vez de reescrever scrape configs.
  • Regras no lugar certo. O alloy-singleton sincroniza os PrometheusRule do cluster para o ruler do Mimir. Recording rules pré-agregam o que você consulta sempre; alerting rules disparam. Mas cuidado: uma regra colocada no componente errado (no Alloy em vez de no ruler central, ou vice-versa) é silenciosamente ignorada — vale entender onde cada uma mora.

Para traces de aplicação, o par OpenTelemetry Operator + Alloy fecha o ciclo: o operator injeta a auto-instrumentação nos pods por annotation, e a app passa a exportar OTLP para o alloy-receiver, que repassa ao Tempo. Zero código de instrumentação na aplicação.

Cardinalidade, retenção e custo: como a observabilidade "se paga"?

A stack só "se paga" quando cardinalidade e retenção são decisões de projeto, não acaso. Três alavancas concentram o custo:

  1. Cardinalidade de labels. Cada combinação única de labels é uma série (ou stream). Um label de alta cardinalidade — user_id, request_id, pod cru — multiplica o custo de armazenamento e de query. Honre os labels da fonte, não invente dimensão que ninguém consulta, e use recording rules para pré-agregar as consultas quentes: em vez de recalcular um sum(rate(...)) pesado a cada abertura de dashboard, você o materializa uma vez.
  2. Retenção por tier. Nem todo dado merece 1 ano. Métricas agregadas podem viver muito; logs de debug podem viver dias. O object storage torna a retenção uma linha de configuração — e o custo, previsível.
  3. Multi-tenancy. Um único backend serve vários tenants (por cluster, por app, por time) via header X-Scope-OrgID, sem duplicar a stack. Um Mimir, um Loki, um Tempo — vários inquilinos isolados.

O resultado é uma governança de custo tratada como observabilidade, não como planilha: você mede o que a telemetria custa e ajusta cardinalidade e retenção com a mesma disciplina com que ajusta um HPA.

Quando escolher — e o que considerar antes

Self-hosted não é para todo mundo, e ser honesto sobre isso é parte da recomendação:

  • Vá de self-hosted LGTM quando você já opera Kubernetes, quer portabilidade e previsibilidade de custo, tem volume que torna o SaaS caro, ou tem requisito de soberania do dado de telemetria. É o caso da maioria de quem leva infra a sério no Brasil.
  • Fique no SaaS se o time é pequeno, o volume é baixo e você prefere não ter a stack de observabilidade como mais um sistema para operar de plantão.
  • Distributed vs. SingleBinary/monolithic: não comece grande. SingleBinary no Loki e um Mimir enxuto atendem muito mais casos do que o hype sugere. Suba para distributed quando a cardinalidade e o volume realmente pedirem — não antes.
  • As armadilhas reais de operar você mesmo são conhecidas: PVC ReadWriteOnce travando upgrade (por isso o Grafana efêmero), dependências stateful mal isoladas, e regras no lugar errado sendo ignoradas em silêncio. Nenhuma é impeditiva — todas são gerenciáveis quando você as conhece de antemão.

Portabilidade combina com portabilidade: a mesma lógica anti-lock-in que guia essa stack vale para o cluster embaixo dela — o que discutimos em Kubernetes on-premises: infraestrutura estatal sem lock-in. E a razão de fundo para investir em observabilidade — enxergar para governar — é a mesma que trata de avaliar, medir e governar agentes de IA em produção: não se governa o que não se mede.

Conclusão

A stack LGTM + Pyroscope entrega os quatro sinais da observabilidade com charts oficiais, protocolo aberto e portabilidade real. O fio que costura tudo é o object storage S3-compatível: é ele que torna cada serviço stateless e, portanto, altamente disponível, escalável e barato de reter. Some a isso um Grafana efêmero governado por Git via sidecar, e um Alloy que coleta os quatro sinais com um agente só, e você tem uma plataforma de observabilidade que não amarra a fornecedor, roda em qualquer nuvem e cujo custo é uma decisão sua. Comece pequeno, use padrões abertos, e deixe o object storage carregar o peso do estado.

Perguntas Frequentes

Preciso mesmo de object storage S3 para rodar a stack LGTM?
Para produção com alta disponibilidade, sim. É o S3 que torna os serviços stateless: os dados vivem no bucket (durável e replicado pelo provedor), não no disco do pod. Sem ele, cada componente vira stateful com PV, e você perde a réplica descartável e a retenção barata. Vale qualquer object storage S3-compatível — MinIO, Ceph, Backblaze, OCI, AWS S3.

Qual a diferença entre os modos SingleBinary, SimpleScalable e Distributed do Loki?
SingleBinary roda todos os componentes num processo (com réplicas + S3 já dá HA para volumes pequenos/médios). SimpleScalable separa os caminhos de write, read e backend, cada um escalando sozinho. Distributed expõe cada microsserviço individualmente, para volumes muito altos. Comece no menor que atende e suba conforme a cardinalidade cresce.

Por que rodar o Grafana sem persistência (sem PVC)?
Porque o estado que importa — dashboards e datasources — vive em ConfigMaps versionados em Git, carregados por um sidecar. O pod do Grafana vira descartável e idêntico a cada restart, sem PVC RWO travando upgrade nem estado divergente entre réplicas. É GitOps aplicado à camada de visualização.

O Alloy substitui o Prometheus, o Promtail e o OpenTelemetry Collector?
Na prática, sim. O Alloy é um coletor único que descobre alvos no cluster e envia métricas (remote_write ao Mimir), logs (ao Loki), traces (OTLP ao Tempo) e profiles (ao Pyroscope). Um chart, um agente, os quatro sinais — em vez de três ou quatro coletores separados.

Dá para começar sem todos os cinco componentes?
Dá, e é o recomendado. Comece por Mimir (métricas) e Grafana, adicione Loki (logs) em seguida, e traga Tempo e Pyroscope quando tracing e profiling entrarem na sua necessidade. A arquitetura é a mesma; você só liga mais destinos no Alloy.


Fontes:

Gostou? Compartilhe:
Precisa de ajuda?Fale com nossos especialistas 👋
Avatar Walcew - Headset