Наблюдаемость
Async context на разных языках, service mesh, миграция B3 и безопасность
Java-сервис мигрирует с thread-pool-based request handling на Project Loom virtual threads. После миграции 30% трейсов — orphan’ы. Версия OTel SDK не менялась. Что сломалось?
Async context propagation: язык за языком
Каждый runtime имеет свой механизм для «что такое текущий execution-контекст» — и OTel хукается в этот механизм. Когда пересекаешь примитив, через который runtime не несёт контекст автоматически, контекст тихо теряется.
Node.js: AsyncLocalStorage (встроена в Node 12+) — субстрат. OTel хукает её для in-process propagation. Подводные камни: setTimeout, setImmediate, queueMicrotask и любые third-party promise wrapping, создающие новый AsyncLocalStorage-контекст, могут потерять trace-контекст. OTel auto-instrumentation патчит распространённые, но custom-библиотеки ломают. Фикс: context.bind(ctx, fn) перед передачей callback’а на любую async-границу.
Python: contextvars из PEP 567 — субстрат; работает автоматически для asyncio, но не для threading без ContextVar.copy(). При ручном создании потоков OTel-контекст родительского потока не наследуется. Фикс: передавай текущий контекст дочернему потоку и устанавливай через context.attach(ctx) на входе.
Java: классический ThreadLocal плюс Span.makeCurrent() в try-with-resources. Project Loom virtual threads в большинстве случаев прозрачны, но требуют аккуратного вложения Scope — если virtual thread создаётся внутри Scope, scope обязан пережить virtual thread, иначе контекст закрывается пока поток ещё работает. Фикс: структурируй создание virtual thread внутри scope, не снаружи.
Go: явный context.Context везде — каждая функция принимает ctx, каждый span живёт в ctx. Go сделал правильный архитектурный выбор рано; контекст никогда не течёт неявно. Failure mode в Go — не случайная потеря контекста, а забытый ctx в цепочке вызовов функций. Фикс: передавай ctx везде; используй go vet и staticcheck для отметки отсутствующих параметров контекста.
Браузеры: zone.js (решение Angular для патчинга async-примитивов) или предложение TC39 AsyncContext; OTel-JS поддерживает оба через плагины. Service workers и Web Workers требуют явной передачи контекста.
| Runtime | Субстрат контекста | Типичный failure mode | Фикс |
|---|---|---|---|
| Node.js | AsyncLocalStorage | setTimeout / custom async wrappers | context.bind(ctx, fn) |
| Python | contextvars (PEP 567) | Ручное threading обходит asyncio | ContextVar.copy() при spawn потока |
| Java | ThreadLocal + Scope | Loom virtual threads переживают Scope | Создавать virtual thread внутри Scope; использовать context-aware executor |
| Go | Явный context.Context | ctx не протянут через функцию | Передавать ctx везде; vet/staticcheck |
B3 vs W3C: история миграции и безопасная последовательность
До W3C Trace Context, Twitter’s Zipkin/Brave использовали B3 с двумя вариантами: B3 multi-header (X-B3-TraceId, X-B3-SpanId, X-B3-Sampled как отдельные заголовки) и B3 single-header (все три в одном). Оригинальный B3 trace-id был 64-битный; расширен до 128 бит для совместимости с W3C.
Безопасная последовательность миграции:
- Аудит: найти каждый сервис, всё ещё эмитирующий только B3-заголовки.
- Развернуть composite propagator везде: зарегистрировать W3C TraceContext + B3 multi + B3 single на каждом сервисе. Писать исходящий только W3C; читать входящий из обоих. Это фаза «читать оба, писать W3C».
- Верификация: подтвердить, что orphan-span rate не регрессирует.
- Убрать B3 outbound: после подтверждения, что downstream-сервисы читают W3C, отключить B3 outbound у каждого upstream.
- Удалить B3 extractor: после квартала при нулевом B3 inbound заменить composite propagator на W3C-only.
Что ломается при пропуске шагов:
- Пропуск шага 2: upstream отправляет W3C, downstream читает только B3 → трейсы расщепляются.
- Пропуск шага 3: propagation-регрессия тихо длится неделями.
- Пропуск шага 5: двойные байты заголовков на запрос бесконечно.
Trace-контекст через service mesh
Envoy, Linkerd, Istio, Cilium data plane участвуют в tracing двумя способами:
- Pass-through (всегда): sidecar прозрачно перенаправляет заголовки
traceparent/tracestate/baggageна каждом HTTP и gRPC запросе. - Эмитировать собственные span’ы (опционально, но рекомендуется): при включении sidecar создаёт span для сетевого hop’а, показывая sidecar-latency, connection pooling и TLS handshake timing отдельно от application-latency.
Конфигурация: mesh-proxy нужен адрес tracing-collector’а и решение по sampling (обычно — наследовать входящий флаг). Sampling-решение mesh’а должно совпадать с приложением; рассинхронизированные частоты дают несогласованные трейсы.
Ограничение: service mesh обрабатывает только HTTP и gRPC. Consumer’ы очередей, таймеры и fire-and-forget callback’и всё равно требуют явной application-level propagation. Mesh не замена OTel SDK instrumentation; он добавляет network-hop span, не заменяет application span’ы.
Почему это работает
Когда mesh эмитирует собственный span, получаешь трёхспановый вид одного HTTP-вызова: client app, mesh sidecar, server app. Это позволяет разграничить «приложение было медленным» от «mesh был медленным» — критическое различие при инцидентах с обновлениями sidecar, исчерпанием connection pool или штормами обновления mTLS-сертификатов.
Безопасность: trace-id как tracking-идентификатор
Trace-id уникальны на запрос, 128 бит энтропии, пропагируются в HTTP-заголовках и видны всем, кто может инспектировать трафик между клиентом и origin. Это делает их мощными debug-инструментами и столь же мощными потенциальными trackers.
Риск: если third party (CDN, marketing pixel, CSP-allowed analytics-сервис) может прочитать заголовок traceparent из исходящих запросов пользователя, он может коррелировать user-активность между сайтами, делящими одну tracing-инфру.
Смягчения:
- W3C-спека рекомендует, чтобы user-facing-сервисы не пропагировали
traceparentв response’ах (response — пользователю, не часть upstream-вызова). - Browser-side OTel SDK должны ограничивать propagation same-origin и явно разрешёнными CORS-origin’ами (список
TraceContextPropagator.allowedOrigins). - Production-команды держат allowlist downstream-hostname’ов, получающих
traceparent, и аудитируют квартально. - Baggage применяется идентично — всё в baggage наблюдаемо каждым downstream, включая third-party.
- Дефолтный OTel propagator
- TraceContext + Baggage composite
- B3 single-header ширина trace-id (оригинал)
- 64 бита (позже расширено до 128)
- Overhead tracing service-mesh sidecar
- ~1–2% extra CPU
- Байт заголовков на запрос (traceparent + tracestate малый)
- ~80–200 байт
- W3C Trace Context Level 1
- Recommendation 2020-02
- W3C Trace Context Level 2
- Recommendation 2024
Команда мигрирует с B3 на W3C propagation. Они деплоят W3C-write на upstream-сервисах до деплоя W3C-read на downstream. Что происходит?
Service mesh (Envoy) настроен пропагировать traceparent и эмитировать собственные mesh-hop span'ы. После включения команда всё равно видит orphan-трейсы для некоторых Kafka consumer'ов. Почему?
- 01Java-сервис мигрирует на Project Loom virtual threads и orphan-span rate растёт до 30%. Диагностируй и фиксируй.
- 02Опиши безопасную 5-шаговую последовательность миграции с B3 на W3C TraceContext и что ломается при пропуске шага 2.
- 03Каков traceparent privacy-риск в браузерных приложениях и каковы три смягчения?
Propagation контекста в каждом runtime хукается в разный субстрат: AsyncLocalStorage в Node, contextvars в Python, ThreadLocal плюс Scope в Java, явный context.Context в Go. У каждого свой failure mode, когда пересекаешь примитив, через который runtime не несёт контекст автоматически — фикс всегда явная привязка контекста на этой границе. Миграция B3 на W3C требует read-both до write-W3C, верифицированного мониторингом orphan rate. Service mesh передаёт traceparent для HTTP/gRPC прозрачно и может эмитировать mesh-hop span’ы, но не инструментирует queue consumer’ов — они всё равно требуют application-level inject/extract. Заголовок traceparent в браузерных запросах — tracking-вектор при пропагировании к cross-origin third-party; ограничивай same-origin и явно разрешёнными CORS-origin’ами.
встречается в40
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Hidden classes, деревья переходов и расположение в памятиmiddle
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Что такое воркеры и зачем они нужныjunior
- Механика web workers: dedicated, shared и OffscreenCanvasmiddle
- Structured clone и transferablesmiddle
- SharedArrayBuffer, Atomics и cross-origin isolationsenior
- Пулы воркеров, Comlink и наблюдаемость в продакшенеsenior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- JSONB, массивы и когда side table побеждаетmiddle
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Где происходит data fetching — и почему это решает LCPjunior
- React Server Components и Suspense streamingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Конверт IPjunior
- Читаем IP-заголовокmiddle
- Что делает TLS и зачем он нуженjunior
- Расписание ключей, SNI, ALPN и расширенияsenior
- Защита 0-RTT, ECH, гибридный PQ и продакшн TLSsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior