Трассировка цепочки микросервисов
Трассировка цепочки микросервисов
Цель урока
Создадим цепочку из трёх микросервисов и научимся отлаживать проблемы с помощью трассировки. Вы увидите:
- Автоматическое распространение 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Ключевые наблюдения:
- Автоматическая correlation: все spans связаны одним Trace ID
- Cascade timing: видно, что User Service ждёт завершения Auth Service
- Database visibility: каждый SQL-запрос — отдельный span
- Events timeline: события внутри spans помогают debugging
Практическое задание: Debugging медленного запроса
Сценарий: Добавьте искусственную задержку в Auth Service (100ms вместо 50ms) и найдите её в Jaeger.
Задача:
- Измените
setTimeout(50)наsetTimeout(100)в Auth Service - Перезапустите сервис
- Сделайте новый запрос
- Сравните два trace в Jaeger UI (используйте Compare функцию)
- Определите, какой 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 и научились находить проблемы за минуты вместо часов.