Depurando microsserviços no Kubernetes com Istio, OpenTelemetry e Tempo — Parte 1

Recentemente trabalhei em um projeto paralelo para melhorar o rastreamento em Otomi implementando Grafana Tempo e OpenTelemetry. Vou compartilhar minhas experiências e configuração em dois posts (porque há muito envolvido aqui). Esta é a primeira.

Não espere uma história complicada sobre rastreamento em geral. Vou explicar a configuração completa e compartilhar minhas experiências.

Porquê este projeto

O Otomi (um PaaS auto-hospedado para Kubernetes) usa o Istio em seu núcleo e inclui uma pilha de observabilidade multilocatária avançada com registro (Loki), métricas (Prometheus), alerta (gerenciador de alertas) e rastreamento (Jaeger). Mas recebemos algumas perguntas de usuários sobre a configuração do rastreamento. Perguntas como: “Por que vejo apenas dados parciais com contexto parcial (extensões únicas)” e “Onde os rastreamentos são armazenados e por quanto tempo?

O que você precisa saber sobre rastreamento com o Istio

Em primeiro lugar, configurar o Istio para rastreamento é fácil de configurar. O Istio é responsável pelo gerenciamento do tráfego, portanto, também pode relatar rastreamentos que permitem visibilidade ao Istio e ao comportamento do aplicativo. Mas como não há código em execução dentro do próprio aplicativo para coletar dados, o Istio só pode coletar dados parciais com contexto parcial.

Ehh, o que isso significa? Bem, quando o serviço A chama o serviço B, o Istio cria uma extensão que representa o evento. No entanto, quando o serviço B chama o serviço C, o Istio não pode reconhecer que isso faz parte do mesmo rastreamento contínuo originado do serviço A. Para resolver isso, você precisará instrumentar cada serviço para extrair a propagação de contexto do Istio e injetá-la no(s) serviço(s) downstream. A instrumentação pode ser feita (manualmente) usando o SDK do OpenTelemetry ou automaticamente usando o Operador do OpenTelemetria.

Onde os vestígios são armazenados?

O Jaeger suporta nativamente dois bancos de dados NoSQL de código aberto como back-ends de armazenamento de rastreamento, Cassandra e Elasticsearch. No Otomi usamos apenas o armazenamento de objetos. Existem alguns projetos de código aberto que você pode usar para conectar o armazenamento de objetos (como o AWS S3) ao Jaeger, mas esses projetos não são mantidos ativamente. É por isso que não configuramos o Jaeger com um back-end de armazenamento. Isso me levou a considerar o uso do Grafana Tempo como backend. Então, para responder à pergunta, em um volume K8s. Isso não é o ideal, especialmente quando você tem requisitos de retenção longos.

Estendendo a configuração do rastreamento no Otomi com OpenTelemetry e Tempo

Percebi que a configuração de rastreamento no Otomi era bastante limitada, então comecei um pequeno projeto paralelo para integrar o Tempo como um backend de rastreamento e fornecer equipes (locatários) na plataforma para consultar o Tempo para ver todos os elementos envolvidos na solicitação e ver todas as inter-relações entre seus vários serviços usando um gráfico de nó no Grafana.

Otomi usa malha de serviço Istio em seu núcleo. O Istio aproveita o recurso de rastreamento distribuído do Envoy para fornecer integração de rastreamento pronta para uso. Embora os proxies do Istio possam enviar extensões automaticamente, informações adicionais são necessárias para unir essas extensões em um único rastreamento. Então, precisamos de propagação de contexto.

Isso levou à seguinte arquitetura de solução:

  • Instalar o Grafana Tempo
  • Instalar o OpenTracing Operator
  • Configurar o Coletor OpenTelemetry
  • Configure o Istio para usar o provedor de rastreamento opentelemetry e enviar extensões para o OpenTracing Collector
  • Configurar a fonte de dados do Grafana para o Tempo
  • Configure a fonte de dados Grafana para Loki fornecer um link direto de um traceID nos logs para o rastreamento no Tempo
  • Configurar instrumentação para propagação de contexto

Instalar o Grafana Tempo

Vou usar o Tempo como backend para os traços. O Tempo pode ser configurado para usar serviços de armazenamento de objetos como AWS S3, Azure Blob ou (no meu caso) uma instância Minio local (compatível com o S3) em execução no cluster.

Antes de instalarmos o gráfico Tempo Distributed Helm, vamos primeiro examinar alguns valores importantes. Eu sempre instalo gráficos com meus próprios valores ;-)

metricsGenerator:
  enabled: true
  config:
    storage:
      path: /var/tempo/wal
      wal:
      remote_write_flush_deadline: 1m
      remote_write:
       - url: http://po-prometheus.monitoring:9090/api/v1/write
storage:
  trace:
    backend: s3
    s3:
      bucket: tempo
      endpoint: minio.minio.svc.cluster.local:9000
      access_key: my-access-key                          
      secret_key: my-secret-key                            
      insecure: true

traces:
  otlp:
    http:
      enabled: true
    grpc:
      enabled: true

metaMonitoring:
  serviceMonitor:
    enabled: true
    labels:
      prometheus: system

Instale o gráfico:

helm repo add grafana https://grafana.github.io/helm-chartshelm install -f my-values.yaml tempo grafana/tempo-distributed -n tempo

Como você pode ver, estou instalando o Metrics Generator. Isso nos permitirá ver métricas relacionadas ao rastreamento no Grafana Dashboards. Mais sobre isso mais tarde. Observe também que não analisamos as opções de configuração e dimensionamento de recursos. Isso ainda é um PoC certo!

Se você estiver usando Prometheus, certifique-se de ativar o receptor de gravação remota assim:

prometheus:
  prometheusSpec:
    enableRemoteWriteReceiver: true

Agora você deve ver os seguintes pods em execução:

# kubectl get po -n tempo                                                                              
NAME                                       READY   STATUS    RESTARTS        AGE
tempo-compactor-d59b598b5-8287b            1/1     Running   4 (6h19m ago)   16h
tempo-distributor-7b5b649487-fbzf2         1/1     Running   4 (6h19m ago)   16h
tempo-ingester-0                           1/1     Running   4 (6h19m ago)   16h
tempo-ingester-1                           1/1     Running   4 (6h19m ago)   16h
tempo-ingester-2                           1/1     Running   4 (6h19m ago)   16h
tempo-memcached-0                          1/1     Running   0               16h
tempo-metrics-generator-66c5dfc565-5dhsv   1/1     Running   4 (6h19m ago)   16h
tempo-querier-694cbf6d7-gxjzj              1/1     Running   4 (6h20m ago)   16h
tempo-query-frontend-67b4ff47c6-9msmv      1/1     Running   4 (6h19m ago)   16h

