
Bună, Habr! Sunt Artem Karamyshev, șeful echipei de administrare a sistemului . Am avut multe lansări de produse noi în ultimul an. Am vrut să ne asigurăm că serviciile API sunt ușor scalabile, tolerante la erori și pregătite pentru creșterea rapidă a încărcăturii utilizatorilor. Platforma noastră este implementată pe OpenStack și vreau să vă spun ce probleme de toleranță la erori a componentelor a trebuit să rezolvăm pentru a obține un sistem tolerant la erori. Cred că acest lucru va fi interesant pentru cei care dezvoltă și produse pe OpenStack.
Toleranța generală la defecțiuni a unei platforme constă în rezistența componentelor sale. Deci vom trece treptat prin toate nivelurile în care am identificat riscurile și le-am închis.
Versiunea video a acestei povești, a cărei sursă principală a fost un raport la conferința Uptime ziua 4, organizată de , poti sa vezi .
Reziliența arhitecturii fizice
Partea publică a cloud-ului MCS este acum bazată în două centre de date Tier III, între ele existând propria sa fibră întunecată, rezervată la nivel fizic pe diferite rute, cu un throughput de 200 Gbit/s. Nivelul III oferă nivelul necesar de toleranță la erori pentru infrastructura fizică.
Fibra întunecată este rezervată atât la nivel fizic, cât și la nivel logic. Procesul de rezervare a canalelor a fost iterativ, au apărut probleme și îmbunătățim constant comunicarea între centrele de date.
De exemplu, nu cu mult timp în urmă, în timp ce lucra într-o fântână din apropierea unuia dintre centrele de date, un excavator a spart o țeavă, iar în interiorul acestei țevi erau atât un cablu optic principal, cât și unul de rezervă. Canalul nostru de comunicare tolerant la erori cu centrul de date s-a dovedit a fi vulnerabil la un moment dat, în puț. În consecință, am pierdut o parte din infrastructură. Am tras concluzii și am întreprins o serie de acțiuni, inclusiv instalarea de optice suplimentare în puțul adiacent.
În centrele de date există puncte de prezență a furnizorilor de comunicații cărora le transmitem prefixele prin BGP. Pentru fiecare direcție de rețea, este selectată cea mai bună metrică, ceea ce permite diferiților clienți să li se ofere cea mai bună calitate a conexiunii. Dacă comunicarea prin intermediul unui furnizor scade, ne reconstruim rutarea prin furnizorii disponibili.
Dacă un furnizor eșuează, trecem automat la următorul. În cazul unei defecțiuni a unuia dintre centrele de date, avem o copie în oglindă a serviciilor noastre în al doilea centru de date, care preia întreaga sarcină.

Reziliența infrastructurii fizice
Ce folosim pentru toleranța la erori la nivel de aplicație
Serviciul nostru este construit pe o serie de componente opensource.
ExaBGP este un serviciu care implementează o serie de funcții folosind protocolul de rutare dinamică bazat pe BGP. Îl folosim în mod activ pentru a face publicitate adreselor noastre IP incluse în lista albă prin care utilizatorii accesează API-ul.
HAProxy este un echilibrator de sarcină mare care vă permite să configurați reguli foarte flexibile de echilibrare a traficului la diferite niveluri ale modelului OSI. Îl folosim pentru a echilibra în fața tuturor serviciilor: baze de date, brokeri de mesaje, servicii API, servicii web, proiectele noastre interne - totul se află în spatele HAProxy.
aplicație API — o aplicație web scrisă în python, cu ajutorul căreia utilizatorul își administrează infrastructura și serviciul său.
Aplicație de muncitor (denumit în continuare pur și simplu lucrător) - în serviciile OpenStack, acesta este un daemon de infrastructură care vă permite să difuzați comenzi API către infrastructură. De exemplu, crearea discului are loc în lucrător, iar cererea de creare are loc în API-ul aplicației.
Arhitectura standard de aplicație OpenStack
Majoritatea serviciilor care sunt dezvoltate pentru OpenStack încearcă să urmeze o singură paradigmă. Un serviciu constă de obicei din 2 părți: API și lucrători (executori backend). De regulă, un API este o aplicație WSGI în python, care este lansată fie ca proces independent (daemon), fie folosind un server web Nginx sau Apache gata făcut. API-ul procesează cererea utilizatorului și transmite instrucțiuni suplimentare aplicației de lucru pentru execuție. Transferul are loc folosind un broker de mesaje, de obicei RabbitMQ, celelalte sunt slab acceptate. Când mesajele ajung la broker, acestea sunt procesate de lucrători și, dacă este necesar, returnează un răspuns.
Această paradigmă implică puncte comune izolate de eșec: RabbitMQ și baza de date. Dar RabbitMQ este izolat într-un singur serviciu și, teoretic, poate fi individual pentru fiecare serviciu. Deci la MCS separăm aceste servicii cât mai mult posibil; pentru fiecare proiect individual creăm o bază de date separată, un RabbitMQ separat. Această abordare este bună deoarece în cazul unui accident în unele puncte vulnerabile, nu se defectează întregul serviciu, ci doar o parte din acesta.
Numărul de aplicații de lucru este nelimitat, astfel încât API-ul se poate scala cu ușurință pe orizontală în spatele balansoarelor pentru a crește performanța și toleranța la erori.
Unele servicii necesită coordonare în cadrul serviciului atunci când au loc operațiuni secvențiale complexe între API-uri și lucrători. În acest caz, se folosește un singur centru de coordonare, un sistem de cluster, cum ar fi Redis, Memcache, etcd, care permite unui lucrător să-i spună altuia că această sarcină îi este atribuită („te rog nu o lua”). Folosim etcd. De regulă, lucrătorii comunică în mod activ cu baza de date, scriu și citesc informații de acolo. Folosim mariadb ca bază de date, care se află într-un cluster multimaster.
Acest serviciu unic clasic este organizat într-o manieră general acceptată pentru OpenStack. Poate fi considerat ca un sistem închis, pentru care metodele de scalare și toleranța la erori sunt destul de evidente. De exemplu, pentru toleranța la erori API, este suficient să le puneți un echilibrator în fața lor. Scalarea lucrătorilor se realizează prin creșterea numărului acestora.
Punctul slab din întreaga schemă este RabbitMQ și MariaDB. Arhitectura lor merită un articol separat. În acest articol vreau să mă concentrez pe toleranța la erori API.

Arhitectura aplicației Openstack. Echilibrarea și toleranța la erori a platformei cloud
Faceți echilibrul HAProxy tolerant la erori folosind ExaBGP
Pentru a face API-urile noastre scalabile, rapide și tolerante la erori, le-am pus un echilibrator de încărcare în fața lor. Am ales HAProxy. În opinia mea, are toate caracteristicile necesare sarcinii noastre: echilibrare la mai multe niveluri OSI, o interfață de management, flexibilitate și scalabilitate, un număr mare de metode de echilibrare, suport pentru tabele de sesiune.
Prima problemă care trebuia rezolvată a fost toleranța la erori a balansierului în sine. Simpla instalare a unui echilibrator creează, de asemenea, un punct de eșec: echilibrerul se întrerupe și serviciul se blochează. Pentru a preveni acest lucru, am folosit HAProxy împreună cu ExaBGP.
ExaBGP vă permite să implementați un mecanism pentru verificarea stării unui serviciu. Am folosit acest mecanism pentru a verifica funcționalitatea HAProxy și, în caz de probleme, pentru a dezactiva serviciul HAProxy de la BGP.
Schema ExaBGP+HAProxy
- Instalăm software-ul necesar, ExaBGP și HAProxy, pe trei servere.
- Creăm o interfață loopback pe fiecare server.
- Pe toate cele trei servere atribuim aceeași adresă IP albă acestei interfețe.
- O adresă IP albă este anunțată pe Internet prin ExaBGP.
Toleranța la erori este obținută prin promovarea aceleiași adrese IP de la toate cele trei servere. Din punct de vedere al rețelei, aceeași adresă este accesibilă din trei hopuri diferite diferite. Routerul vede trei rute identice, selectează cea mai mare prioritate dintre ele pe baza propriei metrice (aceasta este de obicei aceeași opțiune), iar traficul merge doar către unul dintre servere.
În caz de probleme cu funcționarea HAProxy sau o defecțiune a serverului, ExaBGP încetează să anunțe traseul, iar traficul trece fără probleme pe un alt server.
Astfel, am atins toleranța la erori a balansierului.

Toleranța la erori a echilibratoarelor HAProxy
Schema s-a dovedit a fi imperfectă: am învățat cum să rezervăm HAProxy, dar nu am învățat cum să distribuim încărcătura în cadrul serviciilor. Prin urmare, am extins puțin această schemă: am trecut la echilibrarea între mai multe adrese IP albe.
Echilibrare bazată pe DNS plus BGP
Problema echilibrării încărcării pentru HAProxy rămâne nerezolvată. Cu toate acestea, poate fi rezolvată destul de simplu, așa cum am făcut aici.
Pentru a echilibra trei servere veți avea nevoie de 3 adrese IP albe și DNS vechi bun. Fiecare dintre aceste adrese este determinată pe interfața de loopback a fiecărui HAProxy și anunțată pe Internet.
În OpenStack, pentru a gestiona resurse, este utilizat un director de servicii, care specifică API-ul punctului final al unui anumit serviciu. În acest director înregistrăm un nume de domeniu - public.infra.mail.ru, care este rezolvat prin DNS prin trei adrese IP diferite. Ca rezultat, obținem distribuția sarcinii între trei adrese prin DNS.
Dar din moment ce atunci când anunțăm adrese IP albe nu controlăm prioritățile de selecție a serverului, acest lucru nu este încă echilibrat. De obicei, un singur server va fi selectat pe baza vechimii adresei IP, iar celelalte două vor fi inactive, deoarece nu sunt specificate valori în BGP.
Am început să trimitem rute prin ExaBGP cu diferite metrici. Fiecare echilibrator face publicitate pentru toate cele trei adrese IP albe, dar una dintre ele, cea principală pentru acest echilibrator, este promovată cu valoarea minimă. Deci, în timp ce toate cele trei echilibratoare sunt în funcțiune, apelurile către prima adresă IP merg către primul echilibrator, apelurile către al doilea către al doilea și apelurile către a treia către a treia.
Ce se întâmplă când unul dintre echilibratori cade? Dacă vreun echilibrator eșuează, adresa sa principală este în continuare anunțată de la celelalte două, iar traficul este redistribuit între ele. Astfel, oferim utilizatorului mai multe adrese IP simultan prin DNS. Prin echilibrarea prin DNS și valori diferite, obținem o distribuție uniformă a sarcinii în toate cele trei echilibrare. Și, în același timp, nu pierdem toleranța la greșeală.

Echilibrarea HAProxy bazată pe DNS + BGP
Interacțiunea dintre ExaBGP și HAProxy
Deci, am implementat toleranța la erori în cazul în care serverul pleacă, pe baza opririi anunțării rutelor. Dar HAProxy se poate opri din alte motive decât eșecul serverului: erori de administrare, eșecuri în cadrul serviciului. Dorim să scoatem echilibrul rupt de sub sarcină și în aceste cazuri și avem nevoie de un mecanism diferit.
Prin urmare, extinzând schema anterioară, am implementat ritmul cardiac între ExaBGP și HAProxy. Aceasta este o implementare software a interacțiunii dintre ExaBGP și HAProxy, când ExaBGP utilizează scripturi personalizate pentru a verifica starea aplicațiilor.
Pentru a face acest lucru, trebuie să configurați un verificator de sănătate în configurația ExaBGP, care poate verifica starea HAProxy. În cazul nostru, am configurat backend-ul de sănătate în HAProxy, iar din partea ExaBGP verificăm cu o simplă solicitare GET. Dacă anunțul încetează, atunci HAProxy nu funcționează cel mai probabil și nu este nevoie să-l faceți publicitate.

Verificare de sănătate HAProxy
HAProxy Peers: sincronizarea sesiunii
Următorul lucru de făcut a fost să sincronizați sesiunile. Când lucrați prin echilibrare distribuite, este dificil să organizați stocarea informațiilor despre sesiunile client. Dar HAProxy este unul dintre puținii echilibratori care pot face acest lucru datorită funcționalității Peers - capacitatea de a transfera tabele de sesiune între diferite procese HAProxy.
Există diferite metode de echilibrare: unele simple precum , și extins, atunci când sesiunea clientului este amintită și de fiecare dată când acesta ajunge pe același server ca înainte. Am vrut să implementăm a doua opțiune.
HAProxy folosește stick-tables pentru a salva sesiunile client ale acestui mecanism. Acestea salvează adresa IP inițială a clientului, adresa țintă selectată (backend) și unele informații de serviciu. În mod obișnuit, tabelele stick sunt folosite pentru a stoca o pereche sursă-IP + destinație-IP, ceea ce este util în special pentru aplicațiile care nu pot transfera contextul sesiunii utilizator atunci când comută la un alt echilibrator, de exemplu, în modul de echilibrare RoundRobin.
Dacă o masă stick este învățată să se deplaseze între diferite procese HAProxy (între care are loc echilibrarea), echilibratorii noștri vor putea lucra cu un singur grup de mese stick. Acest lucru va face posibilă comutarea perfectă a rețelei clientului dacă unul dintre echilibratori eșuează; lucrul cu sesiunile client va continua pe aceleași backend-uri care au fost selectate mai devreme.
Pentru o funcționare corectă, trebuie rezolvată problema adresei IP sursă a echilibratorului de la care a fost stabilită sesiunea. În cazul nostru, aceasta este o adresă dinamică pe interfața de loopback.
Munca corectă a colegilor se realizează numai în anumite condiții. Adică, expirările TCP trebuie să fie suficient de mari sau comutarea trebuie să fie suficient de rapidă, astfel încât sesiunea TCP să nu aibă timp să se termine. Cu toate acestea, permite comutarea fără întreruperi.
În IaaS avem un serviciu construit folosind aceeași tehnologie. Acest , care se numește Octavia. Se bazează pe două procese HAProxy și include inițial suport pentru egali. S-au dovedit excelenți în acest serviciu.
Imaginea arată schematic mișcarea tabelelor peer între trei instanțe HAProxy, este propusă o configurație despre cum poate fi configurată:

