27 апреля на конференции , в рамках секции «DevOps», прозвучал доклад «Автомасштабирование и управление ресурсами в Kubernetes». В нём рассказывается о том, как с помощью K8s обеспечить высокую доступность приложений и гарантировать их максимальную производительность.

Sipas traditës, jemi të lumtur të paraqesim (44 минуты, гораздо информативнее статьи) и основную выжимку в текстовом виде. Поехали!
Разберём тему доклада по словам и начнём с конца.
Kubernetes
Пусть у нас на хосте есть Docker-контейнеры. Зачем? Для обеспечения повторяемости и изоляции, которые в свою очередь позволяют сделать просто и хорошо деплой, CI/CD. Таких машин с контейнерами у нас много.
Что в этом случае даёт Kubernetes?
- Мы перестаём думать про эти машины и начинаем работать с «облаком», кластером из контейнеров или pod’ов (групп из контейнеров).
- Более того, мы не думаем даже про отдельные pod’ы, а управляем ещё бoльшими группами. Такие высокоуровневые примитивы позволяют нам сказать, что есть шаблон для запуска некой рабочей нагрузки, а вот нужное количество экземпляров для её запуска. Если мы впоследствии поменяем шаблон — поменяются и все экземпляры.
- Me декларативного API мы вместо выполнения последовательности конкретных команд описываем «устройство мира» (в YAML), который создаётся Kubernetes’ом. И снова: при изменениях описания будет меняться и его реально отображение.
Menaxhimi i burimeve
CPU
Пусть мы запускаем на сервере nginx, php-fpm и mysql. У этих служб в действительности будет ещё больше работающих процессов, каждый из которых требует вычислительных ресурсов:

(числа на слайде — «попугаи», абстрактная потребность каждого процесса в вычислительных мощностях)
Чтобы с этим можно было удобно работать, логично объединить процессы по группам (например, все процессы nginx в одну группу «nginx»). Простой и очевидный способ сделать это — поместить каждую группу в контейнер:

Чтобы продолжить, необходимо вспомнить, что же такое контейнер (в Linux). Их появление стало возможным благодаря трём ключевым возможностям в ядре, реализованным уже достаточно давно: , dhe . А дальнейшему развитию способствовали другие технологии (включая удобные «оболочки» типа Docker):

В контексте доклада нас интересует только cgroups, потому что именно контрольные группы — та часть функциональных возможностей контейнеров (Docker’а и т.п.), что реализует управление ресурсами. Процессы, объединённые в группы, как мы того хотели, — это и есть контрольные группы.
Вернёмся к потребностям в CPU у этих процессов, а теперь уже — у групп процессов:

(повторюсь, что все числа — абстрактное выражение потребности в ресурсах)
При этом у самого CPU есть некий конечный ресурс (в примере это 1000), которого всем может не хватать (сумма потребностей всех групп — 150+850+460=1460). Что будет происходить в таком случае?
Ядро начинает раздавать ресурсы и делает это «честно», выдавая одинаковое количество ресурсов каждой группе. Но в первом случае их больше нужного (333>150), поэтому излишек (333-150=183) остаётся в резерве, который тоже равно распределяется между двумя другими контейнерами:

В итоге: первому контейнеру хватило ресурсов, второму — сильно не хватило, третьему — немного не хватило. Таков результат действий «честного» планировщика в Linux — . Его работу можно регулировать с помощью назначения веса каждому из контейнеров. Например, так:

Посмотрим на случай нехватки ресурсов у второго контейнера (php-fpm). Все ресурсы контейнера распределяются между процессами поровну. В результате, master-процесс работает хорошо, а все worker’ы тормозят, получив менее половины от нужного:

Так работает планировщик CFS. Веса, которые мы назначаем контейнерам, в дальнейшем будем назвать request’ами. Почему именно так — см. дальше.
Взглянем на всю ситуацию с другой стороны. Как известно, все дороги ведут в Рим, а в случае компьютера — в CPU. CPU один, задач много — нужен светофор. Самый простой способ управления ресурсами — «светофорный»: выдали одному процессу фиксированное время доступа к CPU, затем — следующему и т.п.

Этот подход называется жёстким квотированием (hard limiting). Запомним его просто как лимиты. Однако, если раздать всем контейнерами лимиты, возникает проблема: mysql ехал по дороге и в какой-то момент его потребность в CPU закончилась, но все остальные процессы вынуждены ждать, пока CPU простаивает.

Вернёмся к ядру Linux и его взаимодействию с CPU — общая картина получается следующей:

У cgroup есть две настройки — по сути это две простые «крутилки», позволяющие определять:
- вес для контейнера (request’ы) — это shares;
- pjesë e përgjithshme e kohës CPU për të punuar mbi detyrat e kontejnerit (kufizimet) - është kuota.
Si matet CPU?
Ka mënyra të ndryshme:
- Çfarë është papagajë, askush nuk e di - duhet të bien dakord çdo herë.
- Pjesët janë më të qarta, por të relativizueshme: 50% nga serveri me 4 bërthama dhe me 20 bërthama janë gjëra krejtësisht të ndryshme.
- Mund të përdoren ato që janë përmendur веса, të cilat i njeh Linux, por ato gjithashtu janë të relativizuara.
- Zgjidhja më e arsyeshme - matja e burimeve të përpunimit në sekonda. Kështu, në sekonda të kohës së procesorit në lidhje me sekondat e kohës reale: u dhanë 1 sekondë të kohës së procesorit në 1 sekondë reale - kjo do të thotë një bërthamë CPU tërësisht.
Për ta bërë më të thjeshtë, matjet filluan të bëhen direkt në bërthama, duke e kuptuar atë kohë CPU në lidhje me realen. Çfarëdo që të kuptojë Linux peshat, dhe jo një kohë të tillë procesori/bërthama, nevojitej një mekanizëm për konvertimin nga një në tjetrën.
Le të shqyrtojmë një shembull të thjeshtë me një server me 3 bërthama CPU, ku tre pod'e do të kenë të zgjedhura këto peshë (500, 1000 dhe 1500), të cilat lehtësisht konvertohen në pjesët përkatëse të bërthamave të dedikuara (0,5, 1 dhe 1,5).

Nëse marrim një server të dytë, ku bërthamat do të jenë dyfish më shumë (6), dhe vendosim atje të njëjtat pod'e, shpërndarja e bërthamave lehtë llogaritet me thjeshtim duke e shumëzuar me 2 (1, 2 dhe 3 përkatësisht). Por një moment i rëndësishëm ndodh kur në këtë server shfaqet një pod i katërt, pesha e të cilit le të jetë për lehtësi 3000. Ai merr një pjesë të burimeve CPU (gjysmën e bërthamave), dhe për pod'e e tjera ato rishpërndahen (do të ulen në gjysmë):

Kubernetes dhe burimet CPU
Në Kubernetes, burimet CPU janë të zakonshme të maten në milijardë, dmth, si një peshë bazë merret 0,001 bërthama. (E njëjta gjë në terminologjinë Linux/cgroups quhet ndarje e CPU, edhe pse, nëse flasim me saktësi, 1000 miliardë = 1024 ndarje CPU.) K8s ndjek që të mos vendoset më shumë pod'e në server se sa burimet e CPU për shumën e peshave të të gjithë pod'eve.
Si ndodh kjo? Kur një server shtohet në klusterin Kubernetes, njoftohet se sa bërthama CPU janë të disponueshme. Dhe kur krijohet një pod i ri, planifikuesi Kubernetes di se sa bërthama do t'i duhen këtij pod'i. Kështu, pod'i do të përcaktohet në një server ku ka mjaft bërthama.
Çfarë do të ndodhë nëse nuk në vend t'i ipet një kërkesë (dmth. për pod'in nuk është e përcaktuar ndonjëherë numri i bërthamave të nevojshme)? Le të kuptojmë se si i llogarit Kubernetes burimet.
Në pod mund të përcaktohen si kërkesat (planifikuesi CFS), ashtu edhe kufizimet (mos harroni dritën e semaforit):
- Nëse ato janë të përcaktuara të barabarta, atëherë pod'i i jepet një cilësi QoS garantuar. Një numër i tillë gjithmonë i disponueshëm për bërthamat e tij garanti.
- Nëse kërkesa është më e vogël se kufizimi - cilësia QoS burstable. Domethënë, ne presim që pod, për shembull, gjithmonë të përdorë 1 bërthamë, megjithatë ky vlerë nuk është një kufizim për të: ndonjëherë pod'i mund të përdorë edhe më shumë (kur në server ka burime të lira për këtë).
- Ka gjithashtu një cilësi QoS përpjekje më të mira - ndërlidhet me ata pod'e që nuk kanë përcaktuar kërkesën. Burimet u jepen atyre si një mundësi të fundit.
Memoria
Situata me kujtesën është e ngjashme, por pak ndryshe - gjithsesi natyra e këtyre burimeve është ndryshe. Në përgjithësi, krahasimi është i tillë:

Le të shohim si implementohen kërkesat në memorie. Le të themi se pod'e jetojnë në server, duke ndryshuar kujtesën e konsumuar, derisa njëra prej tyre të bëhet aq e madhe, sa të përfundojë memoria. Në këtë rast shfaqet OOM killer dhe vret procesin më të madh:

Kjo nuk na përshtatet gjithmonë, prandaj ekziston mundësia për të rregulluar se cilët procese janë të rëndësishme për ne dhe nuk duhet të vriten. Për këtë përdoret parametri oom_score_adj.
Le të kthehemi te cilësitë QoS të CPU dhe të bëjmë një analogji me vlerat oom_score_adj, të cilat përcaktojnë për pod'in prioritetet sipas konsumit të memories:
- Vlera më e ulët oom_score_adj për një pod është -998 - do të thotë se ky pod duhet të vritet në fund të fundit, kjo është garantuar.
- Vlera më e lartë - 1000 - është përpjekje më të mira, këto pod'e vriten para të gjithë të tjerëve.
- Për llogaritjen e vlerave të tjera (burstable) ekziston një formulë, e cila në thelb i përmbledh se sa më shumë burime kërkon pod'i, aq më pak është mundësia që ta vrasin.

Rregulla e dytë - limit_in_bytes - për kufizimet. Me të është më e thjeshtë: thjesht caktuam numrin maksimal të memories që do t'i jepet, dhe këtu (në dallim nga CPU) nuk ka pyetje në çfarë e matim (memorinë).
Në përfundim
Çdo pod'i në Kubernetes i caktohen requests dhe kufizimet - të dy parametrat për CPU dhe për memory:
- në bazë të kërkesave punon planifikuesi Kubernetes, i cili shpërndan pod'e nëpër serverë;
- në bazë të të gjitha parametrave të përcaktohet cilësia QoS e pod'it;
- në bazë të kërkesave të CPU llogariten peshat relative;
- në bazë të kërkesave të CPU konfigurimi i planifikuesit CFS;
- në bazë të kërkesave të memories konfigurimi i OOM killer;
- në bazë të kufizimeve të CPU konfigurimi i "semaforit";
- në bazë të kufizimeve të memories konfigurimi i kufizimit në cgroup.

Në përgjithësi, kjo imazh përgjigjet për të gjitha pyetjet se si zhvillohet pjesa kryesore e menaxhimit të burimeve në Kubernetes.
Automatikisht shkallëzimi
K8s cluster-autoscaler
Le të imagjinojmë se e gjithë klasteri është i zënë dhe duhet të krijohet një pod i ri. Deri sa pod-i nuk mund të shfaqet, ai ngelet në statusin Pending. Për ta bërë atë të shfaqet, mund të lidhim një server të ri me klasterin ose… të vendosim cluster-autoscaler, i cili do ta bëjë këtë për ne: do të porosisë një makinë virtuale nga ofruesi i shërbimeve në cloud (me një kërkesë për API) dhe do ta lidh atë me klasterin, pas së cilës pod-i do të shtohet.

Kjo është automashtimi i klasterit Kubernetes, i cili punon mrekullisht (sipas përvojës sonë). Megjithatë, si gjithmonë, ka nuanca...
Ndërsa ne rritëm përmasat e klasterit, gjithçka ishte mirë, por çfarë ndodh kur klasteri fillon të lirohet? Проблема в том, что мигрировать pod’ы (для освобождения хостов) очень технически сложно и дорого по ресурсам. В Kubernetes работает совсем другой подход.
Le të shqyrtojmë një klaster me 3 servera, të cilët kanë një Deployment. Ai ka 6 podë: tani ka nga 2 në çdo server. Për një arsye ndonjëherë dëshirojmë të fikim një nga serverat. Për këtë do të përdorim komandën kubectl drain, e cila:
- ndalon dërgimin e podëve të rinj në këtë server;
- heq pod-ët ekzistues në server.
Duke qenë se Kubernetes monitoron ruajtjen e numrit të podëve (6), ai do të rikrijojë ato në nodet e tjera, por jo në atë të fikur, pasi ai tashmë është shënuar si i papërshtatshëm për vendosjen e podëve të rinj. Kjo është mekanika themelore për Kubernetes.

Megjithatë, këtu ka një nuancë. Në një situatë të ngjashme për StatefulSet (në vend të Deployment) veprimet do të ishin të tjera. Tani kemi një aplikacion stateful — për shembull, tre podë me MongoDB, njëri prej të cilëve ka pasur ndonjë problem (të dhënat janë prishur ose një tjetër gabim që nuk lejon që pod-i të startojë siç duhet). Dhe përsëri, ne vendosim të fikim një server. Çfarë do të ndodhë?

MongoDB mund të do të vdesë, pasi i nevojitet një kuorum: për një klaster me tre instalime, të paktën dy duhet të funksionojnë. Sidoqoftë, kjo nuk ndodh — falë Buxheti i Shkëputjes së Pod-ëve. Ky parametër përcakton numrin minimal të pod-ëve që duhet të funksionojnë. Duke e ditur se një nga pod-ët me MongoDB tashmë nuk punon dhe duke parë se për MongoDB në PodDisruptionBudget është vendosur minAvailable: 2, Kubernetes nuk do të lejojë heqjen e pod-it.
Përfundimi: për të siguruar që lëvizja (dhe në të vërtetë — rikrijimi) i pod-ëve funksionon siç duhet kur klasteri lirohet, duhet të konfiguroni PodDisruptionBudget.
Zgjerimi horizontal
Tani le të shqyrtojmë një situatë tjetër. Ka një aplikacion të nisur si Deployment në Kubernetes. Në pod-ët e tij (për shembull, tre) vjen trafiku i përdoruesit, dhe ne matim një tregues të caktuar (le të themi, ngarkesën në CPU). Kur ngarkesa rritet, ne e regjistrojmë këtë në grafik dhe rrisim numrin e pod-ëve për të shpërndarë kërkesat.
Sot në Kubernetes nuk është e nevojshme të bëhet manualisht: konfiguroni rritjen/reduktimin automatik të numrit të pod-ëve në përputhje me vlerat e treguesve të matjes.

Pyetjet kryesore këtu janë çfarë saktësisht të masim dhe si të interpretojmë vlerat e marra (për të marrë një vendim për ndryshimin e numrit të pod-ëve). Mund të matim shumë gjëra:

Si të bëjmë këtë teknikisht — të mbledhim metrika etj. — kam folur në detaje në prezantimin për . Dhe këshilla kryesore për të zgjedhur parametrat optimalë është eksperimentoni!
Ka (Utilization Saturation and Errors), ku qëllimi është si më poshtë. Në bazë të çfarë është e arsyeshme të bëhet zgjerimi, për shembull, php-fpm? Në bazë të faktit se punëtorët po mbarojnë — kjo është shfrytëzim. Dhe nëse punëtorët kanë përfunduar dhe lidhjet e reja nuk pranohen — kjo është saturim. Të dy këta parametra duhet të maten, dhe në varësi të vlerave të kryhet zgjerimi.
Në vend të përfundimit
Prezantimi ka një vazhdim: mbi zgjerimin vertikal dhe se si të zgjidhni në mënyrë të saktë burimet. Për këtë do të flas në videot e ardhshme në — abonohuni që të mos e humbisni!
Video dhe prezentime
Video nga prezantimi (44 minuta):

Prezantimi i referatit:
P.S.
Prezantime të tjera mbi Kubernetes në blogun tonë:
- «» (Andrey Polovov; 8 Prill 2019 në Saint HighLoad++);
- «» (Dmitri Stolyarov; 8 Nëntor 2018 në HighLoad++);
- «» (Dmitri Stolyarov; 28 Maj 2018 në RootConf);
- «» (Dmitri Stolyarov; 7 Nëntor 2017 në HighLoad++);
- «» (Dmitri Stolyarov; 6 qershor 2017 në RootConf).
Burimi: habr.com
