Tinder-övergång till Kubernetes

Notera. transl.: Anställda på den världsberömda Tinder-tjänsten delade nyligen några tekniska detaljer om att migrera sin infrastruktur till Kubernetes. Processen tog nästan två år och resulterade i lanseringen av en mycket storskalig plattform på K8s, bestående av 200 tjänster på 48 tusen containrar. Vilka intressanta svårigheter stötte Tinder-ingenjörerna på och vilka resultat kom de fram till? Läs den här översättningen.

Tinder-övergång till Kubernetes

Varför?

För nästan två år sedan bestämde sig Tinder för att flytta sin plattform till Kubernetes. Kubernetes skulle tillåta Tinder-teamet att containerisera och flytta till produktion med minimal ansträngning genom oföränderlig distribution (oföränderlig distribution). I det här fallet skulle sammansättningen av applikationer, deras distribution och själva infrastrukturen vara unikt definierad av kod.

Vi letade också efter en lösning på problemet med skalbarhet och stabilitet. När skalningen blev kritisk fick vi ofta vänta flera minuter på att nya EC2-instanser skulle snurra upp. Idén att lansera containrar och börja betjäna trafik på några sekunder istället för minuter blev väldigt attraktiv för oss.

Processen visade sig vara svår. Under vår migrering i början av 2019 nådde Kubernetes-klustret kritisk massa och vi började stöta på olika problem på grund av trafikvolym, klusterstorlek och DNS. Längs vägen löste vi många intressanta problem relaterade till migrering av 200 tjänster och underhåll av ett Kubernetes-kluster bestående av 1000 15000 noder, 48000 XNUMX pods och XNUMX XNUMX körande containrar.

Hur?

Sedan januari 2018 har vi gått igenom olika stadier av migration. Vi började med att behålla alla våra tjänster och distribuera dem till Kubernetes testmolnmiljöer. Från och med oktober började vi metodiskt migrera alla befintliga tjänster till Kubernetes. I mars följande år slutförde vi migreringen och nu körs Tinder-plattformen exklusivt på Kubernetes.

Bygga bilder för Kubernetes

Vi har över 30 källkodsförråd för mikrotjänster som körs på ett Kubernetes-kluster. Koden i dessa repositories är skriven på olika språk (till exempel Node.js, Java, Scala, Go) med flera runtime-miljöer för samma språk.

Byggsystemet är utformat för att tillhandahålla en helt anpassningsbar "byggkontext" för varje mikrotjänst. Den består vanligtvis av en Dockerfile och en lista med skalkommandon. Deras innehåll är helt anpassningsbart, och samtidigt är alla dessa byggkontexter skrivna enligt ett standardiserat format. Genom att standardisera byggkontexter kan ett enda byggsystem hantera alla mikrotjänster.

Tinder-övergång till Kubernetes
Bild 1-1. Standardiserad byggprocess via Builder-behållare

För att uppnå maximal överensstämmelse mellan körtiderna (runtime miljöer) samma byggprocess används under utveckling och testning. Vi stod inför en mycket intressant utmaning: vi var tvungna att utveckla ett sätt att säkerställa konsekvens i byggmiljön över hela plattformen. För att uppnå detta utförs alla monteringsprocesser i en speciell behållare. Builder.

Hans containerimplementering krävde avancerad Docker-teknik. Builder ärver det lokala användar-ID och hemligheter (som SSH-nyckel, AWS-uppgifter, etc.) som krävs för att komma åt privata Tinder-förråd. Den monterar lokala kataloger som innehåller källor för att naturligt lagra byggartefakter. Detta tillvägagångssätt förbättrar prestandan eftersom det eliminerar behovet av att kopiera byggartefakter mellan Builder-behållaren och värden. Lagrade byggartefakter kan återanvändas utan ytterligare konfiguration.

För vissa tjänster var vi tvungna att skapa en annan behållare för att mappa kompileringsmiljön till runtime-miljön (till exempel, Node.js bcrypt-biblioteket genererar plattformsspecifika binära artefakter under installationen). Under kompileringsprocessen kan kraven variera mellan tjänsterna, och den slutliga Dockerfilen kompileras i farten.

Kubernetes klusterarkitektur och migration

Hantering av klusterstorlek

