Tinders flytting til Kubernetes

Skrevet av: Chris O'Brien, Ingeniørsjef | Chris Thomas, Ingeniørsjef Jinyong Lee, Senior Software Engineer | Redigert av: Cooper Jackson, Software Engineer

Hvorfor

For snart to år siden bestemte Tinder seg for å flytte plattformen sin til Kubernetes. Kubernetes ga oss en mulighet til å drive Tinder Engineering mot containerisering og lav berøringsdrift gjennom uforanderlig distribusjon. Applikasjonsbygging, distribusjon og infrastruktur vil bli definert som kode.

Vi var også ute etter å møte utfordringer med skala og stabilitet. Når skaleringen ble kritisk, led vi ofte gjennom flere minutters ventetid på at nye EC2-tilfeller skulle komme på nettet. Ideen om containere som skulle planlegge og betjene trafikk i løpet av sekunder i motsetning til minutter var tiltalende for oss.

Det var ikke lett. Under migrasjonen vår tidlig i 2019 nådde vi kritisk masse innenfor Kubernetes-klyngen og begynte å møte forskjellige utfordringer på grunn av trafikkmengde, klyngestørrelse og DNS. Vi løste interessante utfordringer med å migrere 200 tjenester og drive en Kubernetes-klynge i skala på til sammen 1 000 noder, 15 000 pods og 48 000 løpende containere.

Hvordan

Fra januar 2018 jobbet vi oss gjennom forskjellige stadier av migrasjonsinnsatsen. Vi startet med å containere alle tjenestene våre og distribuere dem til en serie Kubernetes-vertscenesatte miljøer. Fra begynnelsen av oktober begynte vi metodisk å flytte alle våre tidligere tjenester til Kubernetes. I mars året etter fullførte vi migrasjonen vår, og Tinder-plattformen kjører nå utelukkende på Kubernetes.

Bygge bilder for Kubernetes

Det er mer enn 30 kildekodelagre for mikroservicene som kjører i Kubernetes-klyngen. Koden i disse depotene er skrevet på forskjellige språk (f.eks. Node.js, Java, Scala, Go) med flere kjøretidsmiljøer for det samme språket.

Byggesystemet er designet for å fungere på en fullt tilpassbar "build-kontekst" for hver mikroservice, som vanligvis består av en Dockerfile og en serie med skallkommandoer. Selv om innholdet er fullstendig tilpassbar, skrives disse byggesammenhengene ved å følge et standardisert format. Standardiseringen av build-sammenhenger gjør at et enkelt build-system kan håndtere alle mikroservices.

Figur 1–1 Standardisert byggeprosess gjennom Builder-beholderen

For å oppnå maksimal konsistens mellom kjøretidsmiljøer brukes den samme byggeprosessen i utviklings- og testfasen. Dette innebar en unik utfordring når vi trengte å utvikle en måte å garantere et konsistent byggemiljø på tvers av plattformen. Som et resultat blir alle byggeprosesser utført i en spesiell "Builder" -container.

Implementeringen av Builder-containeren krevde en rekke avanserte Docker-teknikker. Denne Builder-beholderen arver lokal bruker-ID og hemmeligheter (f.eks. SSH-nøkkel, AWS-legitimasjon osv.) Etter behov for å få tilgang til private Tinder-lagre. Den monterer lokale kataloger som inneholder kildekoden for å ha en naturlig måte å lagre byggegjenstander på. Denne tilnærmingen forbedrer ytelsen, fordi den eliminerer kopiering av bygde gjenstander mellom Builder-beholderen og vertsmaskinen. Lagrede gjenstander gjenbrukes neste gang uten ytterligere konfigurasjon.

For visse tjenester, trengte vi å opprette en annen beholder i Builder for å matche kompilertidsmiljøet med kjøretidsmiljøet (f.eks. Å installere Node.js bcrypt bibliotek genererer plattformspesifikke binære artefakter). Krav til kompileringstid kan variere mellom tjenester og den endelige Dockerfile er satt sammen.

Kubernetes Cluster Architecture And Migration

Cluster Sizing

Vi bestemte oss for å bruke kube-aws for automatisert klyngeprogramming i Amazon EC2-forekomster. Tidlig kjørte vi alt i ett generelt node basseng. Vi identifiserte raskt behovet for å skille ut arbeidsmengder i forskjellige størrelser og typer forekomster for å utnytte ressursene bedre. Resonnementet var at det å kjøre færre tungt gjengede belter sammen ga mer forutsigbare resultatresultater for oss enn å la dem sameksistere med et større antall enkeltrådede pods.

