Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

27 април на конференцията Стачка-2019, в рамките на секция «DevOps», бе представен докладът «Автомасштабиране и управление на ресурсите в Kubernetes». В него се описва как с помощта на K8s да се осигури висока наличност на приложенията и да се гарантира максималната им производителност.

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

По традиция с удоволствие представяме видеото с доклада (44 минути, много по-информативно от статия) и основната извадка в текстов вид. Да тръгваме!

Да разгледаме темата на доклада по ключови точки и да започнем от края.

Kubernetes

Да предположим, че имаме Docker контейнери на хост. Защо? За осигуряване на повторяемост и изолация, което от своя страна позволява лесно и качествено внедряване, CI/CD. Имаме много машини с контейнери.

Какво предлага Kubernetes в този случай?

  1. Преставаме да мислим за тези машини и започваме да работим с «облака», кластера от контейнери или pod'ове (групи от контейнери).
  2. По-голямо от това, ние не мислим дори за отделни pod'ове, а управляваме още по-оголеми групи. Такива високо ниво примитиви ни позволяват да кажем, че има шаблон за стартиране на определена работна натовареност, а ето нужния брой инстанции за нейното стартиране. Ако в последствие променим шаблона — променят се и всички инстанции.
  3. С помощта на представеното декларативно API вместо изпълнение на последователността от конкретни команди описваме «устройството на света» (в YAML), който се създава от Kubernetes. И отново: при промени в описанието ще се променя и реалното му представяне.

Управление на ресурсите

CPU

Представете си, че стартираме nginx, php-fpm и mysql на сървър. Тези услуги всъщност ще имат още повече работещи процеси, всеки от които изисква изчислителни ресурси:

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)
(числата на слайда — «попугаи», абстрактна нужда за всеки процес от изчислителна мощност)

За да работим удобно с това, е логично да обединим процесите по групи (например всички процеси на nginx в една група «nginx»). Прост и очевиден начин да го направим — да поставим всяка група в контейнер:

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

За да продължим, е важно да припомним какво точно представлява контейнерът (в Linux). Появата им стана възможна благодарение на три ключови възможности в ядрото, реализирани преди доста време: възможности, namespaces и cgroups. А по-нататъшното им развитие бе подпомогнато от други технологии (включително удобни «обвивки» като Docker):

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

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

Да се върнем към нуждите от CPU на тези процеси, а сега вече — на групите от процеси:

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)
(повтарям, че всички числа — абстрактно изражение на нуждите от ресурси)

При това самият CPU има определен краен ресурс (в примера това е 1000), от който може да не стигне на всички (сумата на нуждите на всички групи — 150+850+460=1460). Какво ще се случи в такъв случай?

Ядрото започва да разпределя ресурсите и го прави "честно", възлагайки еднакво количество ресурси на всяка група. Но в първия случай те са повече от необходимото (333>150), следователно излишъкът (333-150=183) остава в резерв, който също равномерно се разпределя между другите два контейнера:

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

В крайна сметка: на първия контейнер му стигнали ресурсите, на втория — значително не стигнали, на третия — малко не стигнали. Такъв е резултатът от действията "честния" планировчик в LinuxCFS. Неговата работа може да се регулира чрез назначаване на тежести на всеки от контейнерите. Например, така:

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

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

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Така работи планировчикът CFS. Тежестите, които назначаваме на контейнерите, в последствие ще наречем заявки. Защо точно така — вижте по-нататък.

Нека да погледнем на цялата ситуация от друга страна. Както е известно, всички пътища водят до Рим, а в случая с компютъра — до CPU. CPU е един, задачите са много — нужен е светофар. Най-простият начин за управление на ресурсите е "светофарният": дадоха на един процес фиксирано време за достъп до CPU, след това — на следващия и т.н.

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Този подход се нарича строги квоти (hard limiting). Нека го запомним просто като лимити. Въпреки това, ако разпределим лимити на всички контейнери, възниква проблем: mysql минава по пътя и в някакъв момент нуждата му от CPU свършва, но всички останали процеси са принудени да чакат, докато CPU простаива.

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

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

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Cgroup има две настройки — по същество това са две прости "контроли", които определят:

  1. теглото за контейнера (request’и) — това е shares;
  2. процент от общото CPU време за работа по задачите на контейнера (лимити) — това е quota.

С какво да измерваме CPU?

Има различни начини:

  1. Какво е папагали, никой не знае — трябва всеки път да се договаряме.
  2. Процентите са по-разбираеми, но относителни: 50% от сървър с 4 ядра и сървър с 20 ядра — това са съвсем различни неща.
  3. Можете да използвате вече споменатите тежести, които Linux познава, но те също са относителни.
  4. Най-адекватният вариант е да измерваме изчислителните ресурси в секунди. Т.е. в секунди процесорно време спрямо секунди реално време: предоставили сме 1 секунда процесорно време в 1 реална секунда — това е един цял CPU ядро.

За да стане още по-лесно, започнахме да измерваме направо в ядра, имайки предвид именно това време на CPU относително към реалното. Тъй като Linux разбира теглата, а не такова процесорно време/ядра, беше необходим механизъм за превод от едното в другото.

Нека разгледаме прост пример със сървър с 3 CPU ядра, където на трите pod’а ще бъдат зададени такива тегла (500, 1000 и 1500), които лесно се конвертират в съответните части от тяхното зададено ядро (0,5, 1 и 1,5).

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Ако вземем втори сървър, където ядрото ще е два пъти повече (6), и разположим там същите pod’и, разпределението на ядрата може да се изчисли просто като умножим на 2 (1, 2 и 3 съответно). Но важен момент настъпва, когато на този сървър се появи четвърти pod, чие тегло да е например 3000. Той взема част от ресурсите на CPU (половината ядра), а за останалите pod’и те трябва да се пренастроят (намалят наполовина):

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Kubernetes и ресурсите на CPU

В Kubernetes ресурсите на CPU обикновено се измерват в милиядри, т.е. като основно тегло се взема 0.001 ядро. (Същото в терминологията на Linux/cgroups се нарича CPU share, въпреки че, ако говорим точно, 1000 милияди = 1024 CPU shares.) K8s следи да не разполага на сървъра повече pod’и, отколкото ресурси на CPU са налични за сумата от теглата на всички pod’и.

Как става това? При добавяне на сървър в кластера Kubernetes се съобщава колко CPU ядра са налични. А при създаване на нов pod, планировчикът на Kubernetes знае колко ядра ще са необходими на този pod. Така че pod’ът ще бъде определен на сървър, където има достатъчно ядра.

Какво ще се случи, ако не посочване на request (т.е. на pod’а не е зададено количество нужни ядрa)? Нека разгледаме как Kubernetes изчислява ресурсите.

На pod’а могат да се зададат както request-и (планиращ CFS), така и лимити (помните ли светофора?):

  • Ако те са зададени равни, pod’ът получава QoS-клас guaranteed. Тази сума ядра е винаги налична за него.
  • Ако request е по-малък от лимита — QoS-клас burstable. Т.е. очакваме, че pod’ът например винаги ще използва 1 ядро, но това значение не е ограничение за него: понякога pod’ът може да използва и повече (когато на сървъра има свободни ресурси за това).
  • Има и QoS-клас best effort — те са pod’овете, за които не е зададен request. Ресурсите им се предоставят в последна норма.

Памет

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

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Нека видим как в паметта се реализират request-ите. Нека pod’овете работят на сървъра, променяйки използваната памет, докато един от тях не стане толкова голям, че паметта да свърши. В този случай се появява OOM killer и убива най-голямия процес:

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Това не винаги е приемливо за нас, затова имаме възможност да регулираме кои процеси са важни за нас и не трябва да бъдат убивани. За това се използва параметър oom_score_adj.

Нека се върнем на QoS-класовете за CPU и направим аналогия с стойностите oom_score_adj, които определят приоритетите на pod’овете по отношение на консумацията на памет:

  • Най-ниската стойност на oom_score_adj за pod’а — -998 — означава, че такъв pod трябва да бъде убит в последния момент, това guaranteed.
  • Най-високата стойност — 1000 — е best effort, такива pod’ове се убиват първи.
  • За изчисляване на останалите стойности (burstable) има формула, чиято същност е, че колкото повече ресурси е поискал pod, толкова по-малко шансове има да бъде убит.

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Втората „крутилка“ — limit_in_bytes — за лимитите. С нея е по-просто: просто задаваме максимално количество предавана памет и тук (в отличие от CPU) няма въпрос в какво да я измерваме.

Итого

На всеки pod в Kubernetes се задават requests и limits — и двата параметъра за CPU и памет:

  1. планиращият Kubernetes работи на базата на requests, който разпределя pod’овете по сървъри;
  2. на базата на всички параметри се определя QoS-класът на pod’а;
  3. на базата на CPU requests се изчисляват относителни тегла;
  4. на базата на CPU requests се настраива CFS-планиращият;
  5. на базата на memory requests се настраива OOM killer;
  6. въз основа на ограниченията на CPU се настраива "светофор";
  7. въз основа на ограниченията на паметта се настраива лимит за cgroup.

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

