Перейти к содержимому
К программе курса
Распределенная трассировка: от основ до production
6 / 1833%

Трассировка цепочки микросервисов

70 минут

Трассировка цепочки микросервисов

Цель урока

Создадим цепочку из трёх микросервисов и научимся отлаживать проблемы с помощью трассировки. Вы увидите:

  • Автоматическое распространение trace context между сервисами
  • Полную картину запроса через несколько сервисов
  • Реальные примеры debugging (медленные запросы, ошибки, ретраи)
  • Корреляцию span'ов из разных сервисов в единый trace

Готовые примеры кода

Вся цепочка микросервисов доступна в репозитории курса с Docker Compose setup для одноклико вого запуска.

Готовые примеры: Цепочка микросервисов

См. директорию: 04-microservices-chain/ в примерах курса

Быстрый старт:

cd 04-microservices-chain
docker-compose up -d
 
# Подождать 10-15 секунд пока все сервисы запустятся
sleep 15
 
# Выполнить запрос через цепочку
curl -X POST http://localhost:3000/api/checkout \
  -H "Content-Type: application/json" \
  -d '{"userId": 42, "amount": 100}'
 
# Открыть Jaeger и найти trace
open http://localhost:16686

Что включено:

  • API Gateway (Node.js) на порту 3000
  • Auth Service (Python) на порту 3001
  • User Service (Go) на порту 3002
  • PostgreSQL + Jaeger

Подробные инструкции: README.md

Архитектура системы

Что мы увидим в Jaeger:

Trace Timeline:


Шаг 1: Инфраструктура (Docker Compose)

Создайте файл docker-compose.yml:

version: "3.8"
 
services:
  jaeger:
    image: jaegertracing/all-in-one:1.53
    ports:
      - "16686:16686" # Jaeger UI
      - "4317:4317" # OTLP gRPC
      - "4318:4318" # OTLP HTTP
    environment:
      - COLLECTOR_OTLP_ENABLED=true
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: microservices_db
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
 
  # Сервисы будут запускаться отдельно для удобства debugging

Файл init.sql:

-- Users table
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    balance DECIMAL(10, 2) DEFAULT 0.00,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
-- Auth tokens table
CREATE TABLE IF NOT EXISTS tokens (
    token VARCHAR(255) PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
-- Purchases table
CREATE TABLE IF NOT EXISTS purchases (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    amount DECIMAL(10, 2) NOT NULL,
    product_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
-- Test data
INSERT INTO users (name, email, balance) VALUES
    ('Alice Smith', 'alice@example.com', 1000.00),
    ('Bob Johnson', 'bob@example.com', 500.00),
    ('Charlie Brown', 'charlie@example.com', 250.00);
 
-- Valid test token (expires in 2050)
INSERT INTO tokens (token, user_id, expires_at) VALUES
    ('test-token-alice', 1, '2050-01-01 00:00:00'),
    ('test-token-bob', 2, '2050-01-01 00:00:00');

Запуск инфраструктуры:

docker-compose up -d

Шаг 2: Auth Service (проверка токенов)

Язык программирования:

Шаг 3: User Service (бизнес-логика)

Сервис обрабатывает покупки: проверяет баланс, создаёт запись о покупке, обновляет баланс пользователя.

Ключевые моменты реализации:

  • HTTP-клиент автоматически передаёт trace context
  • Custom spans для бизнес-операций
  • Транзакции в БД трассируются автоматически

Код аналогичен Auth Service, но с endpoint'ом /users/:id/purchase:

// Node.js example (server.js фрагмент)
app.post("/users/:id/purchase", async (req, res) => {
  return tracer.startActiveSpan("processPurchase", async (span) => {
    const userId = parseInt(req.params.id);
    const { product_name, amount } = req.body;
 
    span.setAttribute("user.id", userId);
    span.setAttribute("purchase.product", product_name);
    span.setAttribute("purchase.amount", amount);
 
    const client = await pool.connect();
 
    try {
      await client.query("BEGIN");
 
      // Check balance
      const userResult = await client.query(
        "SELECT balance FROM users WHERE id = $1",
        [userId]
      );
 
      if (userResult.rows.length === 0) {
        throw new Error("User not found");
      }
 
      const balance = parseFloat(userResult.rows[0].balance);
 
      if (balance < amount) {
        span.addEvent("Insufficient balance", {
          balance,
          required: amount,
        });
        throw new Error("Insufficient balance");
      }
 
      // Create purchase
      await client.query(
        "INSERT INTO purchases (user_id, product_name, amount) VALUES ($1, $2, $3)",
        [userId, product_name, amount]
      );
 
      // Update balance
      await client.query(
        "UPDATE users SET balance = balance - $1 WHERE id = $2",
        [amount, userId]
      );
 
      await client.query("COMMIT");
 
      span.setStatus({ code: SpanStatusCode.OK });
      span.addEvent("Purchase completed");
 
      res.json({ success: true, new_balance: balance - amount });
    } catch (error) {
      await client.query("ROLLBACK");
      span.recordException(error);
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      res.status(400).json({ error: error.message });
    } finally {
      client.release();
      span.end();
    }
  });
});

Шаг 4: API Gateway (точка входа)

API Gateway объединяет вызовы Auth Service и User Service в единый endpoint.

// server.js (api-gateway)
const axios = require("axios");
 
app.post("/api/checkout", async (req, res) => {
  return tracer.startActiveSpan("checkout", async (span) => {
    try {
      const token = req.headers.authorization;
      const { product_name, amount } = req.body;
 
      span.setAttribute("checkout.product", product_name);
      span.setAttribute("checkout.amount", amount);
 
      // Step 1: Validate token
      let authResponse;
      try {
        authResponse = await axios.get("http://localhost:3001/validate", {
          headers: { Authorization: token },
        });
      } catch (error) {
        span.addEvent("Auth failed");
        return res.status(401).json({ error: "Authentication failed" });
      }
 
      const userId = authResponse.data.user_id;
      span.setAttribute("user.id", userId);
 
      // Step 2: Process purchase
      const purchaseResponse = await axios.post(
        `http://localhost:3002/users/${userId}/purchase`,
        { product_name, amount }
      );
 
      span.setStatus({ code: SpanStatusCode.OK });
      span.addEvent("Checkout completed");
 
      res.json({
        success: true,
        ...purchaseResponse.data,
      });
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      res.status(500).json({ error: error.message });
    } finally {
      span.end();
    }
  });
});

Шаг 5: Запуск и тестирование

Запустите все сервисы:

# Terminal 1: Auth Service
cd auth-service
npm install && npm start
 
# Terminal 2: User Service
cd user-service
npm install && npm start
 
# Terminal 3: API Gateway
cd api-gateway
npm install && npm start

Тестовый запрос:

curl -X POST http://localhost:3000/api/checkout \
  -H "Authorization: Bearer test-token-alice" \
  -H "Content-Type: application/json" \
  -d '{
    "product_name": "Premium Subscription",
    "amount": 99.99
  }'

Откройте Jaeger UI: http://localhost:16686

Выберите сервис api-gateway и найдите свой trace!


Что вы увидите в Jaeger

Полный trace с тремя сервисами:

Trace ID: a1b2c3d4e5f6...
Duration: 850ms
 
├─ POST /api/checkout [850ms] (api-gateway)
   ├─ checkout [845ms] (api-gateway custom span)
   │  ├─ GET http://localhost:3001/validate [125ms] (HTTP client)
   │  │  └─ GET /validate [120ms] (auth-service)
   │  │     ├─ validateToken [115ms] (auth-service custom span)
   │  │     │  └─ SELECT FROM tokens [15ms] (postgres)
   │  │     └─ Event: Token validated successfully
   │  │
   │  ├─ POST http://localhost:3002/users/1/purchase [710ms] (HTTP client)
   │     └─ POST /users/1/purchase [700ms] (user-service)
   │        ├─ processPurchase [695ms] (user-service custom span)
   │           ├─ BEGIN [5ms] (postgres)
   │           ├─ SELECT balance FROM users [20ms] (postgres)
   │           ├─ INSERT INTO purchases [35ms] (postgres)
   │           ├─ UPDATE users SET balance [10ms] (postgres)
   │           ├─ COMMIT [5ms] (postgres)
   │           └─ Event: Purchase completed

   └─ Event: Checkout completed

Ключевые наблюдения:

  1. Автоматическая correlation: все spans связаны одним Trace ID
  2. Cascade timing: видно, что User Service ждёт завершения Auth Service
  3. Database visibility: каждый SQL-запрос — отдельный span
  4. Events timeline: события внутри spans помогают debugging

Практическое задание: Debugging медленного запроса

Сценарий: Добавьте искусственную задержку в Auth Service (100ms вместо 50ms) и найдите её в Jaeger.

Задача:

  1. Измените setTimeout(50) на setTimeout(100) в Auth Service
  2. Перезапустите сервис
  3. Сделайте новый запрос
  4. Сравните два trace в Jaeger UI (используйте Compare функцию)
  5. Определите, какой span стал медленнее и на сколько

Ожидаемый результат: Вы должны увидеть увеличение длительности span'а validateToken с ~115ms до ~165ms.


Debugging реальных проблем

Проблема 1: N+1 Query Problem

Добавьте код в User Service, который делает несколько запросов вместо одного:

// Плохо: N+1 queries
for (let i = 0; i < 5; i++) {
  await client.query("SELECT * FROM users WHERE id = $1", [userId]);
}
 
// В Jaeger вы увидите:
// └─ processPurchase [850ms]
//    ├─ SELECT * FROM users [20ms] ← дубликат 1
//    ├─ SELECT * FROM users [20ms] ← дубликат 2
//    ├─ SELECT * FROM users [20ms] ← дубликат 3
//    ├─ SELECT * FROM users [20ms] ← дубликат 4
//    └─ SELECT * FROM users [20ms] ← дубликат 5

Проблема 2: Failed Authentication

Сделайте запрос с неверным токеном:

curl -X POST http://localhost:3000/api/checkout \
  -H "Authorization: Bearer invalid-token" \
  -H "Content-Type: application/json" \
  -d '{"product_name": "Test", "amount": 10}'

В Jaeger:

  • Trace будет помечен как ошибка (красный цвет)
  • Span validateToken будет иметь status.code = ERROR
  • Event "Token validation failed" покажет точку отказа

Best Practices

1. Именование spans:

  • Используйте операционные имена: validateToken, processPurchase
  • Не используйте переменные значения: ❌ validate-test-token-alice

2. Атрибуты:

// ✅ Хорошо
span.setAttribute("user.id", 123);
span.setAttribute("purchase.amount", 99.99);
 
// ❌ Плохо (PII данные)
span.setAttribute("user.email", "alice@example.com");
span.setAttribute("credit_card", "4111-1111-1111-1111");

3. Events vs Attributes:

  • Attributes: статические свойства (user_id, http.method)
  • Events: моментные действия (Token validated, Purchase completed)

Следующий урок

В следующем уроке мы глубоко изучим Context Propagation и W3C Trace Context стандарт — как именно trace context передаётся между сервисами и что делать, если автоматическая инструментация не работает.

Поздравляем! Вы создали полноценную микросервисную систему с distributed tracing и научились находить проблемы за минуты вместо часов.