Tinders flytning til Kubernetes

Skrevet af: Chris O'Brien, ingeniørchef | Chris Thomas, ingeniørchef | Jinyong Lee, Senior Software Engineer | Redigeret af: Cooper Jackson, softwareingeniør

Hvorfor

For næsten to år siden besluttede Tinder at flytte sin platform til Kubernetes. Kubernetes gav os en mulighed for at drive Tinder Engineering mod containerisering og low-touch-drift gennem uforanderlig implementering. Applikationsopbygning, implementering og infrastruktur vil blive defineret som kode.

Vi var også på udkig efter at tackle udfordringer med skala og stabilitet. Da skalering blev kritisk, led vi ofte gennem flere minutters ventetid på, at nye EC2-tilfælde skulle komme online. Ideen om containere, der planlægger og betjener trafik inden for sekunder i modsætning til minutter, appellerede til os.

Det var ikke let. Under vores migration i begyndelsen af ​​2019 nåede vi kritisk masse inden for vores Kubernetes-klynge og begyndte at møde forskellige udfordringer på grund af trafikmængde, klyngestørrelse og DNS. Vi løste interessante udfordringer med at migrere 200 tjenester og køre en Kubernetes-klynge i skala i alt 1.000 knudepunkter, 15.000 pods og 48.000 kørende containere.

Hvordan

Fra januar 2018 arbejdede vi os gennem forskellige faser i migrationsindsatsen. Vi startede med at containere alle vores tjenester og distribuere dem til en række Kubernetes hostede iscenesættelsesmiljøer. Fra begyndelsen af ​​oktober begyndte vi metodisk at flytte alle vores gamle tjenester til Kubernetes. I marts året efter afsluttede vi vores migration, og Tinder-platformen kører nu udelukkende på Kubernetes.

Bygning af billeder til Kubernetes

Der er mere end 30 kildekodelagre til de mikroservices, der kører i Kubernetes-klyngen. Koden i disse lagre er skrevet på forskellige sprog (f.eks. Node.js, Java, Scala, Go) med flere runtime-miljøer for det samme sprog.

Build-systemet er designet til at fungere på en fuldt tilpasselig “build-kontekst” for hver mikroservice, som typisk består af en Dockerfile og en række shell-kommandoer. Mens deres indhold kan tilpasses fuldt ud, er disse build-kontekster alle skrevet ved at følge et standardiseret format. Standardiseringen af ​​build-sammenhængen giver et enkelt build-system mulighed for at håndtere alle mikroservices.

Figur 1–1 Standardiseret byggeproces gennem Builder-containeren

For at opnå den maksimale konsistens mellem runtime-miljøer bruges den samme build-proces i udviklings- og testfasen. Dette pålagde en unik udfordring, da vi var nødt til at udtænke en måde at garantere et ensartet bygningsmiljø på tværs af platformen. Som et resultat udføres alle build-processer i en speciel "Builder" -container.

Implementeringen af ​​Builder-containeren krævede en række avancerede Docker-teknikker. Denne Builder-container arver lokal bruger-ID og hemmeligheder (f.eks. SSH-nøgle, AWS-legitimationsoplysninger osv.) Efter behov for at få adgang til private Tinder-opbevaringssteder. Det monterer lokale biblioteker, der indeholder kildekoden, så de har en naturlig måde at gemme bygningsartikler. Denne tilgang forbedrer ydeevnen, fordi den eliminerer kopiering af bygget artefakter mellem Builder-containeren og værtsmaskinen. Gemte artefakter genbruges næste gang uden yderligere konfiguration.

For visse tjenester var vi nødt til at oprette en anden container i Builder for at matche kompileringstidsmiljøet med runtime-miljøet (f.eks. Ved at installere Node.js bcrypt bibliotek genererer platformspecifikke binære artefakter). Krav til kompileringstid kan variere mellem tjenester, og den endelige Dockerfile er sammensat undervejs.

Kubernetes Cluster Architecture And Migration

Cluster Sizing

Vi besluttede at bruge kube-aws til automatisk levering af klynger i Amazon EC2-tilfælde. Tidligt kørte vi alt i en generel nodepool. Vi identificerede hurtigt behovet for at opdele arbejdsmængder i forskellige størrelser og typer af forekomster for at udnytte ressourcerne bedre. Begrundelsen var, at kørsel af færre tungt trådede bælg sammen gav mere forudsigelige resultater for os end at lade dem sameksistere med et større antal enkelttrådede bælge.

Vi besluttede os med:

  • m5.4xlarge til overvågning (Prometheus)
  • c5.4xlarge til Node.js-arbejdsbelastning (enkelt-gevind arbejdsbelastning)
  • c5.2xlarge til Java og Go (multi-threaded workload)
  • c5.4xlarge til kontrolplanet (3 noder)

Migration

Et af forberedelsestrinnene til overgangen fra vores arveanlæg til Kubernetes var at ændre eksisterende service-til-service-kommunikation til at pege på nye Elastic Load Balancers (ELB'er), der blev oprettet i et specifikt Virtual Private Cloud (VPC) subnet. Dette undernet blev peered til Kubernetes VPC. Dette gjorde det muligt for os at granulere migrere moduler uden hensyntagen til specifik bestilling af serviceafhængigheder.

Disse slutpunkter blev oprettet ved hjælp af vægtede DNS-postsæt, der havde en CNAME, der peger på hver nye ELB. For at reducere tilføjede vi en ny rekord, der pegede på den nye Kubernetes-tjeneste ELB, med en vægt på 0. Vi satte derefter Time To Live (TTL) på postsættet til 0. De gamle og nye vægte blev derefter langsomt justeret til til sidst ender med 100% på den nye server. Efter at overgangen var afsluttet, blev TTL indstillet til noget mere rimeligt.

Vores Java-moduler hædret lavt DNS-TTL, men vores Node-applikationer gjorde det ikke. En af vores ingeniører omskrev en del af forbindelsespoolkoden for at indpakke den i en manager, der ville opdatere puljerne hver 60s. Dette fungerede meget godt for os uden nogen mærkbar performance hit.

learnings

Netværk stofgrænser

I de tidlige morgentimer den 8. januar 2019 led Tinders's platform en vedvarende strømafbrydelse. Som svar på en ikke-relateret stigning i platform latenstid tidligere samme morgen blev pod- og knudetællinger skaleret på klyngen. Dette resulterede i udmattelse af ARP-cache på alle vores noder.

Der er tre Linux-værdier, der er relevante for ARP-cachen:

Kredit

gc_thresh3 er en hård hætte. Hvis du får "nabobordoverløb" -loggposter, indikerer dette, at selv efter en synkron affaldsopsamling (GC) i ARP-cachen, var der ikke nok plads til at gemme naboindgangen. I dette tilfælde taber kernen bare pakken helt.

Vi bruger Flannel som vores netværksstof i Kubernetes. Pakker videresendes via VXLAN. VXLAN er et Layer 2-overlayskema over et Layer 3-netværk. Den bruger MAC Address-in-User Datagram Protocol (MAC-in-UDP) indkapsling til at tilvejebringe et middel til at udvide Layer 2-netværkssegmenter. Transportprotokollen over det fysiske datacenternetværk er IP plus UDP.

Figur 2–1 Flanelldiagram (kredit)

Figur 2–2 VXLAN-pakke (kredit)

Hver Kubernetes-arbejderknude tildeler sin egen / 24 virtuelle adresseplads ud af en større / 9-blok. For hver knudepunkt resulterer dette i 1 rutetabellepost, 1 ARP-tabelindgang (på flannel.1-interface) og 1 viderestillingsdatabase (FDB) -indgang. Disse tilføjes, når arbejderknuden først lanceres, eller når hver ny knude opdages.

Derudover flyder node-til-pod (eller pod-til-pod) -kommunikation i sidste ende over eth0-grænsefladen (afbildet i Flannel-diagrammet ovenfor). Dette resulterer i en yderligere indgang i ARP-tabellen for hver tilsvarende nodekilde og nodedestination.

I vores miljø er denne form for kommunikation meget almindelig. For vores Kubernetes serviceobjekter oprettes en ELB, og Kubernetes registrerer hver node med ELB. ELB er ikke pod-opmærksom, og den valgte knude er muligvis ikke pakkens endelige destination. Dette skyldes, at når noden modtager pakken fra ELB, evaluerer den dens iptables-regler for tjenesten og vælger tilfældigt en pod på en anden knude.

På tidspunktet for udfaldet var der 605 samlede knuder i klyngen. Af de grunde, der er skitseret ovenfor, var dette nok til at formørge standardgc_thresh3-værdien. Når dette sker, tabes ikke kun pakker, men hele Flannel / 24'erne af virtuel adresserum mangler fra ARP-tabellen. Knude til pod-kommunikation og DNS-opslag mislykkes. (DNS er hostet i klyngen, som det vil blive forklaret mere detaljeret senere i denne artikel.)

For at løse dette hæves værdierne gc_thresh1, gc_thresh2 og gc_thresh3, og Flannel skal genstartes for at omregistrere manglende netværk.

Uventet kører DNS på skala

For at imødekomme vores migration udnyttede vi DNS kraftigt for at lette trafikformning og trinvis overgang fra arv til Kubernetes for vores tjenester. Vi indstiller relativt lave TTL-værdier på de tilknyttede Route53 RecordSets. Da vi kørte vores gamle infrastruktur i EC2-tilfælde, pegede vores resolverkonfiguration på Amazons DNS. Vi tog dette for givet, og omkostningerne ved en relativt lav TTL for vores tjenester og Amazons tjenester (f.eks. DynamoDB) gik stort set ubemærket.

Da vi ombordbragte flere og flere tjenester til Kubernetes, befandt vi os med en DNS-service, der besvarede 250.000 anmodninger pr. Sekund. Vi stødte på periodiske og effektive DNS-opslag i vores applikationer. Dette skete på trods af en udtømmende tuningindsats og en DNS-udbyder skiftede til en CoreDNS-installation, der på et tidspunkt toppede med 1.000 pods, der forbruger 120 kerner.

Mens vi undersøgte andre mulige årsager og løsninger, fandt vi en artikel, der beskriver en race-tilstand, der påvirker Linux-pakkefiltreringsrammen netfilter. De DNS-timeouts, vi så, sammen med en inkrementerende insert_failed-tæller på Flannel-grænsefladen, tilpasset artikelens fund.

Problemet opstår under kilde- og destinationsnetværksadresse-oversættelse (SNAT og DNAT) og efterfølgende indsættelse i conntrack-tabellen. En løsning, der blev drøftet internt og foreslået af samfundet, var at flytte DNS til selve arbejderknuden. I dette tilfælde:

  • SNAT er ikke nødvendigt, fordi trafikken forbliver lokalt på noden. Det behøver ikke at overføres på tværs af eth0-grænsefladen.
  • DNAT er ikke nødvendigt, fordi destinations-IP'en er lokal for noden og ikke en tilfældigt valgt pod pr. Iptables-regler.

Vi besluttede at gå videre med denne tilgang. CoreDNS blev distribueret som et DaemonSet i Kubernetes, og vi injicerede nodens lokale DNS-server i hver pods resolv.conf ved at konfigurere kommandoflagget kubelet - cluster-dns. Løsningen var effektiv til DNS-timeouts.

Vi ser dog stadig faldne pakker og Flannel-interfaceens insert_failed tællerforøgelse. Dette vil fortsætte, selv efter ovenstående løsning, fordi vi kun undgik SNAT og / eller DNAT for DNS-trafik. Løbstilstanden vil stadig forekomme for andre typer trafik. Heldigvis er de fleste af vores pakker TCP, og når betingelsen opstår, vil pakker blive sendt videre. En langvarig løsning for alle typer trafik er noget, vi stadig diskuterer.

Brug af udsending for at opnå bedre belastningsbalance

Da vi migrerede vores backend-tjenester til Kubernetes, begyndte vi at lide under ubalanceret belastning på tværs af bælg. Vi opdagede, at på grund af HTTP Keepalive blev ELB-forbindelser klæbet fast til de første paraplyer i hver rullende installation, så mest trafik flydede gennem en lille procentdel af de tilgængelige pods. En af de første begrænsninger, vi forsøgte, var at bruge en 100% MaxSurge på nye indsættelser til de værste lovovertrædere. Dette var marginalt effektivt og ikke bæredygtigt på lang sigt med nogle af de større implementeringer.

En anden afhjælpning, vi brugte, var at kunstigt opblæse ressourceanmodninger på kritiske tjenester, så colocated pods ville have mere lofthøjde sammen med andre tunge bælg. Dette blev heller ikke holdbart i det lange løb på grund af ressourceaffald, og vores Node-applikationer var enkelttrådede og dermed effektivt afgrænset ved 1 kerne. Den eneste klare løsning var at bruge bedre belastningsbalancering.

Vi havde internt været på udkig efter at evaluere udsending. Dette gav os en chance for at indsætte det på en meget begrænset måde og høste øjeblikkelige fordele. Envoy er en open source, højtydende Layer 7-proxy designet til store serviceorienterede arkitekturer. Det er i stand til at implementere avancerede belastningsafbalanceringsteknikker, inklusive automatiske forsøg, kredsløb og global hastighedsbegrænsning.

Den konfiguration, vi kom frem til, var at have en udsendes sidevogn langs hver pod, der havde en rute og klynge, der ramte den lokale containerhavn. For at minimere potentiel cascading og for at holde en lille eksplosionsradius brugte vi en flåde af front-proxy Envoy pods, en distribution i hver Tilgængelighedszone (AZ) til hver service. Disse ramte en lille serviceopdagelsesmekanisme, som en af ​​vores ingeniører sammensatte, der simpelthen returnerede en liste med bælge i hver AZ for en given service.

Tjeneste-frontsendingerne benyttede derefter denne serviceopdagelsesmekanisme med en opstrøms klynge og rute. Vi konfigurerede rimelige timeouts, øgede alle indstillingerne for afbryder og satte derefter en minimal konfiguration igen for at hjælpe med kortvarige fejl og glatte implementeringer. Vi frontede hver af disse front Envoy-tjenester med en TCP ELB. Selv hvis keepaliven fra vores vigtigste proxy-lag blev fastgjort på bestemte Envoy-pods, var de meget bedre i stand til at håndtere belastningen og var konfigureret til at balancere via mindst_anmodning til backend.

Til implementeringer brugte vi en preStop-krog på både applikationen og sidevognens pod. Denne krog kaldes sidevognens sundhedscheck mislykkes af admin-endepunktet sammen med en lille søvn for at give lidt tid til at lade inflight-forbindelserne fuldføre og dræne.

En af grundene til, at vi var i stand til at bevæge os så hurtigt, skyldtes de rige metrics, som vi let kunne integrere med vores normale Prometheus-opsætning. Dette gjorde det muligt for os at se nøjagtigt, hvad der skete, da vi itererede med konfigurationsindstillinger og skar trafik over.

Resultaterne var øjeblikkelige og åbenlyse. Vi startede med de mest ubalancerede tjenester og har på dette tidspunkt kørt foran tolv af de vigtigste tjenester i vores klynge. I år planlægger vi at flytte til et net med fuld service med mere avanceret serviceopdagelse, kredsløbsafbrydelse, outlier-detektion, hastighedsbegrænsning og sporing.

Figur 3–1 CPU-konvergens af en tjeneste under overgang til udsending

Slutresultatet

Gennem disse erfaringer og yderligere forskning har vi udviklet et stærkt internt infrastrukturteam med stor fortrolighed om, hvordan man designer, implementerer og driver store Kubernetes-klynger. Tinders hele ingeniørorganisation har nu viden og erfaring om, hvordan man kan containere og implementere deres applikationer på Kubernetes.

Når vores ekstra skala var påkrævet på vores arv, har vi ofte lidt gennem flere minutters ventetid på, at nye EC2-tilfælde kom online. Containere planlægger og betjener nu trafik inden for sekunder i modsætning til minutter. Planlægning af flere containere på et enkelt EC2-tilfælde giver også forbedret vandret densitet. Som et resultat projicerer vi betydelige omkostningsbesparelser på EC2 i 2019 sammenlignet med året før.

Det tog næsten to år, men vi færdiggjorde vores migration i marts 2019. Tinder-platformen kører udelukkende på en Kubernetes-klynge, der består af 200 tjenester, 1.000 knudepunkter, 15.000 pods og 48.000 kørende containere. Infrastruktur er ikke længere en opgave forbeholdt vores operationsteams. I stedet deler ingeniører i hele organisationen i dette ansvar og har kontrol over, hvordan deres applikationer er bygget og implementeret med alt som kode.