Балансіроўка нагрузкі і маштабаванне доўгажывучых злучэнняў у Kubernetes
Гэты артыкул, які дапаможа разабрацца ў тым, як уладкованая балансіроўка нагрузкі ў 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 and secured WebSockets
HTTP / 2
gRPC
RSockets
AMQP
Вы павінны быць ужо знаёмыя з большасцю гэтых пратаколаў.
Але калі гэтыя пратаколы такія папулярныя, чаму няма стандартызаванага рашэння для балансавання? Чаму патрабуецца змена логікі кліента? Ці існуе натыўнае рашэнне Kubernetes?
Kube-proxy і iptables створаны, каб закрыць большасць стандартных сцэнарыяў выкарыстання пры разгортванні ў Kubernetes. Гэта зроблена для зручнасці.
Калі вы выкарыстоўваеце вэб-сэрвіс, які дае REST API, вам пашанцавала – у гэтым выпадку пастаянныя TCP-злучэнні не выкарыстоўваюцца, вы можаце выкарыстоўваць любы сэрвіс Kubernetes.
Але як толькі вы пачнеце выкарыстоўваць пастаянныя TCP-злучэнні, давядзецца разбірацца, як раўнамерна размеркаваць нагрузку на бэкэнды. Kubernetes ня ўтрымлівае гатовых рашэньняў на гэты выпадак.
Аднак, канешне ж, існуюць варыянты, якія могуць дапамагчы.
Балансаванне доўгажывучых злучэнняў у Kubernetes
У Kubernetes існуе чатыры тыпу сэрвісаў:
ClusterIP
NodePort
LoadBalancer
Без галавы
Першыя тры сэрвісы працуюць на базе віртуальнага 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 дапамагае кіраваць трафікам усярэдзіне кластара, але ён даволі рэсурсаёмісты. Іншыя варыянты – гэта выкарыстанне іншых бібліятэк, напрыклад Netflix Ribbon, ці праграмуемых проксі, напрыклад Envoy.
Што адбудзецца, калі ігнараваць пытанні балансавання?
Вы можаце не выкарыстоўваць балансаванне нагрузкі і пры гэтым не заўважыць ніякіх змен. Давайце паглядзім на некалькі сцэнарыяў працы.
Калі ў вас больш кліентаў, чым сервераў, гэта не такая вялікая праблема.
Выкажам здагадку, ёсць пяць кліентаў, якія канэктуюцца да двух сервераў. Нават калі няма балансавання, абодва сервера будуць выкарыстоўвацца:
Злучэнні могуць быць размеркаваны нераўнамерна: магчыма, чатыры кліенты падключыліся да аднаго і таго ж серверу, але ёсць добры шанец, што абодва серверы будуць скарыстаны.
Што больш праблематычна, дык гэта супрацьлеглы сцэнар.
Калі ў вас менш кліентаў і больш сервераў, вашы рэсурсы могуць недастаткова выкарыстоўвацца, і з'явіцца патэнцыйнае вузкае месца.
Выкажам здагадку, ёсць два кліента і пяць сервераў. У лепшым выпадку будзе два пастаянных злучэння да двух сервераў з пяці.
Астатнія серверы будуць прастойваць:
Калі гэтыя два серверы не могуць справіцца з апрацоўкай запытаў кліентаў, гарызантальнае маштабаванне не дапаможа.
Заключэнне
Сэрвісы Kubernetes створаны для працы ў большасці стандартных сцэнарыяў вэб-прыкладанняў.
Аднак, як толькі вы пачынаеце працаваць з пратаколамі прыкладанняў, якія выкарыстоўваюць пастаянныя злучэнні TCP, такімі як базы даных, gRPC або WebSockets, сэрвісы ўжо не падыходзяць. Kubernetes не дае ўнутраных механізмаў для балансавання пастаянных TCP-злучэнняў.
Гэта значыць, вы павінны пісаць прыкладанні з улікам магчымасці балансавання на баку кліента.