Moviment de Tinder a Kubernetes

Escrit per: Chris O'Brien, director d'enginyeria | Chris Thomas, director d'enginyeria | Jinyong Lee, enginyer de programari principal | Editat per: Cooper Jackson, enginyer de programari

Per què

Fa gairebé dos anys, Tinder va decidir traslladar la seva plataforma a Kubernetes. Kubernetes ens va oferir l'oportunitat de conduir Tinder Engineering cap a una containerització i una operació de baix toc mitjançant un desplegament immutable. La creació, el desplegament i la infraestructura d'aplicacions es definirien com a codi.

També estàvem buscant fer front als reptes d’escala i estabilitat. Quan l'escalat es va convertir en crític, sovint vam patir diversos minuts d'espera de que apareguessin nous casos d'EC2 en línia. La idea de programar contenidors i servir el trànsit en qüestió de segons enfront dels minuts ens cridava l’atenció.

No va ser fàcil. Durant la nostra migració a principis del 2019, vam arribar a la massa crítica dins del nostre clúster Kubernetes i vam començar a trobar diversos reptes a causa del volum de trànsit, la mida del clúster i el DNS. Vam resoldre reptes interessants per migrar 200 serveis i executar un clúster Kubernetes a escala de 1.000 nodes, 15.000 pods i 48.000 contenidors en marxa.

Com

A partir de gener de 2018, vam treballar durant diverses etapes de l’esforç migratori. Vam començar contenint tots els nostres serveis i desplegant-los a una sèrie d’entorns d’escenificació allotjats a Kubernetes. A partir d’octubre, vam començar a traslladar de forma metòdica tots els nostres serveis heretats a Kubernetes. Al març de l'any següent, vam finalitzar la nostra migració i la plataforma Tinder ara funciona exclusivament a Kubernetes.

Imatges de construcció per a Kubernetes

Hi ha més de 30 dipòsits de codi font per als microservicis que s’executen al clúster de Kubernetes. El codi d’aquests dipòsits està escrit en diferents idiomes (per exemple, Node.js, Java, Scala, Go) amb diversos entorns d’execució per al mateix idioma.

El sistema de creació està dissenyat per funcionar en un "context de creació" totalment personalitzable per a cada microservici, que consisteix normalment en un fitxer Dockerfile i una sèrie de comandes de shell. Si bé els seus continguts són totalment personalitzables, aquests contextos de creació s’escriuen tot seguint un format normalitzat. L'estandardització dels contextos de creació permet que un sistema de creació únic pugui gestionar tots els microserveis.

Figura 1–1 Procés de creació normalitzada a través del contenidor Builder

Per aconseguir la màxima coherència entre ambients d’execució, s’està utilitzant el mateix procés de creació durant la fase de desenvolupament i proves. Això va suposar un repte únic quan necessitàvem idear una manera de garantir un entorn de construcció consistent a tota la plataforma. Com a resultat, tots els processos de creació s'executen dins d'un contenidor especial "Builder".

La implementació del contenidor Builder requeria diverses tècniques avançades de Docker. Aquest contenidor de Builder hereta identificadors i secrets de l'usuari local (per exemple, clau SSH, credencials AWS, etc.) segons es requereix per accedir als dipòsits privats de Tinder. Inclou directoris locals que contenen el codi font per tenir una forma natural d'emmagatzemar artefactes de creació. Aquest enfocament millora el rendiment, ja que elimina copiar artefactes construïts entre el contenidor Builder i la màquina host. Els artefactes de creació emmagatzemats es reutilitzen la propera vegada sense configurar més.

Per a certs serveis, necessitàvem crear un altre contenidor dins del Builder per combinar l’entorn de temps de compilació amb l’entorn d’execució (per exemple, la instal·lació de la biblioteca de bcrypt de Node.js genera artefactes binaris específics per a la plataforma). Els requisits de temps de compilació poden diferir entre els serveis i el Dockerfile final es compon al llarg del vol.

Arquitectura i migració de clústers de Kubernetes

Dimensionament de clústers

Vam decidir utilitzar kube-aws per a l'aprovisionament automatitzat de clústers en instàncies d'Amazon EC2. Ja ben aviat, estàvem funcionant tot en un node general. Ràpidament vam identificar la necessitat de separar les càrregues de treball en diferents mides i tipus d’instàncies per aprofitar millor els recursos. El raonament va ser que el fet d’executar menys vaquetes fortament roscades va donar resultats de rendiment més previsibles per a nosaltres que deixar-los conviure amb un nombre més gran de beines de fil únic.

