Tinders flytt till Kubernetes

Skrivet av: Chris O'Brien, teknikchef | Chris Thomas, teknikchef | Jinyong Lee, Senior Software Engineer | Redigerad av: Cooper Jackson, programvaruingenjör

Varför

För nästan två år sedan beslutade Tinder att flytta sin plattform till Kubernetes. Kubernetes gav oss en möjlighet att driva Tinder Engineering mot containerisering och drift med låg beröring genom immutable distribution. Applikationsbyggnad, distribution och infrastruktur skulle definieras som kod.

Vi var också ute efter att ta itu med utmaningar av skala och stabilitet. När skalningen blev kritisk led vi ofta genom flera minuters väntan på att nya EC2-instanser skulle komma online. Idén att containrar schemalägga och betjäna trafik inom några sekunder till skillnad från minuter var tilltalande för oss.

Det var inte lätt. Under vår migration i början av 2019 nådde vi kritisk massa i vårt Kubernetes-kluster och började möta olika utmaningar på grund av trafikvolym, klusterstorlek och DNS. Vi löste intressanta utmaningar för att migrera 200 tjänster och driva ett Kubernetes-kluster i skala om totalt 1 000 noder, 15 000 skidor och 48 000 körcontainrar.

På vilket sätt

Från och med januari 2018 arbetade vi oss igenom olika stadier av migrationsinsatsen. Vi började med att containerisera alla våra tjänster och distribuera dem till en serie Kubernetes-värdgivande miljöer. I början av oktober började vi metodiskt flytta alla våra gamla tjänster till Kubernetes. I mars året efter slutförde vi vår migration och Tinderplattformen körs nu exklusivt på Kubernetes.

Bygga bilder för Kubernetes

Det finns mer än 30 källkodförvar för mikroservicen som körs i Kubernetes-klustret. Koden i dessa förvar är skriven på olika språk (t.ex. Node.js, Java, Scala, Go) med flera runtime-miljöer för samma språk.

Build-systemet är utformat för att fungera på ett helt anpassningsbart "build-sammanhang" för varje mikroservice, som vanligtvis består av en Dockerfile och en serie skalkommandon. Även om deras innehåll är helt anpassningsbara, skrivs dessa byggsammanhang alla genom att följa ett standardiserat format. Standardiseringen av byggkontexterna tillåter ett enda build-system att hantera alla mikroservicer.

Figur 1–1 Standardiserad byggprocess genom Builder-behållaren

För att uppnå maximal konsistens mellan runtime-miljöer används samma byggprocess under utvecklings- och testfasen. Detta innebar en unik utmaning när vi behövde utforma ett sätt att garantera en konsekvent byggnadsmiljö över plattformen. Som ett resultat utförs alla byggprocesser i en speciell "Builder" -behållare.

Implementeringen av Builder-behållaren krävde ett antal avancerade Docker-tekniker. Denna Builder-behållare ärver lokalt användar-ID och hemligheter (t.ex. SSH-nyckel, AWS-referenser etc.) efter behov för att få åtkomst till privata lagringsplatser från Tinder. Den monterar lokala kataloger som innehåller källkoden för att ha ett naturligt sätt att lagra byggnadsföremål. Detta tillvägagångssätt förbättrar prestanda, eftersom det eliminerar kopiering av byggda artefakter mellan Builder-behållaren och värdmaskinen. Lagrade artefakter återanvändas nästa gång utan ytterligare konfiguration.

För vissa tjänster behövde vi skapa en annan behållare i Builder för att matcha kompileringstidsmiljön med körtidsmiljön (t.ex. genom att installera Node.js bcrypt-bibliotek genererar plattformspecifika binära artefakter). Kraven på sammanställningstider kan skilja sig åt mellan tjänsterna och den slutliga Dockerfile är sammansatt i farten.

Kubernetes Cluster Architecture And Migration

Cluster Sizing

Vi beslutade att använda kube-aws för automatiserad klustertillförsel i Amazon EC2-instanser. Tidigt körde vi allt i en generell nodpool. Vi identifierade snabbt behovet av att separera arbetsbelastningen i olika storlekar och typer av instanser för att bättre utnyttja resurserna. Resonemanget var att körning av färre tungt gängade skidor tillsammans gav mer förutsägbara resultat för oss än att låta dem samexistera med ett större antal enstrådiga skidor.

