One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Aloha, oameni buni! Numele meu este Oleg Anastasyev, lucrez la Odnoklassniki în echipa Platformei. Și în afară de mine, există o mulțime de hardware care funcționează în Odnoklassniki. Avem patru centre de date cu aproximativ 500 de rafturi cu peste 8 mii de servere. La un moment dat, ne-am dat seama că introducerea unui nou sistem de management ne va permite să încărcăm echipamentele mai eficient, să facilităm gestionarea accesului, să automatizăm (re)distribuirea resurselor de calcul, să grăbim lansarea de noi servicii și să accelerăm răspunsurile. la accidente de amploare.

Ce a venit din asta?

Pe lângă mine și o grămadă de hardware, mai sunt și oameni care lucrează cu acest hardware: ingineri care sunt localizați direct în centrele de date; rețelei care instalează software de rețea; administratori, sau SRE, care asigură reziliența infrastructurii; și echipele de dezvoltare, fiecare dintre ele este responsabil pentru o parte din funcțiile portalului. Software-ul creat de ei funcționează cam așa:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Solicitările utilizatorilor sunt primite atât pe fronturile portalului principal www.ok.ru, și pe altele, de exemplu pe fronturile API muzicale. Pentru a procesa logica afacerii, ei apelează serverul de aplicații, care, la procesarea cererii, apelează microserviciile specializate necesare - one-graph (graficul conexiunilor sociale), user-cache (cache-ul profilurilor de utilizator) etc.

Fiecare dintre aceste servicii este implementat pe multe mașini, iar fiecare dintre ele are dezvoltatori responsabili responsabili de funcționarea modulelor, funcționarea acestora și dezvoltarea tehnologică. Toate aceste servicii rulează pe servere hardware, iar până de curând lansam exact o sarcină pe server, adică era specializată pentru o anumită sarcină.

De ce este asta? Această abordare a avut mai multe avantaje:

  • Ușurat managementul de masă. Să presupunem că o sarcină necesită niște biblioteci, niște setări. Și apoi serverul este atribuit exact unui grup specific, politica cfengine pentru acest grup este descrisă (sau a fost deja descrisă), iar această configurație este implementată central și automat la toate serverele din acest grup.
  • Simplificat diagnosticare. Să presupunem că vă uitați la sarcina crescută pe procesorul central și vă dați seama că această sarcină ar putea fi generată doar de sarcina care rulează pe acest procesor hardware. Căutarea cuiva pe care să-l vină se termină foarte repede.
  • Simplificat monitorizarea. Dacă ceva nu este în regulă cu serverul, monitorul îl raportează și știi exact cine este de vină.

Un serviciu format din mai multe replici îi sunt alocate mai multe servere - câte unul pentru fiecare. Apoi resursa de calcul pentru serviciu este alocată foarte simplu: numărul de servere pe care le are serviciul, cantitatea maximă de resurse pe care o poate consuma. „Ușor” aici nu înseamnă că este ușor de utilizat, ci în sensul că alocarea resurselor se face manual.

Această abordare ne-a permis să facem configurații de fier specializate pentru o sarcină care rulează pe acest server. Dacă sarcina stochează cantități mari de date, atunci folosim un server 4U cu un șasiu cu 38 de discuri. Dacă sarcina este pur computațională, atunci putem cumpăra un server 1U mai ieftin. Acest lucru este eficient din punct de vedere computațional. Printre altele, această abordare ne permite să folosim de patru ori mai puține mașini cu o sarcină comparabilă cu o rețea socială prietenoasă.

O astfel de eficiență în utilizarea resurselor de calcul ar trebui să asigure și eficiența economică, dacă pornim de la premisa că cel mai scump lucru sunt serverele. Multă vreme, hardware-ul a fost cel mai scump și am depus mult efort pentru a reduce prețul hardware-ului, venind cu algoritmi de toleranță la erori pentru a reduce cerințele de fiabilitate hardware. Și astăzi am ajuns la stadiul în care prețul serverului a încetat să mai fie decisiv. Dacă nu luați în considerare cele mai recente exotice, atunci configurația specifică a serverelor din rack nu contează. Acum avem o altă problemă - prețul spațiului ocupat de server în centrul de date, adică spațiul din rack.

Dându-și seama că acesta este cazul, am decis să calculăm cât de eficient folosim rafturile.
Am luat prețul celui mai puternic server din cele justificabile din punct de vedere economic, am calculat câte astfel de servere am putea plasa în rafturi, câte sarcini am rula pe ele pe baza vechiului model „un server = o sarcină” și câte astfel de servere. sarcinile ar putea utiliza echipamentul. Au numărat și au vărsat lacrimi. S-a dovedit că eficiența noastră în utilizarea rafturilor este de aproximativ 11%. Concluzia este evidentă: trebuie să creștem eficiența utilizării centrelor de date. S-ar părea că soluția este evidentă: trebuie să rulați mai multe sarcini pe un server deodată. Dar de aici încep dificultățile.

Configurarea în masă devine dramatic mai complicată - acum este imposibil să atribuiți un grup unui server. La urma urmei, acum mai multe sarcini de comenzi diferite pot fi lansate pe un server. În plus, configurația poate fi conflictuală pentru diferite aplicații. Diagnosticarea devine, de asemenea, mai complicată: dacă observați un consum crescut de CPU sau de disc pe un server, nu știți care sarcină cauzează probleme.

