Как реализуется трассировка запросов в распределенных системах?

Ответ

Трассировка (Distributed Tracing) — это механизм отслеживания запроса через все сервисы распределенной системы. Ключевые концепции: Trace (весь путь запроса), Span (отдельная операция), Context Propagation (передача идентификаторов).

1. Основные компоненты трассировки:

Trace (traceId: abc123)
├── Span 1 (spanId: 1, service: Gateway, operation: auth) [50ms]
│   └── Span 2 (spanId: 2, service: AuthService, operation: validate) [30ms]
└── Span 3 (spanId: 3, service: OrderService, operation: create) [120ms]
    ├── Span 4 (spanId: 4, service: PaymentService, operation: charge) [80ms]
    └── Span 5 (spanId: 5, service: InventoryService, operation: reserve) [40ms]

2. Реализация с Spring Cloud Sleuth + Zipkin:

# application.yml
spring:
  sleuth:
    sampler:
      probability: 1.0 # Процент трассируемых запросов (1.0 = 100%)
  zipkin:
    base-url: http://localhost:9411
    sender:
      type: web # Отправка через HTTP
// Автоматическое создание span в контроллере
@RestController
@Slf4j
public class OrderController {

    private final Tracer tracer; // Инжектируется Sleuth

    @PostMapping("/orders")
    public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
        // Создание кастомного span
        Span customSpan = tracer.nextSpan().name("validateOrderRequest").start();
        try (SpanInScope ws = tracer.withSpan(customSpan)) {
            // Логи с traceId и spanId
            log.info("Validating order request for user: {}", request.getUserId());

            // Бизнес-логика
            Order order = orderService.create(request);

            // Добавление тегов для аналитики
            customSpan.tag("order.amount", request.getAmount().toString());
            customSpan.tag("user.tier", request.getUserTier());

            return ResponseEntity.ok(order);
        } finally {
            customSpan.end();
        }
    }

    // Асинхронная трассировка
    @Async
    public CompletableFuture<Void> asyncOperation() {
        // Sleuth автоматически передает контекст в @Async методы
        log.info("Async operation with traceId");
        return CompletableFuture.completedFuture(null);
    }
}

3. Контекст и propagation:

// Ручная передача контекста между потоками
Span span = tracer.currentSpan();
TraceContext context = span.context();
String traceId = context.traceId();
String spanId = context.spanId();

// Для передачи вручную (например, в кастомный executor)
TraceContext traceContext = tracer.currentTraceContext().context();
executor.submit(() -> {
    try (TraceContext.Scope scope = tracer.currentTraceContext().newScope(traceContext)) {
        // Код с унаследованным контекстом трассировки
        log.info("Executing in thread with traceId: {}", traceId);
    }
});

4. Интеграция с другими инструментами:

// Добавление traceId в HTTP-заголовки для внешних вызовов
@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();

    // Sleuth автоматически добавляет заголовки X-B3-TraceId, X-B3-SpanId
    restTemplate.setInterceptors(Collections.singletonList(
        new RestTemplateInterceptor()
    ));

    return restTemplate;
}

// Интеграция с Messaging (Kafka)
@Bean
public ProducerFactory<String, String> producerFactory() {
    Map<String, Object> config = new HashMap<>();
    config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");

    // Добавление инжектора контекста для Kafka
    return new DefaultKafkaProducerFactory<>(config, 
        new StringSerializer(), 
        new StringSerializer(),
        new PropagatingSenderInterceptor<>() // Передает trace контекст в headers
    );
}

5. Best Practices:

  • Сэмплинг: На production использовать adaptive sampling (например, 10-20% запросов)
  • Теги: Добавлять business-теги (userId, orderId, endpoint) для поиска
  • Производительность: Использовать асинхронную отправку spans в collector
  • Корреляция: Связывать трассировку с логами через MDC и метриками
  • Алертинг: Настраивать алерты на медленные traces (по percentiles: p95, p99)

Ответ 18+ 🔞

А, слушай, вот эта ваша трассировка распределённая... Это ж как если бы ты, блядь, пошёл в пять разных контор за справкой, а в каждой тебе на лоб штамп ставят, и потом по отпечаткам можно восстановить, какой же ты, сука, путь прошёл и где тебя больше всего ебали. Вот это и есть Trace — весь маршрут твоего запроса, от начала и до пиздеца.

А Span — это каждая отдельная остановка, где тебя конкретно имели. Зашёл в шлюз — один спан. Потом тебя авторизовали — второй спан. Создали заказ — третий. И так далее, пока запрос не сдохнет или не выполнится.

Вот, смотри, как это выглядит, если нарисовать:

Trace (traceId: abc123)
├── Span 1 (spanId: 1, service: Gateway, operation: auth) [50ms]
│   └── Span 2 (spanId: 2, service: AuthService, operation: validate) [30ms]
└── Span 3 (spanId: 3, service: OrderService, operation: create) [120ms]
    ├── Span 4 (spanId: 4, service: PaymentService, operation: charge) [80ms]
    └── Span 5 (spanId: 5, service: InventoryService, operation: reserve) [40ms]

Видишь? Весь путь — это трейс. А каждый шаг, где сервис что-то делал — это спан. И главная фишка — Context Propagation, то есть как этот штамп traceId передаётся из одного сервиса в другой, чтобы все они знали, что они мучают один и тот же запрос.

Ну и как это, блядь, прикрутить?

Возьмём Spring Cloud Sleuth, он как раз этим и занимается. И Zipkin, куда все эти спаны слать. В конфиге пишешь:

# application.yml
spring:
  sleuth:
    sampler:
      probability: 1.0 # Трассируем ВСЕ запросы, нахуй. На проде так не делай, а то логов будет овердохуища.
  zipkin:
    base-url: http://localhost:9411
    sender:
      type: web

А в коде Sleuth сам всё сделает, но если хочешь ручками поковыряться, то вот:

@RestController
@Slf4j
public class OrderController {

    private final Tracer tracer; // Sleuth его сам подсунет

    @PostMapping("/orders")
    public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
        // Хочешь свой, кастомный спан? Пожалуйста!
        Span customSpan = tracer.nextSpan().name("validateOrderRequest").start();
        try (SpanInScope ws = tracer.withSpan(customSpan)) {
            // В логах теперь автоматом будут traceId и spanId. Красота, блядь.
            log.info("Validating order request for user: {}", request.getUserId());

            // Делаем свои делишки
            Order order = orderService.create(request);

            // Можно навесить теги, чтобы потом в Zipkin искать. Типа "а покажи-ка мне все заказы дороже тысячи рублей".
            customSpan.tag("order.amount", request.getAmount().toString());
            customSpan.tag("user.tier", request.getUserTier());

            return ResponseEntity.ok(order);
        } finally {
            customSpan.end(); // Не забудь закрыть, а то память потечёт.
        }
    }

    // А если асинхронно? С @Async Sleuth тоже умеет, контекст протащит.
    @Async
    public CompletableFuture<Void> asyncOperation() {
        log.info("Async operation with traceId");
        return CompletableFuture.completedFuture(null);
    }
}

А если тебе надо контекст между потоками передать вручную, потому что твой Executor — говно и Sleuth его не знает?

// Лови текущий контекст
Span span = tracer.currentSpan();
TraceContext context = span.context();
String traceId = context.traceId(); // Вот он, родной!

// И тащи его в свой поток
TraceContext traceContext = tracer.currentTraceContext().context();
executor.submit(() -> {
    try (TraceContext.Scope scope = tracer.currentTraceContext().newScope(traceContext)) {
        // Всё, теперь этот поток в том же trace. Магия, ёпта.
        log.info("Executing in thread with traceId: {}", traceId);
    }
});

Интеграция с другими штуками? Да легко. Например, для HTTP-вызовов через RestTemplate Sleuth сам в заголовки X-B3-TraceId и X-B3-SpanId всё засунет. Для Kafka тоже есть специальные инжекторы, которые контекст в хедеры сообщения запихивают.

// Для Kafka Producer
@Bean
public ProducerFactory<String, String> producerFactory() {
    // ... конфиг
    return new DefaultKafkaProducerFactory<>(config, 
        new StringSerializer(), 
        new StringSerializer(),
        new PropagatingSenderInterceptor<>() // Вот этот пацан trace контекст в headers засунет
    );
}

И напоследок, блядь, советы, чтобы не обосраться:

  • Сэмплинг: На проде не трассируй 100% запросов, ёбта. Выставь probability на 0.1 или 0.2. Хватит. Или используй adaptive sampling, который умный.
  • Теги: Вешай бизнес-теги (userId, orderId, errorCode). Потом в Zipkin сможешь искать все трейсы, где юзер Vasya купил телевизор. Очень удобно для дебага.
  • Производительность: Отправку спанов в коллектор (Zipkin) делай асинхронной, чтобы твоё приложение не ждало, пока они улетят.
  • Корреляция: Связывай трейсы с логами (через MDC) и метриками. Чтобы когда алерт сработал, ты мог сразу найти все логи по этому ебанному traceId.
  • Алертинг: Настрой алерты на медленные трейсы. Следи за перцентилями p95, p99. Если p99 время выполнения заказа стало 5 секунд — пора, сука, копать, что там с платежкой случилось.

Вот и вся философия. Запрос идёт, как конь по полю, а ты за ним следишь, где он споткнулся, где его волки съели. Удобная хуйня, в общем.