Распределённая трассировка с OpenTelemetry
Updated Jul 2026
Зачем нужна распределённая трассировка?
В микросервисной архитектуре один пользовательский запрос может пройти через 5, 10 или 20 сервисов. Когда этот запрос медленный или завершается с ошибкой, вам нужен ответ: какой сервис является узким местом? Распределённая трассировка даёт ответ, отслеживая путь каждого запроса через границы сервисов, записывая время, статус и метаданные на каждом переходе.
Архитектура OpenTelemetry
OpenTelemetry (OTel) — это стандарт CNCF для сбора телеметрических данных. Он предоставляет вендор-нейтральные API, SDK и коллектор для трассировок, метрик и логов.
[Service A] [Service B] [Service C]
OTel SDK OTel SDK OTel SDK
| | |
v v v
+-------------------------------------------------------+
| OpenTelemetry Collector |
| (receives, processes, exports telemetry data) |
+-------+-----------+-----------+-----------+-----------+
| | | |
v v v v
[Jaeger] [Grafana [Datadog] [Cloud
Tempo] provider]
Почему именно OpenTelemetry?
- Вендор-нейтральный. Меняйте бэкенды (Jaeger, Datadog, Grafana Tempo) без изменения кода приложения
- Стандарт CNCF. Поддерживается Google, Microsoft, Splunk и большинством вендоров наблюдаемости
- Автоинструментирование. Инструментируйте HTTP, библиотеки баз данных и обмена сообщениями без изменения кода
- Распространение контекста. Автоматически передаёт контекст трассировки через границы сервисов с помощью HTTP-заголовков
Как работают трассировки, спаны и контекст
User clicks "Submit Order"
|
v
[TRACE: order-submit-abc123] -------- spans the entire request flow
|
+---> [Service: API Gateway]
| LOG: "Received POST /orders from user_id=42"
| METRIC: http_requests_total{method="POST", path="/orders"} +1
|
+---> [Service: Order Service]
| LOG: "Creating order for user_id=42, items=3, total=$149.99"
| METRIC: order_creation_duration_seconds = 0.045
| SPAN: order-service.create_order (45ms)
|
+---> [Service: Payment Service]
| LOG: "Charging $149.99 to card ending 4242"
| METRIC: payment_processing_duration_seconds = 1.2
| SPAN: payment-service.charge (1200ms) <-- slow!
|
+---> [Service: Inventory Service]
LOG: "Reserved 3 items for order_id=ORD-789"
METRIC: inventory_reservations_total +3
SPAN: inventory-service.reserve (23ms)
Трассировка показывает, что сервис платежей является узким местом (1200 мс из ~1300 мс общего времени). Метрика подтверждает, что это тренд. Лог предоставляет конкретный контекст для отладки.
Инструментирование Python-сервиса
Настройка автоматического инструментирования
# otel_setup.py -- OpenTelemetry configuration for a Flask service
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
def setup_telemetry(service_name: str, service_version: str):
"""Initialize OpenTelemetry with OTLP export."""
resource = Resource.create({
"service.name": service_name,
"service.version": service_version,
"deployment.environment": "production",
})
# Traces
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
trace.set_tracer_provider(trace_provider)
# Metrics
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint="http://otel-collector:4317"),
export_interval_millis=10000,
)
metrics.set_meter_provider(MeterProvider(
resource=resource, metric_readers=[metric_reader]
))
# Auto-instrument frameworks
FlaskInstrumentor().instrument()
RequestsInstrumentor().instrument()
SQLAlchemyInstrumentor().instrument()
Ручные спаны для бизнес-логики
# app.py -- Adding manual spans for business-critical operations
from opentelemetry import trace
tracer = trace.get_tracer("order-service")
def process_order(order_data):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order_data["id"])
span.set_attribute("order.item_count", len(order_data["items"]))
span.set_attribute("order.total_cents", order_data["total"])
with tracer.start_as_current_span("validate_order"):
validate(order_data)
with tracer.start_as_current_span("charge_payment") as payment_span:
result = charge(order_data["payment"])
payment_span.set_attribute("payment.method", result.method)
payment_span.set_attribute("payment.processor", result.processor)
with tracer.start_as_current_span("reserve_inventory"):
reserve(order_data["items"])
span.set_attribute("order.status", "completed")
Тестирование на основе трассировок
Мощный паттерн — написание утверждений на основе трассировок, проверяющих не только то, что запрос успешен, но и то, что он прошёл ожидаемый путь через систему:
# test_trace_assertions.py
import requests
import time
def test_order_flow_creates_expected_trace(trace_client):
"""Verify the order flow hits all expected services in the correct order."""
# Trigger the order flow
response = requests.post("https://api.example.com/orders", json={
"items": [{"sku": "LAPTOP-1", "qty": 1}],
"payment": {"method": "card", "token": "tok_test_123"},
})
assert response.status_code == 201
trace_id = response.headers["X-Trace-Id"]
# Allow time for spans to propagate to the backend
time.sleep(5)
trace_data = trace_client.fetch_trace(trace_id)
# Assert all expected services are present
service_names = [span["service_name"] for span in trace_data["spans"]]
assert "api-gateway" in service_names
assert "order-service" in service_names
assert "payment-service" in service_names
assert "inventory-service" in service_names
# Assert ordering: payment happens before inventory reservation
payment_span = next(s for s in trace_data["spans"]
if s["operation"] == "charge_payment")
inventory_span = next(s for s in trace_data["spans"]
if s["operation"] == "reserve_inventory")
assert payment_span["end_time"] <= inventory_span["start_time"]
# Assert performance: total trace duration under 3 seconds
root_span = next(s for s in trace_data["spans"] if s["parent_id"] is None)
assert root_span["duration_ms"] < 3000
# Assert no error spans
error_spans = [s for s in trace_data["spans"] if s.get("status") == "ERROR"]
assert len(error_spans) == 0, f"Unexpected errors in trace: {error_spans}"
Что обнаруживают тесты на основе трассировок
| Утверждение | Что обнаруживает |
|---|---|
| Сервис присутствует в трассировке | Пропущенный вызов сервиса (регрессия интеграции) |
| Порядок спанов | Состояния гонки, неправильная оркестрация |
| Общая длительность трассировки | Сквозная регрессия производительности |
| Отсутствие ошибочных спанов | Проглоченные ошибки, тихие сбои |
| Ожидаемые атрибуты на спанах | Нарушение распространения контекста |
| Количество спанов | Неожиданные вызовы сервисов (N+1 запросы, лишние повторы) |
Конфигурация OpenTelemetry Collector
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1000
# Tail-based sampling: keep all error traces, sample 10% of successful traces
tail_sampling:
decision_wait: 10s
policies:
- name: errors-always
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-traces
type: latency
latency: { threshold_ms: 2000 }
- name: probabilistic-sample
type: probabilistic
probabilistic: { sampling_percentage: 10 }
exporters:
otlp/tempo:
endpoint: tempo.monitoring:4317
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:8889
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, tail_sampling]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
Стратегии сэмплирования
В высоконагруженных системах сбор каждой трассировки обходится дорого. Выберите стратегию сэмплирования:
| Стратегия | Описание | Преимущества | Недостатки |
|---|---|---|---|
| Head-based (вероятностный) | Решение в начале трассировки — сэмплировать или нет | Простой, предсказуемая стоимость | Может пропустить редкие ошибки |
| Tail-based | Решение после завершения трассировки, на основе результата | Сохраняет все ошибки и медленные трассировки | Более высокое потребление памяти в коллекторе |
| С ограничением частоты | Сэмплирование N трассировок в секунду | Предсказуемая стоимость | Пропускает всплески |
| Всегда включён для ошибок | 100% сэмплирование ошибочных трассировок | Никогда не пропускает сбои | Не снижает объём ошибочных трассировок |
Рекомендация: Используйте tail-based сэмплирование с постоянным сбором ошибок и медленных трассировок. Это даёт 100% видимость проблем при контролируемых затратах на успешные быстрые запросы.
Распределённая трассировка — это основа наблюдаемости в микросервисах. В сочетании со структурированным логированием и метриками она обеспечивает полную картину, необходимую для эффективного тестирования в продакшене.