Vi nöjde oss med:

  • m5.4xlarge för övervakning (Prometheus)
  • c5.4xlarge för Node.js arbetsbelastning (enkeltrådig arbetsbelastning)
  • c5.2xlarge för Java och Go (flergängad arbetsbelastning)
  • c5.4xlarge för kontrollplanet (3 noder)

migration

Ett av förberedelsestegen för migrationen från vår gamla infrastruktur till Kubernetes var att ändra befintlig kommunikation mellan tjänster och tjänster för att peka på nya Elastic Load Balancers (ELB) som skapades i ett specifikt Virtual Private Cloud (VPC) subnät. Detta undernät kändes till Kubernetes VPC. Detta tillät oss att granulera migrera moduler utan hänsyn till specifik beställning av serviceavhängighet.

Dessa slutpunkter skapades med hjälp av vägda DNS-postuppsättningar som hade en CNAME som pekade på varje ny ELB. För att öka tillägget, lägger vi till en ny rekord som pekade på den nya Kubernetes-tjänsten ELB, med en vikt på 0. Vi satte sedan Time To Live (TTL) på skivuppsättningen till 0. De gamla och nya vikterna justerades sedan långsamt till slutligen slutar med 100% på den nya servern. Efter att övergången var klar inställdes TTL till något mer rimligt.

Våra Java-moduler hedrade låg DNS-TTL, men våra Node-applikationer gjorde det inte. En av våra ingenjörer skrivit om en del av anslutningspoolkoden för att pakera in den i en chef som skulle uppdatera poolerna var 60-tal. Detta fungerade mycket bra för oss utan någon märkbar prestationshit.

lärdomar

Nätverkstyggränser

Under de tidiga morgontimmarna 8 januari 2019 fick Tinders plattform ett konstant avbrott. Som svar på en oberoende ökning av plattformsfördröjningen tidigare samma morgon, skalades och räknar antalet på klustret. Detta resulterade i utmattning av ARP-cache på alla våra noder.

Det finns tre Linux-värden som är relevanta för ARP-cachen:

Kreditera

gc_thresh3 är ett hårt mössa. Om du får "grann tabellöverskridning" loggposter, indikerar detta att även efter en synkron avfallssamling (GC) i ARP-cache, fanns det inte tillräckligt med utrymme att lagra grannposten. I detta fall tappar kärnan bara paketet helt.

Vi använder Flannel som vårt nätverkstyg i Kubernetes. Paket skickas vidare via VXLAN. VXLAN är ett Layer 2-överläggsschema över ett Layer 3-nätverk. Den använder MAC Address-in-User Datagram Protocol (MAC-in-UDP) inkapsling för att tillhandahålla ett sätt att utöka Layer 2-nätverkssegment. Transportprotokollet över det fysiska datacenternätet är IP plus UDP.

Bild 2–1 Flanelldiagram (kredit)

Bild 2–2 VXLAN-paket (kredit)

Varje Kubernetes-arbetarnod tilldelar sitt eget / 24 virtuella adressutrymme ur ett större / 9-block. För varje nod resulterar detta i en ruttabellinmatning, 1 ARP-tabellinmatning (på flanell.1-gränssnitt) och 1 vidarebefordringsdatabas (FDB). Dessa läggs till när arbetarknoden först startas eller när varje ny nod upptäcks.

Dessutom flödar slutligen kommunikation mellan nod-till-pod (eller pod-to-pod) över eth0-gränssnittet (avbildat i Flannel-diagrammet ovan). Detta kommer att resultera i en ytterligare post i ARP-tabellen för varje motsvarande nodkälla och noddestination.

I vår miljö är denna typ av kommunikation mycket vanligt. För våra Kubernetes serviceobjekt skapas en ELB och Kubernetes registrerar varje nod med ELB. ELB är inte medveten om poden och den valda noden kanske inte är paketets slutdestination. Detta beror på att när noden tar emot paketet från ELB utvärderar den sina iptablesregler för tjänsten och väljer slumpmässigt en pod på en annan nod.

