Mutarea lui Tinder în Kubernetes

Scris de: Chris O'Brien, Director Inginerie | Chris Thomas, Manager Inginerie | Jinyong Lee, Senior Software Engineer | Editat de: Cooper Jackson, inginer software

De ce

În urmă cu aproape doi ani, Tinder a decis să-și mute platforma în Kubernetes. Kubernetes ne-a oferit o oportunitate de a conduce Tinder Engineering către containerizarea și operarea la atingere scăzută prin implementare imuabilă. Construirea, implementarea și infrastructura aplicațiilor ar fi definite drept cod.

Am căutat, de asemenea, să abordăm provocările de scară și stabilitate. Când scalarea a devenit critică, am suferit adesea prin câteva minute de așteptare pentru ca noile instanțe EC2 să vină online. Ideea de programare a containerelor și de servire a traficului în câteva secunde, față de minute, ne atrăgea.

Nu a fost ușor. În timpul migrației noastre la începutul anului 2019, am ajuns la o masă critică în clusterul nostru Kubernetes și am început să întâlnim diverse provocări din cauza volumului de trafic, a dimensiunii clusterului și a DNS. Am rezolvat provocări interesante pentru a migra 200 de servicii și a rula un cluster Kubernetes la scara totalizând 1.000 de noduri, 15.000 de poduri și 48.000 de containere rulante.

Cum

Începând cu luna ianuarie 2018, ne-am desfășurat prin diferite etape ale efortului de migrare. Am început prin containerizarea tuturor serviciilor noastre și implementarea lor într-o serie de medii de stadializare găzduite de Kubernetes. Începând cu luna octombrie, am început să mutăm metodic toate serviciile moștenite către Kubernetes. Până în luna martie a anului următor, ne-am finalizat migrația, iar platforma Tinder rulează acum exclusiv pe Kubernetes.

Construirea de imagini pentru Kubernetes

Există mai mult de 30 de depozite de coduri sursă pentru microservicii care rulează în clusterul 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 construire este proiectat să funcționeze pe un „context de construire” complet personalizabil pentru fiecare microserviciu, care constă de obicei dintr-un Dockerfile și o serie de comenzi shell. În timp ce conținutul lor este complet personalizabil, aceste contexte de compilare sunt scrise toate urmând un format standardizat. Standardizarea contextelor de construire permite unui singur sistem de construire să se ocupe de toate microserviciile.

Figura 1–1 Proces de construcție standardizat prin containerul Builder

Pentru a obține coerența maximă între mediile de rulare, același proces de construire este utilizat în faza de dezvoltare și testare. Acest lucru a impus o provocare unică atunci când a fost necesar să concepem o modalitate de a garanta un mediu de construire consecvent pe toată platforma. Ca urmare, toate procesele de construire sunt executate într-un container special „Builder”.

Implementarea containerului Builder a necesitat o serie de tehnici Docker avansate. Acest container Builder moștenește ID-ul utilizatorului local și secretele (de exemplu, cheia SSH, acreditările AWS, etc.), după cum este necesar pentru a accesa depozitele private Tinder. Acesta montează directoare locale care conțin codul sursă pentru a avea un mod natural de a stoca artefacte de construire. Această abordare îmbunătățește performanța, deoarece elimină copierea artefactelor construite între containerul Builder și mașina gazdă. Artefactele de stocare stocate sunt reutilizate data viitoare fără configurare suplimentară.

Pentru anumite servicii, a fost necesar să creăm un alt container în Builder pentru a se potrivi mediului timp de compilare cu mediul de rulare (de exemplu, instalarea bibliotecii Nc.js bcrypt generează artefacte binare specifice platformei). Cerințele timpului de compilare pot diferi între servicii, iar Dockerfile final este compus din mers.

Arhitectura și migrația clusterului Kubernetes

Dimensiunea clusterului

Am decis să folosim kube-aws pentru furnizarea automată de cluster în instanțele Amazon EC2. La început, rulam totul într-un singur nod general. Am identificat rapid necesitatea de a separa volumul de muncă în diferite dimensiuni și tipuri de cazuri, pentru a utiliza mai bine resursele. Motivul a fost faptul că rularea mai puține poduri cu filetări puternice a dat rezultate mai performante pentru noi decât să le lăsăm să coexiste cu un număr mai mare de păstăi cu o singură filă.

Ne-am stabilit pe:

  • m5.4xlarge pentru monitorizare (Prometeu)
  • c5.4xlarge pentru Node.js volum de muncă (sarcină de lucru cu un singur filet)
  • c5.2xlarge pentru Java și Go (sarcină de lucru cu mai multe filete)
  • c5.4xlarge pentru planul de control (3 noduri)

migrațiune

Unul dintre pașii de pregătire pentru migrația de la infrastructura noastră moștenită spre Kubernetes a fost schimbarea comunicării de la serviciu la serviciu, pentru a îndrepta spre noi balante de sarcină elastică (ELB) create într-o subrețea specifică Virtual Private Cloud (VPC). Această subrețea a fost legată de Kubernetes VPC. Acest lucru ne-a permis să migrăm granular modulele fără a ține cont de comanda specifică pentru dependențele de servicii.

Aceste puncte finale au fost create folosind seturi de înregistrări DNS ponderate care aveau un CNAME îndreptat către fiecare ELB nou. La cutover, am adăugat un nou record, indicând noul serviciu Kubernetes ELB, cu o greutate de 0. Am setat apoi Time To Live (TTL) pe înregistrarea setată la 0. Greutările vechi și noi au fost apoi ajustate lent la în cele din urmă, ajunge cu 100% pe noul server. După ce cutover-ul a fost complet, TTL a fost setat la ceva mai rezonabil.

Modulele noastre Java au onorat TTL-ul DNS scăzut, dar aplicațiile noastre Node nu. Unul dintre inginerii noștri a rescris o parte din codul bazei de conexiuni pentru a-l înfășura într-un manager care ar reîmprospăta piscinele la fiecare 60 de ani. Acest lucru a funcționat foarte bine pentru noi, fără un succes apreciabil.

invataturi

Limitele țesăturilor de rețea

În primele ore ale dimineții, 8 ianuarie 2019, Platforma Tinder a suferit o criză persistentă. Ca răspuns la o creștere fără legătură a latenței platformei mai devreme în acea dimineață, numărul de pod și noduri au fost reduse pe cluster. Aceasta a dus la epuizarea cache-ului ARP pe toate nodurile noastre.

Există trei valori Linux relevante pentru cache-ul ARP:

Credit

gc_thresh3 este o capacă. Dacă primiți înregistrări de jurnal „revărsare de masă vecină”, acest lucru indică faptul că, chiar și după colectarea de gunoi sincronă (GC) a memoriei cache ARP, nu a fost suficient spațiu pentru a stoca intrarea vecină. În acest caz, nucleul pică doar pachetul în întregime.

Folosim Flannel ca țesătură de rețea în Kubernetes. Pachetele sunt expediate prin VXLAN. VXLAN este o schemă de suprapunere Layer 2 printr-o rețea Layer 3. Acesta folosește încapsularea MAC Address-in-User Datagram Protocol (MAC-in-UDP) pentru a oferi un mijloc de extindere a segmentelor de rețea Layer 2. Protocolul de transport prin rețeaua centrelor de date fizice este IP plus UDP.

Figura 2–1 Schema flanelelor (credit)

Figura 2–2 Pachetul VXLAN (credit)

Fiecare nod al lucrătorului Kubernetes alocă propriul său / 24 spațiu de adrese virtuale dintr-un bloc mai mare / 9. Pentru fiecare nod, rezultă 1 intrare în tabel de rute, 1 intrare în tabel ARP (pe interfața flannel.1) și 1 intrare în baza de date de redirecționare (FDB). Acestea sunt adăugate atunci când nodul lucrător se lansează pentru prima dată sau pe măsură ce fiecare nou nod este descoperit.

În plus, comunicarea node-to-pod (sau pod-to-pod) curge în cele din urmă peste interfața eth0 (ilustrată în diagrama Flannel de mai sus). Aceasta va duce la o intrare suplimentară în tabelul ARP pentru fiecare sursă de nod corespunzătoare și destinația nodului.

În mediul nostru, acest tip de comunicare este foarte frecvent. Pentru obiectele noastre de serviciu Kubernetes, este creat un ELB și Kubernetes înregistrează fiecare nod cu ELB. ELB nu are cunoștință de pod și nodul selectat poate să nu fie destinația finală a pachetului. Acest lucru se datorează faptului că atunci când nodul primește pachetul de la ELB, își evaluează regulile iptables pentru serviciu și selectează la întâmplare un pod pe un alt nod.

În momentul întreruperii, în cluster erau 605 noduri totale. Din motivele expuse mai sus, acest lucru a fost suficient pentru a eclipsa valoarea implicită gc_thresh3. Odată ce se întâmplă acest lucru, nu numai că sunt abandonate pachetele, dar întreaga Flannel / 24s din spațiul de adrese virtuale lipsesc din tabelul ARP. Nodul pentru comunicarea pod și căutările DNS nu reușesc. (DNS este găzduit în cadrul clusterului, așa cum va fi explicat mai detaliat mai târziu în acest articol.)

Pentru rezolvare, valorile gc_thresh1, gc_thresh2 și gc_thresh3 sunt ridicate și Flannel trebuie repornit pentru a reînregistra rețelele lipsă.

Rulează în mod neașteptat DNS la scară

Pentru a ne adapta migrației, am utilizat puternic DNS pentru a facilita modelarea traficului și încetinirea treptelor de la moștenire la Kubernetes pentru serviciile noastre. Am stabilit valori TTL relativ scăzute pe Route53 RecordSets asociate. Când am gestionat infrastructura moștenită pe instanțe EC2, configurația noastră de rezolvare a indicat DNS-ul Amazon. Am luat acest lucru de fapt, iar costul unui TTL relativ scăzut pentru serviciile noastre și serviciile Amazon (de exemplu, DynamoDB) a trecut în mare parte neobservat.

Pe măsură ce urcam tot mai multe servicii către Kubernetes, ne-am trezit să executăm un serviciu DNS care răspundea la 250.000 de solicitări pe secundă. Am întâlnit intervale de timp de căutare DNS intermitente și de impact în aplicațiile noastre. Acest lucru s-a produs în ciuda unui efort exhaustiv de ajustare și a unui furnizor DNS a trecut la o implementare CoreDNS care la un moment dat a atins un maxim la 1.000 de poduri care consumă 120 de nuclee.

În timp ce cercetam alte cauze și soluții posibile, am găsit un articol care descrie o stare de rasă care afectează netfilterul cadrului de filtrare a pachetelor Linux. Intervalele de timp DNS pe care le vedeam, împreună cu un contor de introducere a e-mailului incorect pe interfața Flannel, aliniat la concluziile articolului.

Problema apare în timpul Traducerii adresei de rețea sursă și a destinației (SNAT și DNAT) și în inserarea ulterioară în tabelul conntrack. O soluție discutată pe plan intern și propusă de comunitate a fost mutarea DNS pe nodul lucrătorului. În acest caz:

  • SNAT nu este necesar, deoarece traficul rămâne local pe nod. Nu este necesar să fie transmis prin interfața eth0.
  • DNAT nu este necesar deoarece IP-ul de destinație este local pentru nod și nu este un pod selectat la întâmplare după regulile iptables.

Am decis să continuăm cu această abordare. CoreDNS a fost implementat ca DaemonSet în Kubernetes și am injectat serverul DNS local al nodului în rezolv.conf-ul fiecărui pod, configurând indicatorul de comandă kubelet - cluster-dns. Soluția a fost eficientă pentru expirarea DNS.

Cu toate acestea, vom vedea în continuare pachete abandonate și creșterea contorului insert_failed a interfeței Flannel. Acest lucru va persista chiar și după soluționarea de mai sus, deoarece am evitat doar SNAT și / sau DNAT pentru trafic DNS. Starea cursei va apărea în continuare pentru alte tipuri de trafic. Din fericire, majoritatea pachetelor noastre sunt TCP și atunci când apare această condiție, pachetele vor fi retransmise cu succes. O soluție pe termen lung pentru toate tipurile de trafic este ceva despre care discutăm în continuare.

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

Pe măsură ce ne-am migrat serviciile de backend către Kubernetes, am început să suferim de încărcare dezechilibrată pe păstăi. Am descoperit că, datorită HTTP Keepalive, conexiunile ELB s-au blocat la primele păstăi gata ale fiecărei implementări de rulare, astfel că majoritatea traficului curgeau printr-un procent mic din podurile disponibile. Una dintre primele atenuări pe care am încercat a fost să folosim un MaxSurge 100% pe noile implementări pentru cei mai răi infractori. Aceasta a fost marginal eficientă și nu durabilă pe termen lung, cu unele dintre implementările mai mari.

O altă atenuare pe care am folosit-o a fost să umflăm artificial cererile de resurse pe serviciile critice, astfel încât păstăile colocate să aibă mai mult spațiu alături de alte păstăi grele. Acest lucru nu va fi, de asemenea, pe termen lung, din cauza deșeurilor de resurse, iar aplicațiile noastre pentru noduri au fost cu un singur filet și, astfel, au fost limitate în mod eficient la 1 miez. Singura soluție clară a fost utilizarea unei mai bune echilibrări a sarcinii.

