27. aprill konverentsil , DevOps sektsiooni raames, esitati ettekande „Kuberneteses automaatne skaleerimine ja ressursside haldamine“. Ettekandes räägitakse, kuidas K8s saab tagada rakenduste kõrge kättesaadavuse ja maksimaalse jõudluse.

Traditsiooniliselt oleme uhked esitama (44 minutit, oluliselt informatiivsem kui artikkel) ja põhijooned tekstivormis. Alustame!
Arutame ettekande teemat sõna-sõnalt ja alustame lõpust.
Kubernetes
Kujutage ette, et meil on serveris Docker-konteinerid. Miks? Et tagada korduvus ja isoleeritus, mis omakorda võimaldab lihtsat ja kvaliteetset juurutamist, CI/CD. Meil on palju selliseid konteineritega masinaid.
Mida me sellisel juhul Kuberneteselt saame?
- Me lõpetame nende masinate üle mõtlemise ja hakkame töötama „pilve“ konteinerite klastriga või pod‘idega (konteinerigruppidega).
- Veelgi enam, me ei mõtle isegi erilistele pod’idele, vaid haldame veelgiosuuremaid gruppe. Tooted kõrgtaseme primitivid annavad meile võimaluse öelda, et on mall teatud töökoormuse käivitamiseks, ja siin on vajalik nende instantside arv. Kui hiljem muudame malli — muudavad ka kõik instantsid.
- Käesoleva abil deklaratiivne API meie kirjeldame maailma struktuuri » (YAML-is), mis luuakse Kubernetes'i abil, selle asemel, et täita konkreetse käskude järjestust. Ja jälle: muudatuste korral muutub ka selle tegelik kuvamine.
Ressursside haldamine
CPU
Kujutage ette, et serveris käivitame nginx, php-fpm ja mysql. Neil teenustel on tegelikult veel rohkem töötavaid protsesse, igaühel neist on vaja arvutusressursse:

(slaidil olevad numbrid — „papagoi“, iga protsessi arvutusvõimsuse abstraktne vajadus)
Selle mugavaks käsitlemiseks on mõistlik rühmitada protsessid gruppidesse (näiteks kõik nginx protsessid ühte gruppi „nginx“). Lihtsaim ja ilmsem viis selleks on paigutada iga grupp konteinerisse:

Enne jätkamist tuleb meeles pidada, mis on konteiner (Linuxis). Nende teke sai võimalikuks kolme peamise funktsiooni tõttu tuumas, mis on juba ammu rakendatud: , ja . Edasisele arengule aitasid kaasa ka teised tehnoloogiad (sealhulgas mugavad „shellid“ nagu Docker):

Käesolevas ettekandes huvitab meid ainult cgroups, kuna see just kontrollgrupid on konteinerite (nt Docker) funktsioonide osa, mis haldab ressursse. Protsessid, mis on ühendatud gruppidesse, nagu soovisime, ongi need kontrollgrupid.
Naaseme nende protsesside CPU vajaduste juurde ning nüüd — protsessigruppide vajadustele:

(kordan, et kõik numbrid on abstraktne ressursivajaduse väljendus)
Samuti on CPU-l teatud lõplik ressurss (näites on see 1000), mille kõigile ei pruugi piisata (kõigi gruppide vajaduste summa — 150+850+460=1460). Mis sel juhul juhtub?
Tuuma hakkab ressursse jaotama ja teeb seda "ausalt", jagades igale grupile sama hulga ressursse. Kuid esimeses olukorras on neid rohkem kui vajalik (333>150), seega jääb ülejääk (333-150=183) reservi, mis jagatakse samuti võrdselt kahe teise konteineri vahel:

Lõpuks: esimesel konteineril oli ressursse piisavalt, teisel — palju ei piisanud, kolmandal — veidi ei piisanud. Nii on tulemuseks "aus" planeerija Linuxis — . Tema tööd saab reguleerida, määrates kaalu iga konteineri jaoks. Näiteks selliselt:

Vaata olukorda, kus teise konteineri (php-fpm) ressursid on piiratud. Kõik konteineri ressursid jagatakse protsesside vahel ühtlaselt. Seetõttu töötab master-protsess hästi, kuid kõik worker'id peavad hädalikult leppima vähem kui poolega vajalikust:

Nii töötab CFS planeerija. Kaalud, mille me konteineritele määrame, nimetame edaspidi päringuteks. Miks just nii — vt lähemalt.
Vaata olukorda ka teiselt poolt. Nagu öeldakse, kõik teed viivad Rooma, ja arvuti puhul — CPU juurde. CPU on üks, ülesandeid palju — vajalik on reguleerimine. Lihtsaim viis ressursside juhtimiseks on 'reguleeringuline': anname ühele protsessile fikseeritud aja CPU juurde pääsemiseks, siis järgmisele jne.

Seda lähenemist nimetatakse rangeks kvoteerimiseks (hard limiting). Salvestame selle lihtsalt kui piirid. Siiski, kui jagada kõigile konteineritele piirid, tekib probleem: mysql sõitis teel ja ühel hetkel lõppes selle vajadus CPU järele, kuid kõik teised protsessid peavad ootama, kuni CPU on vabastatud.

Naaseme Linuxi tuuma juurde ja selle suhtlemisse CPU-ga — üldine pilt on järgmine:

Cgroup'il on kaks seadistust — põhimõtteliselt kaks lihtsat 'nuppu', mis määravad:
- konteineri kaal (request'id) — see on shares;
- CPU üldise töötamise ajast konteineri ülesannete täitmiseks (limiidid) — see on quota.
Kuidas CPU-d mõõta?
On erinevaid viise:
- Mis see on papagei, keegi ei tea — peab iga kord kokku leppima.
- Protsendid on arusaadavamad, kuid suhtelised: 50% 4 tuumaga serverist ja 20 tuumaga serverist on täiesti erinevad asjad.
- Võib kasutada juba mainitud kaalu, mida Linux tunneb, kuid need on samuti suhtelised.
- Kõige adekvaatsem valik on mõõta arvutusressursse sekundites. St. protsessoritööaja sekundid võrreldes tegeliku aja sekunditega: anti 1 sekund protsessoritööd 1 tegelikus sekundis — see on täielik CPU tuum.
Et asi veel lihtsamaks teha, hakati mõõtma otse tuumade, mõistes nende all CPU aega võrreldes tegeliku ajaga. Kuna Linux mõistab kaalu, mitte protsessoritööd/tuumasid, oli vajalik mehanism ühe teisest teisendamiseks.
Vaatame lihtsat näidet serverist, kus on 3 CPU tuuma, kus kolmele pod'ile on valitud kaalukategooriad (500, 1000 ja 1500), mis muudetakse kergesti vastavaks nendele eraldatud tuumadele (0,5, 1 ja 1,5).

Kui võtta teine server, kus tuumade arv on kahekordne (6), ja jagada sinna samad pod'id, on tuumade jaotus kergesti arvutatav lihtsa korrutamise abil 2-ga (1, 2 ja 3 vastavalt). Kuid oluline hetk saabub siis, kui sellel serveril ilmub neljas pod, mille kaal on mugavuse huvides 3000. See võtab endale osa CPU ressursidest (pool tuumadest), samas kui teiste pod'ide ressursid arvestatakse ümber (väheneb poole võrra):

Kubernetes ja CPU ressursid
Kubernetes'is mõõdetakse CPU ressursse milliarhades, st põhikaaluks on 0,001 tuuma. (Seda samme nimetatakse Linuxi/cgroups terminoloogias CPU share, kuigi täpsemalt öeldes on 1000 milliarhad = 1024 CPU jagu.) K8s jälgib, et serveris ei oleks rohkem pod'e kui CPU ressursid kõigi pod'ide kaalude kogusummale.
Kuidas see toimub? Kui server lisatakse Kubernetes'i klastrisse, teatatakse, mitu CPU tuuma on sellel saadaval. Uue pod'i loomisel teab Kubernetes'i planeerija, kui palju tuumi see pod vajab. Niisiis määratakse pod serverisse, kus on piisavalt tuumi.
Mis juhtub, kui ei request on määratud (st pod'il ei ole määratud vajalike tuumade arvu)? Vaatame, kuidas Kubernetes üldiselt ressursse arvutab.
Pod'il saab määrata nii request'id (CFS planeerija) kui ka piirangud (kas meenutate foori?):
- Kui need on määratud ühesugustena, määratakse pod'ile QoS-klass garanteeritud. See tähendab, et antud hulga tuumade kättesaadavus on garanteeritud.
- Kui request on väiksem, siis QoS-klass on burstable. See tähendab, et me eeldame, et pod kasutab näiteks alati 1 tuuma, kuid see väärtus ei ole tema jaoks piirang: mõnikord pod võib kasutada ka rohkem (kui serveris on sellele vabu ressursse).
- On veel QoS-klass parim pingutus — sinna kuuluvad need pod'id, mille jaoks request'i ei ole määratud. Ressursse antakse neile viimases järjekorras.
Mälu
Mälu puhul on olukord sarnane, kuid natuke erinev — need ressursid on oma olemuselt erinevad. Üldiselt on analoogia järgmine:

Vaadakem, kuidas mälus request'id ellu viiakse. Oletame, et pod'id elavad serveris, muutes tarbitavat mälu, kuni üks neist on nii suur, et mälu lõpeb. Sellisel juhul tuleb OOM killer ja tapab suurima protsessi:

Kuid see ei ole alati vastuvõetav, seetõttu on võimalus reguleerida, millised protsessid on meie jaoks olulised ja ei tohiks ära mõõda. Selleks kasutatakse parameetrit oom_score_adj.
Naaseme CPU QoS klasside juurde ja teeme analoogia väärtustega oom_score_adj, mis määravad pod'ide prioriteedid mälutarbimise osas:
- Madalaim oom_score_adj väärtus pod'il on -998, mis tähendab, et selline pod tuleks tapmist edasi lükata, see garanteeritud.
- Kõrgeim väärtus on 1000, see parim pingutus, need pod'id tapetakse kõige varem.
- Ülejäänud väärtuste arvutamiseks (burstable) on olemas valem, mille sisu seisneb selles, et mida rohkem pod on ressursse tellinud, seda väiksem on tõenäosus, et teda tapetakse.

Teine "nupp" — limit_in_bytes — piiride jaoks. Siin on kõik lihtsam: me määrame lihtsalt maksimaalse mäluhulga ja siin (erinevalt CPU-st) ei ole küsimust, milles seda (mälu) mõõta.
Kokku
Iga pod'ile Kuberneteses määratakse requests ja limits — nii CPU kui ka mälumõõturite parameetrid:
- Kubernetesi planeerija töötab requests'i alusel, jaotades pod'id serverite vahel;
- kõikide parameetrite alusel määratakse pod'i QoS-klass;
- CPU requests'i alusel arvutatakse suhtelised kaalud;
- CPU requests'i alusel konfigureeritakse CFS-plaane;
- mälurequestide põhjal konfigureeritakse OOM tapja;
- CPU limitide alusel konfigureeritakse "foor";
- mälulimitide alusel seadistatakse cgroup'i piirang.

Kokkuvõttes vastab see pilt kõigile küsimustele, kuidas toimub peamine ressursihaldus Kuberneteses.
Automaatne skaleerimine
K8s kluster-automaatskaalija
Kujutame ette, et kogu kluster on juba hõivatud ja peab olema loodud uus pod. Seni kuni pod ei saa ilmuda, on see staatuses Ootel. Et see ikkagi ilmuks, saame liita uue serveri klusteriga või… installida cluster-automaatskaalija, mis teeb selle meie eest: tellib virtuaalse masina pilveteenuse pakkujalt (API päringu kaudu) ja liidab selle klusteriga, pärast mida pod lisatakse.

See ongi Kubernetesi klustere automaatne skaleerimine, mis meie kogemuse kohaselt töötab suurepäraselt. Kuid nagu alati, on siin oma nüansid...
Kuna me klastrit suurendasime, oli kõik hästi, aga mis juhtub, kui klaster hakkab vabastama? Проблема в том, что мигрировать pod’ы (для освобождения хостов) очень технически сложно и дорого по ресурсам. В Kubernetes работает совсем другой подход.
Vaatame klastrit, kus on 3 serverit ja Deployment. Seal on 6 pod'i: praegu on need igaühes 2. Kui mingil põhjusel tahame ühe serveri välja lülitada. Selleks kasutame käsku kubectl drain, mis:
- keelab uute pod'ide saatmise sellele serverile;
- eemaldab serveris olemasolevad pod'id.
Kuna Kubernetes jälgib pod'ide arvu (6), siis lihtsalt loob need teistele sõlmedele, kuid mitte välja lülitatud, kuna see on juba märgitud kui mitte saadaval uute pod'ide paigutamiseks. See on Kubernetes'i põhiprintsiip.

siiski on siin üks nüanss. Sarnases olukorras StatefulSet'i (või Deployment'i asemel) puhul on tegevused erinevad. Nüüd on meil juba stateful-rakendus — näiteks kolm pod'i MongoDB-ga, millest ühel on tekkinud mingi probleem (andmed on rikutud või mõni muu viga, mis ei võimalda pod'il korralikult käivituda). Ja me otsustame jälle ühe serveri välja lülitada. Mis siis juhtub?

MongoDB võiks suretuks, kuna tal on vajalik kvorum: kolmest installeeritud klastrist peab vähemalt kaks toimima. Kuid seda ei juhtu — tänu PodDisruptionBudget. See parameeter määrab minimaalset vajalikku töötavate pod'ide arvu. Teades, et üks MongoDB pod'idest ei tööta, ja nähes, et MongoDB jaoks on PodDisruptionBudget seadistatud minAvailable: 2, Kubernetes ei luba pod'i eemaldada.
Kokkuvõtteks: et pod'ide liigutamine (tegelikult nende taastamine) klastris töötaks õigesti, tuleb seadistada PodDisruptionBudget.
Horiontaalne skaleerimine
Vaatame teist olukorda. On rakendus, mis on käivitatud kui Deployment Kuberneteses. Selle pod'ides (näiteks kolme) on kasutajaliiklus ja me mõõdame seal mingit mõõdikku (ütleme, CPU-koormust). Kui koormus suureneb, paneme seda graafikult tähele ja suurendame pod'ide arvu, et jagada päringuid.
Tänapäeval ei pea Kuberneteses seda käsitsi tegema: seadistatakse automaatne pod'ide arvu suurendamine/vähenemine sõltuvalt mõõdetud koormuse väärtustest.

Siin on peamised küsimused, mida täpselt mõõta ja kuidas tõlgendada saadud väärtused (otsustamiseks, et muuta pod'ide arvu). Saame mõõta väga palju:

Kuidas seda tehniliselt teha — mõõtmete kogumine jne — olen ma detailselt kirjeldanud oma ettekandes . Peamine soovitus optimaalseid parameetreid valides on katsetage!
On (Kasutuse Saturatsioon ja Vead), mille mõte on järgmine. Millal on mõistlik skaleerida, näiteks php-fpm? Tõenduseks on see, et töörühmad ei lõppe, — see on kasutamine. Ja kui töötajad on otsas ja uusi ühendusi ei võeta vastu — siis on see juba saturatsioon. Mõlemat parameetrit tuleb mõõta ning väärtuste põhjal tuleks skaleerimisega edasi minna.
Kokkuvõtte asemel
Ettekandel on jätk: vertikaalsest skaleerimisest ja sellest, kuidas õigesti ressursse valida. Räägin sellest tulevastes videotes — tellige, et mitte midagi maha magada!
Video ja slaidid
Esitluse video (44 minutit):

Ettekande esitlus:
P.S.
Teised ettekanded Kubernetesest meie blogis:
- «» (Andrei Polovov; 8. aprill 2019 Saint HighLoad++);
- «» (Dmitri Stolyarov; 8. november 2018 HighLoad++);
- «» (Dmitri Stolyarov; 28. mai 2018 RootConf'is);
- «» (Dmitri Stolyarov; 7. november 2017 HighLoad++);
- «» (Dmitri Stolyarov; 6. juuni 2017 RootConf'il).
Allikas: habr.com
