CPU лимити и агресивен тротлинг в Kubernetes

Прим. прев.: тази поучителна история на Omio — европейски агрегатор за пътуване — провежда читателите от основната теория до интересните практически детайли в конфигурацията на Kubernetes. Запознаването с такива случаи не само разширява мирогледа, но и предотвратява нетривиални проблеми.

CPU лимити и агресивен тротлинг в Kubernetes

Случвало ли ви се е да се сблъскате с това, че приложението "задържа" на едно място, спира да отговаря на запитвания за проверка на състоянието (health check) и не можете да разберете причината за такова поведение? Едно от възможните обяснения е свързано с лимита на квотите за ресурси CPU. За него ще стане въпрос в тази статия.

TL;DR:
Настоятелно препоръчваме да се откажете от CPU лимитите в Kubernetes (или да изключите квотите CFS в Kubelet), ако се използва версия на ядрото на Linux с грешка в CFS-квотите. В ядрото има сериозна и добре известна бъг, който води до излишно тротлинг и забавяния
.

В Omio цялата инфраструктура се управлява с Kubernetes. Всички наши stateful и stateless натоварвания работят изцяло на Kubernetes (използваме Google Kubernetes Engine). През последните шест месеца започнахме да наблюдаваме произволни забавяния. Приложенията замръзват или спират да отговарят на health check, губят свързаност с мрежата и т.н. Такова поведение дълго време ни оставяше в задънена улица, и накрая решихме да се заемем с проблема сериозно.

Кратко резюме на статията:

  • Няколко думи за контейнерите и Kubernetes;
  • Как са реализирани CPU request-ите и limit-ите;
  • Как работи CPU limit в среди с множество ядра;
  • Как да следим тротлинга на CPU;
  • Решение на проблема и нюансите.

Няколко думи за контейнерите и Kubernetes

Kubernetes, по същество, е съвременният стандарт в света на инфраструктурата. Неговата основна задача е оркестрация на контейнери.

Контейнери

В миналото ни се налагаше да създаваме артефакти като Java JAR/WAR файлове, Python Egg файл или изпълними файлове, за последващо стартиране на сървърите. Въпреки това, за да ги накараме да функционират, трябваше да извършваме допълнителна работа: инсталиране на среда за изпълнение (Java/Python), разполагане на необходимите файлове на правилните места, осигуряване на съвместимост с конкретна версия на операционната система и т.н. С други думи, трябваше да обърнем внимание на управлението на конфигурациите (което често е причината за конфликти между разработчиците и системните администратори).

Контейнерите промениха всичко. Сега артефактът представлява контейнерен образ. Може да си го представите като разширен изпълним файл, съдържащ не само програма, но и пълноценна среда за изпълнение (Java/Python/…), както и необходимите файлове/пакети, предварително инсталирани и готови за стартиране. Контейнерите могат да бъдат разгръщани и стартирани на различни сървъри без допълнителни действия.

Освен това, контейнерите работят в собствена среда-пясъчник. Те имат свой собствен виртуален мрежов адаптер, своя файлова система с ограничен достъп, своя йерархия на процесите, свои ограничения на CPU и памет и т.н. Всичко това е реализирано благодарение на специална подсистема на ядрото на Linux — namespaces (пространства от имена).

Kubernetes

Както вече споменахме, Kubernetes е оркестратор на контейнери. Работи по следния начин: предоставяте му пул от машини и след това му казвате: „Ей, Kubernetes, стартирай десет инстанции от контейнера ми с 2 процесора и 3 Гб памет на всяка и ги поддържай в работно състояние!“. Kubernetes ще се погрижи за всичко останало. Той ще намери свободните ресурси, ще стартира контейнерите и ще ги рестартира при необходимост, ще прокара актуализации при смяна на версиите и т.н. По същество Kubernetes позволява да се абстрахирате от хардуерната част и прави всичките различия в системите подходящи за разгръщане и работа на приложения.

CPU лимити и агресивен тротлинг в Kubernetes
Kubernetes от гледна точка на обикновения потребител

Какво представляват request’ите и limit’ите в Kubernetes

Окей, справихме се с контейнерите и Kubernetes. Също така знаем, че няколко контейнера могат да бъдат на една машина.

Може да направите аналогия с обща квартира. Взема се просторно помещение (машини/възли) и се отдава под наем на няколко наематели (контейнери). Kubernetes играе ролята на посредник. Въпросът е как да се предотвратят конфликтите между наемателите? Какво ще стане, ако един от тях, да речем, реши да заеме банята за половин ден?

Тук в играта влизат request’ите и limit’ите. CPU Request е необходим единствено за планиране. Това е нещо като „списък на желанията“ на контейнера и се използва за избиране на най-подходящия възел. В същото време CPU Limit може да се сравни с договор за наем — веднага щом подберем възел за контейнера, той няма да може да излезе извън установените граници. И тук възникава проблемът…

Как са реализирани request-ите и limit-ите в Kubernetes

Kubernetes използва вграден в ядрото механизъм на тротлинг (пропускане на цикли) за реализиране на CPU limit-ите. Ако приложението превиши лимита, активира се тротлинг (т.е. получава по-малко цикли CPU). Request-ите и limit-ите за паметта са организирани по различен начин, така че е по-лесно да бъдат открити. Достатъчно е да се провери последният статус на повторното стартиране на pod-а: не е ли "OOMKilled". С тротлинга на CPU не е толкова просто, тъй като K8s предоставя само метрики за използването, а не за cgroups.

CPU Request

CPU лимити и агресивен тротлинг в Kubernetes
Как е реализиран CPU request

За простота да разгледаме процеса на примера на машина с 4-ядрен CPU.

K8s използва механизмът за контролни групи (cgroups) за управление на разпределението на ресурсите (памет и процесор). За него е налична йерархична структура: дъщерната група наследява limit-ите на родителската група. Подробностите за разпределението се съхраняват във виртуалната файлова система (/sys/fs/cgroup). В случая на процесора това /sys/fs/cgroup/cpu,cpuacct/*.

K8s използва файл cpu.share за разпределение на ресурсите на процесора. В нашия случай коренната контролна група получава 4096 дялове ресурси на CPU — 100% от наличната мощност на процесора (1 ядро = 1024; това е фиксирана стойност). Коренната група разпределя ресурсите пропорционално в зависимост от дяловете на дъщерните групи, зададени в cpu.share, а те, от своя страна, извършват същото със своите дъщерни групи и т.н. В типичен възел на Kubernetes коренната контролна група има три дъщерни групи: system.slice, user.slice и kubepods. Първите две подгрупи се използват за разпределение на ресурсите между критично важни системни натоварвания и потребителски програми извън K8s. Последната — kubepods — е създадена от Kubernetes за разпределение на ресурсите между pod-овете.

На схемата по-горе се вижда, че първата и втората подгрупи получават по 1024 дялове, като подгрупата kubepod получава 4096 дялове. Как е възможно това: нали коренната група има достъп само до 4096 дялове, а сумата на дяловете на нейните дъщерни групи значително надвишава това число (6144)? Фактът е, че стойността има логически смисъл, затова планировчикът на Linux (CFS) я използва за пропорционално разпределение на ресурсите на CPU. В нашия случай първите две групи получават по 680 реални дялове (16,6% от 4096), а kubepod получава останалите 2736 долей. В случай на простои първите две групи няма да използват посочените ресурси.

За щастие, в планировчика има механизъм, който позволява да се избегне загубата на неизползвани ресурси на CPU. Той прехвърля "простаещите" мощности в глобален пул, от който те се разпределят на групи, нуждаещи се от допълнителни процесорни мощности (прехвърлянето става на партиди, за да се избегнат загуби от закръгляне). Подобен метод се прилага и за всички наследници на наследниците.

Този механизъм осигурява справедливо разпределение на процесорните мощности и следи да не се случва "кражба" на ресурси от една задача от друга.

CPU Limit

Въпреки че конфигурациите на лимитите и заявките в K8s изглеждат подобно, тяхната реализация се различава коренно: това е най-сбърканият и най-малко документираният компонент.

K8s използва механизма CFS квоти за реализиране на лимитите. Настройките им се задават в файловете cfs_period_us и cfs_quota_us в директорията cgroup (там е разположен и файлът cpu.share).

В отличие от cpu.share, квотата се основава на периода на време, а не на наличната процесорна мощ. cfs_period_us задава продължителността на периода (епохата) — това винаги е 100000 мкс (100 мс). В K8s има възможност да се промени това значение, но за момента тя е налична само в алфа версия. Планировчикът използва епохата, за да рестартира използваните квоти. Вторият файл, cfs_quota_us, задава наличното време (квотата) във всяка епоха. Обърнете внимание, че тя също така е посочена в микросекунди. Квотата може да надвишава продължителността на епохата; с други думи, тя може да бъде повече от 100 мс.

Нека разгледаме два сценария на 16-ядрени машини (най-разпространеният тип компютри у нас в Omio):

CPU лимити и агресивен тротлинг в Kubernetes
Сценарий 1: 2 потока и лимит от 200 мс. Без тротлинг

CPU лимити и агресивен тротлинг в Kubernetes
Сценарий 2: 10 потока и лимит от 200 мс. Тротлингът започва след 20 мс, достъпът до процесорните ресурси се възобновява още след 80 мс

Да предположим, че сте задали лимит на CPU на 2 ядра; Kubernetes ще преобразува това значение в 200 мс. Това означава, че контейнерът може да използва максимум 200 мс процесорно време без тротлинг.

И тук започва най-интересното. Както бе споменато по-горе, наличната квота е 200 мс. Ако имате паралелно работещи десет потокове на 12-ядрена машина (вижте илюстрацията към сценарий 2), докато всички останали pod’ове са в застой, квотата ще бъде изчерпана само след 20 мс (тъй като 10 * 20 мс = 200 мс), и всички потоци на този pod ще «замръзнат» (ограничаване) за следващите 80 мс. Усложнява ситуацията вече споменатият баг на планировщика, поради който се случва излишно ограничаване и контейнерът не може да произведе дори наличната квота.

Как да оценим ограничаването в pod’овете?

Просто влезте в pod и изпълнете cat /sys/fs/cgroup/cpu/cpu.stat.

  • nr_periods — общ брой периоди на планировщика;
  • nr_throttled — брой на ограничените периоди в състава на nr_periods;
  • throttled_time — общо време на ограничаване в наносекунди.

CPU лимити и агресивен тротлинг в Kubernetes

Какво наистина се случва?

В крайна сметка получаваме високо ограничаване във всички приложения. Понякога то е половин аргумент по-силно от пресметнатото!

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

Решение и последици

Тук всичко е просто. Отказахме се от лимитите на ЦП и се заехме с обновяване на ядрото на ОС в клъстери до най-новата версия, в която багът беше поправен. Броят на грешките (HTTP 5xx) в нашите услуги веднага значително спадна:

Грешки HTTP 5xx

CPU лимити и агресивен тротлинг в Kubernetes
Грешки HTTP 5xx на един критично важен сервис

Vреме за отговор p95

CPU лимити и агресивен тротлинг в Kubernetes
Закъснение на запитванията на критично важен сервис, 95-ти процентилен

Разходи за експлоатация

CPU лимити и агресивен тротлинг в Kubernetes
Броят на изразходваните екземпляри-часа

В чем е уловката?

Както беше казано в началото на статията:

Може да се направи аналогия с комунална квартира… Kubernetes играе роля на риелтор. Но как да се предотврати конфликт между наемателите? Какво, ако един от тях, да кажем, реши да заеме банята за половин ден?

Ето в какво е уловката. Един немарлив контейнер може да погълне всички налични ресурси на процесора на машината. Ако имате добър стек от приложения (например, правилно настроени JVM, Go, Node VM), тогава това не е проблем: можете да работите в такива условия за дълго време. Но ако приложенията са лошо оптимизирани или изобщо не са оптимизирани (FROM java:latest), ситуацията може да излезе извън контрол. В Omio имаме автоматизирани основни Dockerfiles с адекватни настройки по подразбиране за основните езици, така че подобен проблем не е съществувал.

Препоръчваме да следите метриките ИЗПОЛЗВАНЕ (използване, наситеност и грешки), закъснения в API и честотата на появата на грешки. Уверете се, че резултатите отговарят на очакванията.

Връзки

Такава е нашата история. Следните материали много помогнаха да разберем какво се случва:

Доклади за грешки в Kubernetes:

Срещали ли сте подобни проблеми в практиката си или имате опит, свързан с тротлинг в контейнеризирани производствени среди? Споделете историята си в коментарите!

P.S. от преводача

Прочетете също в нашия блог:

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

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