Vi slo oss til ro med:

  • m5.4xlarge for overvåking (Prometheus)
  • c5.4xlarge for Node.js arbeidsmengde (enkeltrådig arbeidsmengde)
  • c5.2xlarge for Java og Go (flertrådet arbeidsmengde)
  • c5.4xlarge for kontrollplanet (3 noder)

migrasjon

Et av forberedelsestrinnene for overføringen fra vår gamle infrastruktur til Kubernetes var å endre eksisterende tjeneste-til-tjeneste-kommunikasjon for å peke på nye Elastic Load Balancers (ELB) som ble opprettet i et spesifikt Virtual Private Cloud (VPC) subnet. Dette undernettet ble kikket til Kubernetes VPC. Dette tillot oss å granulere migrere moduler uten hensyn til spesifikk bestilling av serviceavhengigheter.

Disse sluttpunktene ble opprettet ved å bruke vektede DNS-postsett som hadde en CNAME som peker til hver nye ELB. For å kutte, la vi til en ny post, og pekte på den nye Kubernetes-tjenesten ELB, med en vekt på 0. Vi satte deretter Time To Live (TTL) på platesettet til 0. De gamle og nye vektene ble deretter sakte justert til til slutt ender opp med 100% på den nye serveren. Etter at overgangen var fullført, var TTL satt til noe mer fornuftig.

Java-modulene våre hedret lav DNS TTL, men våre Node-applikasjoner gjorde det ikke. En av våre ingeniører skrev om en del av tilkoblingsbassengkoden for å pakke den inn i en leder som vil friske opp bassengene hvert 60. år. Dette fungerte veldig bra for oss uten nevneverdig ytelse.

lærdommen

Nettverksstoffgrenser

I de tidlige morgentimene 8. januar 2019 fikk Tinders plattform et vedvarende strømbrudd. Som svar på en ikke-relatert økning i plattformsforsinkelse tidligere samme morgen, ble antall pod og node skalert på klyngen. Dette resulterte i utmattelse av ARP-cache på alle noder.

Det er tre Linux-verdier som er relevante for ARP-cachen:

Kreditt

gc_thresh3 er en hard cap. Hvis du får "nabobordoverløp" -loggoppføringer, indikerer dette at selv etter en synkron søppelsamling (GC) av ARP-cachen, var det ikke nok plass til å lagre nabooppføringen. I dette tilfellet slipper kjernen bare pakken helt.

Vi bruker Flannel som vårt nettverksstoff i Kubernetes. Pakker blir videresendt via VXLAN. VXLAN er et Layer 2-overleggsskjema over et Layer 3-nettverk. Den bruker MAC Address-in-User Datagram Protocol (MAC-in-UDP) innkapsling for å gi et middel til å utvide Layer 2-nettverkssegmenter. Transportprotokollen over det fysiske datasentralenettverket er IP pluss UDP.

Figur 2–1 Flanelldiagram (kreditt)

Figur 2–2 VXLAN-pakke (kreditt)

Hver Kubernetes-arbeidernode tildeler sin egen / 24 virtuelle adresserom fra en større / 9-blokk. For hver node resulterer dette i en rutetabelloppføring, 1 ARP-tabelloppføring (på flanell.1-grensesnitt) og 1 videresendedatabaseoppføring (FDB). Disse legges til når arbeiderknoden først lanseres, eller når hver nye node oppdages.

I tillegg flyter node-til-pod-kommunikasjon (eller pod-to-pod) -kommunikasjon til slutt over eth0-grensesnittet (avbildet i Flannel-diagrammet over). Dette vil resultere i en ekstra oppføring i ARP-tabellen for hver tilsvarende nodekilde og nodedestinasjon.

I vårt miljø er denne typen kommunikasjon veldig vanlig. For våre Kubernetes serviceobjekter opprettes en ELB og Kubernetes registrerer hver node hos ELB. ELB-enheten er ikke klar over pod, og den valgte noden er kanskje ikke pakkenes endelige destinasjon. Dette fordi når noden mottar pakken fra ELB, evaluerer den sine iptables-regler for tjenesten og velger tilfeldig en pod på en annen node.