В общи линии, тази картинка отговаря на всички въпроси относно основната част на управлението на ресурсите в Kubernetes.

Автоматично мащабиране

K8s cluster-autoscaler

Да си представим, че целият клъстер вече е зает и трябва да бъде създаден нов pod. Докато pod не може да се появи, той остава в статус Pending. За да се появи все пак, можем да свържем нов сървър към клъстера или… да инсталираме cluster-autoscaler, който ще направи това вместо нас: ще поръча виртуална машина от облачен доставчик (чрез API) и ще я свърже към клъстера, след което pod ще бъде добавен.

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Това е автомасштабиране на клъстерите в Kubernetes, което работи прекрасно (според нашия опит). Въпреки това, както навсякъде, и тук има нюанси…

Докато увеличавахме размерите на клъстера, всичко беше наред, но какво се случва, когато клъстерът започне да се освобождава? Проблема в том, что мигрировать pod’ы (для освобождения хостов) очень технически сложно и дорого по ресурсам. В Kubernetes работает совсем другой подход.

Нека разгледаме клъстер от 3 сървъра, в който има Deployment. Има 6 pod-а: сега по 2 на всеки сървър. По някаква причина искаме да изключим един от сървърите. За целта ще използваме командата kubectl drain, която:

  • ще забрани изпращането на нови pod-ове на този сървър;
  • ще изтрие съществуващите pod-ове на сървъра.

Тъй като Kubernetes следи за поддържането на броя pod-ове (6), той просто ще ги пресъздаде на другите възли, но не на изключения сървър, тъй като той вече е маркиран като недостъпен за разполагане на нови pod-ове. Това е основополагаещата механика за Kubernetes.

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Но и тук има нюанс. В аналогична ситуация за StatefulSet (вместо Deployment) действията ще бъдат различни. Сега имаме stateful-приложение — например, три pod-а с MongoDB, при който един от тях е имал проблем (данните са повредени или друга грешка, която не позволява на pod-а да стартира коректно). И отново решаваме да изключим един сървър. Какво ще се случи?

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

MongoDB може да Той ще умре, тъй като му е необходим кворум: за клъстер от три инсталации поне две трябва да функционират. Обаче това не се случва — благодарение на PodDisruptionBudget. Този параметър определя минимално необходимо количество работещи pod-ове. Знаейки, че един от pod-овете с MongoDB вече не работи и виждайки, че за MongoDB в PodDisruptionBudget е зададено minAvailable: 2, Kubernetes няма да уреди изтриването на pod.

Резюме: за да функционира правилно преместването (всъщност — реконструирането) на pod’овете при освобождаване на клъстера, е необходимо да се конфигурира PodDisruptionBudget.

Хоризонтално мащабиране

Нека разгледаме друга ситуация. Има приложение, стартирано като Deployment в Kubernetes. Към неговите pod’ове (например, три на брой) постъпва потребителски трафик, а ние в тях измерваме някакъв показател (да кажем, натоварването на CPU). Когато натоварването се увеличи, ние го фиксираме по графика и увеличаваме броя на pod’овете за разпределение на заявките.

Днес в Kubernetes не е необходимо да се прави ръчно: конфигурира се автоматично увеличаване/намаляване на броя pod’ове в зависимост от стойностите на измерените показатели за натоварване.

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Основните въпроси тук са: какво точно да измерваме и как да интерпретираме получените стойности (за вземане на решение за промяна на броя pod’ове). Може да се измерва много:

Автоматично мащабиране и управление на ресурсите в Kubernetes (преглед и видео доклад)

Как технически да го направим — събиране на метрики и т.н. — подробно изложих в доклада си за Мониторинг и Kubernetes. Основният съвет за избор на оптимални параметри е: експериментирайте!

Има методът USE (Utilization Saturation and Errors), смисълът на който е следният. На каква база има смисъл да се мащабира, например, php-fpm? На базата на това, че worker’ите свършват, — това е utilization. А ако worker’ите свършат и нови връзки не се приемат — това вече е saturation. И двата тези параметъра трябва да се измерват, а в зависимост от стойностите да се провежда мащабиране.

Вместо заключение

Докладът има продължение: за вертикалното мащабиране и за това как правилно да подбираме ресурсите. За това ще разкажа в бъдещи видеа на нашия YouTube — абонирайте се, за да не пропуснете!

Видео и слайдове

Видео от изявата (44 минути):

Пуснете видеото

Презентация на доклада:

P.S.

Други доклади за Kubernetes в нашия блог:

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster