Tinder-overgang til Kubernetes

Bemærk. overs.: Medarbejdere i den verdensberømte Tinder-tjeneste delte for nylig nogle tekniske detaljer om migrering af deres infrastruktur til Kubernetes. Processen tog næsten to år og resulterede i lanceringen af ​​en meget storstilet platform på K8s, bestående af 200 tjenester hostet på 48 tusind containere. Hvilke interessante vanskeligheder stødte Tinder-ingeniørerne på, og hvilke resultater kom de til? Læs denne oversættelse.

Tinder-overgang til Kubernetes

Hvorfor?

For næsten to år siden besluttede Tinder at flytte sin platform til Kubernetes. Kubernetes ville give Tinder-teamet mulighed for at containerisere og flytte til produktion med minimal indsats gennem uforanderlig implementering (uforanderlig implementering). I dette tilfælde vil samlingen af ​​applikationer, deres implementering og selve infrastrukturen være entydigt defineret af kode.

Vi ledte også efter en løsning på problemet med skalerbarhed og stabilitet. Når skalering blev kritisk, var vi ofte nødt til at vente flere minutter på, at nye EC2-forekomster snurrede op. Idéen om at lancere containere og begynde at betjene trafik på få sekunder i stedet for minutter blev meget attraktiv for os.

Processen viste sig at være svær. Under vores migrering i begyndelsen af ​​2019 nåede Kubernetes-klyngen kritisk masse, og vi begyndte at støde på forskellige problemer på grund af trafikmængde, klyngestørrelse og DNS. Undervejs løste vi en masse interessante problemer i forbindelse med migrering af 200 tjenester og vedligeholdelse af en Kubernetes-klynge bestående af 1000 noder, 15000 pods og 48000 kørende containere.

Hvordan?

Siden januar 2018 har vi været igennem forskellige stadier af migration. Vi startede med at containerisere alle vores tjenester og implementere dem til Kubernetes test cloud-miljøer. Fra oktober begyndte vi metodisk at migrere alle eksisterende tjenester til Kubernetes. I marts det følgende år afsluttede vi migreringen, og nu kører Tinder-platformen udelukkende på Kubernetes.

Opbygning af billeder til Kubernetes

Vi har over 30 kildekodelagre til mikrotjenester, der kører på en Kubernetes-klynge. Koden i disse repositories er skrevet på forskellige sprog (for eksempel Node.js, Java, Scala, Go) med flere runtime-miljøer for det samme sprog.

Byggesystemet er designet til at give en fuldt tilpasselig "byggekontekst" for hver mikrotjeneste. Det består normalt af en Dockerfile og en liste over shell-kommandoer. Deres indhold er fuldstændigt tilpasseligt, og samtidig er alle disse byggekontekster skrevet i et standardiseret format. Standardisering af byggekontekster gør det muligt for et enkelt byggesystem at håndtere alle mikrotjenester.

Tinder-overgang til Kubernetes
Figur 1-1. Standardiseret byggeproces via Builder container

For at opnå maksimal sammenhæng mellem kørselstiderne (runtime miljøer) den samme byggeproces bruges under udvikling og test. Vi stod over for en meget interessant udfordring: Vi skulle udvikle en måde at sikre konsistens i byggemiljøet på tværs af hele platformen. For at opnå dette udføres alle monteringsprocesser i en speciel beholder. Builder.

Hans containerimplementering krævede avancerede Docker-teknikker. Builder arver det lokale bruger-id og hemmeligheder (såsom SSH-nøgle, AWS-legitimationsoplysninger osv.), der kræves for at få adgang til private Tinder-lagre. Det monterer lokale mapper, der indeholder kilder til naturligt at gemme byggeartefakter. Denne tilgang forbedrer ydeevnen, fordi den eliminerer behovet for at kopiere byggeartefakter mellem Builder-beholderen og værten. Lagrede byggeartefakter kan genbruges uden yderligere konfiguration.

For nogle tjenester var vi nødt til at oprette en anden container for at kortlægge kompileringsmiljøet til runtime-miljøet (for eksempel genererer Node.js bcrypt-biblioteket platformsspecifikke binære artefakter under installationen). Under kompileringsprocessen kan kravene variere mellem tjenesterne, og den endelige Dockerfil kompileres på farten.

Kubernetes klyngearkitektur og migration

Klyngestørrelsesstyring

