Заўв. перав.: гэтая павучальная гісторыя Omio - еўрапейскага агрэгатара падарожжаў - праводзіць чытачоў ад базавай тэорыі да займальных практычных тонкасцяў у канфігурацыі Kubernetes. Знаёмства з такімі выпадкамі дапамагае не толькі пашыраць кругагляд, але і прадухіляць нетрывіяльныя праблемы.
Ці даводзілася вам сутыкацца з тым, што дадатак «захрасаў» на месцы, пераставала адказваць на запыты аб праверцы стану (health check'і) і вы не маглі зразумець прычыну такіх паводзін? Адно з магчымых тлумачэнняў злучана з лімітам квот на рэсурсы CPU. Аб ім і пайдзе прамову ў гэтым артыкуле.
TL; DR:
Мы настойліва раім адмовіцца ад CPU limit'аў у 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 дазваляе абстрагавацца ад апаратнага складніка і робіць усю разнастайнасць сістэм прыдатным для разгортвання і працы прыкладанняў.
Kubernetes з пункту гледжання простага абывацеля
Што такое request'ы і limit'ы ў Kubernetes
Окей, мы разабраліся з кантэйнерамі і Kubernetes. Таксама мы ведаем, што некалькі кантэйнераў могуць знаходзіцца на адной машыне.
Можна правесці аналогію з камунальнай кватэрай. Бярэцца прасторнае памяшканне (машыны / вузлы) і здаецца некалькім арандатарам (кантэйнерам). Kubernetes выступае ў ролі рыэлтара. Узнікае пытанне, як утрымаць кватарантаў ад канфліктаў адзін з адным? Што, калі адзін з іх, скажам, вырашыць заняць ванны пакой на паўдня?
Менавіта тут у гульню ўступаюць request'ы і limit'ы. CPU Запыт патрэбен выключна для планавання. Гэта нешта накшталт "спісу жаданняў" кантэйнера, і выкарыстоўваецца ён для падбору самага прыдатнага вузла. У той жа час CPU Limit можна параўнаць з дамовай арэнды - як толькі мы падбярэм вузел для кантэйнера, той не зможа выйсці за ўсталяваныя межы. І вось тут узнікае праблема…
Як рэалізаваны request'ы і limit'ы ў Kubernetes
Kubernetes выкарыстоўвае ўбудаваны ў ядро механізм тротлінга (пропускі тактаў) для рэалізацыі CPU limit'аў. Калі дадатак перавышае ліміт, уключаецца тротлінг (г.зн. яно атрымлівае менш тактаў CPU). Request'ы і limit'ы для памяці арганізаваны інакш, таму іх лягчэй выявіць. Для гэтага дастаткова праверыць апошні статус перазапуску pod'а: ці не з'яўляецца ён "OOMKilled". З тротлінгам CPU усё не так проста, паколькі K8s робіць даступнымі толькі метрыкі па выкарыстанні, а не па cgroups.
CPU Request
Як рэалізаваны 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 долі, пры гэтым падгрупе kuberpod выдзелена 4096 доляй. Як такое магчыма: бо каранёвай групе даступныя за ўсё 4096 доляй, а сума доляй яе нашчадкаў значна перавышае гэты лік (6144)? Справа ў тым, што значэнне мае лагічны сэнс, таму планавальнік Linux (CFS) выкарыстае яго для прапарцыйнага размеркавання рэсурсаў CPU. У нашым выпадку першыя дзве групы атрымліваюць па 680 рэальных доляй (16,6% ад 4096), а kubepod атрымлівае пакінутыя 2736 доляй. У выпадку прастою першыя дзве групы не будуць выкарыстоўваць выдзеленыя рэсурсы.
На шчасце, у планавальніку ёсць механізм, які дазваляе пазбегнуць страты невыкарыстоўваных рэсурсаў CPU. Ён перадае «прастойваюць» магутнасці ў глабальны пул, з якога яны размяркоўваюцца па групах, якія маюць патрэбу ў дадатковых магутнасцях працэсара (перадача адбываецца партыямі, каб пазбегнуць страт ад акруглення). Аналагічны метад прымяняецца і да ўсіх нашчадкаў нашчадкаў.
Гэты механізм забяспечвае справядлівае размеркаванне магутнасцяў працэсара і сочыць за тым, каб ніводны працэс не «краў» рэсурсы ў іншых.
CPU Limit
Нягледзячы на тое, што канфігурацыі limit'аў і request'аў у 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):
Сцэнар 1: 2 патоку і ліміт у 200 мс. Без тратлінгу
Сцэнар 2: 10 патокаў і ліміт у 200 мс. Тротлінг пачынаецца пасля 20 мс, доступ да рэсурсаў працэсара аднаўляецца яшчэ праз 80 мс
Дапушчальны, вы ўсталявалі CPU limit на 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 - Лік throttled-перыядаў у складзе nr_periods;
throttled_time - сукупны throttled-час у нанасекундах.
Што ж насамрэч адбываецца?
У выніку мы атрымліваем высокі тротлінг ва ўсіх прыкладаннях. Часам ён у паўтара раза мацней разліковага!
Гэта прыводзіць да розных памылак – збоям праверак гатоўнасці (readiness), завісання кантэйнераў, разрывам сеткавых падлучэнняў, таймаўтам ўнутры сэрвісных выклікаў. У канчатковым рахунку гэта выяўляецца ў павялічанай затрымцы і падвышэнні колькасці памылак.
Рашэнне і наступствы
Тут усё проста. Мы адмовіліся ад limit'ов CPU і заняліся абнаўленнем ядра АС у кластарах на самую свежую версію, у якой баг быў выпраўлены. Колькасць памылак (HTTP 5xx) у нашых сэрвісах адразу ж значна ўпала:
Можна правесці аналогію з камунальнай кватэрай ... Kubernetes выступае ў ролі рыэлтара. Але як утрымаць кватарантаў ад канфліктаў адно з адным? Што, калі адзін з іх, скажам, вырашыць заняць ванны пакой на паўдня?
Вось у чым падвох. Адзін нядбайны кантэйнер можа паглынуць усе даступныя рэсурсы працэсара на машыне. Калі ў вас тлумачальны стэк прыкладанняў (напрыклад, належным чынам настроены JVM, Go, Node VM), тады гэта не праблема: можна працаваць у такіх умовах на працягу працяглага часу. Але калі прыкладанні аптымізаваны дрэнна ці зусім не аптымізаваны (FROM java:latest), сітуацыя можа выйсці з-пад кантролю. У нас у Omio маюцца аўтаматызаваныя базавыя Dockerfiles з адэкватнымі наладамі па змаўчанні для стэка асноўных моў, таму падобнай праблемы не існавала.
Мы рэкамендуем назіраць за метрыкамі ВЫКАРЫСТАННЕ (выкарыстанне, насычэнне і памылкі), затрымкамі API і частатой з'яўлення памылак. Сачыце за тым, каб вынікі адпавядалі чаканням.
Спасылкі
Такая наша гісторыя. Наступныя матэрыялы моцна дапамаглі разабрацца ў тым, што адбываецца:
Ці сутыкаліся вы з падобнымі праблемамі ў сваёй практыцы ці валодаеце досведам, звязаным з тротлінгам у кантэйнерызаваных production-асяроддзях? Падзяліцеся сваёй гісторыяй у каментарах!