HAProxy Peers (sincronizarea sesiunii)
Dacă implementați aceeași schemă, funcționarea acesteia trebuie testată cu atenție. Nu este un fapt că va funcționa în același mod 100% din timp. Dar cel puțin nu veți pierde tabele stick atunci când trebuie să vă amintiți IP-ul sursă al clientului.
Limitarea numărului de solicitări simultane de la același client
Orice servicii care sunt disponibile publicului, inclusiv API-urile noastre, pot fi supuse unor avalanșe de solicitări. Motivele pentru ele pot fi complet diferite, de la erori ale utilizatorilor până la atacuri direcționate. Suntem periodic DDoSed de adrese IP. Clienții fac adesea greșeli în scripturile lor și ne oferă mini-DDoS.
Într-un fel sau altul, trebuie asigurată o protecție suplimentară. Soluția evidentă este limitarea numărului de solicitări API și nu pierderea timpului CPU procesând cererile rău intenționate.
Pentru a implementa astfel de restricții, folosim limite de rată, organizate pe baza HAProxy, folosind aceleași tabele stick. Configurarea limitelor este destul de simplă și vă permite să limitați utilizatorul cu numărul de solicitări către API. Algoritmul reține IP-ul sursă de la care se fac cererile și limitează numărul de solicitări simultane de la un utilizator. Desigur, am calculat profilul mediu de încărcare API pentru fiecare serviciu și am stabilit o limită de ≈ de 10 ori această valoare. Continuăm să monitorizăm îndeaproape situația și să ne ținem degetul pe puls.
Cum arată asta în practică? Avem clienți care folosesc API-urile noastre de autoscaling tot timpul. Ei creează aproximativ două până la trei sute de mașini virtuale dimineața și le șterg seara. Pentru OpenStack, crearea unei mașini virtuale, tot cu servicii PaaS, necesită cel puțin 1000 de solicitări API, deoarece interacțiunea între servicii are loc și prin intermediul API.
Un astfel de transfer de sarcini determină o sarcină destul de mare. Am evaluat această sarcină, am colectat vârfuri zilnice, le-am mărit de zece ori, iar aceasta a devenit limita noastră de rată. Ținem degetul pe puls. Vedem adesea roboți și scanere care încearcă să se uite la noi pentru a vedea dacă avem scripturi CGA care pot fi rulate, le tăiem activ.
Cum să vă actualizați baza de cod fără ca utilizatorii să observe
De asemenea, implementăm toleranța la erori la nivelul proceselor de implementare a codului. Pot exista erori în timpul lansărilor, dar impactul acestora asupra disponibilității serviciului poate fi minimizat.
Ne actualizăm în mod constant serviciile și trebuie să ne asigurăm că baza de cod este actualizată fără a afecta utilizatorii. Am reușit să rezolvăm această problemă folosind capacitățile de management ale HAProxy și implementarea Graceful Shutdown în serviciile noastre.
Pentru a rezolva această problemă, a fost necesar să se asigure controlul echilibratorului și închiderea „corectă” a serviciilor:
- În cazul HAProxy, controlul se realizează printr-un fișier de statistici, care este în esență un socket și este definit în configurația HAProxy. Îi puteți trimite comenzi prin stdio. Dar principalul nostru instrument de control al configurației este ansible, așa că are un modul încorporat pentru gestionarea HAProxy. Pe care îl folosim în mod activ.
- Majoritatea serviciilor noastre API și Engine acceptă tehnologii de închidere grațioase: atunci când se închid, așteaptă finalizarea sarcinii curente, fie că este vorba de o solicitare http sau de o sarcină de serviciu. Același lucru se întâmplă și cu muncitorul. Știe toate sarcinile pe care le face și se termină când a finalizat totul cu succes.
Datorită acestor două puncte, algoritmul sigur pentru implementarea noastră arată astfel.
- Dezvoltatorul asamblează un nou pachet de cod (pentru noi acesta este RPM), îl testează în mediul de dezvoltare, îl testează în etapă și îl lasă în depozitul de etapă.
- Dezvoltatorul stabilește sarcina pentru implementare cu cea mai detaliată descriere a „artefactelor”: versiunea noului pachet, o descriere a noii funcționalități și alte detalii despre implementare, dacă este necesar.
- Administratorul de sistem începe actualizarea. Lansează Ansible Playbook, care, la rândul său, face următoarele:
- Preia un pachet din depozitul de etapă și îl folosește pentru a actualiza versiunea pachetului din depozitul de produse.
- Compilează o listă de backend-uri ale serviciului actualizat.
- Oprește primul serviciu care urmează să fie actualizat în HAProxy și așteaptă ca procesele sale să se termine de rulat. Datorită închiderii grațioase, suntem încrezători că toate solicitările actuale ale clienților se vor finaliza cu succes.
- După ce API-ul și lucrătorii sunt complet opriți și HAProxy este dezactivat, codul este actualizat.
- Ansible rulează servicii.
- Pentru fiecare serviciu, sunt trase anumite „mânere”, care efectuează testarea unitară pe un număr de teste cheie predefinite. Are loc o verificare de bază a noului cod.
- Dacă nu au fost găsite erori în pasul anterior, backend-ul este activat.
- Să trecem la următorul backend.
- După ce toate backend-urile sunt actualizate, testele funcționale sunt lansate. Dacă acestea lipsesc, atunci dezvoltatorul se uită la orice funcționalitate nouă pe care a creat-o.
Aceasta completează implementarea.

Ciclul de actualizare a serviciului
Această schemă nu ar funcționa dacă nu am avea o singură regulă. Suportăm atât versiunile vechi, cât și cele noi în luptă. În prealabil, în etapa de dezvoltare a software-ului, se prevede că, chiar dacă există modificări în baza de date a serviciilor, acestea nu vor rupe codul anterior. Ca rezultat, baza de cod este actualizată treptat.
Concluzie
Împărtășindu-mi propriile gânduri despre o arhitectură WEB tolerantă la erori, aș dori să remarc din nou punctele sale cheie:
- toleranță la erori fizice;
- toleranță la erori de rețea (echilibrare, BGP);
- toleranța la erori a software-ului utilizat și dezvoltat.
Timp de funcționare stabil pentru toată lumea!
Sursa: www.habr.com