Dar principalul lucru este că nu există nicio izolare între sarcinile care rulează pe aceeași mașină. Iată, de exemplu, un grafic al timpului mediu de răspuns al unei sarcini de server înainte și după lansarea unei alte aplicații de calcul pe același server, în niciun fel legat de prima - timpul de răspuns al sarcinii principale a crescut semnificativ.

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Evident, trebuie să rulați sarcini fie în containere, fie în mașini virtuale. Deoarece aproape toate sarcinile noastre rulează sub un singur sistem de operare (Linux) sau sunt adaptate pentru acesta, nu este nevoie să acceptăm multe sisteme de operare diferite. În consecință, virtualizarea nu este necesară; din cauza supraîncărcării suplimentare, va fi mai puțin eficientă decât containerizarea.

Ca implementare a containerelor pentru rularea sarcinilor direct pe servere, Docker este un bun candidat: imaginile sistemului de fișiere rezolvă bine problemele cu configurațiile conflictuale. Faptul că imaginile pot fi compuse din mai multe straturi ne permite să reducem semnificativ cantitatea de date necesară pentru a le implementa pe infrastructură, separând părțile comune în straturi de bază separate. Apoi, straturile de bază (și cele mai voluminoase) vor fi stocate în cache destul de rapid în întreaga infrastructură și pentru a furniza multe tipuri diferite de aplicații și versiuni, vor trebui transferate doar straturi mici.

În plus, un registru gata făcut și etichetarea imaginilor în Docker ne oferă primitive gata făcute pentru versiunea și livrarea codului în producție.

Docker, ca orice altă tehnologie similară, ne oferă un anumit nivel de izolare a containerului din cutie. De exemplu, izolarea memoriei - fiecărui container îi este dată o limită de utilizare a memoriei mașinii, dincolo de care nu se va consuma. De asemenea, puteți izola containerele în funcție de utilizarea procesorului. Pentru noi, însă, izolația standard nu a fost suficientă. Dar mai multe despre asta mai jos.

Rularea directă a containerelor pe servere este doar o parte a problemei. Cealaltă parte este legată de găzduirea containerelor pe servere. Trebuie să înțelegeți ce container poate fi plasat pe ce server. Aceasta nu este o sarcină atât de ușoară, deoarece containerele trebuie plasate pe servere cât mai dens posibil, fără a le reduce viteza. O astfel de plasare poate fi dificilă și din punct de vedere al toleranței la erori. Adesea dorim să plasăm replici ale aceluiași serviciu în rafturi diferite sau chiar în camere diferite ale centrului de date, astfel încât, dacă un rack sau o cameră eșuează, să nu pierdem imediat toate replicile serviciului.

Distribuirea manuală a containerelor nu este o opțiune când aveți 8 mii de servere și 8-16 mii de containere.

În plus, am dorit să oferim dezvoltatorilor mai multă independență în alocarea resurselor, astfel încât să-și poată găzdui ei înșiși serviciile în producție, fără ajutorul unui administrator. În același timp, am vrut să menținem controlul, astfel încât un serviciu minor să nu consume toate resursele centrelor noastre de date.

Evident, avem nevoie de un strat de control care să facă acest lucru automat.

Așa că am ajuns la o imagine simplă și de înțeles pe care toți arhitecții o adoră: trei pătrate.

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

one-cloud masters este un cluster de failover responsabil pentru orchestrarea cloud. Dezvoltatorul trimite un manifest către master, care conține toate informațiile necesare găzduirii serviciului. Pe baza acestuia, maestrul dă comenzi minionilor selectați (mașini concepute pentru a rula containere). Minionii au agentul nostru, care primește comanda, își lansează comenzile către Docker, iar Docker configurează nucleul Linux pentru a lansa containerul corespunzător. În plus față de executarea comenzilor, agentul raportează în mod continuu maestrului despre modificările stării atât a mașinii minion, cât și a containerelor care rulează pe ea.

Alocare resurselor

Acum să ne uităm la problema alocării mai complexe a resurselor pentru mulți slujitori.

O resursă de calcul într-un singur cloud este:

  • Cantitatea de putere a procesorului consumată de o anumită sarcină.
  • Cantitatea de memorie disponibilă pentru sarcină.
  • Trafic de rețea. Fiecare dintre minioni are o interfață de rețea specifică cu lățime de bandă limitată, astfel încât este imposibil să distribuiți sarcini fără a ține cont de cantitatea de date pe care o transmit prin rețea.
  • Discuri. Pe lângă spațiul pentru aceste sarcini, evident, mai alocăm și tipul de disc: HDD sau SSD. Discurile pot servi un număr finit de solicitări pe secundă - IOPS. Prin urmare, pentru sarcinile care generează mai multe IOPS decât poate gestiona un singur disc, alocam și „spindles” - adică dispozitive de disc care trebuie rezervate exclusiv pentru sarcină.

Apoi, pentru un serviciu, de exemplu pentru user-cache, putem înregistra resursele consumate în acest fel: 400 de nuclee de procesor, 2,5 TB de memorie, 50 Gbit/s trafic în ambele sensuri, 6 TB de spațiu HDD situat pe 100 de axe. Sau într-o formă mai familiară ca aceasta:

alloc:
    cpu: 400
    mem: 2500
    lan_in: 50g
    lan_out: 50g
    hdd:100x6T

Resursele serviciului cache al utilizatorului consumă doar o parte din toate resursele disponibile în infrastructura de producție. Prin urmare, vreau să mă asigur că brusc, din cauza unei erori de operator sau nu, cache-ul utilizatorului nu consumă mai multe resurse decât îi sunt alocate. Adică trebuie să limităm resursele. Dar de ce am putea lega cota?

Să revenim la diagrama noastră foarte simplificată a interacțiunii componentelor și să o redesenăm cu mai multe detalii - astfel:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Ce atrage privirea:

  • Interfața web și muzica utilizează clustere izolate ale aceluiași server de aplicații.
  • Putem distinge straturile logice cărora le aparțin aceste clustere: fronturi, cache, stocare de date și strat de management.
  • Interfața este eterogenă; constă din diferite subsisteme funcționale.
  • Cache-urile pot fi, de asemenea, împrăștiate în subsistemul ale cărui date le memorează.

Să redesenăm din nou imaginea:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Bah! Da, vedem o ierarhie! Aceasta înseamnă că puteți distribui resursele în bucăți mai mari: atribuiți un dezvoltator responsabil unui nod al acestei ierarhii corespunzător subsistemului funcțional (cum ar fi „muzică” din imagine) și atașați o cotă la același nivel al ierarhiei. Această ierarhie ne permite, de asemenea, să organizăm serviciile într-un mod mai flexibil pentru o gestionare ușoară. De exemplu, împărțim tot web-ul, deoarece aceasta este o grupare foarte mare de servere, în câteva grupuri mai mici, prezentate în imagine ca grup1, grup2.

Prin eliminarea liniilor suplimentare, putem scrie fiecare nod al imaginii noastre într-o formă mai plată: grup1.web.front, api.music.front, user-cache.cache.

Așa ajungem la conceptul de „coadă ierarhică”. Are un nume ca „group1.web.front”. I se atribuie o cotă pentru resurse și drepturi de utilizator. Vom acorda persoanei de la DevOps drepturile de a trimite un serviciu la coadă, iar un astfel de angajat poate lansa ceva în coadă, iar persoana de la OpsDev va avea drepturi de administrator, iar acum poate gestiona coada, poate atribui oameni acolo, acordați acestor persoane drepturi etc. Serviciile care rulează în această coadă vor rula în limita cotei de așteptare. Dacă cota de calcul a cozii nu este suficientă pentru a executa toate serviciile simultan, atunci acestea vor fi executate secvenţial, formând astfel coada în sine.

Să aruncăm o privire mai atentă asupra serviciilor. Un serviciu are un nume complet calificat, care include întotdeauna numele cozii. Apoi, serviciul web frontal va avea numele ok-web.group1.web.front. Și serviciul server de aplicații pe care îl accesează va fi apelat ok-app.group1.web.front. Fiecare serviciu are un manifest, care specifică toate informațiile necesare pentru plasarea pe anumite mașini: câte resurse consumă această sarcină, ce configurație este necesară pentru aceasta, câte replici ar trebui să existe, proprietăți pentru gestionarea defecțiunilor acestui serviciu. Și după ce serviciul este plasat direct pe mașini, apar instanțele sale. Ele sunt, de asemenea, denumite fără ambiguitate - ca număr de instanță și nume de serviciu: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …

Acest lucru este foarte convenabil: uitându-ne doar la numele containerului care rulează, putem afla imediat multe.

Acum să aruncăm o privire mai atentă la ceea ce efectuează de fapt aceste instanțe: sarcini.

Clasele de izolare a sarcinilor

Toate sarcinile din OK (și, probabil, peste tot) pot fi împărțite în grupuri:

  • Sarcini cu latență scurtă - prod. Pentru astfel de sarcini și servicii, întârzierea răspunsului (latența) este foarte importantă, cât de repede va fi procesată fiecare dintre cereri de către sistem. Exemple de sarcini: fronturi web, cache, servere de aplicații, stocare OLTP etc.
  • Probleme de calcul - lot. Aici, viteza de procesare a fiecărei cereri specifice nu este importantă. Pentru ei, este important câte calcule va face această sarcină într-o anumită perioadă de timp (debit). Acestea vor fi orice sarcini ale MapReduce, Hadoop, machine learning, statistici.
  • Sarcini de fundal - inactiv. Pentru astfel de sarcini, nici latența și nici debitul nu sunt foarte importante. Aceasta include diverse teste, migrări, recalculări și conversie a datelor dintr-un format în altul. Pe de o parte, sunt similare cu cele calculate, pe de altă parte, nu contează pentru noi cât de repede sunt finalizate.

Să vedem cum astfel de sarcini consumă resurse, de exemplu, procesorul central.

Sarcini cu întârziere scurtă. O astfel de sarcină va avea un model de consum al procesorului similar cu acesta:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

O solicitare de la utilizator este primită pentru procesare, sarcina începe să folosească toate nucleele CPU disponibile, o procesează, returnează un răspuns, așteaptă următoarea solicitare și se oprește. Următoarea cerere a sosit - din nou am ales tot ce era acolo, am calculat-o și o așteptăm pe următoarea.

Pentru a garanta latența minimă pentru o astfel de sarcină, trebuie să luăm resursele maxime pe care le consumă și să rezervăm numărul necesar de nuclee pe minion (mașina care va executa sarcina). Atunci formula de rezervare pentru problema noastră va fi următoarea:

alloc: cpu = 4 (max)

iar dacă avem o mașină minion cu 16 nuclee, atunci exact patru astfel de sarcini pot fi plasate pe ea. Remarcăm în special că consumul mediu al procesorului pentru astfel de sarcini este adesea foarte scăzut - ceea ce este evident, deoarece o parte semnificativă a timpului sarcina așteaptă o solicitare și nu face nimic.

Sarcini de calcul. Modelul lor va fi ușor diferit:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Consumul mediu de resurse CPU pentru astfel de sarcini este destul de mare. Adesea dorim ca o sarcină de calcul să fie finalizată într-o anumită perioadă de timp, așa că trebuie să rezervăm numărul minim de procesoare de care are nevoie, astfel încât întregul calcul să fie finalizat într-un timp acceptabil. Formula sa de rezervare va arăta astfel:

alloc: cpu = [1,*)

„Te rog, așezați-l pe un servitor unde există cel puțin un nucleu liber și apoi câte sunt, va devora totul.”

Aici eficiența utilizării este deja mult mai bună decât la sarcinile cu o scurtă întârziere. Dar câștigul va fi mult mai mare dacă combinați ambele tipuri de sarcini pe o singură mașină minion și îi distribuiți resursele din mers. Când o sarcină cu o întârziere scurtă necesită un procesor, acesta îl primește imediat, iar când resursele nu mai sunt necesare, acestea sunt transferate la sarcina de calcul, adică ceva de genul acesta:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Dar cum să o faci?

Mai întâi, să ne uităm la prod și alocul său: cpu = 4. Trebuie să rezervăm patru nuclee. În rularea Docker, acest lucru se poate face în două moduri:

  • Folosind opțiunea --cpuset=1-4, adică alocați patru nuclee specifice pe mașină sarcinii.
  • Использовать --cpuquota=400_000 --cpuperiod=100_000, atribuiți o cotă pentru timpul procesorului, adică indicați că la fiecare 100 ms de timp real sarcina consumă nu mai mult de 400 ms de timp de procesor. Se obțin aceleași patru nuclee.

Dar care dintre aceste metode este potrivită?

cpuset arată destul de atractiv. Sarcina are patru nuclee dedicate, ceea ce înseamnă că memoria cache a procesorului va funcționa cât mai eficient posibil. Acest lucru are, de asemenea, un dezavantaj: ar trebui să ne asumăm sarcina de a distribui calculele între nucleele descărcate ale mașinii în loc de sistemul de operare, iar aceasta este o sarcină destul de netrivială, mai ales dacă încercăm să plasăm sarcini pe lot pe un astfel de mașinărie. Testele au arătat că opțiunea cu cotă este mai potrivită aici: astfel sistemul de operare are mai multă libertate în alegerea nucleului pentru a efectua sarcina în momentul actual și timpul procesorului este distribuit mai eficient.

Să ne dăm seama cum să facem rezervări în Docker pe baza numărului minim de nuclee. Cota pentru sarcini de lot nu mai este aplicabilă, deoarece nu este nevoie să se limiteze maximul, este suficient să se garanteze doar minimul. Și aici opțiunea se potrivește bine docker run --cpushares.

Am convenit că, dacă un lot necesită o garanție pentru cel puțin un miez, atunci indicăm --cpushares=1024, iar dacă există cel puțin două nuclee, atunci indicăm --cpushares=2048. Acțiunile CPU nu interferează în niciun fel cu distribuția timpului procesorului atâta timp cât este suficient. Astfel, dacă prod nu folosește în prezent toate cele patru nuclee ale sale, nu există nimic care să limiteze sarcinile batch și pot folosi timp suplimentar de procesor. Dar într-o situație în care există o lipsă de procesoare, dacă prod și-a consumat toate cele patru nuclee și și-a atins cota, timpul rămas de procesor va fi împărțit proporțional cu cpushares, adică în situația de trei nuclee libere, unul va fi dat unei sarcini cu 1024 cpushares, iar restul de două vor fi date unei sarcini cu 2048 cpushares.

Dar folosirea cotelor și a acțiunilor nu este suficientă. Trebuie să ne asigurăm că o sarcină cu o întârziere scurtă primește prioritate față de o sarcină batch atunci când alocăm timpul procesorului. Fără o astfel de prioritizare, sarcina batch va ocupa tot timpul procesorului în momentul în care este nevoie de produs. Nu există opțiuni de prioritizare a containerelor în rularea Docker, dar politicile de planificare CPU Linux sunt utile. Puteți citi despre ele în detaliu aici, iar în cadrul acestui articol le vom parcurge pe scurt:

  • SCHED_OTHER
    În mod implicit, toate procesele normale ale utilizatorului de pe o mașină Linux primesc.
  • SCHED_BATCH
    Proiectat pentru procese care necesită resurse intensive. Când plasați o sarcină pe un procesor, este introdusă o așa-numită penalizare de activare: o astfel de sarcină este mai puțin probabil să primească resurse procesor dacă este utilizată în prezent de o sarcină cu SCHED_OTHER
  • SCHED_IDLE
    Un proces de fundal cu o prioritate foarte scăzută, chiar mai mică decât frumosul -19. Folosim biblioteca noastră open source one-nio, pentru a seta politica necesară la pornirea containerului prin apelare

one.nio.os.Proc.sched_setscheduler( pid, Proc.SCHED_IDLE )

Dar chiar dacă nu programați în Java, același lucru poate fi făcut folosind comanda chrt:

chrt -i 0 $pid

Să rezumam toate nivelurile noastre de izolare într-un singur tabel pentru claritate:

Clasa de izolare
Exemplu de aloc
Opțiuni de rulare Docker
sched_setscheduler chrt*

Prod
CPU = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

Lot
CPU = [1, *)
--cpushares=1024
SCHED_BATCH

Idle
CPU= [2, *)
--cpushares=2048
SCHED_IDLE

*Dacă faceți chrt din interiorul unui container, este posibil să aveți nevoie de capacitatea sys_nice, deoarece Docker elimină în mod implicit această capacitate la pornirea containerului.

Dar sarcinile consumă nu numai procesorul, ci și traficul, ceea ce afectează latența unei sarcini de rețea chiar mai mult decât alocarea incorectă a resurselor procesorului. Prin urmare, dorim în mod natural să obținem exact aceeași imagine pentru trafic. Adică, atunci când o sarcină prod trimite niște pachete în rețea, limităm viteza maximă (formula alloc: lan=[*,500mbps) ), cu care prod poate face acest lucru. Și pentru lot, garantăm doar debitul minim, dar nu limităm cel maxim (formula alloc: lan=[10Mbps,*) ) În acest caz, traficul de produse ar trebui să primească prioritate față de sarcinile lot.
Aici Docker nu are nicio primitivă pe care să le putem folosi. Dar ne vine în ajutor Controlul traficului Linux. Am reușit să obținem rezultatul dorit cu ajutorul disciplinei Curba Serviciului Echitabil Ierarhic. Cu ajutorul acestuia, distingem două clase de trafic: prod cu prioritate mare și lot/inactiv cu prioritate scăzută. Ca urmare, configurația pentru traficul de ieșire este astfel:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

aici 1:0 este „rădăcina qdisc” al disciplinei hsfc; 1:1 - clasă copil hsfc cu o limită de lățime de bandă totală de 8 Gbit/s, sub care sunt plasate clasele copil ale tuturor containerelor; 1:2 - clasa copil hsfc este comună pentru toate sarcinile batch și inactive cu o limită „dinamică”, care este discutată mai jos. Celelalte clase copil hsfc sunt clase dedicate pentru containerele prod care rulează în prezent cu limite corespunzătoare manifestelor lor - 450 și 400 Mbit/s. Fiecărei clase hsfc i se atribuie o coadă qdisc fq sau fq_codel, în funcție de versiunea nucleului Linux, pentru a evita pierderea pachetelor în timpul exploziilor de trafic.

De obicei, disciplinele tc servesc la prioritizarea numai a traficului de ieșire. Dar vrem să acordăm prioritate și traficului de intrare - la urma urmei, unele sarcini în lot pot selecta cu ușurință întregul canal de intrare, primind, de exemplu, un lot mare de date de intrare pentru map&reduce. Pentru aceasta folosim modulul ifb, care creează o interfață virtuală ifbX pentru fiecare interfață de rețea și redirecționează traficul de intrare de la interfață către traficul de ieșire pe ifbX. În plus, pentru ifbX, toate aceleași discipline funcționează pentru a controla traficul de ieșire, pentru care configurația hsfc va fi foarte asemănătoare:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

În timpul experimentelor, am descoperit că hsfc arată cele mai bune rezultate atunci când clasa 1:2 de trafic neprioritar/inactiv este limitată pe mașinile minion la nu mai mult de o anumită bandă liberă. În caz contrar, traficul neprioritar are un impact prea mare asupra latenței sarcinilor de producție. miniond determină cantitatea actuală de lățime de bandă liberă în fiecare secundă, măsurând consumul mediu de trafic al tuturor sarcinilor de producție ale unui minion dat One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki și scăzând-o din lățimea de bandă a interfeței de rețea One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki cu o marjă mică, adică

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Benzile sunt definite independent pentru traficul de intrare și de ieșire. Și în funcție de noile valori, miniond reconfigurează limita de clasă neprioritară 1:2.

Astfel, am implementat toate cele trei clase de izolare: prod, batch și idle. Aceste clase influențează foarte mult caracteristicile de performanță ale sarcinilor. Prin urmare, am decis să plasăm acest atribut în vârful ierarhiei, astfel încât atunci când ne uităm la numele cozii ierarhice să fie imediat clar cu ce avem de-a face:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Toți prietenii noștri web и muzică fronturile sunt apoi plasate în ierarhie sub prod. De exemplu, în lot, să plasăm serviciul catalog muzical, care alcătuiește periodic un catalog de melodii dintr-un set de fișiere mp3 încărcate pe Odnoklassniki. Un exemplu de serviciu sub inactiv ar fi transformator muzical, care normalizează nivelul volumului muzicii.

Cu liniile suplimentare eliminate din nou, putem scrie numele serviciilor noastre mai flatate adăugând clasa de izolare a sarcinilor la sfârșitul numelui complet al serviciului: web.front.prod, catalog.muzică.lot, transformator.muzică.inactiv.

Și acum, privind numele serviciului, înțelegem nu numai ce funcție îndeplinește, ci și clasa sa de izolare, ceea ce înseamnă criticitatea sa etc.

Totul este grozav, dar există un adevăr amar. Este imposibil să izolați complet sarcinile care rulează pe o singură mașină.

Ce am reușit să obținem: dacă lotul consumă intens numai Resursele CPU, apoi planificatorul CPU Linux încorporat își face treaba foarte bine și practic nu are niciun impact asupra sarcinii de producție. Dar dacă această sarcină în lot începe să funcționeze activ cu memoria, atunci influența reciprocă apare deja. Acest lucru se întâmplă deoarece sarcina de prod este „spălată” din memoria cache-urilor procesorului - ca urmare, ratarile de cache cresc, iar procesorul procesează sarcina de prod mai lent. O astfel de sarcină de lot poate crește latența containerului nostru tipic pentru produse cu 10%.

Izolarea traficului este și mai dificilă datorită faptului că plăcile de rețea moderne au o coadă internă de pachete. Dacă pachetul de la sarcina batch ajunge primul acolo, atunci va fi primul care va fi transmis prin cablu și nu se poate face nimic în acest sens.

În plus, până acum am reușit doar să rezolvăm problema prioritizării traficului TCP: abordarea hsfc nu funcționează pentru UDP. Și chiar și în cazul traficului TCP, dacă sarcina batch generează mult trafic, acest lucru oferă și o creștere cu aproximativ 10% a întârzierii sarcinii prod.

toleranta la greseli

Unul dintre obiectivele dezvoltării unui singur nor a fost acela de a îmbunătăți toleranța la greșeală a lui Odnoklassniki. Prin urmare, în continuare aș dori să analizez mai detaliat posibilele scenarii de defecțiuni și accidente. Să începem cu un scenariu simplu - o defecțiune a containerului.

Containerul în sine poate eșua în mai multe moduri. Acesta ar putea fi un fel de experiment, eroare sau eroare în manifest, din cauza căruia sarcina de producție începe să consume mai multe resurse decât este indicat în manifest. Am avut un caz: un dezvoltator a implementat un algoritm complex, l-a reluat de multe ori, s-a gândit și a devenit atât de confuz încât, în cele din urmă, problema a fost reluată într-un mod foarte netrivial. Și deoarece sarcina de producție are o prioritate mai mare decât toate celelalte pe aceiași servitori, a început să consume toate resursele de procesor disponibile. În această situație, izolarea, sau mai degrabă cota de timp CPU, a salvat ziua. Dacă unei sarcini i se aloca o cotă, sarcina nu va consuma mai mult. Prin urmare, sarcinile lot și alte produse care rulau pe aceeași mașină nu au observat nimic.

A doua posibilă problemă este căderea containerului. Și aici politicile de repornire ne salvează, toată lumea le cunoaște, Docker însuși face o treabă grozavă. Aproape toate sarcinile de producție au o politică de repornire întotdeauna. Uneori folosim on_failure pentru sarcini pe lot sau pentru depanarea containerelor de produse.

Ce poți face dacă un minion întreg nu este disponibil?

Evident, rulați containerul pe o altă mașină. Partea interesantă aici este ceea ce se întâmplă cu adresele IP atribuite containerului.

Putem atribui containerelor aceleași adrese IP ca și mașinile minion pe care rulează aceste containere. Apoi, când containerul este lansat pe o altă mașină, adresa sa IP se schimbă și toți clienții trebuie să înțeleagă că containerul s-a mutat, iar acum trebuie să meargă la o altă adresă, care necesită un serviciu separat de Descoperire a serviciilor.

Service Discovery este convenabil. Există multe soluții pe piață cu diferite grade de toleranță la erori pentru organizarea unui registru de servicii. Adesea, astfel de soluții implementează logica de echilibrare a sarcinii, stochează configurații suplimentare sub formă de stocare KV etc.
Cu toate acestea, am dori să evităm necesitatea implementării unui registru separat, deoarece aceasta ar însemna introducerea unui sistem critic care este utilizat de toate serviciile din producție. Aceasta înseamnă că acesta este un potențial punct de eșec și trebuie să alegeți sau să dezvoltați o soluție foarte tolerantă la erori, care este, evident, foarte dificilă, consumatoare de timp și costisitoare.

Și încă un mare dezavantaj: pentru ca vechea noastră infrastructură să funcționeze cu cea nouă, ar trebui să rescriem absolut toate sarcinile pentru a folosi un fel de sistem Service Discovery. Există MULTE de lucru și, în unele locuri, este aproape imposibil când vine vorba de dispozitive de nivel scăzut care funcționează la nivelul nucleului sistemului de operare sau direct cu hardware-ul. Implementarea acestei funcționalități folosind modele de soluții stabilite, cum ar fi side-car ar însemna în unele locuri o sarcină suplimentară, în altele - o complicație a funcționării și scenarii suplimentare de defecțiune. Nu am vrut să complicăm lucrurile, așa că am decis să facem opțională utilizarea Service Discovery.

Într-un singur nor, IP-ul urmează containerul, adică fiecare instanță de activitate are propria sa adresă IP. Această adresă este „statică”: este atribuită fiecărei instanțe atunci când serviciul este trimis pentru prima dată în cloud. Dacă un serviciu a avut un număr diferit de instanțe pe parcursul vieții sale, atunci în final i se vor atribui atâtea adrese IP câte instanțe au existat.

Ulterior, aceste adrese nu se schimbă: sunt atribuite o singură dată și continuă să existe pe toată durata de viață a serviciului în producție. Adresele IP urmează containerele din rețea. Dacă containerul este transferat către un alt minion, atunci adresa îl va urma.

Astfel, maparea unui nume de serviciu la lista sa de adrese IP se modifică foarte rar. Dacă te uiți din nou la numele instanțelor de serviciu pe care le-am menționat la începutul articolului (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …), vom observa că acestea seamănă cu FQDN-urile utilizate în DNS. Așa este, pentru a mapa numele instanțelor de serviciu la adresele lor IP, folosim protocolul DNS. Mai mult, acest DNS returnează toate adresele IP rezervate ale tuturor containerelor - atât în ​​rulare, cât și oprite (să spunem că sunt folosite trei replici și avem cinci adrese rezervate acolo - toate cele cinci vor fi returnate). Clienții, după ce au primit aceste informații, vor încerca să stabilească o conexiune cu toate cele cinci replici - și astfel să le determine pe cele care funcționează. Această opțiune pentru determinarea disponibilității este mult mai fiabilă; nu implică nici DNS, nici Service Discovery, ceea ce înseamnă că nu există probleme dificile de rezolvat în asigurarea relevanței informațiilor și a toleranței la erori a acestor sisteme. Mai mult, în serviciile critice de care depinde funcționarea întregului portal, nu putem folosi deloc DNS, ci pur și simplu introducem adrese IP în configurație.

