4. Контекст propagation
Context Propagation: механика связывания распределенных вызовов
«Главное свойство программы — воплощает ли она подлинное намерение своего создателя и пользователя».
— К.А.Р. Хоар
В предыдущей статье мы препарировали спаны и увидели, как они организуются в иерархическое дерево. Но если ваше приложение состоит из одного процесса, трейсинг мало чем отличается от обычного профилировщика или структурированного лога. Настоящая магия Distributed Tracing начинается в тот момент, когда запрос пересекает границу процесса (Inter-Process Communication, IPC) — вылетает по сети через HTTP-клиент, уходит в gRPC-канал или публикуется в топик Apache Kafka.
Чтобы связать Спан вызывающей стороны (Client Span) со Спаном принимающей стороны (Server Span), необходим надежный протокол передачи метаданных сквозь транспортные границы — Context Propagation (Проброс контекста).
Клей распределенной трассировки
Без Context Propagation распределенная система мгновенно слепнет: каждый сервис при получении запроса будет думать, что он является родоначальником нового независимого трейса. Вместо единого сквозного дерева вызовов от пользователя через десяток микросервисов вы получите разрозненные изолированные обрубки.
Контекст — это невидимый клей, связывающий разнородные компоненты кластера в единый организм.
Механизм работы
Физическая модель Context Propagation напоминает эстафетный бег 4х100 метров. Бегун A (сервис A) подбегает к границе своего этапа и на полной скорости вкладывает эстафетную палочку (Trace Context) в руку бегуна B (сервис B). Если палочка упала на дорожку — эстафета сорвана, результаты аннулированы, расследование инцидента невозможно.
В мире OpenTelemetry и Go этот процесс состоит из двух фаз: Inject (внедрение в сетевой заголовок) и Extract (извлечение из заголовка в рантайм):
flowchart LR
subgraph A ["Service A (Client)"]
A1["Tracer.Start"] --> A2["Context<br>TraceID: abc"]
A2 --> A3["Propagator.Inject<br>Context -> Headers"]
end
subgraph B ["Network"]
B1["HTTP Headers<br>traceparent: abc..."]
end
subgraph C ["Service B (Server)"]
C1["Propagator.Extract<br>Headers -> Context"] --> C2["Tracer.Start<br>ParentID: abc"]
end
A3 --> B1
B1 --> C1
style A fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#fff
style B fill:#312e81,stroke:#818cf8,stroke-width:2px,color:#fff
style C fill:#065f46,stroke:#10b981,stroke-width:2px,color:#fff
style B1 fill:#78350f,stroke:#f59e0b,color:#fff
Under the Hood: W3C Trace Context
Стандартом де-факто сейчас является W3C Trace Context. Это спецификация, описывающая, как кодировать Trace ID в HTTP-заголовках.
Главный заголовок: traceparent.
Формат: version-traceid-parentid-flags
Пример заголовка:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
00: Версия формата.4bf92...: Trace ID (уникальный для всего запроса, 32 шестнадцатеричных символа).00f0...: Parent Span ID (ID спана, который инициировал вызов, 16 шестнадцатеричных символов).01: Trace Flags (например,sampled— записывать ли этот трейс).
Реализация в Go
В Go контекст передается через интерфейс context.Context. OpenTelemetry использует propagation пакет для работы с транспортным слоем (HTTP, gRPC).
1. Конфигурация Propagator
Правильный подход — глобально установить пропагатор, поддерживающий W3C.
package main
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
func initTracer() {
// ... создание tracer provider ...
// Устанавливаем глобальный пропагатор (W3C Trace Context + Baggage)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, // Отвечает за traceparent
propagation.Baggage{}, // Отвечает за передачу пользовательских данных (baggage)
))
}
2. Client Side: Inject (Отправка)
Когда вы делаете исходящий HTTP-запрос, вы должны "внедрить" контекст в заголовки.
package main
import (
"context"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
func makeRequest(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
// ВАЖНО: Внедряем traceparent в заголовки запроса
// carrier - это обертка над http.Header, реализующая интерфейс TextMapCarrier
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Чтение ответа...
return nil, nil
}
Если вы используете инструментированные библиотеки (например, oteldhttp), они делают это автоматически. Но если вы пишете свой клиент или работаете с очередями (Kafka/RabbitMQ), вы должны вызвать Inject вручную.
3. Server Side: Extract (Прием)
На стороне сервера (middleware) вы должны извлечь контекст из входящих заголовков.
package main
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
func tracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Извлекаем контекст из заголовков запроса
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
// Создаем span, который автоматически станет дочерним к приходящему traceparent
tr := otel.Tracer("my-service")
ctx, span := tr.Start(ctx, "handle-request")
defer span.End()
// Передаем обновленный ctx дальше в хендлер
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Mechanical Sympathy: Context в Go
Почему Go идеально подходит для трейсинга?
Потому что context.Context — это идиома языка.
- Immutable: Контекст иммутабелен. Добавление значения (или Span) создает новый контекст, но не меняет старый (безопасно для конкурентности). В памяти рантайма формируется связанный список звеньев
valueCtx. - Tree Structure: Дерево контекстов в Go естественно отображается на дерево Спанов.
- Lifetime: Отмена контекста (
ctx.Done()) может сигнализировать о таймауте запроса, что трассировщик может записать как событие.
Baggage: Данные, которые путешествуют
Кроме traceparent, существует механизм Baggage. Это способ передать произвольные пары ключ-значение через все сервисы в рамках одного трейса.
Пример: Вы хотите видеть user_id в логах сервиса Z, но запрос пришел через сервисы A -> B -> C.
В сервисе A:
baggage.Set(ctx, "user.id", "12345")
В сервисе C (если настроен Baggage Propagator):
member, _ := baggage.Member("user.id")
fmt.Println(member.Value()) // "12345"
Итог
- Propagation — это процесс передачи Trace ID и Parent Span ID между сервисами.
- Стандарт — W3C Trace Context (заголовок
traceparent). - В Go используйте
otel.GetTextMapPropagator().Inject/Extract. - Всегда передавайте
ctxкак первый аргумент в функции. Это "правило большого пальца" Go, которое делает трейсинг возможным.
В следующей статье мы рассмотрим инструменты визуализации трейсов — Jaeger и его альтернативы: 5. Jaeger и альтернативы.