Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes
Ця стаття, яка допоможе розібратися в тому, як влаштовано балансування навантаження в Kubernetes, що відбувається при масштабуванні довготривалих з'єднань і чому варто розглядати балансування на стороні клієнта, якщо ви використовуєте HTTP/2, gRPC, RSockets, AMQP або інші довготривалі протоколи. 

Трохи про те, як перерозподіляється трафік у Kubernetes 

Kubernetes надає дві зручні абстракції для викочування додатків: сервіси (Services) та розгортання (Deployments).

Розгортання описують, яким чином і скільки копій вашої програми має бути запущено будь-якої миті часу. Кожна програма розгортається як під (Pod) і йому призначається IP-адреса.

Сервіси з функцій схожі на балансувальник навантаження. Вони призначені для розподілу трафіку по безлічі подів.

Подивимося, як це виглядає.

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

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  2. Балансувальник навантаження називається сервіс (Service), йому надано IP-адресу. Будь-який вхідний запит перенаправляється до одного з подів:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  3. Сценарій розгортання визначає кількість екземплярів програми. Вам практично ніколи не доведеться розвертати безпосередньо під:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  4. Кожному поду присвоюється свою IP-адресу:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

Корисно розглядати послуги як набір IP-адрес. Щоразу, коли ви звертаєтеся до сервісу, одна з IP-адрес вибирається зі списку та використовується як адреса призначення.

Це виглядає так.

  1. Надходить запит curl 10.96.45.152 до сервісу:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  2. Сервіс обирає одну з трьох адрес подів як пункт призначення:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  3. Трафік перенаправляється до конкретного поду:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

Якщо ваш додаток складається з фронтенду та бекенду, то у вас буде і сервіс, і розгортання для кожного.

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

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

Коли фронтенд виконує запит до бекенду, він використовує IP-адресу сервісу бекенда, який не змінюється.

Ось як це виглядає.

  1. Під 1 запитує внутрішній компонент бекенда. Замість того, щоб вибрати конкретний під бекенда, він виконує запит до сервісу:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  2. Сервіс вибирає один з подів бекенда як адреса призначення:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  3. Трафік йде від пода 1 до пода 5, обраному сервісом:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  4. Під 1 не знає, скільки саме таких подів, як під 5, заховано за сервісом:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

Але як саме сервіс розподіляє запити? Начебто використовується балансування round-robin? Давайте розумітися. 

Балансування у сервісах Kubernetes

Сервісів Kubernetes немає. Для сервісу не існує процесу, якому виділено IP-адресу та порт.

Ви можете переконатись у цьому, зайшовши на будь-яку ноду кластера та виконавши команду netstat -ntlp.

Ви навіть не зможете знайти IP-адресу, виділену сервісу.

IP-адреса сервісу розміщена в шарі управління, в контролері, і записана в базу даних - etcd. Ця ж адреса використовується ще одним компонентом – kube-proxy.
Kube-proxy отримує список IP-адрес для всіх сервісів і формує набір правил iptables на кожній ноді кластера.

Ці правила кажуть: «Якщо ми бачимо IP-адресу сервісу, потрібно модифікувати адресу призначення запиту та відправити його на один із подів».

IP-адреса сервісу використовується тільки як точка входу і не обслуговується будь-яким процесом, що слухає цю ip-адресу та порт.

Погляньмо на це

  1. Розглянемо кластер із трьох нід. На кожній ноді присутні поди:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  2. Пов'язані поди, забарвлені бежевим кольором, - це частина сервісу. Оскільки сервіс не існує як процес, він зображений сірим кольором:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  3. Перший під запитує сервіс і повинен потрапити на один із пов'язаних подів:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  4. Але сервіс не існує, процесу немає. Як це працює?

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  5. Перед тим, як запит покине ноду, він проходить через правила iptables:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  6. Правила iptables знають, що сервісу немає, і замінюють його IP-адресу однією з IP-адрес подов, пов'язаних з цим сервісом:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  7. Запит отримує діючу IP-адресу як адресу призначення та нормально обробляється:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  8. Залежно від мережевої топології, запит у результаті досягає пода:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

Чи вміють iptables балансувати навантаження?

Ні, iptables використовуються для фільтрації та не проектувалися для балансування.

Однак є можливість написати набір правил, які працюють як псевдобалансер.

І саме це реалізовано у Kubernetes.

Якщо у вас є три поди, kube-proxy напише наступні правила:

  1. Вибрати перший з ймовірністю 33%, інакше перейти до наступного правила.
  2. Вибрати другий під ймовірністю 50%, інакше перейти до наступного правила.
  3. Вибрати третій під.

Така система призводить до того, що кожен вибирається з ймовірністю 33%.

Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

І немає жодної гарантії, що під 2 буде обрано наступним після пода 1.

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

Тепер, коли ви розумієте, як працюють сервіси, погляньмо на більш цікаві сценарії роботи.

