прослеживать путь каждого запроса через микросервисы в Kubernetes

Распределённое трассирование OpenTelemetry в промышленном Kubernetes

14 минут

Один запрос пользователя проходит через десятки сервисов до ответа. Логи и метрики не показывают, на каком шаге цепочки появилась задержка или ошибка. В материале — OpenTelemetry Operator, коллекторы агент и шлюз, автоинструментирование и передача контекста W3C в Tempo.

Почему логи и метрики не отвечают на вопросы о сквозных запросах

В микросервисных средах Kubernetes один вызов API часто проходит через шлюзы, доменные сервисы, кэши и очереди сообщений. Метрики показывают суммарную задержку и долю ошибок, логи фиксируют локальные события. Ни то ни другое не восстанавливает полный путь по шагам и не указывает, где начался таймаут. Без распределённого трассирования дежурные ищут совпадения по времени в разных пространствах имён и угадывают сломанную зависимость. Неполные трейсы появляются, когда один сервис не передаёт контекст, когда выборка на входе отбрасывает спаны посередине цепочки или когда имена спанов с высокой кардинальностью раздувают хранилище. Трассирование работает, когда передача контекста единообразна, инструментирование согласовано между языками, а конвейер сохраняет ошибки и прореживает обычный трафик.

Промышленная архитектура: Operator, коллекторы, propagation и Tempo

OpenTelemetry отделяет инструментирование от экспорта. Приложения отправляют OTLP-спаны и передают заголовки W3C Trace Context traceparent и tracestate в HTTP, метаданных gRPC и атрибутах сообщений. OpenTelemetry Operator управляет custom resource коллектора и подключает агенты автоинструментирования к подам. Агент DaemonSet на каждом узле принимает локальные спаны, применяет memory_limiter и batch и пересылает их на шлюз Deployment. Шлюз выполняет отложенную выборку трейсов, чтобы сохранять ошибочные и медленные цепочки и снижать объём обычного трафика, затем экспортирует данные в Grafana Tempo или любой OTLP-совместимый backend. Выборка на стороне SDK уменьшает объём заранее, отложенная выборка на шлюзе сохраняет полные трейсы с ошибками, которые входная выборка могла отбросить.

Установка OpenTelemetry Operator и коллектора-шлюза

Установите operator из официального Helm-чарта и зафиксируйте тег образа contrib. Опишите шлюз как custom resource OpenTelemetryCollector: memory_limiter первым в конвейере, tail_sampling для ошибок и медленных запросов, обогащение атрибутов и OTLP-экспорт в Tempo. Задавайте шлюзу достаточно памяти под буферы выборки — для умеренного объёма трейсов разумный старт от одного гибибайта лимита памяти.

Bash · установка OpenTelemetry Operator через Helm
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update

helm upgrade --install otel-operator open-telemetry/opentelemetry-operator \
  --namespace observability --create-namespace \
  --set manager.collectorImage.repository=otel/opentelemetry-collector-contrib \
  --set manager.collectorImage.tag=0.106.1
YAML · шлюз OpenTelemetryCollector с отложенной выборкой
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: otel-gateway
  namespace: observability
spec:
  mode: deployment
  replicas: 3
  resources:
    requests:
      cpu: 200m
      memory: 512Mi
    limits:
      cpu: "1"
      memory: 2Gi
  config: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
    processors:
      memory_limiter:
        check_interval: 1s
        limit_mib: 1536
        spike_limit_mib: 384
      tail_sampling:
        decision_wait: 10s
        num_traces: 100000
        policies:
          - name: errors
            type: status_code
            status_code: { status_codes: [ERROR] }
          - name: slow-requests
            type: latency
            latency: { threshold_ms: 2000 }
          - name: standard
            type: probabilistic
            probabilistic: { sampling_percentage: 10 }
      attributes:
        actions:
          - key: deployment.environment
            action: upsert
            value: production
      batch:
        timeout: 5s
        send_batch_size: 8192
    exporters:
      otlp/tempo:
        endpoint: tempo.observability.svc:4317
        tls:
          insecure: true
        sending_queue:
          enabled: true
        retry_on_failure:
          enabled: true
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, tail_sampling, attributes, batch]
          exporters: [otlp/tempo]