Vi bestämde oss för att använda kube-aws för automatiserad klusterdistribution på Amazon EC2-instanser. I början fungerade allt i en gemensam pool av noder. Vi insåg snabbt behovet av att separera arbetsbelastningar efter storlek och instanstyp för att göra en effektivare användning av resurserna. Logiken var att körning av flera laddade flertrådiga pods visade sig vara mer förutsägbara när det gäller prestanda än deras samexistens med ett stort antal enkeltrådade pods.

Till slut bestämde vi oss för:

  • m5.4xstor — för övervakning (Prometheus);
  • c5.4xlarge - för Node.js-arbetsbelastning (entrådad arbetsbelastning);
  • c5.2xlarge - för Java och Go (flertrådad arbetsbelastning);
  • c5.4xlarge — för kontrollpanelen (3 noder).

migration

Ett av de förberedande stegen för att migrera från den gamla infrastrukturen till Kubernetes var att omdirigera den befintliga direkta kommunikationen mellan tjänsterna till de nya lastbalanserarna (Elastic Load Balancers (ELB). De skapades på ett specifikt undernät av ett virtuellt privat moln (VPC). Detta undernät var anslutet till en Kubernetes VPC. Detta gjorde det möjligt för oss att migrera moduler gradvis, utan att ta hänsyn till den specifika ordningen av tjänstberoenden.

Dessa slutpunkter skapades med hjälp av viktade uppsättningar av DNS-poster som hade CNAME:n som pekade på varje ny ELB. För att byta över lade vi till en ny post som pekar på den nya ELB för Kubernetes-tjänsten med vikten 0. Vi satte sedan in Time To Live (TTL) för posten som var inställd på 0. Efter detta var den gamla och nya vikten långsamt justerade, och så småningom skickades 100% av belastningen till en ny server. Efter att bytet var slutfört återgick TTL-värdet till en mer adekvat nivå.

Java-modulerna vi hade kunde klara låg TTL DNS, men Node-applikationerna kunde inte. En av ingenjörerna skrev om en del av anslutningspoolkoden och slog in den i en manager som uppdaterade poolerna var 60:e sekund. Det valda tillvägagångssättet fungerade mycket bra och utan någon märkbar prestandaförsämring.

Lektionerna

Nätverkets gränser

Tidigt på morgonen den 8 januari 2019 kraschade Tinder-plattformen oväntat. Som svar på en orelaterade ökning av plattformslatens tidigare samma morgon, ökade antalet pods och noder i klustret. Detta gjorde att ARP-cachen tog slut på alla våra noder.

Det finns tre Linux-alternativ relaterade till ARP-cachen:

Tinder-övergång till Kubernetes
(källa)

gc_thresh3 – Det här är en hård gräns. Uppkomsten av "grannbordsöverflöde"-poster i loggen innebar att även efter synkron sophämtning (GC) fanns det inte tillräckligt med utrymme i ARP-cachen för att lagra grannposten. I det här fallet kasserade kärnan helt enkelt paketet helt.

Vi använder Flanell som nätverksväv i Kubernetes. Paket sänds över VXLAN. VXLAN är en L2-tunnel som höjs ovanpå ett L3-nätverk. Tekniken använder MAC-in-UDP (MAC Address-in-User Datagram Protocol) inkapsling och tillåter expansion av Layer 2 nätverkssegment. Transportprotokollet på det fysiska datacenternätverket är IP plus UDP.

Tinder-övergång till Kubernetes
Bild 2–1. Flanelldiagram (källa)

Tinder-övergång till Kubernetes
Bild 2–2. VXLAN-paket (källa)

Varje Kubernetes-arbetsnod tilldelar ett virtuellt adressutrymme med en /24-mask från ett större /9-block. För varje nod är detta innebär en post i routingtabellen, en post i ARP-tabellen (på flanel.1-gränssnittet) och en post i switching-tabellen (FDB). De läggs till första gången en arbetarnod startas eller varje gång en ny nod upptäcks.

Dessutom går nod-pod-kommunikation (eller pod-pod) till slut genom gränssnittet eth0 (som visas i flanelldiagrammet ovan). Detta resulterar i ytterligare en post i ARP-tabellen för varje motsvarande källa och destinationsvärd.

I vår omgivning är den här typen av kommunikation väldigt vanlig. För tjänsteobjekt i Kubernetes skapas en ELB och Kubernetes registrerar varje nod med ELB. ELB vet ingenting om pods och den valda noden kanske inte är paketets slutdestination. Poängen är att när en nod tar emot ett paket från ELB, anser den att det tar hänsyn till reglerna iptables för en specifik tjänst och väljer slumpmässigt en pod på en annan nod.

Vid tidpunkten för felet fanns det 605 noder i klustret. Av ovan angivna skäl var detta tillräckligt för att övervinna betydelsen gc_thresh3, vilket är standard. När detta händer börjar inte bara paket släppas, utan hela Flannel virtuella adressutrymmet med en /24-mask försvinner från ARP-tabellen. Nod-pod-kommunikation och DNS-frågor avbryts (DNS är värd i ett kluster; läs längre fram i den här artikeln för mer information).

För att lösa detta problem måste du öka värdena gc_thresh1, gc_thresh2 и gc_thresh3 och starta om Flannel för att omregistrera de saknade nätverken.

Oväntad DNS-skalning

Under migreringsprocessen använde vi aktivt DNS för att hantera trafik och gradvis överföra tjänster från den gamla infrastrukturen till Kubernetes. Vi ställer in relativt låga TTL-värden för tillhörande RecordSets i Route53. När den gamla infrastrukturen kördes på EC2-instanser pekade vår resolverkonfiguration på Amazon DNS. Vi tog detta för givet och effekten av den låga TTL på våra tjänster och Amazon-tjänster (som DynamoDB) gick i stort sett obemärkt förbi.

När vi migrerade tjänster till Kubernetes upptäckte vi att DNS behandlade 250 tusen förfrågningar per sekund. Som ett resultat började applikationer uppleva konstanta och allvarliga timeouts för DNS-frågor. Detta hände trots otroliga ansträngningar för att optimera och byta DNS-leverantör till CoreDNS (som vid toppbelastning nådde 1000 pods som kördes på 120 kärnor).

När vi undersökte andra möjliga orsaker och lösningar upptäckte vi Artikel, som beskriver rasförhållanden som påverkar ramverket för paketfiltrering netfilter i Linux. Timeouterna vi observerade, tillsammans med en ökande räknare insert_failed i Flanell-gränssnittet överensstämde med resultaten av artikeln.

Problemet uppstår vid översättning av käll- och destinationsnätverksadress (SNAT och DNAT) och efterföljande införande i tabellen conntrack. En av lösningarna som diskuterades internt och föreslogs av communityn var att flytta DNS till själva arbetarnoden. I detta fall:

  • SNAT behövs inte eftersom trafiken stannar inne i noden. Den behöver inte dirigeras genom gränssnittet eth0.
  • DNAT behövs inte eftersom destinations-IP:n är lokal för noden och inte en slumpmässigt vald pod enligt reglerna iptables.

Vi bestämde oss för att hålla fast vid detta tillvägagångssätt. CoreDNS distribuerades som ett DaemonSet i Kubernetes och vi implementerade en lokal nod DNS-server i resolve.conf varje pod genom att sätta en flagga --kluster-dns kommandon kublett . Denna lösning visade sig vara effektiv för DNS-timeout.

Men vi såg fortfarande paketförlust och en ökning av räknaren insert_failed i Flanell-gränssnittet. Detta fortsatte efter att lösningen implementerades eftersom vi kunde eliminera SNAT och/eller DNAT endast för DNS-trafik. Tävlingsförhållandena bevarades för andra typer av trafik. Lyckligtvis är de flesta av våra paket TCP, och om ett problem uppstår sänds de helt enkelt om. Vi försöker fortfarande hitta en lämplig lösning för alla typer av trafik.

Använda Envoy för bättre lastbalansering

När vi migrerade backend-tjänster till Kubernetes började vi lida av obalanserad belastning mellan pods. Vi upptäckte att HTTP Keepalive fick ELB-anslutningar att hänga på de första färdiga poddarna för varje utrullning. Således gick huvuddelen av trafiken genom en liten andel av tillgängliga baljor. Den första lösningen vi testade var att sätta MaxSurge på 100 % på nya distributioner för värsta scenarier. Effekten visade sig vara obetydlig och föga lovande vad gäller större insatser.

En annan lösning vi använde var att på konstgjord väg öka resursbegäran för kritiska tjänster. I det här fallet skulle baljor placerade i närheten ha mer utrymme att manövrera jämfört med andra tunga baljor. Det skulle inte fungera i längden heller eftersom det skulle vara slöseri med resurser. Dessutom var våra Node-applikationer entrådiga och kunde följaktligen bara använda en kärna. Den enda riktiga lösningen var att använda bättre lastbalansering.

Vi har länge velat uppskatta till fullo Sändebud. Den nuvarande situationen gjorde det möjligt för oss att distribuera det på ett mycket begränsat sätt och få omedelbara resultat. Envoy är en högpresterande, öppen källkod, lager-XNUMX proxy designad för stora SOA-applikationer. Den kan implementera avancerade lastbalanseringstekniker, inklusive automatiska återförsök, strömbrytare och global hastighetsbegränsning. (Notera. transl.: Du kan läsa mer om detta i den här artikeln om Istio, som är baserad på Envoy.)

Vi kom fram till följande konfiguration: ha en Envoy sidovagn för varje pod och en enda rutt, och anslut klustret till containern lokalt via hamn. För att minimera potentiell överlappning och bibehålla en liten träffradie använde vi en flotta av Envoy front-proxy-pods, en per tillgänglighetszon (AZ) för varje tjänst. De förlitade sig på en enkel serviceupptäckningsmotor skriven av en av våra ingenjörer som helt enkelt returnerade en lista med pods i varje A-Ö för en given tjänst.

Service Front-Envoys använde sedan denna tjänsteupptäcktsmekanism med ett uppströmskluster och en rutt. Vi ställde in adekvata tidsgränser, ökade alla strömbrytarinställningar och lade till minimal konfiguration av försök igen för att hjälpa till med enstaka fel och säkerställa smidiga implementeringar. Vi placerade en TCP ELB framför var och en av dessa tjänstefrontsändningar. Även om keepalive från vårt huvudsakliga proxylager hade fastnat på vissa Envoy-pods, kunde de fortfarande hantera belastningen mycket bättre och var konfigurerade att balansera genom minst_request i backend.

För distribution använde vi preStop-kroken på både applikations- och sidvagns-kapslar. Kroken utlöste ett fel vid kontroll av statusen för administratörsändpunkten på sidovagnsbehållaren och gick i viloläge ett tag för att tillåta aktiva anslutningar att avslutas.

En av anledningarna till att vi kunde röra oss så snabbt beror på de detaljerade mätvärdena som vi enkelt kunde integrera i en typisk Prometheus-installation. Detta gjorde att vi kunde se exakt vad som hände medan vi justerade konfigurationsparametrar och omfördelade trafik.

Resultaten var omedelbara och uppenbara. Vi började med de mest obalanserade tjänsterna och för tillfället fungerar den framför de 12 viktigaste tjänsterna i klustret. I år planerar vi en övergång till ett fullservicenät med mer avancerad tjänsteupptäckt, kretsavbrott, detektering av extremvärden, hastighetsbegränsning och spårning.

Tinder-övergång till Kubernetes
Bild 3–1. CPU-konvergens av en tjänst under övergången till Envoy

Tinder-övergång till Kubernetes

Tinder-övergång till Kubernetes

Slutresultat

Genom denna erfarenhet och ytterligare forskning har vi byggt ett starkt infrastrukturteam med starka kunskaper i att designa, distribuera och driva stora Kubernetes-kluster. Alla Tinder-ingenjörer har nu kunskapen och erfarenheten att paketera containrar och distribuera applikationer till Kubernetes.

När behovet av ytterligare kapacitet uppstod på den gamla infrastrukturen fick vi vänta flera minuter på att nya EC2-instanser skulle lanseras. Nu börjar containrar köras och börjar bearbeta trafik inom några sekunder istället för minuter. Att schemalägga flera behållare på en enda EC2-instans ger också förbättrad horisontell koncentration. Som ett resultat förutspår vi en betydande minskning av EC2019-kostnaderna under 2 jämfört med förra året.

Migreringen tog nästan två år, men vi slutförde den i mars 2019. För närvarande körs Tinder-plattformen uteslutande på ett Kubernetes-kluster som består av 200 tjänster, 1000 15 noder, 000 48 pods och 000 XNUMX löpande containrar. Infrastruktur är inte längre den enda domänen för operationsteam. Alla våra ingenjörer delar detta ansvar och kontrollerar processen att bygga och distribuera sina applikationer med enbart kod.

PS från översättaren

Läs även en serie artiklar på vår blogg:

Källa: will.com

Lägg en kommentar