Балансіроўка нагрузкі і маштабаванне доўгажывучых злучэнняў у 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, вам пашанцавала – у гэтым выпадку пастаянныя TCP-злучэнні не выкарыстоўваюцца, вы можаце выкарыстоўваць любы сэрвіс Kubernetes.

Але як толькі вы пачнеце выкарыстоўваць пастаянныя TCP-злучэнні, давядзецца разбірацца, як раўнамерна размеркаваць нагрузку на бэкэнды. Kubernetes ня ўтрымлівае гатовых рашэньняў на гэты выпадак.

Аднак, канешне ж, існуюць варыянты, якія могуць дапамагчы.

Балансаванне доўгажывучых злучэнняў у Kubernetes

У Kubernetes існуе чатыры тыпу сэрвісаў:

  1. ClusterIP
  2. NodePort
  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

Дадаць каментар