Implementarea unui astfel de transfer IP în spatele containerelor poate fi netrivială - și ne vom uita la modul în care funcționează cu următorul exemplu:

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Să presupunem că masterul one-cloud dă comanda minionului M1 să ruleze 1.ok-web.group1.web.front.prod cu adresa 1.1.1.1. Lucrează la un minion BIRD, care face publicitate acestei adrese către servere speciale reflector de traseu. Acestea din urmă au o sesiune BGP cu hardware-ul de rețea, în care este tradus ruta adresei 1.1.1.1 pe M1. M1 direcționează pachetele în interiorul containerului folosind Linux. Există trei servere reflectoare de rută, deoarece aceasta este o parte foarte critică a infrastructurii one-cloud - fără ele, rețeaua în one-cloud nu va funcționa. Le plasăm în rafturi diferite, dacă este posibil amplasate în camere diferite ale centrului de date, pentru a reduce probabilitatea ca toate trei să se defecteze în același timp.

Să presupunem acum că conexiunea dintre masterul cu un singur nor și minionul M1 este pierdută. Master-ul cu un singur nor va acționa acum presupunând că M1 a eșuat complet. Adică va da comanda minionului M2 să se lanseze web.group1.web.front.prod cu aceeași adresă 1.1.1.1. Acum avem două rute conflictuale în rețea pentru 1.1.1.1: pe M1 și pe M2. Pentru a rezolva astfel de conflicte, folosim Multi Exit Discriminator, care este specificat în anunțul BGP. Acesta este un număr care arată greutatea traseului anunțat. Dintre rutele aflate în conflict, va fi selectată ruta cu valoarea MED mai mică. Master-ul one-cloud acceptă MED ca parte integrantă a adreselor IP ale containerului. Pentru prima dată, adresa este scrisă cu un MED suficient de mare = 1. În situația unui astfel de transfer de container de urgență, comandantul reduce MED, iar M000 va primi deja comanda de a face publicitate adresei 000 cu MED = 2 1.1.1.1. Instanța care rulează pe M999 va rămâne la în acest caz nu există nicio legătură, iar soarta lui ulterioară ne interesează puțin până la restabilirea conexiunii cu comandantul, când va fi oprit ca o ia veche.

accidente

Toate sistemele de management al centrelor de date gestionează întotdeauna defecțiunile minore în mod acceptabil. Debordarea containerului este o normă aproape peste tot.

Să ne uităm la modul în care gestionăm o urgență, cum ar fi o pană de curent în una sau mai multe camere ale unui centru de date.

Ce înseamnă un accident pentru un sistem de management al unui centru de date? În primul rând, aceasta este o defecțiune masivă unică a multor mașini, iar sistemul de control trebuie să migreze o mulțime de containere în același timp. Dar dacă dezastrul este la scară foarte mare, atunci se poate întâmpla ca toate sarcinile să nu poată fi realocate altor minioni, deoarece capacitatea de resurse a centrului de date scade sub 100% din sarcină.

Adesea accidentele sunt însoțite de defecțiunea stratului de control. Acest lucru se poate întâmpla din cauza defecțiunii echipamentului său, dar mai des din cauza faptului că accidentele nu sunt testate, iar stratul de control însuși cade din cauza sarcinii crescute.

Ce poți face cu toate acestea?

Migrațiile în masă înseamnă că există un număr mare de activități, migrări și implementări în infrastructură. Fiecare dintre migrări poate dura ceva timp necesar pentru a livra și despacheta imaginile containerelor către minions, lansarea și inițializarea containerelor etc. Prin urmare, este de dorit ca sarcinile mai importante să fie lansate înaintea celor mai puțin importante.

Să ne uităm din nou la ierarhia serviciilor cu care suntem familiarizați și să încercăm să decidem ce sarcini dorim să rulăm mai întâi.

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Desigur, acestea sunt procesele care sunt direct implicate în procesarea cererilor utilizatorilor, adică prod. Indicăm acest lucru cu prioritate de plasare — un număr care poate fi alocat la coadă. Dacă o coadă are o prioritate mai mare, serviciile sale sunt plasate pe primul loc.

Pe prod atribuim prioritati mai mari, 0; pe lot - puțin mai jos, 100; la inactiv - chiar mai mic, 200. Prioritățile sunt aplicate ierarhic. Toate sarcinile de mai jos în ierarhie vor avea o prioritate corespunzătoare. Dacă dorim ca cache-urile din interiorul prod să fie lansate înainte de front-end-uri, atunci atribuim priorități cache = 0 și subcozilor frontale = 1. Dacă, de exemplu, dorim ca portalul principal să fie lansat mai întâi de pe fronturi și doar frontul muzical apoi, atunci îi putem atribui o prioritate mai mică celui din urmă - 10.

Următoarea problemă este lipsa resurselor. Așadar, o cantitate mare de echipamente, hale întregi ale centrului de date, au eșuat și am relansat atât de multe servicii încât acum nu mai sunt suficiente resurse pentru toată lumea. Trebuie să decideți ce sarcini să sacrificați pentru a menține în funcțiune principalele servicii critice.

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Spre deosebire de prioritatea de plasare, nu putem sacrifica fără discriminare toate sarcinile lot; unele dintre ele sunt importante pentru funcționarea portalului. Prin urmare, am evidențiat separat prioritate de preempțiune sarcini. Când este plasată, o sarcină cu prioritate mai mare poate anticipa, adică opri, o sarcină cu prioritate mai mică dacă nu mai există slujitori liberi. În acest caz, o sarcină cu o prioritate scăzută va rămâne probabil neplasată, adică nu va mai exista un servitor potrivit pentru ea cu suficiente resurse gratuite.

În ierarhia noastră, este foarte simplu să specificați o prioritate de preempțiune, astfel încât sarcinile prod și batch să preempționeze sau să oprească sarcinile inactive, dar nu reciproc, prin specificarea unei priorități pentru idle egală cu 200. La fel ca în cazul priorității de plasare, vom poate folosi ierarhia noastră pentru a descrie reguli mai complexe. De exemplu, să indicam că sacrificăm funcția de muzică dacă nu avem suficiente resurse pentru portalul web principal, setând prioritatea pentru nodurile corespunzătoare mai jos: 10.

Accidente întregi DC

De ce ar putea eșua întregul centru de date? Element. A fost o postare buna uraganul a afectat activitatea centrului de date. Elementele pot fi considerate persoane fără adăpost care au ars odată optica din colector, iar centrul de date a pierdut complet contactul cu alte site-uri. Cauza defecțiunii poate fi și un factor uman: operatorul va emite o astfel de comandă încât întregul centru de date va cădea. Acest lucru se poate întâmpla din cauza unei erori mari. În general, colapsul centrelor de date nu este neobișnuit. Ni se întâmplă asta o dată la câteva luni.

Și asta facem pentru a împiedica pe oricine să trimită pe Twitter #alive.

Prima strategie este izolarea. Fiecare instanță dintr-un singur cloud este izolată și poate gestiona mașinile într-un singur centru de date. Adică, pierderea unui nor din cauza erorilor sau a comenzilor incorecte ale operatorului este pierderea unui singur centru de date. Suntem pregătiți pentru asta: avem o politică de redundanță în care replicile aplicației și ale datelor sunt amplasate în toate centrele de date. Folosim baze de date tolerante la erori și testăm periodic erorile.
Deoarece astăzi avem patru centre de date, asta înseamnă patru instanțe separate, complet izolate de un singur nor.

Această abordare nu numai că protejează împotriva defecțiunilor fizice, dar poate proteja și împotriva erorilor operatorului.

Ce altceva se mai poate face cu factorul uman? Când un operator dă norului o comandă ciudată sau potențial periculoasă, i se poate cere brusc să rezolve o mică problemă pentru a vedea cât de bine s-a gândit. De exemplu, dacă acesta este un fel de oprire în masă a multor replici sau doar o comandă ciudată - reducerea numărului de replici sau schimbarea numelui imaginii și nu doar a numărului versiunii din noul manifest.

One-cloud - sistem de operare la nivel de centru de date în Odnoklassniki

Rezultatele

Caracteristici distinctive ale unui singur nor:

  • Schema de denumire ierarhică și vizuală pentru servicii și containere, care vă permite să aflați foarte rapid care este sarcina, cu ce se referă și cum funcționează și cine este responsabil pentru aceasta.
  • Aplicam noastre tehnica de combinare a produselor și a lotuluisarcini asupra minions pentru a îmbunătăți eficiența partajării mașinilor. În loc de cpuset, folosim cote CPU, partajări, politici de planificare CPU și QoS Linux.
  • Nu a fost posibilă izolarea completă a containerelor care rulează pe aceeași mașină, dar influența lor reciprocă rămâne în limita a 20%.
  • Organizarea serviciilor într-o ierarhie ajută la utilizarea automată a recuperării în caz de dezastru priorități de plasare și preempțiune.

Întrebări frecvente

De ce nu am luat o soluție gata făcută?

  • Diferitele clase de izolare a sarcinilor necesită o logică diferită atunci când sunt plasate pe slujitori. Dacă sarcinile de producție pot fi plasate prin simpla rezervare a resurselor, atunci sarcinile batch și inactive trebuie plasate, urmărind utilizarea efectivă a resurselor pe mașinile minion.
  • Necesitatea de a lua în considerare resursele consumate de sarcini, cum ar fi:
    • lățimea de bandă a rețelei;
    • tipuri și „fusuri” de discuri.
  • Necesitatea de a indica prioritățile serviciilor în timpul răspunsului la urgență, drepturile și cotele de comenzi pentru resurse, care este rezolvată folosind cozi ierarhice într-un singur nor.
  • Necesitatea unei denumiri umane a containerelor pentru a reduce timpul de răspuns la accidente și incidente
  • Imposibilitatea unei implementări o singură dată pe scară largă a Service Discovery; necesitatea de a coexista mult timp cu sarcini găzduite pe gazde hardware - ceva ce se rezolvă prin adrese IP „statice” în urma containerelor și, în consecință, nevoia de integrare unică cu o infrastructură mare de rețea.

Toate aceste funcții ar necesita modificări semnificative ale soluțiilor existente pentru a ne potrivi și, după ce am evaluat volumul de muncă, ne-am dat seama că ne-am putea dezvolta propria soluție cu aproximativ aceleași costuri cu forța de muncă. Dar soluția dvs. va fi mult mai ușor de operat și dezvoltat - nu conține abstracții inutile care susțin funcționalități de care nu avem nevoie.

Celor care au citit ultimele rânduri, le mulțumesc pentru răbdare și atenție!

Sursa: www.habr.com

Adauga un comentariu