"Kubernetes ökade latensen med 10 gånger": vem är skyldig till detta?

Notera. transl.: Den här artikeln, skriven av Galo Navarro, som innehar positionen som huvudprogramvaruingenjör på det europeiska företaget Adevinta, är en fascinerande och lärorik "undersökning" inom området för infrastrukturdrift. Dess originaltitel utökades något i översättningen av en anledning som författaren förklarade i början.

"Kubernetes ökade latensen med 10 gånger": vem är skyldig till detta?

Anteckning från författaren: Ser ut som det här inlägget lockade mycket mer uppmärksamhet än väntat. Jag får fortfarande arga kommentarer om att rubriken på artikeln är missvisande och att vissa läsare är ledsna. Jag förstår orsakerna till vad som händer, därför, trots risken att förstöra hela intrigen, vill jag omedelbart berätta vad den här artikeln handlar om. En märklig sak jag har sett när team migrerar till Kubernetes är att närhelst ett problem uppstår (som ökad latens efter en migrering), är det första som får skulden Kubernetes, men sedan visar det sig att orkestratorn inte riktigt ska skylla. Den här artikeln berättar om ett sådant fall. Dess namn upprepar utropet från en av våra utvecklare (senare kommer du att se att Kubernetes inte har något med det att göra). Du hittar inga överraskande avslöjanden om Kubernetes här, men du kan förvänta dig ett par bra lektioner om komplexa system.

För ett par veckor sedan migrerade mitt team en enskild mikrotjänst till en kärnplattform som inkluderade CI/CD, en Kubernetes-baserad körtid, mätvärden och andra godsaker. Flytten var av provkaraktär: vi planerade att ta den som grund och överföra ytterligare cirka 150 tjänster under de kommande månaderna. Alla är ansvariga för driften av några av de största onlineplattformarna i Spanien (Infojobs, Fotocasa, etc.).

Efter att vi distribuerat programmet till Kubernetes och omdirigerat lite trafik till det, väntade en alarmerande överraskning på oss. Dröjsmål (latens) begäranden i Kubernetes var 10 gånger högre än i EC2. I allmänhet var det nödvändigt att antingen hitta en lösning på detta problem eller överge migreringen av mikrotjänsten (och, möjligen, hela projektet).

Varför är latensen så mycket högre i Kubernetes än i EC2?

För att hitta flaskhalsen samlade vi in ​​mätvärden längs hela förfrågningsvägen. Vår arkitektur är enkel: en API-gateway (Zuul) fullmaktsförfrågningar till mikrotjänstinstanser i EC2 eller Kubernetes. I Kubernetes använder vi NGINX Ingress Controller, och backends är vanliga objekt som konfiguration med en JVM-applikation på Spring-plattformen.

                                  EC2
                            +---------------+
                            |  +---------+  |
                            |  |         |  |
                       +-------> BACKEND |  |
                       |    |  |         |  |
                       |    |  +---------+  |                   
                       |    +---------------+
             +------+  |
Public       |      |  |
      -------> ZUUL +--+
traffic      |      |  |              Kubernetes
             +------+  |    +-----------------------------+
                       |    |  +-------+      +---------+ |
                       |    |  |       |  xx  |         | |
                       +-------> NGINX +------> BACKEND | |
                            |  |       |  xx  |         | |
                            |  +-------+      +---------+ |
                            +-----------------------------+

Problemet verkade vara relaterat till initial latens i backend (jag markerade problemområdet på grafen som "xx"). På EC2 tog applikationssvaret cirka 20 ms. I Kubernetes ökade latensen till 100-200 ms.

Vi avfärdade snabbt de troliga misstänkta relaterade till körtidsändringen. JVM-versionen förblir densamma. Containeriseringsproblem hade inte heller något att göra med det: applikationen kördes redan framgångsrikt i containrar på EC2. Läser in? Men vi observerade höga latenser även vid 1 begäran per sekund. Pauser för sophämtning kan också försummas.

En av våra Kubernetes-administratörer undrade om programmet hade externa beroenden eftersom DNS-frågor hade orsakat liknande problem tidigare.

Hypotes 1: DNS-namnupplösning

För varje begäran kommer vår applikation åt en AWS Elasticsearch-instans en till tre gånger i en domän som elastic.spain.adevinta.com. Inuti våra containrar det finns ett skal, så att vi kan kontrollera om det tar lång tid att söka efter en domän.

DNS-frågor från behållare:

[root@be-851c76f696-alf8z /]# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 22 msec
;; Query time: 22 msec
;; Query time: 29 msec
;; Query time: 21 msec
;; Query time: 28 msec
;; Query time: 43 msec
;; Query time: 39 msec

Liknande förfrågningar från en av EC2-instanserna där applikationen körs:

bash-4.4# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 77 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec

Med tanke på att uppslagningen tog cirka 30 ms, blev det tydligt att DNS-upplösning vid åtkomst till Elasticsearch verkligen bidrog till ökningen av latensen.

Detta var dock konstigt av två anledningar:

  1. Vi har redan massor av Kubernetes-applikationer som interagerar med AWS-resurser utan att drabbas av hög latens. Oavsett orsaken så gäller det specifikt detta fall.
  2. Vi vet att JVM gör DNS-cache i minnet. I våra bilder är TTL-värdet inskrivet $JAVA_HOME/jre/lib/security/java.security och ställ in på 10 sekunder: networkaddress.cache.ttl = 10. Med andra ord bör JVM:en cachelagra alla DNS-frågor i 10 sekunder.

För att bekräfta den första hypotesen bestämde vi oss för att sluta ringa DNS ett tag och se om problemet försvann. Först bestämde vi oss för att konfigurera om programmet så att det kommunicerade direkt med Elasticsearch via IP-adress, snarare än via ett domännamn. Detta skulle kräva kodändringar och en ny implementering, så vi mappade helt enkelt domänen till dess IP-adress i /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Nu fick containern en IP nästan direkt. Detta resulterade i en viss förbättring, men vi var bara något närmare de förväntade latensnivåerna. Även om DNS-upplösning tog lång tid, gäckade den verkliga anledningen oss fortfarande.

Diagnostik via nätverk

Vi bestämde oss för att analysera trafik från behållaren med hjälp av tcpdumpför att se exakt vad som händer i nätverket:

[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap

Vi skickade sedan flera förfrågningar och laddade ner deras fångst (kubectl cp my-service:/capture.pcap capture.pcap) för ytterligare analys i Wireshark.

Det fanns inget misstänkt med DNS-frågorna (förutom en liten sak som jag ska prata om senare). Men det fanns vissa konstigheter i hur vår tjänst hanterade varje förfrågan. Nedan finns en skärmdump av inspelningen som visar att begäran accepteras innan svaret börjar:

"Kubernetes ökade latensen med 10 gånger": vem är skyldig till detta?

Paketnummer visas i den första kolumnen. För tydlighetens skull har jag färgkodat de olika TCP-strömmarna.

Den gröna strömmen som börjar med paket 328 visar hur klienten (172.17.22.150) upprättade en TCP-anslutning till behållaren (172.17.36.147). Efter den första handskakningen (328-330) kom paket 331 HTTP GET /v1/.. — en inkommande förfrågan till vår tjänst. Hela processen tog 1 ms.

Den grå strömmen (från paket 339) visar att vår tjänst skickade en HTTP-förfrågan till Elasticsearch-instansen (det finns ingen TCP-handskakning eftersom den använder en befintlig anslutning). Detta tog 18 ms.

Än så länge är allt bra, och tiderna motsvarar ungefär de förväntade förseningarna (20-30 ms mätt från klienten).

Den blå sektionen tar dock 86ms. Vad händer i den? Med paket 333 skickade vår tjänst en HTTP GET-förfrågan till /latest/meta-data/iam/security-credentials, och omedelbart efter det, över samma TCP-anslutning, ytterligare en GET-begäran till /latest/meta-data/iam/security-credentials/arn:...

Vi fann att detta upprepades med varje begäran under hela spårningen. DNS-upplösningen är verkligen lite långsammare i våra behållare (förklaringen till detta fenomen är ganska intressant, men jag sparar den till en separat artikel). Det visade sig att orsaken till de långa förseningarna var samtal till AWS Instance Metadata-tjänsten vid varje begäran.

Hypotes 2: onödiga anrop till AWS

Båda ändpunkterna tillhör AWS Instance Metadata API. Vår mikrotjänst använder den här tjänsten medan Elasticsearch körs. Båda samtalen är en del av den grundläggande auktoriseringsprocessen. Slutpunkten som nås vid den första begäran utfärdar IAM-rollen som är kopplad till instansen.

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
arn:aws:iam::<account_id>:role/some_role

Den andra begäran ber den andra slutpunkten om tillfälliga behörigheter för denna instans:

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam::<account_id>:role/some_role`
{
    "Code" : "Success",
    "LastUpdated" : "2012-04-26T16:39:16Z",
    "Type" : "AWS-HMAC",
    "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
    "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "Token" : "token",
    "Expiration" : "2017-05-17T15:09:54Z"
}

Klienten kan använda dem under en kort tidsperiod och måste med jämna mellanrum skaffa nya certifikat (innan de gör det Expiration). Modellen är enkel: AWS roterar tillfälliga nycklar ofta av säkerhetsskäl, men klienter kan cachelagra dem i några minuter för att kompensera för prestandastraffet i samband med att få nya certifikat.

AWS Java SDK bör ta över ansvaret för att organisera denna process, men av någon anledning händer det inte.

Efter att ha sökt efter problem på GitHub stötte vi på ett problem #1921. Hon hjälpte oss att bestämma i vilken riktning vi skulle "gräva" ytterligare.

AWS SDK uppdaterar certifikat när något av följande tillstånd inträffar:

  • Utgångsdatum (Expiration) Falla in i EXPIRATION_THRESHOLD, hårdkodad till 15 minuter.
  • Mer tid har gått sedan senaste försöket att förnya certifikat än REFRESH_THRESHOLD, hårdkodad i 60 minuter.

För att se det faktiska utgångsdatumet för certifikaten vi får, körde vi ovanstående cURL-kommandon från både behållaren och EC2-instansen. Giltighetstiden för certifikatet som mottogs från behållaren visade sig vara mycket kortare: exakt 15 minuter.

Nu har allt blivit klart: för den första förfrågan fick vår tjänst tillfälliga certifikat. Eftersom de inte var giltiga i mer än 15 minuter, skulle AWS SDK besluta att uppdatera dem vid en efterföljande begäran. Och detta hände med varje förfrågan.

Varför har certifikatens giltighetstid blivit kortare?

AWS Instance Metadata är designad för att fungera med EC2-instanser, inte Kubernetes. Å andra sidan ville vi inte ändra applikationsgränssnittet. För detta använde vi KIAM - ett verktyg som, med hjälp av agenter på varje Kubernetes-nod, tillåter användare (ingenjörer som distribuerar applikationer till ett kluster) att tilldela IAM-roller till behållare i pods som om de vore EC2-instanser. KIAM fångar upp samtal till AWS Instance Metadata-tjänsten och bearbetar dem från dess cache, efter att ha tagit emot dem tidigare från AWS. Ur tillämpningssynpunkt förändras ingenting.

KIAM tillhandahåller korttidscertifikat till poddar. Detta är vettigt med tanke på att den genomsnittliga livslängden för en pod är kortare än för en EC2-instans. Standard giltighetstid för certifikat lika med samma 15 minuter.

Som ett resultat, om du lägger båda standardvärdena ovanpå varandra, uppstår ett problem. Varje certifikat som ges till en ansökan upphör att gälla efter 15 minuter. AWS Java SDK tvingar dock fram en förnyelse av alla certifikat som har mindre än 15 minuter kvar innan dess utgångsdatum.

Som ett resultat av detta tvingas det tillfälliga certifikatet förnyas vid varje begäran, vilket innebär ett par anrop till AWS API och orsakar en betydande ökning av latensen. I AWS Java SDK hittade vi funktion begäran, som nämner ett liknande problem.

Lösningen visade sig vara enkel. Vi konfigurerade helt enkelt om KIAM för att begära certifikat med en längre giltighetstid. När detta väl hände började förfrågningar strömma utan deltagande av AWS Metadata-tjänsten, och latensen sjönk till ännu lägre nivåer än i EC2.

Resultat

Baserat på vår erfarenhet av migrering är en av de vanligaste källorna till problem inte buggar i Kubernetes eller andra delar av plattformen. Det tar inte heller upp några grundläggande brister i de mikrotjänster vi porterar. Problem uppstår ofta bara för att vi sätter ihop olika element.

Vi blandar samman komplexa system som aldrig har interagerat med varandra tidigare och förväntar oss att de tillsammans kommer att bilda ett enda större system. Tyvärr, ju fler element, desto mer utrymme för fel, desto högre entropi.

I vårt fall var den höga latensen inte resultatet av buggar eller dåliga beslut i Kubernetes, KIAM, AWS Java SDK eller vår mikrotjänst. Det var resultatet av att kombinera två oberoende standardinställningar: en i KIAM, den andra i AWS Java SDK. Tagna separat är båda parametrarna vettiga: den aktiva certifikatförnyelsepolicyn i AWS Java SDK och den korta giltighetsperioden för certifikat i KAIM. Men när du sätter ihop dem blir resultaten oförutsägbara. Två oberoende och logiska lösningar behöver inte vara vettiga när de kombineras.

PS från översättaren

Du kan lära dig mer om arkitekturen för KIAM-verktyget för att integrera AWS IAM med Kubernetes på den här artikeln från dess skapare.

Läs även på vår blogg:

Källa: will.com

Lägg en kommentar