På tidspunktet for strømbruddet var det 605 totale noder i klyngen. Av de grunnene som er skissert over, var dette nok til å formørke standard gc_thresh3-verdien. Når dette skjer, blir ikke bare pakker droppet, men hele Flannel / 24s virtuell adresseplass mangler fra ARP-tabellen. Node til pod-kommunikasjon og DNS-oppslag mislykkes. (DNS er hostet i klyngen, slik det vil bli forklart nærmere senere i denne artikkelen.)

For å løse dette blir verdiene gc_thresh1, gc_thresh2 og gc_thresh3 hevet, og Flannel må startes på nytt for å kunne registrere manglende nettverk på nytt.

Uventet kjører DNS på skala

For å imøtekomme migrasjonen vår, utnyttet vi DNS kraftig for å lette trafikkforming og trinnvis overgang fra arv til Kubernetes for våre tjenester. Vi setter relativt lave TTL-verdier på de tilhørende Route53 RecordSets. Da vi kjørte vår gamle infrastruktur i EC2-tilfeller, pekte vår resolverkonfigurasjon til Amazons DNS. Vi tok dette for gitt, og kostnadene for en relativt lav TTL for våre tjenester og Amazons tjenester (f.eks. DynamoDB) gikk stort sett ubemerket.

Da vi satte inn flere og flere tjenester til Kubernetes, fant vi oss selv en DNS-tjeneste som svarte på 250 000 forespørsler per sekund. Vi møtte periodiske og effektive tidsoppslag for DNS-oppslag i applikasjonene våre. Dette skjedde til tross for en uttømmende tuninginnsats og en DNS-leverandør byttet til en CoreDNS-distribusjon som på et tidspunkt nådde en topp på 1000 pods som forbrukte 120 kjerner.

Mens vi undersøkte andre mulige årsaker og løsninger, fant vi en artikkel som beskriver en rase-tilstand som påvirker Linux-pakkefiltreringsrammen netfilter. DNS-tidsavbruddene vi så, sammen med en økende insert_failed-teller på Flannel-grensesnittet, i samsvar med artikkelens funn.

Problemet oppstår under kilde- og destinasjonsnettverksadresse-oversettelse (SNAT og DNAT) og deretter innsetting i conntrack-tabellen. En løsning som ble diskutert internt og foreslått av samfunnet, var å flytte DNS til selve arbeiderknuten. I dette tilfellet:

  • SNAT er ikke nødvendig, fordi trafikken holder seg lokalt på noden. Det trenger ikke overføres over eth0-grensesnittet.
  • DNAT er ikke nødvendig fordi destinasjons-IP er lokal for noden og ikke en tilfeldig valgt pod per iptables-regler.

Vi bestemte oss for å gå videre med denne tilnærmingen. CoreDNS ble distribuert som et DaemonSet i Kubernetes, og vi injiserte nodens lokale DNS-server i hver pods resolv.conf ved å konfigurere kommandoflagget kubelet - cluster-dns. Løsningen var effektiv for DNS-timeouts.

Imidlertid ser vi fortsatt droppede pakker og Flannel-grensesnittets insert_failed tellerøkning. Dette vil vedvare selv etter ovennevnte løsning fordi vi bare unngikk SNAT og / eller DNAT for DNS-trafikk. Løpsforholdet vil fortsatt forekomme for andre typer trafikk. Heldigvis er de fleste av våre pakker TCP, og når tilstanden oppstår, vil pakker bli sendt videre. En langsiktig løsning for alle typer trafikk er noe vi fremdeles diskuterer.

Bruke utsending for å oppnå bedre belastningsbalanse

Da vi migrerte backend-tjenestene våre til Kubernetes, begynte vi å lide av ubalansert belastning over pods. Vi oppdaget at på grunn av HTTP Keepalive, ELB-tilkoblinger festet seg til de første klare podene i hver rullende distribusjon, så mest trafikk strømmet gjennom en liten prosentandel av de tilgjengelige podene. En av de første begrensningene vi prøvde, var å bruke en 100% MaxSurge på nye distribusjoner for de verste lovbryterne. Dette var marginalt effektivt og ikke bærekraftig på lang sikt med noen av de større distribusjonene.