Vid avbrottet fanns det 605 totala noder i klustret. Av de skäl som anges ovan räckte detta för att förmörka standardvärdet gc_thresh3. När detta inträffar tappas inte bara paket, utan hela Flannel / 24s virtuella adressutrymme saknas i ARP-tabellen. Nod till pod-kommunikation och DNS-sökningar misslyckas. (DNS är värd inom klustret, vilket kommer att förklaras mer detaljerat senare i den här artikeln.)

För att lösa, höjs värdena gc_thresh1, gc_thresh2 och gc_thresh3 och Flannel måste startas om för att omregistrera saknade nätverk.

Oväntat kör DNS på skalan

För att tillgodose vår migration, utnyttjade vi DNS kraftigt för att underlätta trafikformning och stegvis övergång från arv till Kubernetes för våra tjänster. Vi ställer in relativt låga TTL-värden på de tillhörande Route53 RecordSets. När vi körde vår gamla infrastruktur i EC2-instanser pekade vår resolverkonfiguration på Amazons DNS. Vi tog detta för givet och kostnaden för en relativt låg TTL för våra tjänster och Amazons tjänster (t.ex. DynamoDB) gick till stor del obemärkt.

När vi tog fart på fler och fler tjänster till Kubernetes befann vi oss med en DNS-tjänst som svarade på 250 000 förfrågningar per sekund. Vi stötte på intermittenta och påverkande timeout för DNS-uppslag inom våra applikationer. Detta inträffade trots en uttömmande avstämningsinsats och en DNS-leverantör bytte till en CoreDNS-distribution som på en gång nådde en topp på 1 000 skidor som förbrukade 120 kärnor.

Medan vi undersöker andra möjliga orsaker och lösningar hittade vi en artikel som beskriver ett rasvillkor som påverkar Linux-paketfiltreringsramverket netfilter. DNS-timeouts som vi såg, tillsammans med en inkrementerande insert_failed-räknare på Flannel-gränssnittet, i linje med artikelns resultat.

Problemet inträffar under källa- och destinationsnätverksadressöversättning (SNAT och DNAT) och efterföljande införande i conntrack-tabellen. En lösning som diskuterades internt och föreslog av samhället var att flytta DNS till själva arbetarknoden. I detta fall:

  • SNAT är inte nödvändigt eftersom trafiken stannar lokalt på noden. Det behöver inte överföras över eth0-gränssnittet.
  • DNAT är inte nödvändigt eftersom destinationens IP är lokal för noden och inte en slumpmässigt vald pod per regler för iptables.

Vi beslutade att gå vidare med denna strategi. CoreDNS distribuerades som en DaemonSet i Kubernetes och vi injicerade nodens lokala DNS-server i varje pods resolv.conf genom att konfigurera kommandoflaggan kubelet - cluster-dns. Lösningen var effektiv för DNS-timeouts.

Men vi ser fortfarande tappade paket och Flannel-gränssnittets insert_failed räkneökning. Detta kommer att kvarstå även efter ovanstående lösning eftersom vi bara undviker SNAT och / eller DNAT för DNS-trafik. Racetillståndet kommer fortfarande att inträffa för andra typer av trafik. Lyckligtvis är de flesta av våra paket TCP och när villkoret inträffar kommer paketen att överföras framgångsrikt. En långsiktig fix för alla typer av trafik är något som vi fortfarande diskuterar.

Använd sändebud för att uppnå bättre belastningsbalans

När vi migrerade våra backendtjänster till Kubernetes började vi drabbas av obalanserad belastning över skidor. Vi upptäckte att på grund av HTTP Keepalive fastnade ELB-anslutningar till de första färdiga skidorna i varje rullande installation, så att mest trafik flödade genom en liten procentandel av de tillgängliga skidorna. En av de första begränsningarna vi försökte var att använda en 100% MaxSurge på nya utplaceringar för de värsta gärningsmännen. Detta var marginellt effektivt och inte hållbart på lång sikt med några av de större implementeringarna.

