Introducing Distributed Tracing in a Large Software System
Talk presentation
Software systems are growing in size and complexity when the business is growing, and sometimes it is hard to figure out what is going on. Various teams make different changes for different business capabilities. Distributed Tracing is a useful way to look under the hood and see for yourself what operations are being performed, what services are used in a certain use case, and how performant are they. In this talk, I will present what Distributed Tracing is and how we introduced it into our software system with some tips and tricks on what you should focus on if you want to do the same.
- .NET developer at Jooble.
- 5+ years of professional experience working with .NET.
- Sometimes blogging on my website on various topics - F#, .NET interop with Win32, using NativeAOT, etc.
- Enjoying exploring different ways to build software systems - utilizing various tools, languages, and paradigms.
- Author of the .NET bindings for OBS (Open Broadcaster Software) library.
- Linkedin, Twitter, GitHub
Talk transcription
Окей. Мене звуть Костянтин, і я розробник. Більшу частину свого часу я працюю із .NET. Можете ось там на фоні мої фотографії бачити, що в мене стоїть піаніно, але за останній рік між мною та піаніною перемагає лінь. У своїй професійній діяльності я люблю розбиратися в складних програмних системах. Чому для мене це цікаве? Тому що для мене ці системи – це майже як живий організм, але з однією дуже важливою відмінністю: вони в тому, що в кожному нюансі цієї системи можна розібратися, і навіть більше, можна знайти точний кусок коду, який відповідає саме за ту поведінку, яку ви бачите. Тому для мене тема Distributed Tracing досить близька.
Взагалі, чому це може бути цікаво для вас? Може бути цікаво слухати про Distributed Tracing. Уявіть ситуацію, що ви підтримуєте якийсь веб-застосунок, і вам потрібно оптимізувати реквест, що уповільнює певний сценарій використання для користувача. Як би ви до цього підійшли? Напевно, спочатку вам треба знайти кодову базу сервіса, де виконується цей реквест, та почати розбиратися в коді. Можливо, ви вже знайшли для себе цікавий кусок коду, знаходите, що там виклик до бази даних, шукаєте далі, знаходите виклик до іншого сервісу, і вам потрібно робити те ж саме в іншому сервісі, розбиратися, шукати кодову базу, і так далі.
Як це може допомогти Distributed Tracing? В цьому випадку вам достатньо знайти певний ідентифікатор реквеста, який вас цікавить, і наступний, останній крок: ви можете візуалізувати повну картину того, що відбувається на вашому бекенді. Це виклики HTTP, виклики до бази даних, певні відправки повідомлень. І все це можна побачити не тільки в статичному вигляді, а ще й в динаміці. Що я маю на увазі? Це те, що ви можете переглядати, як виконувався конкретний виклик цього ендпойнту на продакшені, на якому сервері, скільки часу виконувалась та яка саме дія, в яку чергу відправлялося повідомлення та будь-які інші додаткові метадані, які вас будуть цікавити. Отже, для мене Distributed Tracing – це інструмент для дебагінгу Distributed System. Але це все в теорії.
Хочу розповісти трошки, як це допомогло в нашій компанії. Для контексту, я працюю в компанії Jooble. Це українська продуктова IT-компанія. І наш основний продукт – це агрегатор вакансій. Ми збираємо вакансії з більше ніж 140 тисяч ресурсів і надаємо нашим користувачам можливість знаходити саме ту вакансію, яка для них цікава. Трошки для розуміння. З нашого трафіку, ми працюємо в більш ніж 60 країнах, і за останній рік було близько одного мільярда візитів.
По кейсам. Перший кейс, який я хочу розповісти, стосується сторінки із даними про заробітну плату. Цю сторінку нещодавно запустили мої колеги. Тут можна побачити заробітні плати по різних запитах: для нянь, кур'єрів, роботи на дому і так далі. Що пішло не так? В деяких випадках, замість того, щоб відображати заробітну плату, ця сторінка показувала 404 – не знайдено. Чому? Моїм колегам треба було розібратися. Вони могли б піти по першому варіанту, де їм потрібно було б сідати, розбиратися з кодом, формулювати гіпотези, додавати логування і так далі. Або вони могли б поглянути на це, так сказати, з висоти пташиного польоту на всю ситуацію за допомогою трейсінгу. І вони це і зробили. І вони побачили, що сервіс, який повинен віддавати дані по заробітним платам, не виконує свої дії, тому що в запит до нього таймаутиться. І Customer Facing Service, який буде все відображати, думає, що даних немає, відповідно, сторінка 404. Дякую. Далі на трейс можна додати додаткову інформацію про будь-які операції. Мої колеги додали на трейс всі операції, які для них здавалися більш-менш з потенційними проблемами. Далі вони знайшли конкретну операцію, що тормозила весь процес. І після цього було тривіально знайти кусок коду, який відповідав за таймаути. По суті, це був цикл в циклі. І в внутрішньому циклі виконувався метод лінка First, якому приходилось проходитись по всьому масиву. Вони замінили це на вибірку зі словника, і ситуація стала набагато кращою.
Другий кейс пов'язаний із ранжуванням результатів пошуку у Google. Google для цього ранжування, тобто хто вище, хто нижче, використовує великий ряд критеріїв. Один із цих критеріїв – це швидкість завантаження сторінки. І ми хотіли подивитися, як це можна покращити, тому що у нас був не найкращий показник. І в даному кейсі трейсинг надає нам можливість переглянути все, що відбувається при рендерингу тієї чи іншої сторінки. Всі операції, всі запити, всі виклики. Це дозволило нам побудувати певне дерево операцій всього, що відбувалося. Я навів справки, воно насправді дуже велике. Це тільки частина. І що це дозволило знайти? Це різні потенційні проблеми. Наприклад, в певних кейсах фронтенд надсилав одні й ті ж самі реквести за одні й ті ж самими даними декілька разів. В деяких кейсах ми на бекенді виконували один й той ж самий запит за одним й тим самим даними декілька разів. Далі ще один варіант – це те, що певні запити, які можна було б робити паралельно, ми робили один за одним.
З чого можна почати, щоб запровадити трейсинг в вашій системі? Я думаю, що ознайомлення зі стандартом OpenTelemetry буде одним із найкращих рішень. Чому? Бо OpenTelemetry надає термінологію для всіх понять трейсингу та всі операції, які можна над ними проводити. Це, по суті, фундамент, який дозволяє різним мовам програмування, фреймворкам, бібліотекам працювати однаково з трейсингом. Та навіть якщо у вас різні застосунки на .NET, Python, Go, Node, неважливо, вони всі можуть бути візуалізовані на одному й тому самому трейсі. Я хочу вас трошки познайомити з деякими поняттями OpenTelemetry, які можуть вам допомогти. Перше поняття – це SPAN. SPAN, по суті, це як кубік Lego, з якого складається весь трейс. Це певна операція, може бути будь-яка операція. Це може бути виклик до бази даних, запит до сервісу, просто якась обробка даних, що завгодно. У .NET SPAN реалізований за допомогою класу Activity. Тут я навів приклад, як цей Activity можна інстанціювати. Далі поговоримо про трейс.
На попередньому слайді SPAN може мати батьківський SPAN і певні дочірні SPAN. Тобто SPAN-и формуються в дерево. Все дерево операцій називається Trace. У .NET ви зазвичай не працюватимете безпосередньо з трейсом; ви будете працювати з його ідентифікатором - SetParentId. Ви можете приєднати будь-який SPAN або Activity в .NET до будь-якого трейсу за допомогою методу SetParentId. Останній елемент, про який я хочу говорити, - це атрибути. Атрибути представляють собою пари ключ-значення, які можна прикріпити до SPAN-у. Це метадані, які описують те, що відбувається в контексті SPAN. У .NET атрибути реалізовані за допомогою словника тегів, і їх можна приєднувати до Activity в будь-який момент часу.
Тепер давайте подивимося, як це виглядає на візуалізації. Уявімо, що сервіс 1 викликає сервіс 2 за допомогою HTTP. Як виглядатиме трейс? У цьому трейсі буде два SPAN-и. Перший - це SPAN виклику сервісу, де сервіс 1 викликає сервіс 2. У нього будуть атрибути, які притаманні саме цьому SPAN-у. І в нього буде один дочірній SPAN, який відповідає за обробку запиту, що прийшов до сервісу 2. У цьому SPAN-і будуть атрибути, які притаманні саме сервісу 2. Однак зазвичай виглядає це інакше, і вам не потрібно безпосередньо працювати з цими примітивами OpenTelemetry. Існують багато готових бібліотек для різних фреймворків та інших речей, таких як SQL-клієнт, Redis, Handfire, Postgre, AspNetCore, HTTP-клієнт. Вам лише потрібно додати вашу конфігурацію трейсингу. Але як передати цю інформацію іншим сервісам? Якщо сервіс 1 викликає сервіс 2, як сервіс 2 знає, що вони в одному трейсі? Для цього OpenTelemetry використовує Trace Parent. Trace Parent зберігає інформацію про батьківський SPAN, таку як Trace ID та інформацію про Parent Span ID. Також є Trace Flag, який вказує на семплінг, але про це трошки пізніше.
Trace Parent для HTTP додається до хедерів запиту. Сервіс 1 повинен додати Trace Parent з цими даними до хедерів, і сервіс 2 може їх витягти, щоб зрозуміти, хто ініціював запит до нього. У .NET це реалізується легко, підключаючи відповідні бібліотеки для HTTP клієнта та сервера ASP.NET Core та додаючи їх до конфігурації. В розподілених системах HTTP-запити - це не єдина мета передачі інформації. Можна використовувати месенджер. Щоб вирішити це, потрібно визначити, яку систему месенджеру ви використовуєте, і вибрати відповідну бібліотеку. У випадку, коли бібліотека немає інструментів для трейсінгу, можна використовувати точки розширення бібліотеки. Замініть інтерфейси та методи бібліотеки з власними реалізаціями, що підтримують трейсінг. Наприклад, для месенджінгу з Rabbit та бібліотеки EasyNetQ можна створити обгортку, що додає трейсінг.
Для HTTP цей Trace Parent додається до хедерів запиту. Тобто, сервісу 1 потрібно буде додати до хедерів Trace Parent з цими даними. І далі сервіс 2 зможе це вичитати і зрозуміти, хто саме ініціював запит до нього. У .NET це додається елементарно, за допомогою підключення бібліотеки. Відповідної бібліотеки для HTTP клієнта і відповідної бібліотеки для сервера з ASP.NET Core. І останнє, що треба буде зробити, це додати її до вашої конфігурації. У розподілених системах HTTP запити — це не єдиний спосіб, як передавати інформацію. Ще можна використовувати таку штуку, як Messaging.
Перше дуже важливе діло — це розібратися, яку саме систему месенджеру ви використовуєте найчастіше в вашій організації, та яку бібліотеку використовуєте для роботи з нею. Наприклад, у нас, в Jooble, ми використовуємо Rabbit, і для роботи з ним — бібліотеки EasyNetQ. Але існує одна проблема. Дана бібліотека з коробки немає інструментів трейсінгу. Або не мала на момент трейсінгу. І це не відбувається від того, як я готував цю доповідь.
Що робити в таких випадках? Я пропоную наступний алгоритм. По-перше, знайдіть точки розширення вашої бібліотеки, тобто місця, де можна додати свою логіку до логіки бібліотеки. У EasyNetQ можна заміняти будь-які внутрішні компоненти. Це дозволяє замінити інтерфейс iAdvancedBus, який має всі примітиви Rabbit, на свою обгортку з трейсінгом. Далі в цій обгортці потрібно замінити методи, що відправляють та отримують повідомлення, на свою реалізацію, яка підтримує трейсінг. Для відправки повідомлення вам потрібно створити SPAN із інформацією про відправлення і додати TraceParent до хедерів повідомлення. При зчитуванні потрібно обгорнути функцію обробки повідомлення, яка створює операцію при обробці повідомлення і зчитує інформацію про трейс з хедерів повідомлення.
Нарешті, важливо спростити роботу для колег, щоб вони могли легко додати трейсінг до своїх сервісів. Рекомендується виділити логіку в окрему бібліотеку або додати її до інтернальної бібліотеки для роботи з месенджеру. Якщо у вас є великий трафік і ви не хочете виконувати деякі дії синхронно, розгляньте можливість виконання цих дій у фоновому потоці. Однак за дефолтом, якщо нічого не робити, ви ризикуєте втратити контекст запиту користувачу, і всі дії у фоновому потоці не будуть відображатися на вашому трейсі.
Що з цим можна зробити? Для цього, разом із командою на якусь певну дію, можна зберігати ще й спандарт, який був активний на момент обробки запиту юзеру. У .NET це реалізується за допомогою використання activity.current. Там знаходиться activity, яка активна на даний момент. І потім під час хендлінгу цього повідомлення, цієї команди на якусь бекграунд-операцію, треба буде створити стандарт, який є child'ом до того спандарту, що виконувався під час обробки запита юзера. І тоді всі операції в бекграунд-потоках будуть видні на тому ж самому трейсі, що і ініціювали ці операції. Ми, в принципі, додали всі наші цікаві операції на трейс, але з'ясувалося, наприклад, що ми не можемо зберегти всі дані. Тобто нам просто не вистачає місця на диску для того, щоб зберігати кожен трейс. Для цього в OpenTelemetry придумали таку річ, як семплінг.
Секунда. Семплінг. Що це означає? Це означає, що не обов'язково зберігати кожен трейс. Можна зберігати тільки якусь частину. Зазвичай для вибору цієї частини існує дві стратегії. Перша стратегія — це head sampling, тобто рішення про те, чи зберігати трейс, чи ні. Тобто коли перший сервіс створює трейс, він вирішує, чи цей трейс записуємо або ні. А потім вже за допомогою того ж самого trace parent, про який ми говорили, ця інформація передається вниз до інших сервісів. За це відповідають якраз trace флаги, остання секція в trace parent, де флаг 1 означає, що ми записуємо цей трейс.
Друга стратегія. Для семплінгу — це tail sampling, тобто або семплінг з хвоста. У цьому кейсі рішення про те, чи зберігати трейс, приймається, коли трейс закінчився. Це надає певні переваги. Наприклад, ви можете зберігати тільки ті трейси, у яких є хоч одна помилка. Або ви можете зберігати тільки ті трейси, які відбуваються більше 10 секунд, якісь довгі. Але у цього є і певні недоліки. Для того, щоб прийняти рішення про трейс, треба цей трейс зберігати в пам'яті. Тобто це збільшує навантаження та кількість ресурсів на ті компоненти, які відповідають за цей семплінг. Зазвичай, я помічаю, що при використанні семплінгу можуть виникати багато питань. Колеги можуть запитувати, чому конкретний трейс не відображається, коли вони вставляють Trace ID в інструмент візуалізації. Для уникнення таких ситуацій я пропоную вам розглянути наступні пункти.
По-перше, повністю розібратися та чітко спростувати вибрану стратегію трейсінгу. Для цього важливо відповісти на наступні питання: Який сервіс стартує трейс? Який сервіс вирішує, чи записувати трейс чи ні? Яка частина трейсів зберігається? Як змінити цю частину? І як визначити, чи був трейс записаний чи ні?
Другий порада – дати можливість якимось чином перевизначати рішення щодо збереження трейсу. Наприклад, додати можливість записувати всі трейси, що йдуть від конкретного користувача, автоматично. Це може бути корисно для відлагодження на продакшені. На останок, не обов'язково використовувати однакову стратегію семплінгу на тестовому інвайрнменті, як на продакшені. Тестовий інвайрнмент може мати менше навантаження, тому можна розглядати варіант записувати всі трейси для полегшення завдань тестування.
Нарешті, при записі трейсів необхідно вибрати Tracing Backend, і це може бути складним архітектурним рішенням. Використання OpenTelemetry Collector може допомогти уникнути проблем при зміні Tracing Backend, дозволяючи змінити конфігурацію лише в колекторі. І вам не доведеться вносити жодних змін у код своїх сервісів. Крім того, ви зможете спробувати кілька Tracing Backend одночасно, оскільки цей колектор підтримує відправку трейсів на декілька адрес. Останнє, цей колектор може бути місцем, де ви налаштуєте семплінг для ваших застосунків. Після додавання цього middleware, проте, потрібно додати ще один компонент до нашої історії - інструмент для візуалізації і зберігання трейсів. Для різних організацій це рішення може бути прийнято по-різному, оскільки важливість різних критеріїв різна. Наприклад, ми приділяли увагу таким аспектам в Jooble:
По-перше, нам було важливо використовувати наші власні потужності для розгортання цього інструменту, оскільки ми вже мали потужності в своїх дата-центрах, і нам не було потрібно платити за інші у інших компаній. По-друге, ми хотіли, щоб нам не доводилося вивчати новий інструмент для перегляду трейсів, оскільки у нас вже були певні інструменти для моніторингу. І останнє, інструмент повинен бути розрахований на той трафік, який ми маємо, і він повинен бути в змозі обробляти невеликі та великі обсяги даних. Тому ми обрали Grafana Tempo, оскільки він задовольняв ці критерії. Він може зберігати трейси на наших дисках, бути задеплойованим на наші машини, і ми використовуємо Grafana для відображення метрик та візуалізації трейсів на наших машинах.
Несподівано, під час оновлення Tempo до останньої версії виникли проблеми. Пошукові запити стали тормозити, і це стало серйозним завданням для розвитку системи. При ретельному вивченні логів Tempo стало зрозуміло, що він не може видалити папку через наявність файлів у ній. Аналізуючи код Grafana Tempo, який відкритий для перевірки, було виявлено, що проблема полягала в особливостях мережевого файлового сховища з наддисковим серверним перейменуванням (Server Site Silly Rename). Це означало, що при відкритті файлу на сервері операція видалення не фактично видаляє файли, а лише перейменовує їх. Файли видаляються тільки тоді, коли всі клієнти закриють цей файл. Вирішенням було правильне закриття файлів у Tempo, що в інших випадках відбувалося неправильно. Цей фікс був досить простим - виявилася необхідність закривати файли в Tempo, і відповідний pull request був внесений. Велика подяка команді Tempo за допомогу на всіх етапах цього розслідування.
Ця історія підкреслює важливість готовності розв'язувати проблеми при використанні нових та нещодавно введених інструментів, оскільки вони можуть мати недоліки, які ви самі повинні виявляти та вирішувати. Завершуючи свою презентацію, хочу закликати вас спробувати трейсинг в .NET, оскільки всі необхідні засоби вже існують. На ринку існує багато інструментів для візуалізації трейсів, таких як Tempo, Jaeger, Zipkin та Honeycomb. Важливо також зауважити, що не обов'язково впроваджувати трейсинг одразу для всієї системи - можна переходити поетапно, і це не становить проблеми, особливо якщо у вас багато додатків. Якщо у вас виникають питання після конференції, не соромтеся звертатися до мене по електронній пошті або в Twitter. Також ви можете завітати на мій блог. Наприкінці хочу вас запросити розглянути вакансії .NET-розробників у нашій компанії на hiringjubile.org. Дякую за увагу, і тепер перейдемо до питань.