En annen avbøtning vi brukte var å kunstig oppblåse ressursforespørsler på kritiske tjenester slik at colocated pods ville ha større takhøyde sammen med andre tunge pods. Dette skulle heller ikke bli holdbart i det lange løp på grunn av ressursavfall, og Node-applikasjonene våre var enkelt gjenget og dermed effektivt avdekket med en kjerne. Den eneste klare løsningen var å utnytte bedre lastbalansering.

Vi hadde internt sett etter å evaluere utsending. Dette ga oss en sjanse til å distribuere den på en veldig begrenset måte og høste umiddelbare fordeler. Envoy er en åpen kildekode, høyytelses-Layer 7-proxy designet for store serviceorienterte arkitekturer. Den er i stand til å implementere avanserte lastbalanseringsteknikker, inkludert automatiske forsøk, kretsbrudd og global hastighetsbegrensning.

Konfigurasjonen vi kom frem til var å ha en utsending-sidevogn langs hver pod som hadde en rute og klynge for å treffe den lokale containerhavnen. For å minimere potensiell cascading og for å holde en liten eksplosjonsradius, benyttet vi oss av en flåte av proxy-utsending-pods, en distribusjon i hver tilgjengelighetssone (AZ) for hver tjeneste. Disse treffer en liten tjenesteoppdagelsesmekanisme som en av våre ingeniører har satt sammen som ganske enkelt ga tilbake en liste med belter i hver AZ for en gitt tjeneste.

Tjenestefronten-utsendingene benyttet seg deretter av denne oppdagelsesmekanismen med en oppstrøms klynge og rute. Vi konfigurerte rimelige tidsavbrudd, økte alle innstillingene for effektbrytere og satte inn en minimal konfigurasjon på nytt for å hjelpe med kortvarige feil og jevn utrulling. Vi frontet hver av disse utsendingstjenestene med en TCP ELB. Selv om keepaliven fra vårt viktigste proxy-lag ble festet på bestemte Envoy pods, var de mye bedre i stand til å håndtere belastningen og ble konfigurert til å balansere via minst_request til backend.

For distribusjoner brukte vi en preStop-krok på både applikasjonen og sidevognspoden. Denne kroken kalte sidevognens helsekontroll mislykkes med endepunktet, sammen med en liten søvn, for å gi litt tid til å la inflight-tilkoblingene fullføre og renne ut.

En grunn til at vi kunne bevege oss så raskt, skyldtes de rike beregningene vi lett kunne integrere med vårt normale Prometheus-oppsett. Dette tillot oss å se nøyaktig hva som skjedde da vi iterert med konfigurasjonsinnstillinger og kuttet trafikken.

Resultatene var umiddelbare og åpenbare. Vi startet med de mest ubalanserte tjenestene og har på dette tidspunktet kjørt foran tolv av de viktigste tjenestene i klyngen vår. I år planlegger vi å flytte til et nettverk med full service, med mer avansert serviceoppdagelse, kretsbryting, spissdetektering, hastighetsbegrensning og sporing.

Figur 3–1 CPU-konvergens av en tjeneste under overgang til utsending

Sluttresultatet

Gjennom disse læringene og tilleggsforskningen har vi utviklet et sterkt internt infrastrukturteam med stor kunnskap om hvordan man designer, distribuerer og drifter store Kubernetes-klynger. Tinders hele ingeniørorganisasjon har nå kunnskap og erfaring om hvordan de kan containere og distribuere applikasjonene sine på Kubernetes.

Når det var behov for ytterligere skala på vår gamle infrastruktur, led vi ofte gjennom flere minutters ventetid på at nye EC2-tilfeller skulle komme på nettet. Beholdere planlegger og betjener nå trafikk i løpet av sekunder i motsetning til minutter. Å planlegge flere containere på en enkelt EC2-instans gir også forbedret horisontal tetthet. Som et resultat prosjekterer vi betydelige kostnadsbesparelser på EC2 i 2019 sammenlignet med året før.

Det tok nesten to år, men vi avsluttet migrasjonen vår i mars 2019. Tinder-plattformen kjører utelukkende på en Kubernetes-klynge som består av 200 tjenester, 1 000 noder, 15 000 pods og 48 000 løpende containere. Infrastruktur er ikke lenger en oppgave forbeholdt driftsgruppene våre. I stedet deler ingeniører i hele organisasjonen i dette ansvaret og har kontroll over hvordan applikasjonene deres er bygget og distribuert med alt som kode.