Реалізація Correlation ID для трасування запитів через мікросервіси

Коли запит проходить через десяток сервісів, знайти джерело збою без наскрізного ідентифікатора майже неможливо. Ми налаштовуємо передавання Correlation ID під ключ: пов’язуємо журнали всіх сервісів в єдиний ланцюжок і скорочуємо час пошуку причини збою в рази. Обговоримо завдання — повернемося з оцінкою та планом.
Часті запитання

Чому Correlation ID губиться між мікросервісами

Correlation ID зазвичай народжується на самому початку шляху: зворотний проксі або вхідний шлюз присвоює запиту ідентифікатор і кладе його в заголовок. За синхронними викликами він іде рівно — кожен сервіс читає заголовок і додає значення у свої записи журналу. Поки ланцюжок викликів лінійний, трасування мікросервісів виглядає вирішеною задачею.

Розрив трапляється вже на першому асинхронному переході. Сервіс публікує повідомлення в чергу або ставить відкладене завдання, але ідентифікатор залишається в контексті обробника, який цю задачу поставив, і в саме повідомлення не потрапляє.

Обробник на іншому кінці піднімає новий ланцюжок з порожнім значенням — і наскрізний ланцюжок викликів рветься саме там, де починається найнетривіальніше: ретраї, повторна обробка, відкладені списання.

Ціна проявляється під час розбору інциденту. У журналах п'яти сервісів лежать сотні записів, і пов'язати їх можна лише за часом і тілом запиту.

Час у сервісів розходиться, однакових запитів за хвилину десятки, а втрата ідентифікатора запиту означає, що зв'язку між записами немає взагалі. Інженери вручну перебирають журнали, зіставляючи позначки часу, і на кожен збій іде кілька годин роботи двох-трьох людей.

Окремо болить те, що частина інцидентів закривається формулюванням «не відтворюється»: причина залишається в системі й спрацьовує знову. Проблема тут не у відсутності інструментів, а в тому, що значення не переживає межу контексту — чергу, фоновий обробник, відкладене завдання. Розбираємо, як це закривається.

Що дає наскрізне трасування запитів

Без наскрізного трасування розбір інциденту перетворюється на звіряння журналів за позначками часу. Інженер іде п'ятьма-сімома сервісами, зіставляє записи вручну і все одно не бачить, на якому кроці запит загубився.

Correlation ID вирішує це: ідентифікатор народжується на вході й передається в усіх викликах — від шлюзу до черги повідомлень і фонового завдання.

Єдиний ланцюжок подій замість розрізнених журналів

Ідентифікатор кладеться в заголовок вхідного запиту, прокидається в міжсервісні виклики та в метадані повідомлень брокера, потрапляє в контекст відкладених завдань.

Збирач журналів індексує його окремим полем — одна вибірка за значенням дає повну хронологію запиту по всіх сервісах, включно з відповідями та помилками. Формат контексту зводимо до єдиного вигляду: без цього частина сервісів втрачає ідентифікатор під час асинхронної передачі.

Тримаємо сумісність із поширеним стандартом OpenTelemetry, щоб пов'язати трасування з метриками й не плодити власні правила.

Як вимірюємо ефект

Дві метрики: частка запитів з повним ланцюжком — рахуємо за кількістю ідентифікаторів, у яких є події на всьому шляху, і час до визначення винуватця. Друге падає з десятків хвилин ручного звіряння до пари хвилин на вибірку.

Спостережуваність сервісів перестає залежати від того, хто з інженерів знає топологію викликів: ланцюжок читається прямо з журналів.

Варіанти передачі ідентифікатора між сервісами

Correlation ID народжується на межі довіри — там, де запит уперше входить у наш контур: зворотний проксі, шлюз або перший сервіс ланцюжка. Якщо правило не зафіксувати, кожен сервіс заведе своє значення, і пов'язати події в журналах буде нічим. Далі питання одне: як ідентифікатор переживає перехід між процесами й чергами.

Варіант Коли підходить Обмеження
Заголовок запиту Синхронні виклики між сервісами Губиться на редиректах і проміжних проксі
Поле в повідомленні черги Асинхронна обробка та відкладені завдання Залежить від формату повідомлення, легко забути заповнити
Прошарок для контексту запиту Багато сервісів, потрібен єдиний спосіб Правки в усіх сервісах, контекст потрібно зберігати всередині потоку
Ручна передача параметром Разові інтеграції та тести Обростає забутими місцями, не скрізь можна змінити підпис

Де ідентифікатор народжується

Народжується він на вхідному вузлі: той або приймає значення від клієнта, якщо запит надійшов із довіреного джерела, або генерує своє. Приймати чуже значення ззовні не можна — ланцюжки підробляються, а журнали засмічуються. Межу довіри варто провести явно: внутрішні сервіси довіряють одне одному, зовнішній контур — ні.

Як ідентифікатор живе в чергах

В асинхронному контурі ідентифікатор їде у властивостях повідомлення, а не в заголовках запиту — заголовків у повідомлення немає. Обробник відновлює контекст під час читання і кладе назад під час публікації наступної події.

За повторних спроб і перенесення в чергу необроблених повідомлень значення зберігається, тому збійна гілка залишається пов'язаною з вихідним запитом.

Впровадження Correlation ID: етапи інженерної роботи

Порядок робіт тут не формальність: якщо точку генерації ідентифікатора вибрати неправильно, дублі й розриви ланцюжків доведеться шукати заново на всьому контурі. Тому починаємо з обстеження, а закінчуємо передачею в експлуатацію з готовим регламентом.

Обстеження та карта викликів

  1. Інвентаризуємо сервіси й будуємо карту викликів: синхронні ланцюжки, черги повідомлень, фонові завдання та планувальники — усе, де запит може втратити контекст.
  2. Фіксуємо точки генерації ідентифікатора: він народжується один раз на вході в систему, всередині — лише прокидання без перестворення на проміжних ланках.
  3. Узгоджуємо єдиний формат ідентифікатора та правила його передачі через заголовки запитів і контекст виконання, включно з асинхронними гілками.

Перевірка та передача в експлуатацію

  • Правимо журналювання й трасування: ідентифікатор потрапляє в кожен запис, включно з помилками та повідомленнями сторонніх бібліотек.
  • Перевіряємо трасування на відтворених відмовах — перевищення часу відповіді, розрив з'єднання, повторна доставка: ланцюжок має збиратися одним запитом до журналів.
  • Заміряємо накладні витрати та поведінку під навантаженням, щоб ідентифікатор не став вузьким місцем на гарячому шляху.
  • Передаємо в експлуатацію: регламент розбору інцидентів, готові шаблони запитів до журналів, навчання чергової зміни.

Комплект артефактів і регламентів трасування

Після впровадження наскрізного трасування у команди залишається не лише робочий код, а й комплект артефактів: за ними черговий інженер і новий розробник розбирають інцидент без автора змін. Комплект живе в репозиторії та внутрішній базі знань і оновлюється разом із сервісами.

Схеми та правила найменування полів

  • Схема проходження запиту — точки входу, синхронні виклики й асинхронні ділянки: де ідентифікатор створюється, де передається далі, де відновлюється із заголовка.
  • Правила найменування полів — єдине ім'я заголовка, поля в журналі та мітки в метриках у всіх сервісах: ланцюжок збирається одним пошуковим запитом.
  • Угода про формат ідентифікатора — генерація на вході, подання в журналах, поведінка за відсутності заголовка від зовнішнього клієнта.
  • Схема зв'язків із фоновими завданнями — відповідність ідентифікаторів запиту та відкладеної операції, щоб ланцюжок не рвався на чергах.

Регламент чергової зміни

  • Інструкція черговому — з якого сервісу починати розбір, які поля дивитися, як зібрати повний ланцюжок за одним ідентифікатором.
  • Налаштування збирача журналів — індексовані поля, термін зберігання, правила розбору та маскування чутливих даних.
  • Документація з трасування — опис полів, приклади запитів, відомі обмеження, порядок підключення нового сервісу.
  • Чек-лист приймання — сценарій із розривом ланцюжка та перевірка наскрізного проходу через асинхронну ділянку.

Кейс: пошук причини збою скоротився у вісім разів

Як було до впровадження

Кожен сервіс писав у свій журнал, і спільного ідентифікатора запиту між ними не було. Коли надходила скарга «заявка не створилася», інженер відкривав журнали по черзі — шлюз, сервіс замовлень, білінг, чергу повідомлень — і зіставляв записи за часом і за номером користувача.

Збіг за часом давав хибні зв'язки: за одну секунду через систему проходять десятки запитів, і пару «хто кого викликав» доводилося вгадувати.

Розбір одного звернення займав від двох до чотирьох годин, а частину причин так і не знаходили — інцидент закривали за фактом відновлення, без кореневої причини. Частка відмов класу «не відтворюється» сягала третини звернень.

Що змінили і що стало

Ми впровадили наскрізний Correlation ID: він генерується на вході, прокидається через усі виклики, включно з асинхронними — заголовки викликів, метадані повідомлень черги, контекст фонових завдань, — і потрапляє в кожен запис журналу.

Поверх цього налаштували збирання пов'язаних ланцюжків у Grafana, щоб один ідентифікатор розгортався в дерево викликів із тривалістю кожного кроку.

Час розбору звернення впав із двох-чотирьох годин до 15-25 хвилин — у вісім разів за медіаною.

Частка інцидентів без знайденої причини скоротилася з 30% до 4%: перевірка гіпотез за журналами тепер займає хвилини, бо фільтр за одним ідентифікатором повертає весь ланцюжок замість розрізнених рядків.

Планові роботи змінилися так само помітно. Перед релізом ми проганяємо контрольні сценарії та звіряємо, що ідентифікатор не губиться на жодній ділянці — такі перевірки ловлять розриви трасування в асинхронних гілках, де раніше помилки спливали вже в робочому середовищі.

Чим наскрізне трасування відрізняється від логування