Am căutat intern să-l evaluăm pe Envoy. Aceasta ne-a oferit o șansă de a o implementa într-o manieră foarte limitată și de a obține beneficii imediate. Envoy este un proxy de înaltă performanță Layer 7 de înaltă performanță proiectat pentru arhitecturi mari orientate către servicii. Este capabil să pună în aplicare tehnici avansate de echilibrare a sarcinii, incluzând încercări automate, întrerupere a circuitului și limitarea globală a vitezei.

Configurația cu care am venit a fost să avem un sideport Envoy alături de fiecare pod care avea un traseu și un cluster pentru a atinge portul containerului local. Pentru a minimiza potențialele cascade și pentru a menține o rază mică de explozie, am utilizat o flotă de păstăreți de tip Envoy proxy frontal, o desfășurare în fiecare zonă de disponibilitate (AZ) pentru fiecare serviciu. Acestea au lovit un mic mecanism de descoperire a serviciilor pe care unul dintre inginerii noștri l-au pus la punct, care a returnat pur și simplu o listă de păstăi în fiecare AZ pentru un serviciu dat.

Serviciul front-Envoys a folosit apoi acest mecanism de descoperire a serviciului cu un cluster și o rută în amonte. Am configurat perioade de timp rezonabile, am îmbunătățit toate setările întrerupătorului și apoi am introdus o configurație de reîncărcare minimă pentru a ajuta la defecțiuni tranzitorii și implementări fluide. Am confruntat fiecare dintre aceste servicii de trimis frontal cu un ELB TCP. Chiar dacă agentul de protecție principal al stratului nostru principal de proxy a fost fixat pe anumite păstăi Envoy, acestea au fost mult mai capabile să se ocupe de sarcină și au fost configurate pentru a echilibra prin minimum_request la backend.

Pentru implementări, am utilizat un cârlig preStop atât pentru aplicație, cât și pentru podul lateral. Acest cârlig numit controlul de sănătate al sidecar-ului nu reușește punctul de administrare, împreună cu un somn mic, pentru a oferi un timp pentru a permite conexiunile inflight să se completeze și să se scurgă.

Unul dintre motivele pentru care am reușit să ne mișcăm atât de repede a fost datorită valorilor bogate pe care am putut să le integrăm cu ușurință în configurația noastră normală a Prometeu. Acest lucru ne-a permis să vedem exact ce se întâmplă pe măsură ce am iterat la setările de configurare și am redus traficul.

Rezultatele au fost imediate și evidente. Am început cu cele mai dezechilibrate servicii și, în acest moment, îl rulăm în fața a douăsprezece dintre cele mai importante servicii din clusterul nostru. În acest an intenționăm să trecem la o plasă cu servicii complete, cu descoperire de serviciu mai avansată, întrerupere a circuitului, detectare exterioară, limitare de viteză și urmărire.

Figura 3–1 Convergența CPU a unui serviciu în timpul procesului de trimitere către trimis

Rezultatul final

Prin aceste învățări și cercetări suplimentare, am dezvoltat o echipă puternică de infrastructură internă, cu o mare familiaritate cu privire la modul de proiectare, implementare și funcționare a clusterelor Kubernetes mari. Întreaga organizație de inginerie a lui Tinder are acum cunoștințe și experiență cu privire la modul în care se pot containeriza și implementa aplicațiile lor pe Kubernetes.

În ceea ce privește infrastructura noastră veche, atunci când a fost necesară o scară suplimentară, am suferit adesea prin câteva minute de așteptare pentru ca noile instanțe EC2 să vină online. Containerele planifică acum și servesc traficul în câteva secunde, față de minute. Planificarea mai multor containere pe o singură instanță EC2 asigură, de asemenea, o densitate orizontală îmbunătățită. Drept urmare, proiectăm economii substanțiale de costuri pe EC2 în 2019, comparativ cu anul precedent.

A durat aproape doi ani, dar ne-am finalizat migrația în martie 2019. Platforma Tinder rulează exclusiv pe un cluster Kubernetes format din 200 de servicii, 1.000 de noduri, 15.000 de poduri și 48.000 de containere rulante. Infrastructura nu mai este o sarcină rezervată echipelor noastre de operații. În schimb, inginerii din întreaga organizație împărtășesc această responsabilitate și au control asupra modului în care aplicațiile lor sunt construite și desfășurate cu tot ca cod.