
Тази статия ще ви помогне да разберете как работи балансирането на натоварването в Kubernetes, какво се случва при мащабиране на дългосрочни съединения и защо трябва да се обмисли клиентското балансиране, ако използвате HTTP/2, gRPC, RSockets, AMQP или други дългосрочни протоколи.
Няколко думи за това как трафикът се разпределя в Kubernetes
Kubernetes предоставя две удобни абстракции за внедряване на приложения: услуги (Services) и разгръщания (Deployments).
Разгръщанията описват как и колко копия от вашето приложение трябва да бъдат стартирани по всяко време. Всяко приложение се разгръща като под (Pod) и му се назначава IP адрес.
Услугите по функции наподобяват балансир на натоварването. Те са предназначени за разпределяне на трафика между множество подове.
Нека видим как изглежда това.
- На диаграмата по-долу виждате три инстанции на едно приложение и балансир на натоварването:

- Балансирът на натоварването се нарича услуга (Service) и му е присвоен IP адрес. Всеки входящ запрос се пренасочва към един от подовете:

- Сценарият за разгръщане определя броя на инстанциите на приложението. Вие практически никога няма да разгръщате непосредственно под:

- На всеки под се назначава негов собствен IP адрес:

Полезно е да се разглеждат услугите като набор от IP адреси. Всеки път, когато се обърнете към услугата, един от IP адресите се избира от списъка и се използва като адрес на дестинация.
Това изглежда по следния начин.
- Постъпва запитване curl 10.96.45.152 към услугата:

- Услугата избира един от трите адреса на подовете като дестинация:

- Трафикът се пренасочва към конкретен под:

Ако вашето приложение се състои от фронтенд и бекенд, то ще имате и услуга, и разгръщане за всеки.
Когато фронтендът прави запитване към бекенда, той няма нужда да знае колко точно пода обслужва бекендът: те могат да бъдат един, десет или сто.
Също така, фронтендът не знае адресите на подовете, обслужващи бекенда.
Когато фронтендът прави запитване към бекенда, той използва IP адреса на услугата на бекенда, който не се променя.
Ето как изглежда това.
- Под 1 прави запитване към вътрешния компонент на бекенда. Вместо да избере конкретен под на бекенда, той прави запитване към услугата:

- Сервисът избира един от подовете на бекенда като целеви адрес:

- Трафикът минава от под 1 към под 5, избран от сервиза:

- Под 1 не знае колко точно такива подове, като под 5, са скрити зад сервиза:

Но как точно сервизът разпределя заявките? Изглежда, че се използва балансировка на принципа round-robin? Нека да разберем.
Балансировка в сервисите на Kubernetes
Сервисите на Kubernetes не съществуват. За сервиза не съществува процес, на който да е назначен IP-адрес и порт.
Можете да се уверите в това, като влезете на всяка нода от кластера и изпълните командата netstat -ntlp.
Вие дори няма да успеете да намерите IP-адрес, назначен на сервиза.
IP-адресът на сервиза е разположен в управленския слой, в контролера, и е записан в базата данни - etcd. Този адрес се използва и от друг компонент - kube-proxy.
Kube-proxy получава списък с IP-адреси за всички услуги и формира набор от правила iptables на всяка нода от кластера.
Тези правила казват: «Ако видим IP-адреса на сервиза, трябва да променим целевия адрес на заявката и да я насочим към един от подовете».
IP-адресът на сервиза се използва само като входна точка и не се обслужва от никакъв процес, който да слуша този IP-адрес и порт.
Нека да разгледаме това.
- Нека разгледаме клъстера, състоящ се от три ноди. На всяка нода има подове:

- Свързаните подове, оцветени в бежов цвят, са част от сервиза. Тъй като сервизът не съществува като процес, той е изобразен в сив цвят:

- Първият под отправя заявка към сервиза и трябва да достигне един от свързаните подове:

- Но сервизът не съществува, процес няма. Как тогава работи?

- Преди заявката да напусне нодата, тя преминава през правилата на iptables:

- Правилата на iptables знаят, че сервиза не съществува и заменят неговия IP-адрес с един от IP-адресите на подовете, свързани с този сервиз:

- Заявката получава валиден IP-адрес като целеви адрес и се обработва нормално:

- В зависимост от мрежовата топология, заявката в крайна сметка достига до пода:

Могат ли iptables да балансират натоварването?
Не, iptables се използват за филтриране и не са проектирани за балансировка.
Въпреки това има възможност да се напише набор от правила, които да действат като .
И точно това е реализирано в Kubernetes.
Ако имате три пода, kube-proxy ще напише следните правила:
- Да се избере първият под с вероятност 33%, в противен случай да се премине към следващото правило.
- Изберете втори под с вероятност 50%, в противен случай преминете към следното правило.
- Изберете трети под.
Тази система води до избиране на всеки под с вероятност 33%.

