Пошук уроків, статей та іншого контенту
Дізнайтеся, як трасування допомагає відстежувати запити між сервісами та знаходити затримки й помилки.
У монолітному застосунку запит зазвичай проходить через один процес. Щоб знайти проблему, достатньо переглянути його логи.
У розподіленій системі один користувацький запит може пройти через кілька сервісів:
Клієнт → API Gateway → Сервіс замовлень → Сервіс оплати → База данихКожен сервіс має власні логи та метрики. Без додаткового зв’язку складно визначити:
які саме сервіси обробляли запит;
де виникла помилка;
який сервіс додав найбільшу затримку;
чи пов’язана повільна відповідь із базою даних, мережею або конкретною операцією.
Розподілене трасування — це спосіб відстежити життєвий цикл одного запиту через усі сервіси, які його обробляють.
Trace — повний шлях одного запиту через систему.
Наприклад, trace для оформлення замовлення може містити такі операції:
Trace: 4bf92f3577b34da6a3ce929d0e0e4736
├── API Gateway: 120 ms
├── Order Service: 85 ms
│ ├── PostgreSQL query: 30 ms
│ └── Payment Service: 45 ms
└── Response: 120 msTrace має унікальний trace ID, який однаковий для всіх операцій у межах цього запиту.
Span — окрема операція всередині trace.
Span може описувати:
обробку HTTP-запиту;
виклик іншого сервісу;
запит до бази даних;
звернення до зовнішнього API;
виконання важливої внутрішньої операції.
Span зазвичай містить:
trace_id — ідентифікатор повного запиту;
span_id — ідентифікатор конкретної операції;
parent_span_id — батьківський span;
час початку;
тривалість;
назву операції;
атрибути;
статус;
інформацію про помилку.
Span утворюють ієрархію:
Trace
└── GET /orders
├── SELECT orders
└── HTTP POST payment
└── Validate cardКоли один сервіс викликає інший, span виклику стає батьківським для span, створеного в іншому сервісі.
API Gateway span
└── Order Service span
└── Payment Service spanЗавдяки цьому система трасування може відновити послідовність операцій і показати її як дерево.
Щоб різні сервіси розуміли, що належать до одного trace, сервіс-переможець передає контекст у заголовках запиту.
Один із поширених форматів — traceparent із W3C Trace Context:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01У ньому містяться:
версія формату;
trace_id;
parent_span_id;
прапорці трасування.
Наприклад, Gateway отримує запит і створює span:
trace_id: abc123
span_id: gateway001Перед викликом іншого сервісу він передає:
traceparent: 00-abc123-order001-01Order Service створює дочірній span:
trace_id: abc123
span_id: order001
parent_span_id: gateway001Важливо, що trace_id залишається незмінним протягом усього запиту, а кожен сервіс створює власний span_id.
Розглянемо запит із такою структурою:
GET /checkout — 850 ms
├── Load cart — 40 ms
├── Check inventory — 90 ms
├── Create payment — 680 ms
│ └── External payment API — 650 ms
└── Save order — 40 msЗі звичайного логу Gateway видно лише загальну тривалість — 850 мс. Trace показує, що майже весь час витрачено на зовнішній платіжний API.
Трасування особливо корисне для:
послідовних викликів між сервісами;
паралельних операцій;
повторних спроб;
тайм-аутів;
повільних запитів до бази даних;
пошуку сервісу, який породжує каскадні затримки.
Якщо сервіс послідовно викликає два інші сервіси:
Service A — 300 ms
├── Service B — 100 ms
└── Service C — 200 msЗагальний час буде близьким до 300 мс.
Якщо виклики виконуються паралельно:
Service A — 200 ms
├── Service B — 100 ms
└── Service C — 200 msЗагальний час визначає найдовша гілка — приблизно 200 мс.
Trace допомагає побачити таку структуру, тоді як окремі логи сервісів можуть приховати її.
Коли один сервіс повертає помилку, за одним лише повідомленням на клієнті часто неможливо визначити причину:
500 Internal Server ErrorTrace може показати:
Trace: abc123
├── API Gateway — OK
├── Order Service — OK
└── Payment Service — ERROR
├── status: ERROR
├── error.type: TimeoutError
└── duration: 3000 msОдин trace_id у логах усіх сервісів дозволяє швидко знайти всі події, пов’язані з одним запитом.
Під час помилки до span можна додати:
статус ERROR;
тип помилки;
безпечне текстове повідомлення;
код відповіді;
кількість спроб;
тривалість операції.
Не слід додавати до trace паролі, токени, повні номери карток та інші конфіденційні дані.
Нижче наведено приклад із двома HTTP-сервісами без зовнішніх бібліотек:
Gateway працює на порту 3000;
Order Service працює на порту 3001;
Gateway створює trace;
Order Service отримує traceparent і створює дочірній span;
обидва сервіси записують події у JSON-логах.
Збережіть код у файл tracing-example.js і запустіть командою node tracing-example.js.
const http = require("node:http");
const crypto = require("node:crypto");
function randomHex(bytes) {
return crypto.randomBytes(bytes).toString("hex");
}
function createTraceId() {
return randomHex(16);
}
function createSpanId() {
return randomHex(8);
}
function parseTraceparent(value) {
if (typeof value !== "string") {
return null;
}
const match = value.match(/^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i);
if (!match) {
return null;
}
return {
traceId: match[1].toLowerCase(),
parentSpanId: match[2].toLowerCase(),
flags: match[3].toLowerCase()
};
}
function makeTraceparent(traceId, spanId) {
return `00-${traceId}-${spanId}-01`;
}
function logSpan(event, span) {
console.log(JSON.stringify({
event,
service: span.service,
operation: span.operation,
trace_id: span.traceId,
span_id: span.spanId,
parent_span_id: span.parentSpanId || null,
duration_ms: span.durationMs,
status: span.status,
error: span.error || null
}));
}
function startSpan({ service, operation, traceId, parentSpanId }) {
return {
service,
operation,
traceId,
spanId: createSpanId(),
parentSpanId,
startedAt: process.hrtime.bigint(),
status: "OK",
error: null
};
}
function finishSpan(span, status = "OK", error = null) {
const elapsed = process.hrtime.bigint() - span.startedAt;
span.durationMs = Number(elapsed) / 1_000_000;
span.status = status;
span.error = error;
logSpan("span.finished", span);
}
const orderService = http.createServer((req, res) => {
const parent = parseTraceparent(req.headers.traceparent);
const traceId = parent ? parent.traceId : createTraceId();
const span = startSpan({
service: "order-service",
operation: `${req.method} ${req.url}`,
traceId,
parentSpanId: parent ? parent.parentSpanId : undefined
});
const shouldFail = new URL(req.url, "http://localhost").searchParams.get("fail") === "1";
setTimeout(() => {
if (shouldFail) {
finishSpan(span, "ERROR", "Order storage is unavailable");
res.writeHead(500, {
"content-type": "application/json"
});
res.end(JSON.stringify({
error: "order_service_failure",
trace_id: traceId
}));
return;
}
finishSpan(span);
res.writeHead(200, {
"content-type": "application/json"
});
res.end(JSON.stringify({
order_id: "order-123",
trace_id: traceId
}));
}, 80);
});
const gateway = http.createServer((req, res) => {
const incomingContext = parseTraceparent(req.headers.traceparent);
const traceId = incomingContext
? incomingContext.traceId
: createTraceId();
const gatewaySpan = startSpan({
service: "api-gateway",
operation: `${req.method} ${req.url}`,
traceId,
parentSpanId: incomingContext
? incomingContext.parentSpanId
: undefined
});
const orderServiceSpanId = createSpanId();
const request = http.get(
{
hostname: "localhost",
port: 3001,
path: req.url,
headers: {
traceparent: makeTraceparent(traceId, orderServiceSpanId)
}
},
(orderResponse) => {
let body = "";
orderResponse.setEncoding("utf8");
orderResponse.on("data", (chunk) => {
body += chunk;
});
orderResponse.on("end", () => {
if (orderResponse.statusCode >= 500) {
finishSpan(gatewaySpan, "ERROR", "Order Service returned an error");
} else {
finishSpan(gatewaySpan);
}
res.writeHead(orderResponse.statusCode, {
"content-type": "application/json"
});
res.end(body);
});
}
);
request.on("error", (error) => {
finishSpan(gatewaySpan, "ERROR", error.message);
res.writeHead(502, {
"content-type": "application/json"
});
res.end(JSON.stringify({
error: "bad_gateway",
trace_id: traceId
}));
});
});
orderService.listen(3001, () => {
console.log("Order Service слухає http://localhost:3001");
});
gateway.listen(3000, () => {
console.log("API Gateway слухає http://localhost:3000");
});В іншому терміналі виконайте:
curl http://localhost:3000/ordersДля перевірки помилки:
curl http://localhost:3000/orders?fail=1У логах для одного запиту буде однаковий trace_id, але різні span_id:
{"event":"span.finished","service":"order-service","operation":"GET /orders","trace_id":"...","span_id":"...","parent_span_id":"...","duration_ms":80.4,"status":"OK","error":null}
{"event":"span.finished","service":"api-gateway","operation":"GET /orders","trace_id":"...","span_id":"...","parent_span_id":null,"duration_ms":93.1,"status":"OK","error":null}Цей приклад вручну демонструє головний принцип:
перший сервіс створює trace;
сервіс створює span для власної операції;
контекст передається через HTTP-заголовок;
наступний сервіс створює дочірній span;
усі span можна об’єднати за trace_id.
У реальних системах ручне створення span швидко стає складним. Зазвичай використовують стандартизовані інструменти, зокрема OpenTelemetry, які автоматизують інструментування HTTP-запитів, клієнтів баз даних і міжсервісних викликів.
Окрім ідентифікаторів і тривалості, span може містити атрибути:
http.request.method: GET
http.route: /orders/{id}
http.response.status_code: 200
service.name: order-service
deployment.environment: productionАтрибути допомагають фільтрувати traces:
знайти всі помилки 500;
знайти запити до конкретного маршруту;
порівняти staging і production;
перевірити traces конкретного сервісу.
До span також можна додати події:
order.validation.started
payment.request.sent
payment.response.receivedАле атрибути та події мають бути корисними для діагностики. Надмірна кількість даних збільшує обсяг телеметрії та може ускладнити пошук.
Сервіси зазвичай не аналізують traces безпосередньо. Вони передають дані до системи збору телеметрії.
Типовий процес:
Сервіс → Агент або колектор → Сховище traces → Інтерфейс пошукуСистема зберігання дозволяє:
шукати trace за trace_id;
переглядати дерево span;
сортувати traces за тривалістю;
знаходити помилки;
аналізувати проблеми конкретного сервісу.
Для production-систем важливо контролювати обсяг даних. Зазвичай застосовують sampling — зберігають не всі traces, а лише частину.
Поширені стратегії:
зберігати фіксований відсоток запитів;
завжди зберігати traces із помилками;
зберігати повільні traces;
збільшувати sampling тимчасово під час розслідування проблеми.
Трасування, логи та метрики вирішують різні завдання:
Метрики показують, що проблема існує: наприклад, зросла середня затримка.
Трасування показує, де саме в запиті виникла проблема.
Логи містять деталі події та контекст помилки.
Найбільш корисний зв’язок — додавати trace_id до логів. Тоді після знаходження помилкового trace можна перейти до всіх пов’язаних записів логів.
Наприклад:
trace_id=abc123 span_id=payment001 Payment API timeoutБез trace_id пошук серед логів різних сервісів може бути неточним, особливо під час великого навантаження.
Для HTTP-запиту послідовність дій зазвичай така:
Вхідний сервіс читає контекст із заголовків.
Якщо контексту немає, сервіс створює новий trace_id.
Сервіс створює span для вхідного запиту.
Перед викликом іншого сервісу створюється або активується дочірній контекст.
Контекст передається в заголовках.
Інший сервіс створює span із тим самим trace_id.
Після завершення операції span отримує тривалість і статус.
Дані span передаються до системи збору телеметрії.
Якщо сервіс не передає traceparent, trace розривається на окремі частини.
Trace A
└── Gateway
Trace B
└── Order ServiceПотрібно перевіряти передавання контексту через усі протоколи та клієнти, які використовує система.
Кожен сервіс має створювати новий span, але не новий trace для того самого запиту.
Неправильно:
Gateway trace: A
Order Service trace: B
Payment Service trace: CПравильно:
Gateway trace: A
Order Service trace: A
Payment Service trace: AНе додавайте до span:
паролі;
access token;
секретні ключі;
повні дані платіжних карток;
персональні дані без необхідності.
Перед додаванням атрибутів потрібно перевірити, чи справді вони потрібні для діагностики.
Створення span для кожної дрібної внутрішньої операції може:
збільшити витрати на зберігання;
погіршити продуктивність;
ускладнити перегляд trace;
створити багато шуму.
Трасуйте операції, які впливають на поведінку системи або допомагають знаходити проблеми.
Span має завершуватися зі статусом помилки, якщо операція завершилася невдало. Якщо записувати лише успішні span, трасування не покаже реальну картину роботи системи.
Середня затримка може приховати рідкісні, але важливі повільні запити. Потрібно аналізувати окремі traces, перцентилі та запити з помилками.
Для початку варто інструментувати:
вхідні HTTP-запити;
вихідні HTTP-запити;
запити до основних баз даних;
виклики черг або брокерів повідомлень;
критичні бізнес-операції.
Для кожного span перевірте:
чи є коректний trace_id;
чи зберігається зв’язок із батьківським span;
чи записується тривалість;
чи фіксуються помилки;
чи не потрапляють конфіденційні дані;
чи можна знайти пов’язані логи.
Distributed tracing показує повний шлях запиту через розподілену систему.
Trace об’єднує всі операції одного запиту.
Span описує окрему операцію та має власний span_id.
Усі span одного запиту повинні мати однаковий trace_id.
Контекст передається між сервісами через заголовки, наприклад traceparent.
Трасування допомагає знаходити затримки, помилки та проблемні залежності.
trace_id варто додавати до логів для швидкого пошуку пов’язаних подій.
Необхідно контролювати обсяг телеметрії, використовувати sampling і не записувати конфіденційні дані.