Журнал сервісу фіксує те, що відбувалося всередині одного процесу: час, рівень, повідомлення, іноді ідентифікатор користувача. Запис залишається локальним — він нічого не знає про сусідній сервіс, який обробив той самий запит наступною мілісекундою пізніше.

Наскрізне трасування починається там, де ми присвоюємо запиту Correlation ID на вході й передаємо його далі по всьому ланцюжку викликів, включно з чергами, фоновими завданнями та відкладеними обробниками.

Рівні журналювання відповідають на інше питання — наскільки докладно писати. Переведення сервісу з info на debug додає деталей в окремій точці, але якщо події з різних сервісів не пов'язані спільним ідентифікатором, ці деталі все одно не складаються в картину.

Рівень керує обсягом і шумом, трасування — зв'язністю. Одне не замінює інше: докладний, але незв'язаний журнал дає гору рядків, між якими немає причинно-наслідкових зв'язків.

Ідентифікатор має переживати межі: заголовок вхідного запиту, контекст виконання, поле в завданні для черги, тег у записі журналу. Тоді контекст запиту — хто викликав, яка операція, який клієнт, яка версія збірки — відновлюється за одним фільтром.

Без цього пошук іде за часом, і вже перше паралельне навантаження або повторна спроба ламає вибірку: рядки двох запитів перемішуються, і розбір перетворюється на ручну реконструкцію.

Практичний ефект проявляється в діагностиці розподілених систем. Коли інцидент розбирається за ідентифікатором, інженер бачить весь ланцюжок від вхідної точки до останнього сервісу й знаходить місце відмови за хвилини, а не за години узгоджень між командами.

Якщо ідентифікатор не доходить до якоїсь ділянки — це теж сигнал: отже, тут губиться контекст, і таку ділянку потрібно окремо покрити передачею мітки.

Сумніви щодо впровадження: розбір інженерних питань

Correlation ID присвоюється в точці входу й передається далі по ланцюжку викликів. Перед стартом зазвичай постають ті самі питання: де правити код, що робити з журналами різних команд і чи не постраждає час відповіді. Нижче — короткі відповіді; нестандартний сценарій розберемо окремо на етапі оцінки.

Чи доведеться переписувати сервіси?

Ні. Ідентифікатор присвоюється на вході й передається в заголовку виклику. У сервісах правиться лише шар вихідних запитів — зазвичай один перехоплювач на сервіс, решта працюють без змін.

Що робити з різнорідними форматами журналів?

Ідентифікатор додається в контекст запису, а не в текст повідомлення, — спільне поле з'являється під час запису. Старі файли не переписуємо: нові записи вже склеюються в один ланцюжок.

Як ідентифікатор проходить асинхронну обробку?

Він потрапляє в заголовки повідомлення під час постановки в чергу й відновлюється з них під час читання. Повторні спроби зберігають той самий ідентифікатор, тому всі вони видно як один запит, а не як розрізнені події.

Чи зросте час обробки?

Накладні витрати — генерація рядка й зайве поле в журналі; на тлі мережевих викликів це частки відсотка. Заміряємо до і після на контрольному наборі операцій і показуємо різницю.

А якщо сторонній компонент не передає ідентифікатор?

Він стає межею трасування: на вході ставимо свій ідентифікатор і фіксуємо розрив ланцюжка. Це чесніше, ніж імітувати наскрізне трасування, якого немає.

Обговоримо трасування ваших сервісів

Щоб оцінка була предметною, а не «на око», потрібен мінімальний набір вхідних даних: за ними видно, де Correlation ID доведеться прокидати через ручні місця, а де достатньо типового налаштування. Опишіть задачу й додайте те, що є під рукою — повернемося з планом робіт та оцінкою за етапами.

  • Схема сервісів замовника: перелік сервісів, хто кого викликає, які зв'язки синхронні, а які йдуть через черги та брокер.
  • Точки входу трафіку: зовнішній шлюз, балансувальник, сервіси на периферії — де ідентифікатор запиту зараз створюється, а де губиться.
  • Поточна робота з логами: куди пишуться, чи є централізоване збирання та пошук, як виглядає запис і які поля вже присутні.
  • Способи передачі між сервісами: заголовки запитів, контекст у повідомленнях, фонові завдання, планувальники та нічні завдання.
  • Мови та платформи сервісів: де спільні бібліотеки, а де доведеться писати обв'язку окремо під кожен стек.
  • Вимоги до зберігання: термін життя логів, хто читає трасування, чи потрібне маскування чутливих полів.
  • Організаційні рамки: вікна обслуговування, порядок релізів, власники сервісів, хто підтверджує зміни.

Далі — короткий розбір схеми та ваших обмежень, після чого узгодимо формат впровадження: єдина бібліотека чи шлюз, де фіксуємо ідентифікатор, як передаємо його далі, як перевіряємо, що наскрізне трасування не рветься на переходах.

Наступний крок простий: напишіть, які сервіси зачіпаємо і що вже працює в логах, — і ми повернемося з планом робіт, складом змін і зрозумілою оцінкою.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1393
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1368
  • Розробка логотипу компанії B2B Advance
    Розробка логотипу компанії B2B Advance
    815
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1139