Vi besluttede at bruge kube-aws til automatiseret klyngeimplementering på Amazon EC2-instanser. I begyndelsen fungerede alt i én fælles pulje af noder. Vi indså hurtigt behovet for at adskille arbejdsbelastninger efter størrelse og instanstype for at gøre mere effektiv brug af ressourcer. Logikken var, at kørsel af flere indlæste multi-threaded pods viste sig at være mere forudsigelig med hensyn til ydeevne end deres sameksistens med et stort antal single-threaded pods.

Til sidst blev vi enige om:

  • m5.4xstor — til overvågning (Prometheus);
  • c5.4xlarge - for Node.js-arbejdsbelastning (entråds-arbejdsbelastning);
  • c5.2xlarge - til Java og Go (multithreaded workload);
  • c5.4xlarge — til kontrolpanelet (3 knudepunkter).

Migrationen

Et af de forberedende trin til migrering fra den gamle infrastruktur til Kubernetes var at omdirigere den eksisterende direkte kommunikation mellem tjenester til de nye belastningsbalancere (Elastic Load Balancers (ELB). De blev oprettet på et specifikt undernet af en virtuel privat sky (VPC). Dette undernet var forbundet til en Kubernetes VPC. Dette gjorde det muligt for os at migrere moduler gradvist uden at tage hensyn til den specifikke rækkefølge af serviceafhængigheder.

Disse endepunkter blev oprettet ved hjælp af vægtede sæt af DNS-poster, der havde CNAME'er, der pegede på hver ny ELB. For at skifte over tilføjede vi en ny post, der peger på den nye ELB for Kubernetes-tjenesten med en vægt på 0. Vi satte derefter Time To Live (TTL) for posten sat til 0. Herefter blev den gamle og den nye vægt langsomt justeret, og til sidst blev 100% af belastningen sendt til en ny server. Efter at skiftet var afsluttet, vendte TTL-værdien tilbage til et mere passende niveau.

De Java-moduler, vi havde, kunne klare lav TTL DNS, men det kunne Node-applikationerne ikke. En af ingeniørerne omskrev en del af forbindelsespoolkoden og pakkede den ind i en manager, der opdaterede puljerne hvert 60. sekund. Den valgte tilgang fungerede meget godt og uden nogen mærkbar ydelsesforringelse.

Lektionerne

Netværksstoffets grænser

Tidligt om morgenen den 8. januar 2019 styrtede Tinder-platformen uventet ned. Som svar på en ikke-relateret stigning i platformsforsinkelse tidligere samme morgen steg antallet af pods og noder i klyngen. Dette fik ARP-cachen til at være opbrugt på alle vores noder.

Der er tre Linux-muligheder relateret til ARP-cachen:

Tinder-overgang til Kubernetes
(kilde)

gc_tærsk3 - det er en hård grænse. Forekomsten af ​​"nabobordoverløb"-poster i loggen betød, at selv efter synkron affaldsopsamling (GC), var der ikke nok plads i ARP-cachen til at gemme naboposten. I dette tilfælde kasserede kernen simpelthen pakken fuldstændigt.

Vi bruger flannel som et netværksstof i Kubernetes. Pakker transmitteres over VXLAN. VXLAN er en L2-tunnel, der er rejst oven på et L3-netværk. Teknologien bruger MAC-in-UDP (MAC Address-in-User Datagram Protocol) indkapsling og tillader udvidelse af Layer 2 netværkssegmenter. Transportprotokollen på det fysiske datacenternetværk er IP plus UDP.

Tinder-overgang til Kubernetes
Figur 2–1. Flanelldiagram (kilde)

Tinder-overgang til Kubernetes
Figur 2–2. VXLAN-pakke (kilde)

Hver Kubernetes-arbejderknude tildeler et virtuelt adresseområde med en /24-maske fra en større /9-blok. For hver node er dette betyder én indgang i routingtabellen, én indgang i ARP-tabellen (på flannel.1-grænsefladen) og én indgang i koblingstabellen (FDB). De tilføjes første gang, en arbejderknude startes, eller hver gang en ny knude opdages.

Derudover går node-pod (eller pod-pod) kommunikation i sidste ende gennem grænsefladen eth0 (som vist i flanneldiagrammet ovenfor). Dette resulterer i en ekstra indtastning i ARP-tabellen for hver tilsvarende kilde og destinationsvært.

I vores miljø er denne form for kommunikation meget almindelig. For serviceobjekter i Kubernetes oprettes en ELB, og Kubernetes registrerer hver node med ELB. ELB'en ved intet om pods, og den valgte node er muligvis ikke den endelige destination for pakken. Pointen er, at når en node modtager en pakke fra ELB, betragter den den under hensyntagen til reglerne iptables for en specifik tjeneste og vælger tilfældigt en pod på en anden node.

På tidspunktet for fejlen var der 605 noder i klyngen. Af ovennævnte grunde var dette tilstrækkeligt til at overvinde betydningen gc_tærsk3, som er standard. Når dette sker, begynder pakker ikke kun at blive droppet, men hele Flannel virtuelle adresserum med en /24-maske forsvinder fra ARP-tabellen. Node-pod-kommunikation og DNS-forespørgsler afbrydes (DNS hostes i en klynge; læs senere i denne artikel for detaljer).

For at løse dette problem skal du øge værdierne gc_tærsk1, gc_tærsk2 и gc_tærsk3 og genstart Flannel for at genregistrere de manglende netværk.

Uventet DNS-skalering

Under migreringsprocessen brugte vi aktivt DNS til at styre trafik og gradvist overføre tjenester fra den gamle infrastruktur til Kubernetes. Vi sætter relativt lave TTL-værdier for tilhørende RecordSets i Route53. Da den gamle infrastruktur kørte på EC2-instanser, pegede vores resolver-konfiguration på Amazon DNS. Vi tog dette for givet, og virkningen af ​​den lave TTL på vores tjenester og Amazon-tjenester (såsom DynamoDB) gik stort set ubemærket hen.

Da vi migrerede tjenester til Kubernetes, fandt vi ud af, at DNS behandlede 250 tusinde anmodninger i sekundet. Som et resultat begyndte applikationer at opleve konstante og alvorlige timeouts for DNS-forespørgsler. Dette skete på trods af en utrolig indsats for at optimere og skifte DNS-udbyderen til CoreDNS (som ved spidsbelastning nåede op på 1000 pods, der kører på 120 kerner).

Mens vi undersøgte andre mulige årsager og løsninger, opdagede vi en artikel, der beskriver raceforhold, der påvirker pakkefiltreringsrammerne netfilter i Linux. De timeouts, vi observerede, kombineret med en stigende tæller insert_failed i Flannel-grænsefladen var i overensstemmelse med artiklens resultater.

Problemet opstår på stadiet af kilde- og destinationsnetværksadresseoversættelse (SNAT og DNAT) og efterfølgende indtastning i tabellen conntrack. En af de løsninger, der blev diskuteret internt og foreslået af fællesskabet, var at flytte DNS'en til selve arbejdernoden. I dette tilfælde:

  • SNAT er ikke nødvendig, fordi trafikken forbliver inde i noden. Det behøver ikke at blive dirigeret gennem grænsefladen eth0.
  • DNAT er ikke nødvendigt, da destinations-IP'en er lokal for noden og ikke en tilfældigt valgt pod i henhold til reglerne iptables.

Vi besluttede at holde fast i denne tilgang. CoreDNS blev implementeret som et DaemonSet i Kubernetes, og vi implementerede en lokal node DNS-server i resolve.conf hver pod ved at sætte et flag --cluster-dns hold kubelet . Denne løsning viste sig at være effektiv til DNS-timeouts.

Vi oplevede dog stadig pakketab og en stigning i tælleren insert_failed i Flanell-grænsefladen. Dette fortsatte efter at løsningen blev implementeret, fordi vi var i stand til at eliminere SNAT og/eller DNAT kun for DNS-trafik. Raceforhold blev bevaret for andre typer trafik. Heldigvis er de fleste af vores pakker TCP, og hvis der opstår et problem, bliver de simpelthen gentransmitteret. Vi forsøger stadig at finde en passende løsning til alle typer trafik.

Brug af Envoy til bedre belastningsbalancering

Da vi migrerede backend-tjenester til Kubernetes, begyndte vi at lide af ubalanceret belastning mellem pods. Vi fandt ud af, at HTTP Keepalive fik ELB-forbindelser til at hænge på de første færdige pods af hver implementering, der blev rullet ud. Således gik hovedparten af ​​trafikken gennem en lille procentdel af tilgængelige pods. Den første løsning, vi testede, var at sætte MaxSurge til 100 % på nye implementeringer til worst case-scenarier. Effekten viste sig at være ubetydelig og ikke lovende i forhold til større udsendelser.

En anden løsning, vi brugte, var kunstigt at øge ressourceanmodninger til kritiske tjenester. I dette tilfælde ville bælg placeret i nærheden have mere plads til at manøvrere sammenlignet med andre tunge bælg. Det ville heller ikke fungere i længden, fordi det ville være spild af ressourcer. Derudover var vores Node-applikationer enkelttrådede og kunne derfor kun bruge én kerne. Den eneste rigtige løsning var at bruge bedre belastningsbalancering.

Vi har længe ønsket at værdsætte fuldt ud udsending. Den nuværende situation gjorde det muligt for os at implementere det på en meget begrænset måde og få øjeblikkelige resultater. Envoy er en højtydende, open source, lag-XNUMX proxy designet til store SOA-applikationer. Den kan implementere avancerede belastningsbalanceringsteknikker, herunder automatiske genforsøg, kredsløbsafbrydere og global hastighedsbegrænsning. (Bemærk. overs.: Du kan læse mere om dette i denne artikel om Istio, som er baseret på Envoy.)

Vi kom frem til følgende konfiguration: Hav en Envoy-sidevogn til hver pod og en enkelt rute, og tilslut klyngen til containeren lokalt via havn. For at minimere potentiel cascading og opretholde en lille hitradius brugte vi en flåde af Envoy front-proxy pods, en pr. tilgængelighedszone (AZ) for hver tjeneste. De stolede på en simpel serviceopdagelsesmotor skrevet af en af ​​vores ingeniører, som blot returnerede en liste over pods i hver AZ for en given service.

Servicefrontudsendinge brugte derefter denne serviceopdagelsesmekanisme med én opstrømsklynge og rute. Vi satte passende timeouts, øgede alle kredsløbsafbryderindstillinger og tilføjede en minimal genforsøgskonfiguration for at hjælpe med enkelte fejl og sikre problemfri implementering. Vi placerede en TCP ELB foran hver af disse servicefrontudsendinge. Selvom keepalive fra vores primære proxy-lag sad fast på nogle Envoy-pods, var de stadig i stand til at håndtere belastningen meget bedre og var konfigureret til at balancere gennem least_request i backend.

Til udrulning brugte vi preStop-krogen på både applikationspuder og sidevognspuder. Krogen udløste en fejl ved kontrol af status for admin-slutpunktet placeret på sidevognsbeholderen og gik i dvale i et stykke tid for at tillade aktive forbindelser at afslutte.

En af grundene til, at vi var i stand til at bevæge os så hurtigt, skyldes de detaljerede målinger, som vi nemt kunne integrere i en typisk Prometheus-installation. Dette gjorde det muligt for os at se præcis, hvad der skete, mens vi justerede konfigurationsparametre og omfordelte trafik.

Resultaterne var øjeblikkelige og indlysende. Vi startede med de mest ubalancerede tjenester, og i øjeblikket opererer det foran de 12 vigtigste tjenester i klyngen. I år planlægger vi en overgang til et fuldservicenet med mere avanceret serviceopdagelse, kredsløbsbrud, outlier-detektion, hastighedsbegrænsning og sporing.

Tinder-overgang til Kubernetes
Figur 3–1. CPU-konvergens af én tjeneste under overgangen til Envoy

Tinder-overgang til Kubernetes

Tinder-overgang til Kubernetes

Endelig resultat

Gennem denne erfaring og yderligere forskning har vi bygget et stærkt infrastrukturteam med stærke færdigheder i at designe, implementere og drive store Kubernetes-klynger. Alle Tinder-ingeniører har nu viden og erfaring til at pakke containere og implementere applikationer til Kubernetes.

Da behovet for yderligere kapacitet opstod på den gamle infrastruktur, måtte vi vente flere minutter på, at nye EC2-instanser blev lanceret. Nu begynder containere at køre og begynder at behandle trafik inden for få sekunder i stedet for minutter. Planlægning af flere beholdere på en enkelt EC2-instans giver også forbedret horisontal koncentration. Som følge heraf forventer vi en betydelig reduktion i EC2019-omkostninger i 2 sammenlignet med sidste år.

Migreringen tog næsten to år, men vi afsluttede den i marts 2019. I øjeblikket kører Tinder-platformen udelukkende på en Kubernetes-klynge bestående af 200 tjenester, 1000 noder, 15 pods og 000 kørende containere. Infrastruktur er ikke længere operationsteams eneste domæne. Alle vores ingeniører deler dette ansvar og styrer processen med at bygge og implementere deres applikationer udelukkende ved hjælp af kode.

PS fra oversætteren

Læs også en række artikler på vores blog:

Kilde: www.habr.com

Tilføj en kommentar