Tranziție Tinder la Kubernetes

Notă. transl.: Angajații renumitului serviciu Tinder au împărtășit recent câteva detalii tehnice despre migrarea infrastructurii lor către Kubernetes. Procesul a durat aproape doi ani și a dus la lansarea unei platforme la scară foarte mare pe K8, constând din 200 de servicii găzduite pe 48 de mii de containere. Ce dificultăți interesante au întâmpinat inginerii Tinder și la ce rezultate au ajuns? Citiți această traducere.

Tranziție Tinder la Kubernetes

De ce?

În urmă cu aproape doi ani, Tinder a decis să-și mute platforma pe Kubernetes. Kubernetes ar permite echipei Tinder să containerizeze și să treacă la producție cu efort minim prin implementare imuabilă (implementare imuabilă). În acest caz, ansamblul aplicațiilor, implementarea lor și infrastructura în sine ar fi definite în mod unic prin cod.

De asemenea, căutam o soluție la problema scalabilității și stabilității. Când scalarea a devenit critică, a trebuit adesea să așteptăm câteva minute pentru ca noile instanțe EC2 să apară. Ideea de a lansa containere și de a începe deservirea traficului în câteva secunde în loc de minute a devenit foarte atractivă pentru noi.

Procesul s-a dovedit a fi dificil. În timpul migrării noastre de la începutul anului 2019, clusterul Kubernetes a atins o masă critică și am început să întâmpinăm diverse probleme din cauza volumului de trafic, mărimii clusterului și DNS. Pe parcurs, am rezolvat o mulțime de probleme interesante legate de migrarea a 200 de servicii și menținerea unui cluster Kubernetes format din 1000 de noduri, 15000 de pod-uri și 48000 de containere care rulează.

Cum?

Din ianuarie 2018, am trecut prin diferite etape ale migrației. Am început prin a containeriza toate serviciile noastre și a le implementa în mediile cloud de testare Kubernetes. Începând din octombrie, am început să migrăm metodic toate serviciile existente către Kubernetes. Până în martie a anului următor, am finalizat migrarea, iar acum platforma Tinder rulează exclusiv pe Kubernetes.

Construirea de imagini pentru Kubernetes

Avem peste 30 de depozite de cod sursă pentru microservicii care rulează pe un cluster Kubernetes. Codul din aceste depozite este scris în diferite limbi (de exemplu, Node.js, Java, Scala, Go) cu mai multe medii de rulare pentru aceeași limbă.

Sistemul de compilare este conceput pentru a oferi un „context de construire” complet personalizabil pentru fiecare microserviciu. De obicei, constă dintr-un Dockerfile și o listă de comenzi shell. Conținutul lor este complet personalizabil și, în același timp, toate aceste contexte de construcție sunt scrise după un format standardizat. Standardizarea contextelor de construcție permite unui singur sistem de construcție să gestioneze toate microserviciile.

Tranziție Tinder la Kubernetes
Figura 1-1. Proces de construire standardizat prin containerul Builder

Pentru a obține o consistență maximă între timpi de execuție (medii de rulare) același proces de construire este utilizat în timpul dezvoltării și testării. Ne-am confruntat cu o provocare foarte interesantă: a trebuit să dezvoltăm o modalitate de a asigura coerența mediului de construcție pe întreaga platformă. Pentru a realiza acest lucru, toate procesele de asamblare sunt efectuate în interiorul unui container special. Constructor.

Implementarea lui container a necesitat tehnici Docker avansate. Builder moștenește ID-ul de utilizator local și secretele (cum ar fi cheia SSH, acreditările AWS etc.) necesare pentru a accesa depozitele private Tinder. Montează directoare locale care conțin surse pentru a stoca în mod natural artefactele de construcție. Această abordare îmbunătățește performanța deoarece elimină necesitatea de a copia artefacte de construcție între containerul Builder și gazdă. Artefactele de construcție stocate pot fi reutilizate fără configurație suplimentară.

Pentru unele servicii, a trebuit să creăm un alt container pentru a mapa mediul de compilare la mediul de rulare (de exemplu, biblioteca Node.js bcrypt generează artefacte binare specifice platformei în timpul instalării). În timpul procesului de compilare, cerințele pot varia între servicii, iar fișierul Dockerfile final este compilat din mers.

Arhitectura clusterului Kubernetes și migrarea

Managementul dimensiunii clusterului

Am decis să folosim kube-aws pentru implementarea automată a clusterului pe instanțe Amazon EC2. La început, totul a funcționat într-un singur grup comun de noduri. Am realizat rapid necesitatea de a separa sarcinile de lucru în funcție de dimensiune și tip de instanță pentru a face o utilizare mai eficientă a resurselor. Logica a fost că rularea mai multor poduri multi-threaded încărcate s-a dovedit a fi mai previzibilă din punct de vedere al performanței decât coexistența lor cu un număr mare de pod-uri cu un singur thread.

La final ne-am hotărât pe:

  • m5.4xlarg — pentru monitorizare (Prometheus);
  • c5.4xmare - pentru sarcina de lucru Node.js (sarcina de lucru cu un singur thread);
  • c5.2xmare - pentru Java si Go (sarcina de lucru multithreaded);
  • c5.4xmare — pentru panoul de control (3 noduri).

migrațiune

Unul dintre pașii pregătitori pentru migrarea de la vechea infrastructură la Kubernetes a fost redirecționarea comunicării directe existente între servicii către noile balansoare de încărcare (Elastic Load Balancers (ELB). Acestea au fost create pe o subrețea specifică a unui cloud privat virtual (VPC). Această subrețea a fost conectată la un VPC Kubernetes. Acest lucru ne-a permis să migrăm modulele treptat, fără a lua în considerare ordinea specifică a dependențelor de servicii.

Aceste puncte finale au fost create folosind seturi ponderate de înregistrări DNS care aveau CNAME care indică fiecare ELB nou. Pentru a comuta, am adăugat o nouă intrare care indică noul ELB al serviciului Kubernetes cu o pondere de 0. Apoi am setat Time To Live (TTL) al intrării setat la 0. După aceasta, ponderile vechi și noi au fost ajustat încet și, în cele din urmă, 100% din încărcare a fost trimisă către un nou server. După ce comutarea a fost finalizată, valoarea TTL a revenit la un nivel mai adecvat.

Modulele Java pe care le aveam puteau face față cu TTL DNS scăzut, dar aplicațiile Node nu. Unul dintre ingineri a rescris o parte din codul pool-ului de conexiuni și l-a împachetat într-un manager care a actualizat pool-urile la fiecare 60 de secunde. Abordarea aleasă a funcționat foarte bine și fără nicio degradare vizibilă a performanței.

Lecțiile

Limitele fabricii de rețea

În dimineața devreme a zilei de 8 ianuarie 2019, platforma Tinder s-a prăbușit în mod neașteptat. Ca răspuns la o creștere fără legătură a latenței platformei mai devreme în acea dimineață, numărul de poduri și noduri din cluster a crescut. Acest lucru a făcut ca cache-ul ARP să fie epuizat pe toate nodurile noastre.

Există trei opțiuni Linux legate de memoria cache ARP:

Tranziție Tinder la Kubernetes
(sursă)

gc_thresh3 - aceasta este o limită grea. Apariția intrărilor „depășire a tabelului vecin” în jurnal a însemnat că, chiar și după colectarea sincronă a gunoiului (GC), nu era suficient spațiu în memoria cache ARP pentru a stoca intrarea vecină. În acest caz, nucleul pur și simplu a aruncat complet pachetul.

Folosim Flanel ca o țesătură de rețea în Kubernetes. Pachetele sunt transmise prin VXLAN. VXLAN este un tunel L2 ridicat deasupra unei rețele L3. Tehnologia folosește încapsularea MAC-in-UDP (MAC Address-in-User Datagram Protocol) și permite extinderea segmentelor de rețea de Layer 2. Protocolul de transport pe rețeaua fizică a centrului de date este IP plus UDP.

Tranziție Tinder la Kubernetes
Figura 2–1. Diagrama de flanel (sursă)

Tranziție Tinder la Kubernetes
Figura 2–2. Pachetul VXLAN (sursă)

Fiecare nod de lucru Kubernetes alocă un spațiu de adresă virtuală cu o mască /24 dintr-un bloc /9 mai mare. Pentru fiecare nod aceasta este mijloace o intrare în tabelul de rutare, o intrare în tabelul ARP (pe interfața flannel.1) și o intrare în tabelul de comutare (FDB). Ele sunt adăugate prima dată când un nod lucrător este pornit sau de fiecare dată când este descoperit un nou nod.

În plus, comunicarea nod-pod (sau pod-pod) trece în cele din urmă prin interfață eth0 (așa cum se arată în diagrama Flannel de mai sus). Acest lucru are ca rezultat o intrare suplimentară în tabelul ARP pentru fiecare gazdă sursă și destinație corespunzătoare.

În mediul nostru, acest tip de comunicare este foarte comun. Pentru obiectele de serviciu din Kubernetes, este creat un ELB și Kubernetes înregistrează fiecare nod cu ELB. ELB nu știe nimic despre pod-uri și este posibil ca nodul selectat să nu fie destinația finală a pachetului. Ideea este că atunci când un nod primește un pachet de la ELB, îl consideră ținând cont de reguli iptables pentru un anumit serviciu și selectează aleatoriu un pod pe alt nod.

La momentul defecțiunii, în cluster existau 605 noduri. Din motivele expuse mai sus, acest lucru a fost suficient pentru a depăși semnificația gc_thresh3, care este implicit. Când se întâmplă acest lucru, nu numai că pachetele încep să fie aruncate, dar întregul spațiu de adrese virtuale Flannel cu o mască /24 dispare din tabelul ARP. Comunicarea nod-pod și interogările DNS sunt întrerupte (DNS este găzduit într-un cluster; citiți mai târziu în acest articol pentru detalii).

Pentru a rezolva această problemă, trebuie să măriți valorile gc_thresh1, gc_thresh2 и gc_thresh3 și reporniți Flannel pentru a reînregistra rețelele lipsă.

Scalare DNS neașteptată

În timpul procesului de migrare, am folosit în mod activ DNS pentru a gestiona traficul și pentru a transfera treptat serviciile din vechea infrastructură la Kubernetes. Am stabilit valori TTL relativ scăzute pentru RecordSets asociate în Route53. Când vechea infrastructură rula pe instanțe EC2, configurația noastră de rezolvare a indicat Amazon DNS. Am luat acest lucru de la sine înțeles, iar impactul TTL scăzut asupra serviciilor noastre și serviciilor Amazon (cum ar fi DynamoDB) a trecut în mare măsură neobservat.

Pe măsură ce migram serviciile către Kubernetes, am constatat că DNS procesa 250 de mii de solicitări pe secundă. Ca urmare, aplicațiile au început să experimenteze timeout-uri constante și serioase pentru interogările DNS. Acest lucru s-a întâmplat în ciuda eforturilor incredibile de optimizare și trecere a furnizorului de DNS la CoreDNS (care la sarcina maximă a atins 1000 de poduri care rulează pe 120 de nuclee).

În timp ce cercetam alte cauze și soluții posibile, am descoperit статью, care descrie condițiile de cursă care afectează cadrul de filtrare a pachetelor filtru net în Linux. Timeout-urile pe care le-am observat, cuplate cu un numărător în creștere insert_failed în interfața Flannel au fost în concordanță cu constatările articolului.

Problema apare în etapa de traducere a adresei rețelei sursă și destinație (SNAT și DNAT) și la intrarea ulterioară în tabel contratrack. Una dintre soluțiile discutate intern și sugerate de comunitate a fost mutarea DNS-ului în nodul lucrător în sine. În acest caz:

  • SNAT nu este necesar deoarece traficul rămâne în interiorul nodului. Nu este necesar să fie direcționat prin interfață eth0.
  • DNAT nu este necesar, deoarece IP-ul de destinație este local pentru nod și nu un pod selectat aleatoriu conform regulilor iptables.

Am decis să rămânem cu această abordare. CoreDNS a fost implementat ca DaemonSet în Kubernetes și am implementat un server DNS cu nod local în rezoluție.conf fiecare pod prin stabilirea unui steag --cluster-dns comenzi kubelet . Această soluție s-a dovedit a fi eficientă pentru expirarea timpului DNS.

Cu toate acestea, am văzut încă pierderi de pachete și o creștere a contorului insert_failed în interfața Flannel. Acest lucru a continuat după implementarea soluției, deoarece am reușit să eliminăm SNAT și/sau DNAT numai pentru traficul DNS. Condițiile de cursă au fost păstrate pentru alte tipuri de trafic. Din fericire, majoritatea pachetelor noastre sunt TCP, iar dacă apare o problemă, acestea sunt pur și simplu retransmise. Încă încercăm să găsim o soluție potrivită pentru toate tipurile de trafic.

Utilizarea Envoy pentru o mai bună echilibrare a încărcăturii

Pe măsură ce am migrat serviciile de backend către Kubernetes, am început să suferim de încărcare dezechilibrată între poduri. Am descoperit că HTTP Keepalive a determinat blocarea conexiunilor ELB pe primele poduri gata ale fiecărei implementări lansate. Astfel, cea mai mare parte a traficului a trecut printr-un mic procent de poduri disponibile. Prima soluție pe care am testat-o ​​a fost setarea MaxSurge la 100% pentru noile implementări pentru scenariile cele mai nefavorabile. Efectul s-a dovedit a fi nesemnificativ și nepromițător în ceea ce privește implementările mai mari.

O altă soluție pe care am folosit-o a fost creșterea artificială a cererilor de resurse pentru serviciile critice. În acest caz, păstăile plasate în apropiere ar avea mai mult spațiu de manevră în comparație cu alte păstăi grele. Nici pe termen lung nu ar funcționa pentru că ar fi o risipă de resurse. În plus, aplicațiile noastre Node erau cu un singur thread și, în consecință, puteau folosi doar un nucleu. Singura soluție reală a fost să folosești o mai bună echilibrare a sarcinii.

De mult ne-am dorit să apreciem pe deplin trimis. Situația actuală ne-a permis să o implementăm într-un mod foarte limitat și să obținem rezultate imediate. Envoy este un proxy de înaltă performanță, open-source, de nivel XNUMX, conceput pentru aplicații SOA mari. Poate implementa tehnici avansate de echilibrare a sarcinii, inclusiv reîncercări automate, întrerupătoare de circuit și limitare globală a ratei. (Notă. transl.: Puteți citi mai multe despre asta în acest articol despre Istio, care se bazează pe Envoy.)

Am venit cu următoarea configurație: avem un sidecar Envoy pentru fiecare pod și o singură rută și conectați clusterul la container la nivel local prin port. Pentru a minimiza potențiala cascadă și pentru a menține o rază mică de atingere, am folosit o flotă de pod-uri proxy frontale Envoy, câte unul pentru fiecare zonă de disponibilitate (AZ) pentru fiecare serviciu. S-au bazat pe un motor simplu de descoperire a serviciilor scris de unul dintre inginerii noștri care pur și simplu a returnat o listă de poduri în fiecare AZ pentru un anumit serviciu.

Serviciul front-Envoys a folosit apoi acest mecanism de descoperire a serviciului cu un cluster și o rută în amonte. Am stabilit intervale de timp adecvate, am mărit toate setările întreruptoarelor de circuit și am adăugat o configurație minimă de reîncercare pentru a ajuta cu defecțiuni individuale și pentru a asigura implementări fără probleme. Am plasat un TCP ELB în fața fiecăruia dintre acești trimiși de serviciu. Chiar dacă keepalive din stratul nostru principal de proxy a fost blocat pe unele poduri Envoy, acestea au putut face față încărcăturii mult mai bine și au fost configurate să echilibreze prin minimum_request în backend.

Pentru implementare, am folosit cârligul preStop atât pe podurile de aplicație, cât și pe podurile sidecar. Cârligul a declanșat o eroare la verificarea stării punctului final de administrare situat pe containerul sidecar și a intrat în repaus pentru o perioadă pentru a permite conexiunile active să se termine.

Unul dintre motivele pentru care am reușit să ne mișcăm atât de repede se datorează valorilor detaliate pe care le-am putut integra cu ușurință într-o instalație tipică Prometheus. Acest lucru ne-a permis să vedem exact ce se întâmplă în timp ce ajustam parametrii de configurare și redistribuim traficul.

Rezultatele au fost imediate și evidente. Am început cu cele mai dezechilibrate servicii, iar în momentul de față funcționează în fața celor mai importante 12 servicii din cluster. Anul acesta plănuim o tranziție către o rețea de servicii complete cu descoperire de servicii mai avansate, întrerupere a circuitului, detectarea valorii aberante, limitarea ratei și urmărirea.

Tranziție Tinder la Kubernetes
Figura 3–1. Convergența CPU a unui serviciu în timpul tranziției la Envoy

Tranziție Tinder la Kubernetes

Tranziție Tinder la Kubernetes

Rezultatul final

Prin această experiență și cercetări suplimentare, am construit o echipă puternică de infrastructură, cu abilități puternice în proiectarea, implementarea și operarea clusterelor Kubernetes mari. Toți inginerii Tinder au acum cunoștințele și experiența necesare pentru a împacheta containere și a implementa aplicații în Kubernetes.

Când a apărut nevoia de capacitate suplimentară pe vechea infrastructură, a trebuit să așteptăm câteva minute pentru lansarea noilor instanțe EC2. Acum containerele încep să ruleze și încep să proceseze traficul în câteva secunde în loc de minute. Programarea mai multor containere pe o singură instanță EC2 oferă, de asemenea, o concentrare orizontală îmbunătățită. Ca urmare, prognozăm o reducere semnificativă a costurilor EC2019 în 2 față de anul trecut.

Migrația a durat aproape doi ani, dar am finalizat-o în martie 2019. În prezent, platforma Tinder rulează exclusiv pe un cluster Kubernetes format din 200 de servicii, 1000 de noduri, 15 de poduri și 000 de containere care rulează. Infrastructura nu mai este domeniul exclusiv al echipelor de operațiuni. Toți inginerii noștri împărtășesc această responsabilitate și controlează procesul de construire și implementare a aplicațiilor lor folosind doar cod.

PS de la traducator

Citiți și o serie de articole pe blogul nostru:

Sursa: www.habr.com

Adauga un comentariu