En annan begränsning som vi använde var att artificiellt blåsa resursförfrågningar på kritiska tjänster så att samlokaler skulle ha mer utrymme tillsammans med andra tunga skidor. Detta kommer inte heller att bli hållbart i det långa loppet på grund av resursavfall och våra Node-applikationer var enkeltrådiga och sålunda effektivt täckta med en kärna. Den enda tydliga lösningen var att använda bättre lastbalansering.

Vi hade internt letat efter att utvärdera sändebud. Detta gav oss en chans att distribuera det på ett mycket begränsat sätt och skörda omedelbara fördelar. Envoy är en open source, högpresterande Layer 7-proxy designad för stora serviceorienterade arkitekturer. Den kan implementera avancerade lastbalanseringstekniker, inklusive automatiska försök, kretsbrytning och global hastighetsbegränsning.

Konfigurationen vi kom fram till var att ha en sändebil för utsändare längs varje pod som hade en rutt och ett kluster för att träffa den lokala containerporten. För att minimera potentiell kaskad och för att hålla en liten sprängradie, använde vi en flotta av front-proxy Envoy-pods, en distribution i varje tillgänglighet Zone (AZ) för varje tjänst. Dessa träffade en liten mekanism för upptäckt av tjänster som en av våra ingenjörer satt ihop som helt enkelt returnerade en lista med skida i varje AZ för en viss tjänst.

Servicefront-utsändarna använde sedan denna mekanism för upptäckt av tjänster med en uppströms kluster och rutt. Vi konfigurerade rimliga tidsgränser, förstärkte alla inställningar för strömbrytare och satte sedan in en minimal konfiguration för att försöka hjälpa till med övergående fel och smidig installation. Vi frontade var och en av dessa front Envoy-tjänster med en TCP ELB. Även om Keepalive från vårt främre proxy-lager fastgjordes på vissa Envoy-skidor, var de mycket bättre i stånd att hantera belastningen och var konfigurerade för att balansera via minst_begäran till backend.

För implementeringar använde vi en preStop-krok på både applikationen och sidovagnens pod. Denna krok kallas sidovagnens hälsokontroll misslyckas med administratörens slutpunkt, tillsammans med en liten sömn, för att ge lite tid att låta inflight-anslutningarna slutföra och dränera.

En anledning till att vi kunde flytta så snabbt berodde på de rika mätvärden som vi enkelt kunde integrera med vår normala Prometheus-installation. Detta gjorde det möjligt för oss att se exakt vad som hände när vi upprepade konfigurationsinställningar och skar ner trafiken.

Resultaten var omedelbara och uppenbara. Vi började med de mest obalanserade tjänsterna och har nu kört framför tolv av de viktigaste tjänsterna i vårt kluster. I år planerar vi att flytta till ett fullservicenät, med mer avancerad upptäckt av tjänster, brytning av kretsar, upptäckt av utblickare, hastighetsbegränsning och spårning.

Figur 3–1 CPU-konvergens av en tjänst under övergång till sändebud

Slutresultatet

Genom dessa lärdomar och ytterligare forskning har vi utvecklat ett starkt internt infrastrukturteam med stor kunskap om hur man utformar, distribuerar och driver stora Kubernetes-kluster. Tinders hela ingenjörsorganisation har nu kunskap och erfarenhet av hur man kan containera och distribuera sina applikationer på Kubernetes.

När vi krävde ytterligare skala på vår gamla infrastruktur, led vi ofta genom flera minuters väntan på att nya EC2-instanser skulle komma online. Behållare planerar och betjänar nu trafik inom sekunder i motsats till minuter. Schemaläggning av flera behållare på en enda EC2-instans ger också förbättrad horisontell densitet. Som ett resultat beräknar vi betydande kostnadsbesparingar på EC2 2019 jämfört med föregående år.

Det tog nästan två år, men vi slutförde vår migration i mars 2019. Tinderplattformen körs uteslutande på ett Kubernetes-kluster bestående av 200 tjänster, 1 000 noder, 15 000 skidor och 48 000 körcontainrar. Infrastruktur är inte längre en uppgift som är reserverad för våra operationsteam. Istället delar ingenjörer i hela organisationen i detta ansvar och har kontroll över hur deras applikationer byggs och distribueras med allt som kod.