Observe que minha instância Minio foi configurada independentemente do Tempo. Se você ainda não tiver o Minio em execução (ou não gostar de usar o S3 ou um contêiner de armazenamento do Azure, poderá instalar o Minio usando o gráfico Tempo Helm.

Agora que temos o Tempo em funcionamento, vamos instalar o Operador OpenTelemetria. Por que estou usando o Operador? Bem, eu não sou um fã do gráfico OpenTelemetry Collector Helm porque ele nunca cria a configuração do coletor que eu quero. Se você usar o Operador OpenTelemetria, poderá criar sua própria configuração personalizada do Coletor e mais controle sobre ele. Outro benefício de utilizar o Operador, ele suporta Instrumentação automatizada!

Instalar o OpenTelemetry

A configuração é bastante simples, então vamos apenas instalá-lo:

helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update
helm install opentelemetry-operator open-telemetry/opentelemetry-operator -n otel

Agora vem a parte interessante: configurar o Collector. Crie um recurso OpenTelemetryCollector. Você pode usar o seguinte como exemplo:

apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: otel-collector
spec:
  config: |
    receivers:
      otlp:
        protocols:
          grpc:
          http:
    processors:
      memory_limiter:
        check_interval: 1s
        limit_percentage: 75
        spike_limit_percentage: 15
      batch:
        send_batch_size: 10000
        timeout: 10s
    exporters:
      logging:
        loglevel: info
      otlp:
        endpoint: tempo-distributor.tempo.svc.cluster.local:4317
        sending_queue:
          enabled: true
          num_consumers: 100
          queue_size: 10000
        retry_on_failure:
          enabled: true
        tls:
          insecure: true
    service:
      pipelines:
        traces:
          receivers:
          - otlp
          processors:
          - memory_limiter
          - batch
          exporters:
          - logging
          - otlp
  mode: deployment

Quando o OpenTelemetryCollector for criado, você verá os seguintes pods em execução:

kubectl get po -n otel 
NAME                                        READY   STATUS    RESTARTS   AGE
otel-collector-collector-776cdc65f8-pgmvs   1/1     Running   0          162m
otel-operator-78fc8b6975-h7lkh              2/2     Running   0          16h

Agora que temos um backend (Tempo) e um Collector (OpenTelemetry) em execução, o próximo passo é enviar alguns spans. Comecemos pelo Istio. O Istio controla todo o tráfego e ao depurar aplicativos isso se tornará um aspecto muito relevante.

Configurar o Istio para rastreamento

Bem, isso é mais fácil dizer do que fazer. Há muitas maneiras de configurar o Istio (Envoy) para rastreamento, a documentação é fragmentada e tutoriais completos são difíceis de encontrar. Você pode optar por configurar o rastreamento no defaultConfig ou usar extensionProviders. E há vários extensionProviders. Então a pergunta é: “Qual configuração usar e quando?”. Não tenho todas as respostas.

Eu decidi ir para o OpenTelemetryTracingProvider e usar o provedor de enviado padrão para adicionar o cabeçalho TRACEPARENT aos logs. A ideia aqui é usar o provedor Envoy padrão para adicionar o trace-id aos logs do sidecar istio-proxy. Isso seria bastante útil porque você pode configurar a fonte de dados Loki para criar um link do traceID diretamente para o Tempo. Mais sobre isso mais tarde.

Estou usando o Otomi e o Otomi usa o Istio Operator (versão 1.17.4). Para configurar o rastreamento no Istio, primeiro precisaremos modificar o recurso do operador Istio, usando o seguinte meshConfig.

meshConfig:
  accessLogFile: /dev/stdout
  accessLogFormat: |
    [%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %RESPONSE_CODE_DETAILS% %CONNECTION_TERMINATION_DETAILS% "%UPSTREAM_TRANSPORT_FAILURE_REASON%" %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%" %UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME% traceID=%REQ(TRACEPARENT)%
  enableAutoMtls: true
  extensionProviders:
  - opentelemetry:
      port: 4317
      service: otel-collector-collector.otel.svc.cluster.local
    name: otel-tracing

Para habilitar o extensionProvider, você precisará criar um recurso de Telemetria:

apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
  name: otel-tracing
  namespace: istio-system
spec:
  tracing:
  - providers:
    - name: otel-tracing
    randomSamplingPercentage: 100

Ao criar esse recurso no namespace istio-system, o provedor estará ativo para todos os namespaces.

Defini o randomSamplingPercentage como 100%. Em um ambiente de produção, isso provavelmente será de 0,1%.

Depois que o operador prometheus tiver reconciliado, você verá extensões chegando que são exportadas para o Tempo.

kubectl logs otel-collector-collector-776cdc65f8-pgmvs -n otel
2023-08-09T13:07:58.186Z        info    TracesExporter  {"kind": "exporter", "data_type": "traces", "name": "logging", "resource spans": 5, "spans": 7}
2023-08-09T13:08:08.186Z        info    TracesExporter  {"kind": "exporter", "data_type": "traces", "name": "logging", "resource spans": 2, "spans": 6}

No Otomi também usamos o Nginx Ingress Controller. Para eventualmente ver o rastreamento completo do controlador de entrada, Istio Gateways, Ingresses e, eventualmente, o aplicativo, também configuraremos o controlador Nginx para enviar extensões para o coletor. Configure o Nginx Ingress usando os seguintes valores:

controller:
  opentelemetry:
    enabled: true
  config:
    enable-opentelemetry: true
    otel-sampler: AlwaysOn
    otel-sampler-ratio: 0.1
    otlp-collector-host: otel-collector-collector.otel.svc
    otlp-collector-port: 4317
    opentelemetry-config: "/etc/nginx/opentelemetry.toml"
    opentelemetry-operation-name: "HTTP $request_method $service_name $uri"
    opentelemetry-trust-incoming-span: "true"
    otel-max-queuesize: "2048"
    otel-schedule-delay-millis: "5000"
    otel-max-export-batch-size: "512"
    otel-service-name: "nginx"
    otel-sampler-parent-based: "true"

Veja aqui para saber mais sobre o rastreamento no Nginx Ingress usando OpenTelemetry.

Conclusão (por enquanto)

Agora temos um back-end para nossos rastreamentos, um Coletor para receber extensões de rastreamento e exportá-las para o back-end (Tempo), e o Istio e o Nginx Ingress Controller enviando extensões de rastreamento para o Coletor.

No segundo part, vamos instrumentar nosso aplicativo e configurar fontes de dados no Grafana for Tempo para ver o real poder do rastreamento no Kubernetes.


Artigo Original