Боротьба з латентністю: уроки впровадження GraphQL Federation у архітектурі мікросервісів

GraphQL Federation (Федерація GraphQL) була визнана революційним підходом для організації та об'єднання даних з кількох мікросервісів. Завдяки безшовному комбінуванню підграфів з різних сервісів в єдине, уніфіковане API, команди розробників можуть пропонувати клієнтам більш зручний досвід — і теоретично забезпечити швидші, ефективніші відповіді порівняно з традиційною оркестрацією на основі REST.

Але що відбувається, коли ваш GraphQL Gateway (Шлюз GraphQL) стає вузьким місцем? У цій статті ми поділимося нашим досвідом боротьби з проблемами латентності, коли ми інтегрували GraphQL Federation між нашими мікросервісами. Ми розглянемо:

  1. Чому ми обрали GraphQL Federation у нашій архітектурі.
  2. Де виникли проблеми і чому латентність значно зросла.
  3. Підхід до моніторингу і сповіщень, який виявив наші труднощі.
  4. Діаграми Mermaid для ілюстрації нашої архітектури та проблемних зон.
  5. Найкращі практики та можливі рішення для пом'якшення цих проблем.

Чому ми обрали GraphQL Federation

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

  • Єдине уніфіковане API для клієнтів.
  • Автономія команд: кожен мікросервіс (підграф) може належати різним командам.
  • Композиція схеми: Шлюз формує остаточну схему з кількох підграфів, автоматично вирішуючи взаємозв'язки.
  • Отримання тільки необхідних даних
    Однією з найбільших переваг GraphQL є його гнучкість: клієнти можуть запитувати точно ті поля, які їм потрібні — ані більше, ані менше. Це дозволяє уникати over-fetching (перезавантаження даними, коли отримуємо зайву інформацію) та under-fetching (коли потрібно робити кілька запитів, щоб отримати всі необхідні дані).

У типовій конфігурації Federation у вас можуть бути сервіси, як-от Користувачі, Продукти, Замовлення, кожен з яких надає свій підграф. GraphQL Gateway (Шлюз GraphQL) об'єднує ці підграфи в одну схему.

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

pic

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

Наш кошмар з латентністю

Спочатку все виглядало добре в наших тестових середовищах — відповіді були швидкими, і ми вже могли побачити переваги Federation. Однак коли ми перейшли до продукційного середовища або тестів під високим навантаженням, почали виникати проблеми:

  1. Кілька етапів вирішення
    GraphQL запити можуть вимагати кількох резолверів, кожен з яких може звертатися до різних підграфів. Це призводить до кількох мережевих запитів, якщо шлюз повинен отримати дані з кількох сервісів.
  2. Висока конкуренція
    Під більшим навантаженням Шлюз не справлявся з обсягом запитів. Запити ставали в чергу, що призводило до збільшення часу відповіді.
  3. Накладні витрати на серіалізацію/десеріалізацію
    Дані, що передаються між шлюзом і підграфами, повинні були бути серіалізовані в формат, сумісний з GraphQL. Для великих і складних даних ці накладні витрати не є тривіальними.
  4. Вузьке місце в Шлюзі
    Весь трафік клієнтів проходив через Шлюз, що робило його єдиним місцем затору. Якщо ресурси Шлюзу (CPU, пам'ять, пул з'єднань) ставали перенавантаженими, все сповільнювалося.

Діаграма потоку запиту Federation

Щоб чіткіше показати потік запиту, ось ще одна Mermaid sequence diagram (діаграма послідовностей Mermaid), яка демонструє, як один запит GraphQL може розподілятися між кількома підграфами:

pic

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

Спостереження за латентністю: сповіщення та діагностика

Коли конкуренція зросла, ми почали помічати збільшення повільних запитів і тайм-аутів. Наш інструмент APM (Application Performance Monitoring) (Моніторинг продуктивності застосунків) відзначив:

  1. Піки часу відповіді: Латентність P95 і P99 зросла з 200 мс до понад 1 с.
  2. Високе використання CPU: Контейнер сервісу Gateway (Шлюз) постійно досягав 70-80% використання CPU під навантаженням.
  3. Тиск на пам'ять: Кожен запит до Шлюзу створював кілька потоків резолверів, що призводило до збільшення використання пам'яті.
  4. Перевантаження пулу з'єднань: Як сам Шлюз, так і окремі підграфи вичерпували свої з'єднання.

Причини латентності Federation

  1. Синхронні виклики резолверів: Деякі з наших резолверів виконували послідовні виклики до кількох підграфів. Такий підхід збільшує загальний час обробки.
  2. Складні запити: Чим більше підполя та вкладених даних запитує клієнт, тим більше резолверів працює за лаштунками.
  3. Недостатньо ресурсів для Шлюзу: Початкове виділення ресурсів для інстансу Шлюзу було недостатнім для продукційного трафіку.
  4. Відсутність кешування: Ми не кешували часто запитувані дані, що змушувало виконувати повторні запити для кожного користувацького запиту.

Потенційні рішення та найкращі практики

1. Оптимізація ресурсів Шлюзу

  • Горизонтальне масштабування: Розгорнути кілька інстансів шлюзу за балансувальником навантаження.
  • Розподіл ресурсів: Переконатися, що кожен інстанс шлюзу має достатньо CPU та пам'яті.
  • Налаштування пулів з'єднань: Правильно налаштувати максимальні та мінімальні розміри пулів з'єднань, щоб уникнути заторів.

2. Виконання резолверів паралельно, де це можливо

GraphQL може обробляти поля паралельно, якщо це налаштовано правильно:

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

3. Впровадження кешуючих шарів

Додайте кешування, щоб зменшити кількість повторних запитів:

  • Кеш на рівні Шлюзу: Кешуйте відповіді від часто запитуваних підграфів.
  • Кеш на рівні підграфів: Кожен мікросервіс може реалізувати свій власний кеш для гарячих даних.
  • Redis/Memcached: Використовуйте розподілене кешування, якщо у вас є кілька інстансів Шлюзу.

4. Перемикачі та тайм-аути

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

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

5. Обмеження складності запитів

Надмірно складні запити можуть значно збільшити латентність:

  • Обмеження глибини запитів: Обмежте, на скільки глибоко може йти запит (наприклад, п’ять рівнів).
  • Аналіз вартості запитів: Визначте систему оцінки запитів за допомогою балів, щоб обмежити їхню складність.

6. Асинхронні патерни, де це доречно

Якщо ваш домен це дозволяє, розгляньте асинхронні патерни:

  • Pub/Sub або Event Sourcing: Деякі дані можуть не потребувати миттєвого отримання в реальному часі.
  • Поетапне отримання даних: Надсилайте часткові відповіді, коли дані стають доступними, або розбивайте великі запити на кілька менших.

Приклад: До та після оптимізації

Ось фінальна Mermaid sequence diagram (діаграма послідовностей Mermaid), яка показує, як змінився запит після того, як ми застосували паралельні резолвери + кешування:

pic

У оптимізованому сценарії ми бачимо:

  • Паралельні виклики замість послідовних запитів.
  • Кешування для скорочення повторних звернень.
  • Швидшу загальну час відповіді завдяки меншій кількості мережевих запитів.

Заключні думки

GraphQL Federation може значно спростити клієнтські API та надати командам можливість незалежно володіти схемами своїх мікросервісів. Однак, як і в будь-якій розподіленій системі, він приносить нові шари складності — особливо в контексті латентності та продуктивності.

  1. Моніторьте рано і часто.
    2.

    Забезпечте достатні ресурси для вашого Шлюзу (Gateway).

  2. Розгляньте кешування та паралелізацію, щоб уникнути непотрібних мережевих запитів.

  3. Не забувайте про патерни стійкості, такі як автоматичне відключення (circuit breakers) і тайм-аути.

Вирішуючи ці проблеми безпосередньо, ваша архітектура з GraphQL Federation може зберегти обіцяний рівень автономії для розробників і забезпечити швидкодіючі, масштабовані API, яких очікують ваші користувачі.

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

Перекладено з: Battling Latency: Lessons from Implementing GraphQL Federation in a Microservices Architecture

Leave a Reply

Your email address will not be published. Required fields are marked *