Tinder verhuist naar Kubernetes

Geschreven door: Chris O'Brien, Engineering Manager | Chris Thomas, Engineering Manager | Jinyong Lee, Senior Software Engineer | Bewerkt door: Cooper Jackson, Software Engineer

Waarom

Bijna twee jaar geleden besloot Tinder zijn platform te verplaatsen naar Kubernetes. Kubernetes bood ons de mogelijkheid om Tinder Engineering door middel van onveranderlijke implementatie naar containerisatie en low-touch-bediening te brengen. Het bouwen, implementeren en infrastructuur van applicaties wordt gedefinieerd als code.

We waren ook op zoek naar uitdagingen op het gebied van schaal en stabiliteit. Toen schaalvergroting kritiek werd, moesten we vaak enkele minuten wachten tot nieuwe EC2-instanties online kwamen. Het idee van containers die het verkeer binnen enkele seconden plannen en bedienen in plaats van minuten, sprak ons ​​aan.

Het was niet makkelijk. Tijdens onze migratie begin 2019 bereikten we een kritieke massa binnen ons Kubernetes-cluster en begonnen we verschillende uitdagingen tegen te komen vanwege het verkeersvolume, de clustergrootte en DNS. We hebben interessante uitdagingen opgelost om 200 services te migreren en een Kubernetes-cluster op schaal uit te voeren met in totaal 1.000 knooppunten, 15.000 pods en 48.000 actieve containers.

Hoe

Vanaf januari 2018 hebben we ons een weg gebaand door verschillende fasen van de migratie-inspanning. We zijn begonnen met het containeriseren van al onze services en deze te implementeren in een reeks door Kubernetes gehoste staging-omgevingen. Begin oktober zijn we begonnen met het methodisch verplaatsen van al onze oude services naar Kubernetes. In maart van het volgende jaar hebben we onze migratie afgerond en draait het Tinder-platform nu uitsluitend op Kubernetes.

Afbeeldingen maken voor Kubernetes

Er zijn meer dan 30 broncode-opslagplaatsen voor de microservices die worden uitgevoerd in het Kubernetes-cluster. De code in deze repository's is geschreven in verschillende talen (bijv. Node.js, Java, Scala, Go) met meerdere runtime-omgevingen voor dezelfde taal.

Het build-systeem is ontworpen om te werken op een volledig aanpasbare 'build-context' voor elke microservice, die doorgaans bestaat uit een Dockerfile en een reeks shell-opdrachten. Hoewel de inhoud volledig aanpasbaar is, worden deze buildcontexten allemaal geschreven volgens een gestandaardiseerd formaat. Door de standaardisatie van de buildcontexten kan een enkel buildsysteem alle microservices verwerken.

Afbeelding 1–1 Gestandaardiseerd bouwproces via de Builder-container

Om de maximale consistentie tussen runtime-omgevingen te bereiken, wordt tijdens de ontwikkel- en testfase hetzelfde bouwproces gebruikt. Dit legde een unieke uitdaging op toen we een manier moesten bedenken om een ​​consistente bouwomgeving op het hele platform te garanderen. Als gevolg hiervan worden alle bouwprocessen uitgevoerd in een speciale "Builder" -container.

De implementatie van de Builder-container vereiste een aantal geavanceerde Docker-technieken. Deze Builder-container neemt de lokale gebruikers-ID en geheimen over (bijv. SSH-sleutel, AWS-inloggegevens, etc.) zoals vereist om toegang te krijgen tot Tinder-privérepository's. Het koppelt lokale mappen aan die de broncode bevatten om een ​​natuurlijke manier te hebben om build-artefacten op te slaan. Deze aanpak verbetert de prestaties, omdat het het kopiëren van ingebouwde artefacten tussen de Builder-container en de hostmachine elimineert. Opgeslagen build-artefacten worden de volgende keer opnieuw gebruikt zonder verdere configuratie.

Voor bepaalde services moesten we een andere container binnen de Builder maken om de compile-time omgeving te matchen met de runtime-omgeving (bijv. Het installeren van de Node.js bcrypt-bibliotheek genereert platformspecifieke binaire artefacten). De compileertijdvereisten kunnen per dienst verschillen en de uiteindelijke Dockerfile wordt on-the-fly samengesteld.

Kubernetes-clusterarchitectuur en migratie

Clustergrootte

We hebben besloten om kube-aws te gebruiken voor geautomatiseerde clusterinrichting op Amazon EC2-instanties. Vroeger draaiden we alles in één algemene node-pool. We ontdekten snel de noodzaak om werklasten in verschillende groottes en soorten instanties te verdelen, om de middelen beter te gebruiken. De redenering was dat het runnen van minder pods met een zware schroefdraad voor ons meer voorspelbare prestatieresultaten opleverde dan ze naast elkaar te laten bestaan ​​met een groter aantal pods met één schroefdraad.

We hebben gekozen voor:

  • m5.4xlarge voor monitoring (Prometheus)
  • c5.4xlarge voor Node.js-werkbelasting (werkbelasting met één thread)
  • c5.2xlarge voor Java en Go (werkbelasting met meerdere threads)
  • c5.4xlarge voor het controlevlak (3 knooppunten)

Migratie

Een van de voorbereidende stappen voor de migratie van onze oude infrastructuur naar Kubernetes was het wijzigen van bestaande service-naar-service-communicatie om te verwijzen naar nieuwe Elastic Load Balancers (ELB's) die zijn gemaakt in een specifiek Virtual Private Cloud (VPC) -subnet. Dit subnet is gekoppeld aan de Kubernetes VPC. Hierdoor konden we modules granulair migreren zonder rekening te houden met specifieke volgorde voor serviceafhankelijkheden.

Deze eindpunten zijn gemaakt met behulp van gewogen DNS-recordsets met een CNAME die naar elke nieuwe ELB verwijst. Om over te slaan hebben we een nieuw record toegevoegd, wijzend op de nieuwe Kubernetes-service ELB, met een gewicht van 0. We hebben vervolgens de Time To Live (TTL) op het record ingesteld op 0. De oude en nieuwe gewichten werden vervolgens langzaam aangepast om uiteindelijk eindigen met 100% op de nieuwe server. Nadat de overgang was voltooid, werd de TTL op iets redelijkers ingesteld.

Onze Java-modules respecteerden lage DNS TTL, maar onze Node-applicaties niet. Een van onze ingenieurs herschreef een deel van de verbindingspoolcode om deze in een manager te verpakken die de pools elke 60s zou vernieuwen. Dit werkte heel goed voor ons zonder noemenswaardige prestatiehit.

Lessen

Network Fabric-limieten

In de vroege ochtenduren van 8 januari 2019 leed Tinder's Platform een ​​aanhoudende storing. Als reactie op een niet-gerelateerde toename van platformlatentie eerder die ochtend, werden pod- en knooppuntaantallen op het cluster geschaald. Dit resulteerde in uitputting van de ARP-cache op al onze knooppunten.

Er zijn drie Linux-waarden die relevant zijn voor de ARP-cache:

Credit

gc_thresh3 is een harde pet. Als u logboekvermeldingen van "neighbour table overflow" krijgt, betekent dit dat er zelfs na een synchrone garbage collection (GC) van de ARP-cache niet genoeg ruimte was om het buur-item op te slaan. In dit geval laat de kernel het pakket gewoon helemaal vallen.

We gebruiken Flanel als onze netwerkstructuur in Kubernetes. Pakketten worden doorgestuurd via VXLAN. VXLAN is een Layer 2-overlay-schema via een Layer 3-netwerk. Het maakt gebruik van MAC Address-in-User Datagram Protocol (MAC-in-UDP) inkapseling om een ​​middel te bieden om Layer 2-netwerksegmenten uit te breiden. Het transportprotocol via het fysieke datacenternetwerk is IP plus UDP.

Figuur 2–1 Flaneldiagram (credit)

Afbeelding 2–2 VXLAN-pakket (tegoed)

Elk Kubernetes-werkknooppunt wijst een eigen / 24 virtuele adresruimte toe uit een groter / 9-blok. Voor elk knooppunt resulteert dit in 1 routetabelvermelding, 1 ARP-tabelvermelding (op flanel.1-interface) en 1 doorstuurdatabase (FDB) -vermelding. Deze worden toegevoegd wanneer het werkknooppunt voor het eerst wordt gestart of wanneer elk nieuw knooppunt wordt ontdekt.

Bovendien stroomt de knooppunt-naar-pod (of pod-naar-pod) communicatie uiteindelijk over de eth0-interface (afgebeeld in het Flanel-diagram hierboven). Dit resulteert in een extra vermelding in de ARP-tabel voor elke corresponderende knooppuntbron en knooppuntbestemming.

In onze omgeving is dit type communicatie heel gebruikelijk. Voor onze Kubernetes-serviceobjecten wordt een ELB gemaakt en Kubernetes registreert elk knooppunt bij de ELB. De ELB is zich niet bewust van de pod en het geselecteerde knooppunt is mogelijk niet de uiteindelijke bestemming van het pakket. Dit komt omdat wanneer het knooppunt het pakket van de ELB ontvangt, het zijn iptables-regels voor de service evalueert en willekeurig een pod op een ander knooppunt selecteert.

Op het moment van de storing waren er in totaal 605 knooppunten in de cluster. Om de hierboven uiteengezette redenen was dit voldoende om de standaard gc_thresh3-waarde te overschaduwen. Zodra dit gebeurt, worden niet alleen pakketten verwijderd, maar ontbreken ook volledige Flannel / 24s virtuele adresruimte in de ARP-tabel. Communicatie met knooppunt naar pod en DNS-zoekacties mislukt. (DNS wordt gehost binnen het cluster, zoals later in dit artikel in meer detail zal worden uitgelegd.)

Om dit op te lossen, worden de waarden gc_thresh1, gc_thresh2 en gc_thresh3 verhoogd en moet Flanel opnieuw worden gestart om ontbrekende netwerken opnieuw te registreren.

Onverwacht DNS op schaal uitvoeren

Om onze migratie mogelijk te maken, hebben we veel gebruik gemaakt van DNS om traffic shaping en incrementele overgang van legacy naar Kubernetes voor onze services mogelijk te maken. We hebben relatief lage TTL-waarden ingesteld op de bijbehorende Route53 RecordSets. Toen we onze verouderde infrastructuur op EC2-instanties draaiden, wees onze resolverconfiguratie op Amazon's DNS. We namen dit als vanzelfsprekend aan en de kosten van een relatief lage TTL voor onze diensten en Amazon's diensten (bijv. DynamoDB) bleven grotendeels onopgemerkt.

Toen we steeds meer diensten aan Kubernetes aanboden, merkten we dat we een DNS-service gebruikten die 250.000 verzoeken per seconde beantwoordde. We ondervonden periodieke en invloedrijke time-outs voor DNS-lookups binnen onze applicaties. Dit gebeurde ondanks een uitputtende afstemmingspoging en een DNS-provider schakelde over op een CoreDNS-implementatie die ooit piekte op 1.000 pods die 120 cores verbruikten.

Bij het onderzoeken van andere mogelijke oorzaken en oplossingen, vonden we een artikel dat een race-conditie beschrijft die van invloed is op het Linux packet filtering framework netfilter. De DNS-time-outs die we zagen, samen met een oplopende insert_failed-teller op de Flanel-interface, kwamen overeen met de bevindingen van het artikel.

Het probleem doet zich voor tijdens bron- en bestemmingsnetwerkadresvertaling (SNAT en DNAT) en vervolgens in de conntrack-tabel te plaatsen. Een oplossing die intern werd besproken en door de gemeenschap werd voorgesteld, was om DNS naar het werkknooppunt zelf te verplaatsen. In dit geval:

  • SNAT is niet nodig, omdat het verkeer lokaal op het knooppunt blijft. Het hoeft niet via de eth0-interface te worden verzonden.
  • DNAT is niet nodig omdat het bestemmings-IP lokaal is voor het knooppunt en geen willekeurig geselecteerde pod per iptables-regels.

We hebben besloten om verder te gaan met deze aanpak. CoreDNS werd ingezet als een DaemonSet in Kubernetes en we injecteerden de lokale DNS-server van het knooppunt in de resolv.conf van elke pod door de opdrachtvlag kubelet - cluster-dns te configureren. De oplossing was effectief voor DNS-time-outs.

We zien echter nog steeds gevallen pakketten en de Flannel-interface's insert_failed counter increment. Dit blijft bestaan, zelfs na de bovenstaande oplossing, omdat we alleen SNAT en / of DNAT voor DNS-verkeer hebben vermeden. De raceconditie zal nog steeds voorkomen voor andere soorten verkeer. Gelukkig zijn de meeste van onze pakketten TCP en wanneer de toestand zich voordoet, zullen pakketten met succes opnieuw worden verzonden. Een langetermijnoplossing voor alle soorten verkeer is iets waar we het nog steeds over hebben.

Envoy gebruiken voor een betere taakverdeling

Toen we onze back-endservices naar Kubernetes migreerden, begonnen we te lijden onder een onevenwichtige belasting tussen pods. We ontdekten dat dankzij HTTP Keepalive ELB-verbindingen vasthielden aan de eerste pods van elke rollende implementatie, dus het meeste verkeer stroomde door een klein percentage van de beschikbare pods. Een van de eerste maatregelen die we probeerden, was om 100% MaxSurge te gebruiken op nieuwe implementaties voor de ergste overtreders. Bij enkele van de grotere implementaties was dit marginaal effectief en niet duurzaam.

Een andere beperking die we gebruikten, was het kunstmatig verhogen van hulpbronnenaanvragen voor kritieke services, zodat colocated pods meer hoofdruimte zouden krijgen naast andere zware pods. Dit zou op de lange termijn ook niet houdbaar zijn vanwege het verspillen van hulpbronnen en onze Node-applicaties waren single-threaded en dus effectief beperkt tot 1 kern. De enige duidelijke oplossing was het gebruik van een betere taakverdeling.

We waren intern op zoek geweest om Envoy te evalueren. Dit gaf ons de kans om het op een zeer beperkte manier in te zetten en direct de vruchten te plukken. Envoy is een open source, hoogwaardige Layer 7-proxy die is ontworpen voor grote servicegerichte architecturen. Het is in staat om geavanceerde load balancing-technieken te implementeren, waaronder automatische nieuwe pogingen, circuitonderbreking en wereldwijde snelheidsbegrenzing.

De configuratie die we bedachten was om naast elke pod een Envoy-zijspan te hebben die één route en cluster had om de lokale containerhaven te bereiken. Om potentiële cascadering te minimaliseren en om een ​​kleine explosieradius te behouden, hebben we gebruik gemaakt van een reeks front-proxy Envoy-pods, één inzet in elke beschikbaarheidszone (AZ) voor elke service. Deze troffen een klein mechanisme voor het ontdekken van services dat een van onze technici samenstelde en die eenvoudig een lijst met pods in elke AZ voor een bepaalde service retourneerde.

De servicefront-Envoys maakten vervolgens gebruik van dit service-ontdekkingsmechanisme met één upstream-cluster en route. We hebben redelijke time-outs geconfigureerd, alle instellingen van de stroomonderbrekers verbeterd en vervolgens een minimale nieuwe poging gedaan om te helpen bij tijdelijke storingen en soepele implementaties. We voorzagen elk van deze front Envoy-services met een TCP ELB. Zelfs als de keepalive van onze belangrijkste proxy-laag op bepaalde Envoy-pods werd vastgezet, waren ze veel beter in staat om de belasting aan te kunnen en waren ze geconfigureerd om te balanceren via least_request naar de backend.

Voor implementaties hebben we een preStop-hook gebruikt op zowel de applicatie als de zijspan-pod. Deze haak, het sidecar health check fail admin-eindpunt, samen met een kleine slaap, geeft wat tijd om de inflight-verbindingen te voltooien en leeg te laten lopen.

Een reden waarom we zo snel konden bewegen, was vanwege de rijke statistieken die we gemakkelijk konden integreren met onze normale Prometheus-setup. Hierdoor konden we precies zien wat er gebeurde terwijl we de configuratie-instellingen herhaalden en het verkeer beperkten.

De resultaten waren direct en duidelijk. We zijn begonnen met de meest onevenwichtige services en laten deze op dit moment draaien voor twaalf van de belangrijkste services in ons cluster. Dit jaar zijn we van plan over te stappen op een full-service mesh, met geavanceerdere service-detectie, circuitonderbreking, uitbijterdetectie, snelheidsbeperking en tracering.

Afbeelding 3–1 CPU-convergentie van één service tijdens de overgang naar de gezant

Het eindresultaat

Door deze kennis en aanvullend onderzoek hebben we een sterk intern infrastructuurteam ontwikkeld met grote bekendheid met het ontwerpen, implementeren en exploiteren van grote Kubernetes-clusters. De volledige engineeringorganisatie van Tinder heeft nu kennis en ervaring met het containeriseren en implementeren van hun applicaties op Kubernetes.

Op onze oude infrastructuur, waar extra schaal nodig was, moesten we vaak enkele minuten wachten tot nieuwe EC2-instanties online kwamen. Containers plannen en serveren nu verkeer binnen enkele seconden in plaats van minuten. Het plannen van meerdere containers op één EC2-instantie biedt ook een verbeterde horizontale dichtheid. Als gevolg hiervan projecteren we in 2019 aanzienlijke kostenbesparingen op EC2 in vergelijking met het voorgaande jaar.

Het duurde bijna twee jaar, maar we hebben onze migratie in maart 2019 afgerond. Het Tinder-platform draait uitsluitend op een Kubernetes-cluster dat bestaat uit 200 services, 1.000 nodes, 15.000 pods en 48.000 draaiende containers. Infrastructuur is niet langer een taak voorbehouden aan onze operationele teams. In plaats daarvan delen ingenieurs in de hele organisatie deze verantwoordelijkheid en hebben ze controle over hoe hun applicaties worden gebouwd en geïmplementeerd met alles als code.