Ens vam establir a:

  • m5.4xlarge per a monitorització (Prometeu)
  • c5.4xlarge per Node.js càrrega de treball (càrrega de treball d'un filet)
  • c5.2xlarge per Java i Go (càrrega de treball multi-threaded)
  • c5.4xlarge per al pla de control (3 nodes)

Migració

Un dels passos de preparació per a la migració de la nostra infraestructura antiga a Kubernetes va ser canviar la comunicació servei a servei existent per assenyalar nous balans de càrrega elàstica (ELBs) creats en una subxarxa virtual virtual virtual cloud (VPC). Aquesta subxarxa va ser analitzada al VPC de Kubernetes. Això ens va permetre migrar granularment mòduls sense tenir en compte la comanda específica de les dependències del servei.

Aquests punts finals es van crear mitjançant conjunts de registres DNS ponderats que tenien un CNAME apuntat a cada nou ELB. Al tall de tall, hem afegit un nou registre, apuntant al nou servei Kubernetes ELB, amb un pes de 0. A continuació, hem establert el Time To Live (TTL) del registre establert en 0. Els pesos antics i nous s'ajustaven lentament a acabarà amb el 100% al nou servidor. Un cop finalitzat el cutover, el TTL es va definir en una cosa més raonable.

Els nostres mòduls de Java han respectat un TTL amb DNS baix, però les nostres nodes no ho són. Un dels nostres enginyers va reescriure part del codi de l'agrupació de connexions per incloure'l en un gestor que actualitzaria les piscines cada 60 anys. Això va funcionar molt bé per a nosaltres sense un èxit de rendiment apreciable.

Aprenentatges

Límits de teixit de xarxa

A les primeres hores del matí del 8 de gener del 2019, la plataforma Tinder va patir una aturada persistent. Com a resposta a un augment no relacionat en la latència de la plataforma, aquest matí, es van ampliar els recomptes de pods i nodes al clúster. Això va donar lloc a l'esgotament de la memòria cau ARP a tots els nostres nodes.

Hi ha tres valors de Linux rellevants per a la memòria cau ARP:

Crèdit

gc_thresh3 és un tap dur. Si obteniu entrades de registre de "desbordament de taula de veïns", això indica que fins i tot després d'una recollida de brossa sincronitzada (GC) de la memòria cau ARP, no hi havia prou espai per emmagatzemar l'entrada del veí. En aquest cas, el nucli només deixa caure el paquet completament.

Utilitzem Flannel com a teixit de xarxa a Kubernetes. Els paquets s’envien mitjançant VXLAN. VXLAN és un esquema de superposició de capa 2 a una xarxa de capa 3. Utilitza l'encapsulament MAC Address-in-User Datagram Protocol (MAC-in-UDP) per proporcionar un mitjà per estendre els segments de xarxa de Capa 2. El protocol de transport a la xarxa del centre de dades físiques és IP més UDP.

Figura 2–1 Diagrama de franges (crèdit)

Figura 2–2 Paquet VXLAN (crèdit)

Cada node treballador de Kubernetes assigna el seu propi / 24 d'espai d'adreces virtuals fora d'un bloc més gran / 9. Per a cada node, es tradueix en 1 entrada de taula de ruta, 1 entrada de taula ARP (a la interfície flannel.1) i 1 entrada de base de dades de reenviament (FDB). Aquests s’afegeixen quan el node treballador es llança per primer cop o a mesura que es descobreix cada nou node.

A més, la comunicació node-to-pod (o pod-to-pod) flueix finalment sobre la interfície eth0 (representada al diagrama de Flannel anterior). Això resultarà en una entrada addicional a la taula ARP per a cada font de node corresponent i destinació del node.

Al nostre entorn, aquest tipus de comunicació és molt habitual. Per als nostres objectes de servei Kubernetes, es crea un ELB i Kubernetes registra tots els nodes amb l'ELB. L'ELB no té coneixement del pod i és possible que el node seleccionat no sigui la destinació final del paquet. Això és degut a que quan el node rep el paquet de l’ELB, avalua les seves regles de iptables per al servei i selecciona aleatòriament una pod sobre un altre node.

En el moment de la paralització, hi havia 605 nodes totals al clúster. Per les raons exposades anteriorment, això va ser suficient per eclipsar el valor predeterminat de gc_thresh3. Un cop succeeix això, no només es deixen caure els paquets, sinó que falten Flannel / 24s sencers d’espai d’adreces virtuals a la taula ARP. Fallen el node per a la comunicació de pod i la cerca DNS. (El DNS s'allotja dins del clúster, tal i com s'explica amb més detall més endavant en aquest article.)

Per resoldre, els valors gc_thresh1, gc_thresh2 i gc_thresh3 es plantegen i cal reiniciar Flannel per tornar a registrar les xarxes que falten.

Execució inesperada de DNS a escala

Per acomodar la nostra migració, vam aprofitar molt els DNS per facilitar la conformació del trànsit i la reducció incremental del llegat a Kubernetes per als nostres serveis. Estableixem valors TTL relativament baixos als conjunts de registres Route53 associats. Quan utilitzàvem la nostra infraestructura antiga en instàncies EC2, la configuració del nostre resolver apuntava al DNS d'Amazon. Ens vam donar per fet i el cost d’un TTL relativament baix per als nostres serveis i els serveis d’Amazon (per exemple, DynamoDB) va passar en gran mesura desapercebut.

A mesura que vam incorporar cada vegada més serveis a Kubernetes, ens vam trobar amb un servei DNS que responia 250.000 peticions per segon. Ens hem trobat amb temps de cerca intermitents i impactants de cerca DNS a les nostres aplicacions. Això es va produir malgrat un exhaustiu esforç d’ajust i un proveïdor de DNS va canviar a un desplegament de CoreDNS que al mateix temps va assolir els 1.000 pods que consumien 120 nuclis.

Tot investigant altres possibles causes i solucions, hem trobat un article que descriu una condició de raça que afecta el netfilter del marc de filtratge de paquets Linux. Els temps de espera DNS que estàvem veient, juntament amb un comptador incrementant insert_failed a la interfície de Flannel, es van alinear amb les conclusions de l'article.

El problema es produeix durant la traducció de l'adreça de xarxa de la font i de la destinació (SNAT i DNAT) i la posterior inserció a la taula de conntrack. Una de les solucions discutides internament i proposades per la comunitat era traslladar el DNS al propi node del treballador. En aquest cas:

  • El SNAT no és necessari, perquè el trànsit es manté localment al node. No s'ha de transmetre a la interfície eth0.
  • El DNAT no és necessari perquè la IP de destinació és local del node i no una pod seleccionada aleatòriament per regles iptables.

Vam decidir avançar amb aquest enfocament. CoreDNS es va desplegar com a DaemonSet a Kubernetes i vam injectar el servidor DNS local del node a resolv.conf de cada pod mitjançant la configuració del kubelet - cluster-dns flag flag. La solució va ser efectiva en el temps d'espera de DNS.

Tanmateix, encara veiem paquets caiguts i l’increment del comptador insert_failed de la interfície de Flannel. Això persistirà fins i tot després de la solució anterior, ja que només hem evitat SNAT i / o DNAT per al trànsit DNS. L’estat de la cursa encara es produirà per a altres tipus de trànsit. Per sort, la majoria dels nostres paquets són TCP i quan es produeixi la condició, els paquets es retransmetran amb èxit. Una solució a llarg termini per a tot tipus de trànsit és alguna cosa que encara estem discutint.

Utilitzar l'enviat per aconseguir un millor equilibri de càrregues

Quan migràvem els nostres serveis de backend a Kubernetes, comencem a patir una càrrega desequilibrada a través de les beines. Vam descobrir que, a causa de HTTP Keepalive, les connexions ELB van quedar enganxades als primers pods preparats de cada desplegament rodat, de manera que la majoria de trànsit fluïa a través d’un petit percentatge de les beines disponibles. Una de les primeres mitigacions que vam intentar va ser utilitzar un MaxSurge 100% en nous desplegaments per als pitjors infractors. Aquest va ser marginalment eficaç i no sostenible a llarg termini amb alguns dels desplegaments més grans.

Una altra mitigació que vam utilitzar va ser inflar artificialment les sol·licituds de recursos en serveis crítics de manera que les beines col·loquejades tinguessin més cabina al costat d'altres podes pesades. Això tampoc no seria assumible a llarg termini a causa del malbaratament de recursos i les nostres aplicacions de node tenien un sol fil i, de manera efectiva, es podien captar a 1 nucli. L’única solució clara era utilitzar un millor equilibri de càrrega.

Internament havíem estat buscant avaluar Envoy. Això ens va permetre l'oportunitat de desplegar-ho de forma molt limitada i obtenir beneficis immediats. Envoy és un proxy de capa obert d’alt rendiment de capa 7 que està dissenyat per a arquitectures grans orientades al servei. És capaç d’implementar tècniques avançades d’equilibrament de càrregues, incloent-hi reintents automàtics, interrupció de circuits i limitació de velocitat global.

La configuració amb què vàrem fer era tenir una sidecar Envoy al costat de cada vaina que tingués una ruta i un clúster per assolir el port del contenidor local. Per minimitzar la cascada potencial i mantenir un petit radi de vol, es va utilitzar una flota de pods Envoy de proxy frontal, un desplegament a cada Zona de Disponibilitat (AZ) per a cada servei. Es va produir un petit mecanisme de descobriment de serveis que van combinar els nostres enginyers que simplement van retornar una llista de beines a cada AZ per a un servei determinat.

Els avant-enviats del servei van utilitzar aquest mecanisme de descoberta de serveis amb un cluster i una ruta amunt. Vam configurar terminis raonables, vam millorar tots els paràmetres de l’interruptor i després vam configurar una configuració mínima de reintentació per ajudar a les fallades transitòries i als desplegaments suaus. Vam fer front a cadascun d’aquests serveis frontals enviats amb un ELB TCP. Fins i tot si el manteniment de la nostra capa de proxy principal frontal es va fixar en determinades podes Envoy, van poder controlar la càrrega molt millor i es van configurar per equilibrar-se a través de la petició mínima.

Per a desplegaments, hem utilitzat un ganxo preStop tant a l’aplicació com a la guàrdia sidecar. Aquest ganxo anomenat endpoint endmin administrador de control de salut sidecar, juntament amb un somni petit, per donar un temps per permetre que les connexions inflight es completin i es drenin.

Una de les raons per les quals vam poder avançar tan ràpidament va ser a causa de les riques mètriques que vam poder integrar fàcilment amb la nostra configuració normal de Prometeu. Això ens va permetre veure exactament el que estava passant a mesura que repetíem la configuració de la configuració i reduïm el trànsit.

Els resultats van ser immediats i evidents. Comencem amb els serveis més desequilibrats i en aquest moment el funcionem davant dels dotze dels serveis més importants del nostre clúster. Aquest any tenim previst passar a una malla de servei complet, amb descobriment de serveis més avançat, interrupció del circuit, detecció exterior, limitació de velocitats i traçaments.

Figura 3–1. Convergència de la CPU d'un servei durant el transbordador per enviar

El resultat final

Mitjançant aquests aprenentatges i recerques addicionals, hem desenvolupat un equip intern d’infraestructures fort i amb una gran familiaritat sobre com dissenyar, desplegar i operar grans clústers de Kubernetes. Ara tota l’organització d’enginyeria de Tinder té coneixement i experiència sobre com contenir i desplegar les seves aplicacions a Kubernetes.

A la nostra infraestructura antiga, quan calia una escala addicional, hem patit sovint durant diversos minuts d’espera que vinguessin en línia les noves instàncies de l’EC2. Els contenidors ara programen i serveixen el trànsit en segons segons els minuts. La planificació de diversos contenidors en una sola instància EC2 també proporciona una densitat horitzontal millorada. Com a resultat, projectem un estalvi important de costos a EC2 el 2019 respecte a l'any anterior.

Van passar gairebé dos anys, però vam finalitzar la nostra migració el març del 2019. La plataforma Tinder funciona exclusivament en un clúster Kubernetes format per 200 serveis, 1.000 nodes, 15.000 podes i 48.000 contenidors en marxa. La infraestructura ja no és una tasca reservada als nostres equips d’operacions. En canvi, els enginyers de tota l'organització comparteixen aquesta responsabilitat i tenen control sobre com es construeixen i despleguen les seves aplicacions amb tot com a codi.