Агент DaemonSet и автоинструментирование

OpenTelemetryCollector в режиме DaemonSet пересылает спаны на DNS-имя сервиса шлюза. Создайте custom resource Instrumentation с parentbased_traceidratio на десять процентов для обычного трафика, propagators tracecontext и baggage и образами автоинструментирования под нужные языки. Аннотируйте Deployment через inject-java, inject-nodejs или inject-python с именем ресурса Instrumentation — одной аннотации inject-sdk недостаточно, она не подключает языковые агенты. Перед массовым rollout проверьте сквозной запрос и убедитесь, что в Tempo появляется многоспановый трейс.

YAML · агент DaemonSet и аннотации Instrumentation
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: otel-agent
  namespace: observability
spec:
  mode: daemonset
  config: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
    processors:
      memory_limiter:
        check_interval: 500ms
        limit_mib: 300
        spike_limit_mib: 80
      batch:
        timeout: 2s
        send_batch_size: 4096
    exporters:
      otlp:
        endpoint: otel-gateway-collector.observability.svc:4317
        tls:
          insecure: true
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [otlp]
YAML · Instrumentation CR и аннотации Deployment
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: order-service-instrumentation
  namespace: default
spec:
  exporter:
    endpoint: http://otel-agent-collector.observability.svc:4318
  propagators:
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio
    argument: "0.1"
  java:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:2.6.0
  nodejs:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:0.52.1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: default
spec:
  template:
    metadata:
      annotations:
        instrumentation.opentelemetry.io/inject-java: order-service-instrumentation
    spec:
      containers:
        - name: order-service
          image: registry.example.com/order-service:v1.2.3

Ручная передача контекста, когда автоинструментирования недостаточно

Собственные HTTP-клиенты, устаревшие очереди или обёртки gRPC нужно дополнить передачей W3C Trace Context вручную. Используйте API propagator из OpenTelemetry Go, а не проприетарные injectors. Для gRPC регистрируйте otelgrpc server handler и client handler, чтобы метаданные несли активный span context. Пропущенная передача на одном внутреннем hop разрывает трейс и сводит на нет смысл всего конвейера.

Go · W3C Trace Context для исходящего HTTP и gRPC
import (
	"context"
	"net/http"

	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/propagation"
	"google.golang.org/grpc"
)

func callDownstream(ctx context.Context, url string) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
	return http.DefaultClient.Do(req)
}

func newGRPCServer() *grpc.Server {
	return grpc.NewServer(
		grpc.StatsHandler(otelgrpc.NewServerHandler()),
	)
}

Операционные практики: выборка, именование, стоимость, корреляция и безопасность

Применяйте выборку на стороне SDK для ограничения объёма и отложенную выборку на шлюзе для сохранения ошибок и медленных трейсов. Именуйте спаны по шаблонам маршрутов вроде GET /api/v2/orders/{orderId}, а не по сырым идентификаторам — они создают взрыв кардинальности. Храните данные в Tempo или backend семь–четырнадцать дней, если compliance не требует больше. Добавляйте trace_id и span_id в структурированные JSON-логи, чтобы Grafana или Loki переходили от строки лога к трейсу. Не записывайте персональные данные в атрибуты спанов. Используйте TLS между коллекторами и backend. Следите за otelcol_exporter_send_failed_spans и otelcol_processor_refused_spans на шлюзе. Перед выводом в продакшен проверьте агенты DaemonSet на всех узлах, лимиты ресурсов шлюза, политики отложенной выборки, автоинструментирование критичных сервисов, сквозную передачу W3C и переход от лога к трейсу в панели мониторинга.

Потоки трассировок проходят через те же уровни Collector, что описаны в гайде по единому конвейеру OpenTelemetry Collector.

Задайте допустимые бюджеты задержки и ошибок перед настройкой выборки по практикам SLO, SLI и error budget для платформенных команд.