Няма никаква гаранция, че под 2 ще бъде избран следващия след под 1.
Забележка: iptables използва статистически модул с произволно разпределение. Така алгоритъмът за балансиране се основава на случайния избор.
Сега, когато разбирате как работят услугите, нека разгледаме по-интересни сценарии.
Дългоживеещи връзки в Kubernetes не се мащабират по подразбиране.
Всеки HTTP-запит от фронтенда към бекенда се обслужва от отделна TCP-връзка, която се отваря и затваря.
Ако фронтендът изпрати 100 запита в секунда към бекенда, ще бъдат отворени и затворени 100 различни TCP-връзки.
Можете да намалите времето за обработка на заявките и да намалите натоварването, като отворите едно TCP-соединение и го използвате за всички последващи HTTP-запити.
В HTTP протокола е заложена възможност, наречена HTTP keep-alive или повторно използване на връзката. В този случай едно TCP-съединение се използва за изпращане и получаване на множество HTTP-запити и отговори:

Тази функция не е включена по подразбиране: както сървърът, така и клиентът трябва да бъдат конфигурирани съответно.
Самата настройка е проста и достъпна за повечето програмни езици и среди.
Ето няколко връзки към примери на различни езици:
Какво ще се случи, ако използваме keep-alive в услугата Kubernetes?
Да предположим, че и фронтендът, и бекендът поддържат keep-alive.
Имаме едно копие на фронтенда и три инстанции на бекенда. Фронтендът прави първото запитване и отваря TCP-соединение към бекенда. Запитването достига до услугата, един от подовете на бекенда се избира като целеви адрес. Подът на бекенда изпраща отговор и фронтендът го получава.
В противовес на обичайната ситуация, когато TCP-връзката се затваря след получаване на отговора, в момента тя остава отворена за следващи HTTP-запитвания.
Какво ще се случи, ако фронтендът изпрати още запитвания към бекенда?
За пренасочване на тези запитвания ще бъде използвано вече отвореното TCP-съединение, всички запитвания ще попаднат на същия под на бекенда, на който попадна първото запитване.
Нали ли iptables не трябва да преразпределя трафика?
Не в този случай.
Когато се създава TCP връзка, тя преминава през правилата на iptables, които избират конкретния под на бекенда, към който ще се насочи трафикът.
Тъй като всички следващи заявки преминават по вече отворената TCP връзка, правилата на iptables вече не се извикват.
Нека видим как изглежда това.
- Първият под отправя заявка към услугата:

- Вие вече знаете какво ще стане след това. Услугата не съществува, но има правила на iptables, които ще обработят заявката:

- Един от подовете на бекенда ще бъде избран като адрес на дестинация:

- Заявката достига до пода. В този момент ще бъде установена постоянна TCP връзка между двата пода:

- Всяка следваща заявка от първия под ще минава по вече установената връзка:

В резултат на това получавате по-бърз отговор и по-висока пропускна способност, но губите възможността за скалиране на бекенда.
Дори и да имате два пода в бекенда, при постоянна връзка трафикът през цялото време ще попада на един от тях.
Може ли това да бъде поправено?
Тъй като Kubernetes не знае как да балансира постоянни връзки, задачата е възложена на вас.
Услугите са набор от IP адреси и портове, които наричат крайни точки.
Вашето приложение може да получи списък с крайни точки от услугата и да реши как да разпределя заявките между тях. Можете да отворите постоянна връзка с всеки под и да балансирате заявките между тези връзки с помощта на round-robin.
Или да приложите по- .
Кодът на страната на клиента, отговорен за балансировката, трябва да следва следната логика:
- Да получите списък с крайни точки от услугата.
- За всяка крайна точка да отворите постоянна връзка.
- Когато е необходимо да се направи заявка, да се използва една от отворените връзки.
- Редовно да актуализирате списъка с крайни точки, да създавате нови или да затваряте стари постоянни връзки в случай на промяна на списъка.
Ето как ще изглежда това..
- Вместо първият под да изпрати заявка към услугата, можете да балансирате заявките от страната на клиента:

- Трябва да напишете код, който пита кои подове са част от услугата:

- След като получите списъка, запазете го от страната на клиента и го използвайте за свързване с подовете:

- Вие сами отговаряте за алгоритъма за балансиране на натоварването:

Сега възниква въпросът: Отнася ли се този проблем само за 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 и защитени WebSockets
- HTTP/2
- gRPC
- RSockets
- AMQP
Вие вече би трябвало да сте запознати с повечето от тези протоколи.
Но ако тези протоколи са толкова популярни, защо няма стандартизирано решение за балансиране? Защо е необходимо да се променя логиката на клиента? Съществува ли нативно решение в Kubernetes?
Kube-proxy и iptables са създадени, за да покрият повечето стандартни сценарии при разгръщане в Kubernetes. Това е направено за удобство.
Ако използвате уеб услуга, която предоставя REST API, имате късмет — в този случай постоянни TCP съединения не се използват, можете да използвате всяка услуга в Kubernetes.
Но веднага щом започнете да използвате постоянни TCP съединения, ще трябва да разберете как да разпределите натоварването равномерно между бекендовете. Kubernetes не предлага готови решения за тази ситуация.
Въпреки това, разбира се, съществуват опции, които могат да помогнат.
Балансировка на дългоживеещи съединения в Kubernetes
В Kubernetes има четири типа услуги:
- ClusterIP
- NodePort
- LoadBalancer
- Headless
Първите три услуги работят на базата на виртуален IP адрес, който kube-proxy използва за създаване на правила в iptables. Но основата на всички услуги е headless услугата.
С headless услугата не е свързан никакъв IP адрес, а тя просто предоставя механизъм за получаване на списък с IP адреси и портове, свързани с нейните подове (крайни точки).
Всички услуги са базирани на headless услугата.
Услугата ClusterIP е headless услуга с някои допълнения:
- Управляващият слой назначава IP адрес.
- Kube-proxy формира необходимите правила в iptables.
По този начин можете да игнорирате kube-proxy и директно да използвате списъка с крайни точки, получен от headless услугата за натоварване на баланса във вашето приложение.
Но как да добавите подобна логика към всички приложения, разположени в кластера?
Ако вашето приложение вече е разположено, тази задача може да изглежда невъзможна. Въпреки това, има алтернативен вариант.
Service Mesh може да ви помогне.
Вероятно вече сте забелязали, че стратегията за натоварване на баланса от страната на клиента е напълно стандартна.
Когато приложението стартира, то:
- Получава списък от IP адреси от услугата.
- Отваря и поддържа пул от връзки.
- Периодично актуализира пула, добавяйки или премахвайки крайни точки.
Когато приложението иска да направи заявка, то:
- Избира налична връзка, използвайки някаква логика (например, round-robin).
- Изпълнява заявка.
Тези стъпки работят и за WebSockets, и за gRPC, и за AMQP.
Можете да извлечете тази логика в отделна библиотека и да я използвате в вашите приложения.
Вместо това обаче можете да използвате мрежови услуги, като Istio или Linkerd.
Service Mesh допълва вашето приложение с процес, който:
- Автоматично търси IP адреси на услуги.
- Проверява връзките, такива като WebSockets и gRPC.
- Балансирa заявките, използвайки правилния протокол.
Service Mesh помага за управление на трафика в кластера, но е доста ресурсоемък. Други опции са използването на външни библиотеки, като Netflix Ribbon, или програмирани проксита, като Envoy.
Какво ще се случи, ако игнорираме въпросите за баланса на натоварването?
Можете да не използвате балансиране на натоварването и да не забележите никакви промени. Нека разгледаме няколко сценария на работа.
Ако имате повече клиенти, отколкото сървъри, това не е такава голяма проблема.
Да предположим, че има пет клиенти, които се свързват с два сървъра. Дори и без балансировка, и двата сървъра ще бъдат използвани:

Връзките може да бъдат разпределени неравномерно: може би четирима клиенти са се свързали с един и същ сървър, но има добра вероятност и двата сървъра да бъдат използвани.
По-проблематичен е противоположният сценарий.
Ако имате по-малко клиенти и повече сървъри, ресурсите ви може да не се използват достатъчно и може да се появи потенциално тясно място.
Да предположим, че има двама клиенти и пет сървъра. В най-добрия случай ще има две постоянни връзки към два от петте сървъра.
Останалите сървъри ще останат неактивни:

Ако тези два сървъра не могат да се справят с обработката на клиентските заявки, хоризонталното мащабиране няма да помогне.
Заключение
Услугите на Kubernetes са създадени да работят в повечето стандартни сценарии на уеб приложения.
Въпреки това, веднага щом започнете да работите с приложни протоколи, които използват постоянни TCP връзки, като бази данни, gRPC или WebSockets, услугите не са вече подходящи. Kubernetes не предоставя вградени механизми за балансировка на постоянни TCP връзки.
Това означава, че трябва да пишете приложенията с предвидената възможност за балансировка от страна на клиента.
Преводът е подготвен от екипа .
Какво още да прочетете по темата:
- .
- .
- .
Източник: habr.com




























