NServiceBus Performance: продуктивність та оптимізація рішень

💡 Усі статті, обговорення, новини про .NET — в одному місці. Приєднуйтесь до .NET спільноти!

Всім привіт. Мене звати Олександр Шпортько, я працюю .NET-розробником в компанії ITERA.

У минулій статті ми розглядали сервіс нотифікацій, реалізований за допомогою NServiceBus. Детально проаналізували як саме працюють сповіщення (а також їхня обробка), як реалізуються long-running процеси та інше. У висновках зачепили проблематику масштабування та оптимізації рішень, побудованих за допомогою NServiceBus.

Чому це важливо? Коли ви розгортаєте нове рішення, то зазвичай користуєтесь дефолтними або рекомендованими налаштуваннями та опціями (best practices). Але з часом проєкт росте, отримує нові функції, нові задачі й настає момент, коли його потрібно додатково налаштовувати.

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

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

Предмет тестування

Уявімо, що потрібно оптимізувати обробку платежів на оформлену (або подовжену) підписку Netflix. Нам відомо, що процес оплати вже реалізований за допомогою NServiceBus та має наступні логічні етапи:

  • LogPayment — збереження даних платежу в БД.
  • CheckPermissions — виклик стороннього API для перевірки дозволу на виконання платежу користувачем.
  • CheckBalance — виклик стороннього API для перевірки балансу користувача.
  • ExecutePayment — відправка даних на обробку в основну систему.
  • StoreExecutedPayment — збереження виконаного платежу.

Кожен етап виконує певну логіку та має власний час на обробку даних, який, зрозуміло, не може бути постійним. І хоча ми оптимізували за допомогою коду всі етапи, задача поставлена зробити це ще швидше, адже кожного разу наприкінці місяця надходить великий об’єм платежів. Від бізнесу ми отримали завдання обробляти в середньому 7 200 платежів за годину (тобто 5 за секунду). І тому необхідно, аби обробка була максимально стрімкою та не призводила до простою бізнес-процесу в цілому.

Основна мета в цій статті: знайти те оптимальне налаштування NServiceBus, при якому він спокійно може витримувати поставлені навантаження з обробки сповіщень. Але ж, звичайно, з часом умови можуть змінитися, і тому потрібно буде знову переглядати результати тестів.

Розглядатимемо навантажувальні тести (load testing), які зазвичай підходять для багатокористувацьких систем, клієнт-серверних моделей, із симуляцією великої кількості одночасного доступу до системи. Таке тестування дає змогу вимірювати якість обслуговування рішення на основі фактичної поведінки клієнтів. І це саме те, що нам потрібно.

Такого типу тестування найкраще проводити на тестових середовищах, що ідентичні реальним, але в цьому випадку все відбуватиметься на локальній станції (11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz   1.38 GHz, 8 Cores; RAM 16 Gb).

Щоб оцінити поведінку solution під різними навантаженнями, спочатку оберемо довільні сценарії. Так легше розібратися і з графіками, і з тим, як ми робимо заміри.

Потім змінимо один з параметрів NServiceBus (MaxConcurrency, про який йтиметься згодом) і дослідимо його вплив на швидкість обробки інформації.

Тобто маємо:

  • Scenario A: 3 сповіщення кожні 5 секунд впродовж 1 хв.
  • Scenario B: 1 повідомлення кожну секунду впродовж 1 хв.
  • Scenario C: 5 повідомлень кожну секунду впродовж 5 хв.
  • Scenario D: від 1 до 5 повідомлень за секунду впродовж 5 хв. (MaxConcurrency = 4).
  • Scenario E: від 1 до 5 повідомлень за секунду впродовж 5 хв (MaxConcurrency = 8).
  • Scenario F: від 1 до 5 повідомлень за секунду впродовж 5 хв (MaxConcurrency = 16). (Останні три сценарії ми зведемо в таблицю результатів).

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

Коли ми піднімаємо пропускну здатність воронки, то збільшуємо здатність solution обробляти більше вхідних повідомлень. Отже, параметр NServiceBus використовується для обмеження кількості повідомлень, що обробляються водночас.

Ліміт паралелізму під час виконання не можна змінити — його можна застосувати лише під час створення endpoint-у. Щоб зміни паралелізму набули чинності, потрібно перезапустити екземпляр NServiceBus.

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

Якщо solution викликає стороннього API у спробах отримати відповідь і не отримує її впродовж довгого часу (NotFound), то, чим більше таких запитів без відповідей буде, тим більше помилок згенерується. Тоді NServiceBus намагатиметься через retry policy їх обробити, що призведе до того самого порушення порядку обробки повідомлень.

Важливо зазначити, що значення maxConcurrency за замовчуванням — це max(Number of logical processors, 2). Переважно це значення дорівнює 8 (більшість сучасних комп’ютерів мають по 8+ ядер на борту, як у нашому випадку), тому ми й почнемо з цього, але, звичайно, можуть бути й винятки.

Утім, спочатку трішки інформації про вбудовані в NServiceBus інструменти, які нам знадобляться.

Service Control та Service Pulse

Service Control є серверною частиною для ServicePulse та ServiceInsight. Це фоновий процес, який збирає корисну інформацію про систему NServiceBus, а саме:

  • збирає кожне повідомлення, що проходить крізь систему, і такі, що мають помилки (exceptions) і не можуть бути оброблені;
  • зміни стану саг;
  • виконання кастомних перевірок;
  • стан endpoints (endpoint heartbeats).
Уся ця інформація надається ServicePulse та ServiceInsight через HTTP API.

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

Щоб була можливість спостерігати у Service Pulse зміни в нашому endpoint, необхідно мінімально додати Monitoring instance та ServiceControl instance. Обидва інстанси налаштовуються за замовчуванням, однак Transport Configuration повинен використовувати SQL Server (як варіант, де в нас зберігається черга та саги), як показано нижче:

Moniroting:
Data Source=localhost;Initial Catalog=master;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=False;User Id=sa;Password=sa;Queue Schema=receiver;Subscriptions Table=Receiver@receiver@master
ServiceControl:
Data Source=localhost;Initial Catalog=master;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=False;User Id=sa;Password=sa;Subscriptions Table=SubscriptionRouting@receiver@master;Queue Schema=receiver;

Також обидва інстанси повинні працювати як local service:

Більш детально з ServiceInsight можна ознайомитися в офіційній документації ServiceInsight • Particular Docs.

ServicePulse — вебзастосунок, розроблений для адміністраторів. Він забезпечує чіткий (майже в режимі реального часу) огляд високого рівня того, як система функціонує в цей момент. Інтерфейс користувача також забезпечує звичайні операції відновлення після збою, наприклад, повторну спробу відправки повідомлень з помилками. Також можна дивитися payload-повідомлення та схему його обробки (flow), що дозволяє трасування до моменту exception.
ServicePulse також має багате графічне відтворення детальних показників ефективності. Вони відображаються на рівні логічних endpoints, фізичних екземплярів і навіть окремих типів повідомлень.

Про те, як налаштувати ServicePulse разом із ServiceControl, ви можете дізнатися самостійно завдяки офіційному мануалу від ServicePulse Installation * Particular Software.

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

  • здоров’я нашої системи (heartbeats); який endpoint працює та відповідає на запити, а який може вийти з ладу;
  • failed-сповіщення, які можна відправити в архів або наново повторити (retry);
  • custom checks, які можна сконфігурувати для перевірки зовнішніх ресурсів.

Розглянемо основні елементи інтерфейсу, які нам знадобляться надалі.

Monitoring

Ця сторінка містить дані про запущені endpoints, що мають метрики в реальному часі з деталізацію від 1 хвилини до 1 години. Розшифруємо, що означає кожен з показників:

  • Queue length (msgs) — показник відстежує кількість повідомлень у головній вхідній черзі endpoint.
  • Throughput (msgs/s) — це міра того, скільки роботи виконує endpoint. Це швидкість, з якою він може обробляти повідомлення з вхідної черги (input queue). Інакше кажучи, пропускна здатність (сповіщень за секунду).
  • Scheduled retries (msgs/s) — показник, що вимірює кількість повторних спроб, запланованих endpoint (негайних чи із затримкою).
  • Processing time (ms) — це час, потрібний endpoint для виклику всіх обробників і саг для одного вхідного повідомлення. Він не містить час на отримання повідомлення з черги та витрат на транспортування сповіщення.
  • Critical time (ms) — це час між надсиланням повідомлення та його повною обробкою. Це комбінація network time (час, який повідомлення проводить в мережі, перш ніж прибуде в чергу призначення) queue wait time (час, який повідомлення перебуває в черзі призначення, перш ніж його буде підібрано та оброблено) processing time (час, потрібний endpoint для обробки повідомлення).

Надалі нас цікавитимуть два показники: Throughput та Processing time.

Failed messages

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

Наприклад, коли якийсь сторонній API, до якого ви звертаєтесь, деякий час не відповідає, але потім успішно виходить в ефір, то повідомлення, що спілкується із цим API, можна наново запустити на обробку. Опісля воно зникне й вважатиметься вже не помилковим. Усі налаштування серверної частини NServiceBus відбуваються у файлі Program.cs.

Взаємодія елементів проєкту

Для збору необхідних метрик сервісу під навантаженням знадобиться endpoint, що відправлятиме команди, endpoint, що буде їх обробляти та NBomber, який буде навантажувати наш endpoint. Сам по собі NBomber — це простий framework, що використовується для тестування навантаження будь-якої системи, незалежно від протоколу (HTTP, WebSocket та інші). За його допомогою можна створювати сценарії навантаження з різною інтенсивністю та тривалістю.

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

Схематично маємо взаємодію всіх елементів у проєкті:

Елемент LoadTester.Bomber генерує команди, що надсилаються з певним проміжком часу до NServiceBus. Він їх приймає на своїй стороні за допомогою саги LoadTestingSaga, а після виконує обробку за допомогою кожного handler. Розглянемо детальніше реалізацію цієї схеми.

Implementation

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

LoadTester.Bomber містить сервіс, який надсилання повідомлення безпосередньо за допомогою NBomber.

Метод ExecuteAsync створює сценарій для того, щоб NBomber запустив симуляцію надсилання сповіщень до NServiceBus. Ви можете налаштувати будь-який сценарій за допомогою файлу NBomberConfig або більш детально ознайомитися з ним через офіційну документацію.

LoadTester.Shared — набір допоміжних, які використовуються нашим рішенням (як-от команди та handlers)

LoadTester.Receiver — endpoint, який отримує повідомлення від LoadTester.Bomber та обробляє їх:

За отримання повідомлень відповідає сага LoadTestingSaga, саме вона приймає команду LoadTestingCommand. Далі можна побачити, як відбувається надсилання команд згідно з їхнім логічним порядком. Тобто логування в БД (LogPaymentCommand), перевірка дозволів (CheckPermissionsCommand), балансу (CheckBalanceCommand), виконання (ExecutePaymentCommand) і збереження платежу в БД (StoreExecutedPaymentCommand).

У кожному handler команд є своя тривалість обробки. Вона задається у файлі appsettings.json як найменша (найшвидша) та найбільша (найдовша) величини тривалості виконання обробки команд в мілісекундах. Для того, щоб дослідити продуктивність системи, ми беремо або середнє арифметичне, або рандомне значення між lowest та highest значеннями в методі GetDelayDuration .

Треба враховувати той момент, що ми проводимо тести на середніх значеннях навантаження. Тобто імітуємо постійний, а не стохастичний load на проєкт. Чому саме так? Справа в тому, що зазвичай, у реальних умовах і на добре збалансованих системах, навантаження еластичне. Воно повільно зростає, тримається на піку деякий час, а потім поступово спадає. Це саме наш приклад. І за таких умов NServiceBus встигатиме обробляти чергу запитів, тому, щоб не обирати максимальні затримки в обробці сповіщень, обираємо середнє.

Перейдемо до тестувань та заміру перфомансу.

Тестування

Scenario A

У налаштуваннях NBomber почнемо надсилати 3 сповіщення кожні 5 секунд протягом 1 хвилини і подивимось на графіки в ServicePulse та на те, як швидко NServiceBus обробить усі надіслані до нього сповіщення.

Графіки оціночні. З них ми можемо зрозуміти, що була активна робота нашого endpoint на базі NServiceBus. Довжина черги (queue length) апроксимативно дорівнювала 3 повідомленням. Ми не мали scheduled retries, а це означає, що все відбувалося дуже динамічно і без затримок. Досить логічні графіки за часом обробки самих сповіщень.

Як бачимо по логам NServiceBus, обробка першої команда стартувала за декілька секунд після початку надсилання першого повідомлення:

А завершення по часу маємо таке:

Тобто остання команда завершила своє виконання майже водночас із завершенням відправки повідомлень.

Що це означає? Те, що NServiceBus з легкістю впорався з навантаженням, яке ми запропонували й обробив усі сповіщення за 1 хв. Це були стандартні опції навантаження. Далі — цікавіше, тому що ми ускладнемо задачу для NServiceBus.

Scenario B

Налаштування NBomber: 1 повідомлення кожну секунду впродовж 1 хв:

Як себе поводить у цьому випадку NServiceBus? Він накопичує в черзі повідомлення й одночасно намагається їх обробляти. Але черга поповнюється досить швидко, тому NServiceBus, так би мовити, трішки не встигає. Унаслідок бачимо, що те, що повинно оброблятися одну хвилину, вилилося майже в чотири хвилини. Спробуємо збільшити навантаження в наступному досліді.

Scenario C

Налаштування NBomber: 5 повідомлень кожну секунду впродовж 5 хв.
У цьому випадку NServiceBus виконував поставленне завдання досить довго — 13 хв. відбувалася обробка всіх сповіщень:

Як бачимо на графіках Service Pulse, спочатку черга Queue length круто пішла вгору, адже накопичувала все більше і більше сповіщень. Throughput апроксиматично також зростає через велику інтенсивність надходження повідомлень. Логічно, що Critical time зростатиме як результат збільшення часу їхньої обробки.

Тепер подивимося на результати всіх тестів за сценаріями D, E, F, зведених у таблиці:

Червоним виділені найгірші результати. Чорним — прийнятні. Зеленим — найкращі.

Упродовж трьох етапів тестувань ми змінювали Concurrency Limit та тестували NServiceBus з різною кількістю сповіщень (injects/sec). Планово ми мали вийти на значення period (min), але фактично отримували значення вказані в клітинках tested period (min). Бачимо, що найгірші показники ми мали у разі значення Concurrency Limit = 4. NServiceBus за високих навантажень (кількість надісланих сповіщень уже більше 2 за секунду) почав призводити до того, що збільшувався час на їхню обробку. А тому ми виходили далеко за рамки встановлених 5 хв.

Найкращі показники за Concurrency Limit = 16. При найінтенсивнішому навантаженні ми отримали дуже оптимістичний показник — 7 хвилин обробки всіх повідомлень. Такий показник досягається саме шляхом «розширення воріт», до яких надходять сповіщення ззовні, й інфраструктура NServiceBus дозволяє без напружень обробити такий масив даних.

Цей варіант виглядає найкращим з точки зору тієї кількості повідомлень, що може обробити NServiceBus за відведений йому час.

Висновки

У цій статті ми розглянули досить простий, але ефективний спосіб оптимізації проєкту, побудованого на базі NServiceBus від Particular Software. За допомогою графіків у Service Pulse можна спостерігати в реальному часі за поведінкою NServiceBus, його навантаженням, динамікою обробки сповіщень тощо. І у випадку, коли в нас незадовільна потужність NServiceBus — тобто він не може впоратися з навантаженням розробленої бізнес-логіки, — в діло вступає «тюнінг» його інфраструктури. Завдяки такому простому, але потужному параметру як Concurrency Limit, ми в наших сценаріях змогли віднайти прийнятний для нас варіант налаштування. І можемо сказати, що при Concurrency Limit = 16 та надходженні 5 запитів на оплату за секунду впродовж 5 хв, запити можуть оброблятися 7 хв. Що в порівнянні з попередніми сценаріями — дуже швидко.

Якщо для бізнесу це все ж таки досить повільно, і необхідно ще більше пришвидшення, то потрібно розглядати архітектуру рішення. Можливо, є сенс рознести ваш endpoint NServiceBus на декілька хостів; можливо, переглянути дефолтні налаштування черг тощо.

Слід пам’ятати, що в реальному житті велика кількість запитів на оплату може бути в певні часи доби або навіть дні. Наприклад: чорна п’ятниця, дні перед новорічними святами, останні дні перед сплатою податків та інше. Такі моменти можуть ускладнювати процес обробки запитів ще більше. І load у 5 платежів за секунду в пікові моменти навантаження може бути досить далеким від реальності. Тому наведений метод прискорення NServiceBus — це лише рекомендація або один із засобів пришвидшення обробки інформації. Кожен окремий випадок варто розглядати винятково в рамках тієї задачі, яку ви вирішуєте і обирати той підхід, який має найбільше переваг.

Корисні посилання:

ServiceControl • Particular Docs
ServicePulse • Particular Docs
Overview | NBomber

👍ПодобаєтьсяСподобалось8
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі