Іноді більше – це менше. Коли зменшення навантаження призводить до збільшення затримки

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

Якось я прокинувся від невдоволеного листа через великі затримки у Елвіна, якого ми планували запустити найближчим часом. Зокрема, клієнт зіткнувся із затримкою 99-го процентиля в районі 50 мс, набагато вищим за наш бюджет затримки. Це було дивно, тому що я ретельно тестував сервіс, особливо на затримки, адже це є предметом частих скарг.

Перш ніж віддати Елвіна в тестування, я провів багато експериментів із 40 тис. запитів за секунду (QPS), всі показали затримку менше 10 мс. Я був готовий заявити, що не згоден з їхніми результатами. Але ще раз глянувши на листа, я звернув увагу на щось нове: я точно не тестував умови, які вони згадали, їх QPS був набагато нижчим, ніж мій. Я тестував на 40k QPS, а вони лише на 1k. Я запустив ще один експеримент, на цей раз з більш низьким QPS, просто щоб ублажити їх.

Оскільки я пишу про це в блозі, мабуть, ви вже зрозуміли: їхні цифри виявилися правильними. Я перевіряв свого віртуального клієнта знову і знову, все з тим самим результатом: низька кількість запитів не тільки збільшує затримку, але й збільшує кількість запитів із затримкою більше 10 мс. Іншими словами, якщо на 40k QPS близько 50 запитів на секунду перевищували 50 мс, то на 1k QPS кожну секунду було 100 запитів вище 50 мс. Парадокс!

Іноді більше – це менше. Коли зменшення навантаження призводить до збільшення затримки

Звужаємо коло пошуку

Зіткнувшись із проблемою затримки у розподіленій системі з багатьма компонентами насамперед потрібно скласти короткий список підозрюваних. Копнем трохи глибше в архітектуру Елвіна:

Іноді більше – це менше. Коли зменшення навантаження призводить до збільшення затримки

Хорошою відправною точкою є список виконаних переходів введення-виводу (мережевий виклик/пошук по диску і т. д.). Спробуємо з'ясувати де затримка. Крім очевидного введення-виводу з клієнтом, Елвін робить додатковий крок: він звертається до сховища даних. Однак це сховище працює в одному кластері з Елвіном, тому там затримка має бути меншою, ніж із клієнтом. Отже, список підозрюваних:

  1. Мережевий виклик від клієнта до Елвіна.
  2. Мережевий виклик від Елвіна до сховища даних.
  3. Пошук на диску у сховищі даних.
  4. Мережевий виклик із сховища даних до Елвіна.
  5. Мережевий виклик від Елвіна до клієнта.

Спробуємо викреслити деякі пункти.

Сховище даних ні до чого

Насамперед я перетворив Елвіна на сервер ping-ping, який не обробляє запити. Отримавши запит, він повертає порожню відповідь. Якщо затримка зменшується, то помилка в реалізації Елвіна чи сховища даних – нічого такого нечуваного. У першому експерименті отримуємо такий графік:

Іноді більше – це менше. Коли зменшення навантаження призводить до збільшення затримки

Як бачимо, під час використання сервера ping-ping немає жодних поліпшень. Це означає, що сховище даних не збільшує затримку, а список підозрюваних скорочується вдвічі:

  1. Мережевий виклик від клієнта до Елвіна.
  2. Мережевий виклик від Елвіна до клієнта.

Здорово! Список швидко скорочується. Я гадав, що майже з'ясував причину.

gRPC

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

Наявність gRPC у стеку породило нове питання: може, це моя реалізація чи сам gRPC Чи викликає проблему затримки? Додаємо до списку нового підозрюваного:

  1. Клієнт викликає бібліотеку gRPC
  2. Бібліотека gRPC на клієнті виконує мережевий виклик бібліотеки gRPC на сервері
  3. Бібліотека gRPC звертається до Елвіна (операції немає у випадку сервера ping-pong)

Щоб ви розуміли, як виглядає код, моя реалізація клієнта/Елвіна не сильно відрізняється від клієнт-серверних прикладів async.

Примітка: наведений вище список трохи спрощений, оскільки gRPC дає можливість використання власної (шаблонної?) потокової моделі, в якій переплітаються стек виконання gRPC та реалізація користувача. Задля простоти дотримуватимемося цієї моделі.

Профільування все виправить

Викресливши сховища даних, я подумав, що майже закінчив: «Тепер легко! Застосуємо профіль і дізнаємось, де виникає затримка». Я великий шанувальник точного профілювання, тому що CPU дуже швидкі і найчастіше не є вузьким місцем. Більшість затримок відбувається, коли процесор повинен зупинити обробку, щоб зробити ще щось. Точне профілювання CPU зроблено саме для цього: воно точно записує все контекстні перемикачі і дає зрозуміти, де виникають затримки.

Я взяв чотири профілю: під високим QPS (маленька затримка) та з ping-pong сервером на низькому QPS (велика затримка), як на стороні клієнта, так і на стороні сервера. І просто про всяк випадок також взяв зразок профілю процесора. При порівнянні профілів я зазвичай шукаю аномальний стек дзвінків. Наприклад, на поганій стороні з високою затримкою відбувається набагато більше перемикань контексту (вдесятеро і більше разів). Але в моєму випадку кількість перемикань контексту практично збігалася. На мій страх, там не виявилося нічого істотного.

Додаткове налагодження

Я був у розпачі. Я не знав, які інструменти можна використовувати, а мій наступний план полягав по суті в повторенні експериментів з різними варіаціями, а не чіткому діагностуванні проблеми.

Що якщо

Із самого початку мене непокоїла конкретний час затримки 50 мс. Це дуже багато часу. Я вирішив, що вирізатиму шматки з коду, поки не зможу з'ясувати точно, яка частина викликає цю помилку. Потім був експеримент, який спрацював.

Як завжди, заднім розумом здається, що все було очевидним. Я помістив клієнта на одну машину з Елвіном і відправив запит у localhost. І збільшення затримки зникло!

Іноді більше – це менше. Коли зменшення навантаження призводить до збільшення затримки

Щось було не так із мережею.

Освоюємо навички мережевого інженера

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

На щастя інтернет любить тих, хто хоче вчитися. Поєднання ping та tracert здавалося досить гарним початком для налагодження проблем мережного транспорту.

По-перше, я запустив PsPing на TCP-порт Елвіна. Я використовував параметри за промовчанням – нічого особливого. З понад тисячі пінгів жоден не перевищив 10 мс, за винятком першого для розігріву. Це суперечить збільшенню затримки, що спостерігається, 50 мс в 99-му процентилі: там на кожні 100 запитів ми повинні були побачити близько одного запиту із затримкою 50 мс.

Потім я спробував ЬгасегЬ: може, проблема на одному з вузлів за маршрутом між Елвіном та клієнтом Але й трейсер повернувся із порожніми руками.

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

Тепер на якій ОС ми знаходимося

gRPC широко використовується в Linux, але для Windows це екзотика. Я вирішив провести експеримент, який спрацював: створив віртуальну машину Linux, скомпілював Елвіна для Linux і розгорнув її.

Іноді більше – це менше. Коли зменшення навантаження призводить до збільшення затримки

І ось що вийшло: у ping-pong сервері Linux не було таких затримок, як у аналогічного вузла Windows, хоча джерело даних не відрізнялося. Виявляється, проблема в реалізації gRPC для Windows.

Алгоритм Нейгла

Весь цей час я думав, що мені не вистачає прапора gRPC. Тепер я зрозумів, що насправді це в gRPC не вистачає прапор Windows. Я знайшов внутрішню бібліотеку RPC, у якій був упевнений, що вона добре працює для всіх встановлених прапорів Winsock. Потім додав ці прапори в gRPC і розгорнув Елвіна на Windows, в виправленому сервері ping-pong під Windows!

Іноді більше – це менше. Коли зменшення навантаження призводить до збільшення затримки

майже готово: я почав видаляти додані прапори по одному, доки не повернулася регресія, тож я зміг точно визначити її причину. Це був сумнозвісний TCP_NODELAY, перемикач алгоритму Нейгла

Алгоритм Нейгла намагається зменшити кількість пакетів, відправлених через мережу, шляхом затримки передачі повідомлень до того часу, поки розмір пакета не перевищить певну кількість байт. Хоча це може бути приємно для середнього користувача, але руйнівно для серверів реального часу, оскільки ОС затримуватиме деякі повідомлення, викликаючи затримки на низькому QPS. У gRPC було встановлено цей прапор у реалізації Linux для сокетів TCP, але з Windows. Я це виправив.

Висновок

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

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

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

Джерело: habr.com

Додати коментар або відгук