Довгоживучі з'єднання в Kubernetes не масштабуються за умовчанням

Кожен HTTP-запит від фронтенду до бекенду обслуговується окремим TCP-з'єднанням, яке відкривається та закривається.

Якщо фронтенд надсилає 100 запитів на секунду бекенду, то відкривається та закривається 100 різних TCP-з'єднань.

Можна зменшити час обробки запиту та знизити навантаження, якщо відкрити одне TCP-з'єднання та використовувати його для всіх наступних HTTP-запитів.

У HTTP-протокол закладена можливість, звана HTTP keep-alive, або повторне використання з'єднання. У цьому випадку одне TCP-з'єднання використовується для надсилання та отримання безлічі HTTP-запитів та відповідей:

Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

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

Саме по собі налаштування просте і доступне для більшості мов програмування та середовищ.

Ось кілька посилань на приклади різними мовами:

Що станеться, якщо ми будемо використовувати keep-alive у сервісі Kubernetes?
Давайте вважатимемо, що і фронтенд, і бекенд підтримують keep-alive.

У нас одна копія фронтенду та три екземпляри бекенди. Фронтенд робить перший запит та відкриває TCP-з'єднання до бекенду. Запит досягає сервісу, одне з подов бекенда вибирається як адресу призначення. Під бекенда відправляє відповідь, і фронтенд її отримує.

На відміну від звичайної ситуації, коли після отримання відповіді TCP-з'єднання закривається, зараз воно підтримується відкритим для наступних запитів HTTP.

Що станеться, якщо фронтенд надішле ще запити на бекенд?

Для пересилання цих запитів буде задіяно відкрите TCP-з'єднання, всі запити потраплять на той самий під бекенда, куди потрапив перший запит.

Хіба iptables не повинен перерозподілити трафік?

Чи не в цьому випадку.

Коли створюється TCP-з'єднання, воно проходить через правила iptables, які і вибирають конкретний під бекенда, куди потрапить трафік.

Оскільки всі наступні запити йдуть по вже відкритому з'єднанню TCP, правила iptables більше не викликаються.

Подивимося, як це виглядає.

  1. Перший під надсилає запит до сервісу:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  2. Ви вже знаєте, що буде далі. Сервісу не існує, але є правила iptables, які оброблять запит:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  3. Один з подів бекенда буде обраний як адреса призначення:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  4. Запит досягає пода. У цей момент постійне TCP-з'єднання між двома подами буде встановлено:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  5. Будь-який наступний запит від першого пода буде йти по вже встановленому з'єднанню:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

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

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

Чи можна це виправити?

Оскільки Kubernetes не знає, як балансувати постійні з'єднання, це завдання покладається на вас.

Сервіси — це набір IP-адрес та портів, які називають кінцевими точками.

Ваша програма може отримати список кінцевих точок з сервісу і вирішити, як розподіляти запити між ними. Можна відкрити постійне з'єднання з кожним подом і балансувати запити між цими з'єднаннями за допомогою round-robin.

Або застосувати більше складні алгоритми балансування.

Код на стороні клієнта, який відповідає за балансування, повинен дотримуватися такої логіки:

  1. Отримати список кінцевих точок із сервісу.
  2. Для кожної кінцевої точки відкрити постійне з'єднання.
  3. Коли потрібно зробити запит, використовувати одне з відкритих з'єднань.
  4. Регулярно оновлювати список кінцевих точок, створювати нові або закривати старі постійні з'єднання у разі зміни списку.

Ось як це виглядатиме.

  1. Замість того, щоб перший під відправляв запит у сервіс, ви можете балансувати запити на стороні клієнта:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  2. Потрібно написати код, який запитує, які поди є частиною сервісу:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  3. Як тільки отримаєте список, збережіть його на стороні клієнта та використовуйте для з'єднання з подами:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

  4. Ви самі відповідаєте за алгоритм балансування навантаження:

    Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

Тепер постало питання: чи ця проблема стосується лише HTTP keep-alive?

Балансування навантаження на стороні клієнта

HTTP — це не єдиний протокол, який може використовувати постійні з'єднання TCP.

Якщо ваш додаток використовує базу даних, то TCP-з'єднання не відкривається щоразу, коли вам потрібно виловити запит або отримати документ з бази даних. 

Натомість відкривається та використовується постійне TCP-з'єднання до бази даних.

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

Одна репліка бази даних буде навантажена більше, ніж інші. Kube-proxy та Kubernetes не допоможуть балансувати з'єднання. Ви повинні подбати про балансування запитів до бази даних.

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

Нижче наведено приклад доступу до кластера БД MySQL з Node.js:

var mysql = require('mysql');
var poolCluster = mysql.createPoolCluster();

var endpoints = /* retrieve endpoints from the Service */

for (var [index, endpoint] of endpoints) {
  poolCluster.add(`mysql-replica-${index}`, endpoint);
}

// Make queries to the clustered MySQL database

Існує безліч інших протоколів, що використовують постійні TCP-з'єднання:

  • WebSockets and secured WebSockets
  • HTTP / 2
  • gRPC
  • RSockets
  • AMQP

Ви повинні бути вже знайомі з більшістю цих протоколів.

Але якщо ці протоколи такі популярні, чому немає стандартизованого рішення для балансування? Чому потрібна зміна логіки клієнта? Чи існує нативне рішення Kubernetes?

Kube-proxy та iptables створені, щоб закрити більшість стандартних сценаріїв використання при розгортанні Kubernetes. Це зроблено для зручності.

Якщо ви використовуєте веб-сервіс, який надає REST API, вам пощастило — у цьому випадку постійні з'єднання не використовуються, ви можете використовувати будь-який сервіс Kubernetes.

Але як тільки ви почнете використовувати постійні TCP-з'єднання, доведеться розбиратися, як рівномірно розподілити навантаження на бекенди. Kubernetes не містить готових рішень на цей випадок.

Однак, звичайно, існують варіанти, які можуть допомогти.

Балансування довготривалих з'єднань у Kubernetes

У Kubernetes існує чотири типи сервісів:

  1. ClusterIP
  2. Порт вузла
  3. LoadBalancer
  4. Безголовий

Перші три сервіси працюють на базі віртуальної IP-адреси, яка використовується kube-proxy для побудови правил iptables. Але фундаментальна основа всіх сервісів це сервіс типу headless.

З сервісом headless не пов'язана жодна IP-адреса і вона тільки надає механізм отримання списку IP-адрес і портів пов'язаних з ним подів (кінцеві точки).

Всі послуги базуються на сервісі headless.

Сервіс ClusterIP - це headless сервіс з деякими доповненнями: 

  1. Шар управління призначає IP-адресу.
  2. Kube-proxy формує необхідні правила iptables.

Таким чином, ви можете ігнорувати kube-proxy і безпосередньо використовувати список кінцевих точок, отриманих із сервісу headless для балансування навантаження у вашому додатку.

Але як додати подібну логіку до всіх програм, розгорнутих у кластері?

Якщо ваш додаток вже розгорнутий, то таке завдання може здатися нездійсненним. Проте є альтернативний варіант.

Service Mesh вам допоможе

Ви, напевно, вже помітили, що стратегія балансування навантаження на стороні клієнта є цілком стандартною.

Коли програма стартує, вона:

  1. Отримує список IP-адрес із сервісу.
  2. Відкриває та підтримує пул з'єднань.
  3. Періодично оновлює пул, додаючи або забираючи кінцеві точки.

Як тільки додаток хоче зробити запит, він:

  1. Вибирає доступне з'єднання, використовуючи будь-яку логіку (наприклад, round-robin).
  2. Виконує запит.

Ці кроки працюють і для з'єднань WebSockets, і для gRPC, і AMQP.

Ви можете виділити цю логіку в окрему бібліотеку та використовувати її у ваших програмах.

Однак замість цього можна використовувати сервісні сітки, наприклад Istio або Linkerd.

Service Mesh доповнює вашу програму процесом, який:

  1. Автоматично шукає IP-адреси сервісів.
  2. Перевіряє з'єднання, такі як WebSockets та gRPC.
  3. Балансує запити, використовуючи правильний протокол.

Service Mesh допомагає керувати трафіком усередині кластера, але він досить ресурсомісткий. Інші варіанти - це використання сторонніх бібліотек, наприклад Netflix Ribbon, або програмованих проксі, наприклад Envoy.

Що буде, якщо ігнорувати питання балансування?

Ви можете не використовувати балансування навантаження і при цьому не помітити жодних змін. Погляньмо на кілька сценаріїв роботи.

Якщо у вас більше клієнтів, аніж серверів, це не така велика проблема.

Припустимо, є п'ять клієнтів, які коннектяться до двох серверів. Навіть якщо немає балансування, обидва сервери використовуватимуться:

Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

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

Що більш проблематично, то це протилежний сценарій.

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

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

Інші сервери простоюватимуть:

Балансування навантаження та масштабування довготривалих з'єднань у Kubernetes

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

Висновок

Сервіси Kubernetes створені для роботи у більшості стандартних сценаріїв веб-додатків.

Однак, як тільки ви починаєте працювати з протоколами додатків, які використовують постійні з'єднання TCP, такі як бази даних, gRPC або WebSockets, сервіси не підходять. Kubernetes не надає внутрішніх механізмів балансування постійних TCP-з'єднань.

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

Переклад підготовлений командою Kubernetes aaS від Mail.ru.

Що ще почитати на тему:

  1. Три рівні автомасштабування в Kubernetes та як їх ефективно використовувати
  2. Kubernetes у дусі піратства з шаблоном по впровадженню.
  3. Наш канал у Телеграмі про цифрову трансформацію.

Джерело: habr.com

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