"Kubernetes økte latensen med 10 ganger": hvem har skylden for dette?

Merk. overs.: Denne artikkelen, skrevet av Galo Navarro, som innehar stillingen som hovedprogramvareingeniør i det europeiske selskapet Adevinta, er en fascinerende og lærerik "undersøkelse" innen infrastrukturdrift. Den opprinnelige tittelen ble litt utvidet i oversettelse av en grunn som forfatteren forklarer helt i begynnelsen.

"Kubernetes økte latensen med 10 ganger": hvem har skylden for dette?

Merknad fra forfatteren: Ser ut som dette innlegget tiltrakk mye mer oppmerksomhet enn forventet. Jeg får fortsatt sinte kommentarer om at tittelen på artikkelen er misvisende og at enkelte lesere er triste. Jeg forstår årsakene til det som skjer, og til tross for risikoen for å ødelegge hele intrigen, vil jeg umiddelbart fortelle deg hva denne artikkelen handler om. En merkelig ting jeg har sett når team migrerer til Kubernetes er at når det oppstår et problem (som økt latens etter en migrering), er det første som får skylden Kubernetes, men så viser det seg at orkestratoren egentlig ikke skal skylde på. Denne artikkelen forteller om en slik sak. Navnet gjentar utropet til en av utviklerne våre (senere vil du se at Kubernetes ikke har noe med det å gjøre). Du vil ikke finne noen overraskende avsløringer om Kubernetes her, men du kan forvente et par gode leksjoner om komplekse systemer.

For et par uker siden migrerte teamet mitt en enkelt mikrotjeneste til en kjerneplattform som inkluderte CI/CD, en Kubernetes-basert kjøretid, beregninger og andre godbiter. Flyttingen var av prøvekarakter: Vi planla å legge den til grunn og overføre omtrent 150 flere tjenester i løpet av de kommende månedene. Alle er ansvarlige for driften av noen av de største nettplattformene i Spania (Infojobs, Fotocasa, etc.).

Etter at vi distribuerte applikasjonen til Kubernetes og omdirigerte litt trafikk til den, ventet en alarmerende overraskelse på oss. Forsinkelse (ventetid) forespørsler i Kubernetes var 10 ganger høyere enn i EC2. Generelt var det nødvendig å enten finne en løsning på dette problemet, eller forlate migreringen av mikrotjenesten (og muligens hele prosjektet).

Hvorfor er ventetiden så mye høyere i Kubernetes enn i EC2?

For å finne flaskehalsen samlet vi inn beregninger langs hele forespørselsbanen. Arkitekturen vår er enkel: en API-gateway (Zuul) fullmakter forespørsler til mikrotjenesteinstanser i EC2 eller Kubernetes. I Kubernetes bruker vi NGINX Ingress Controller, og backends er vanlige objekter som Utplassering med en JVM-applikasjon på Spring-plattformen.

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

Problemet så ut til å være relatert til initial latens i backend (jeg markerte problemområdet på grafen som "xx"). På EC2 tok søknadssvaret omtrent 20 ms. I Kubernetes økte ventetiden til 100-200 ms.

Vi avskjediget raskt de sannsynlige mistenkte knyttet til kjøretidsendringen. JVM-versjonen forblir den samme. Containeriseringsproblemer hadde heller ingenting med det å gjøre: applikasjonen kjørte allerede vellykket i containere på EC2. Laster inn? Men vi observerte høye latenser selv ved 1 forespørsel per sekund. Pauser for søppelhenting kan også bli neglisjert.

En av våre Kubernetes-administratorer lurte på om applikasjonen hadde eksterne avhengigheter fordi DNS-spørringer hadde forårsaket lignende problemer tidligere.

Hypotese 1: DNS-navneoppløsning

For hver forespørsel får applikasjonen vår tilgang til en AWS Elasticsearch-forekomst én til tre ganger i et domene som elastic.spain.adevinta.com. Inne i våre containere det er et skall, slik at vi kan sjekke om det tar lang tid å søke etter et domene.

DNS-spørringer fra container:

[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

Lignende forespørsler fra en av EC2-forekomstene der applikasjonen kjører:

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

Tatt i betraktning at oppslaget tok omtrent 30 ms, ble det klart at DNS-oppløsning ved tilgang til Elasticsearch faktisk bidro til økningen i latens.

Dette var imidlertid merkelig av to grunner:

  1. Vi har allerede massevis av Kubernetes-applikasjoner som samhandler med AWS-ressurser uten å lide av høy latenstid. Uansett årsak, gjelder det spesifikt denne saken.
  2. Vi vet at JVM gjør DNS-bufring i minnet. I bildene våre er TTL-verdien skrevet inn $JAVA_HOME/jre/lib/security/java.security og sett til 10 sekunder: networkaddress.cache.ttl = 10. Med andre ord, JVM skal bufre alle DNS-spørringer i 10 sekunder.

For å bekrefte den første hypotesen bestemte vi oss for å slutte å ringe DNS en stund og se om problemet forsvant. Først bestemte vi oss for å rekonfigurere applikasjonen slik at den kommuniserte direkte med Elasticsearch via IP-adresse, i stedet for gjennom et domenenavn. Dette ville kreve kodeendringer og en ny distribusjon, så vi tilordnet rett og slett domenet til IP-adressen /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Nå fikk containeren en IP nesten umiddelbart. Dette resulterte i en viss forbedring, men vi var bare litt nærmere de forventede latensnivåene. Selv om DNS-oppløsning tok lang tid, unngikk den egentlige grunnen oss fortsatt.

Diagnostikk via nettverk

Vi bestemte oss for å analysere trafikk fra containeren ved hjelp av tcpdumpfor å se nøyaktig hva som skjer på nettverket:

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

Vi sendte deretter flere forespørsler og lastet ned fangsten deres (kubectl cp my-service:/capture.pcap capture.pcap) for videre analyse i Wireshark.

Det var ikke noe mistenkelig med DNS-spørringene (bortsett fra en liten ting som jeg skal snakke om senere). Men det var visse merkeligheter i måten tjenesten vår håndterte hver forespørsel på. Nedenfor er et skjermbilde av opptak som viser at forespørselen blir akseptert før svaret begynner:

"Kubernetes økte latensen med 10 ganger": hvem har skylden for dette?

Pakkenummer vises i første kolonne. For klarhetens skyld har jeg fargekodet de forskjellige TCP-flytene.

Den grønne strømmen som starter med pakke 328 viser hvordan klienten (172.17.22.150) etablerte en TCP-forbindelse til containeren (172.17.36.147). Etter det første håndtrykket (328-330), brakte pakke 331 HTTP GET /v1/.. — en innkommende forespørsel til vår tjeneste. Hele prosessen tok 1 ms.

Den grå strømmen (fra pakke 339) viser at tjenesten vår sendte en HTTP-forespørsel til Elasticsearch-forekomsten (det er ingen TCP-håndtrykk fordi den bruker en eksisterende tilkobling). Dette tok 18 ms.

Så langt er alt i orden, og tidene tilsvarer omtrent de forventede forsinkelsene (20-30 ms målt fra klienten).

Den blå delen tar imidlertid 86ms. Hva skjer i den? Med pakke 333 sendte tjenesten vår en HTTP GET-forespørsel til /latest/meta-data/iam/security-credentials, og umiddelbart etter den, over samme TCP-tilkobling, en annen GET-forespørsel til /latest/meta-data/iam/security-credentials/arn:...

Vi fant ut at dette gjentok seg med hver forespørsel gjennom hele sporet. DNS-oppløsning er faktisk litt tregere i våre containere (forklaringen på dette fenomenet er ganske interessant, men jeg lagrer den til en egen artikkel). Det viste seg at årsaken til de lange forsinkelsene var anrop til AWS Instance Metadata-tjenesten på hver forespørsel.

Hypotese 2: unødvendige anrop til AWS

Begge endepunktene tilhører AWS Instance Metadata API. Mikrotjenesten vår bruker denne tjenesten mens den kjører Elasticsearch. Begge samtalene er en del av den grunnleggende autorisasjonsprosessen. Endepunktet som er tilgjengelig på den første forespørselen, utsteder IAM-rollen knyttet til forekomsten.

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

Den andre forespørselen ber det andre endepunktet om midlertidige tillatelser for denne forekomsten:

/ # 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 bruke dem i en kort periode og må med jevne mellomrom skaffe nye sertifikater (før de er det Expiration). Modellen er enkel: AWS roterer midlertidige nøkler ofte av sikkerhetsmessige årsaker, men klienter kan bufre dem i noen minutter for å kompensere for ytelsesstraffen knyttet til å få nye sertifikater.

AWS Java SDK bør ta over ansvaret for å organisere denne prosessen, men av en eller annen grunn skjer ikke dette.

Etter å ha søkt etter problemer på GitHub, kom vi over et problem #1921. Hun hjalp oss med å bestemme retningen vi skulle "grave" videre i.

AWS SDK oppdaterer sertifikater når en av følgende forhold oppstår:

  • Utløpsdato (Expiration) Fall inn i EXPIRATION_THRESHOLD, hardkodet til 15 minutter.
  • Det har gått mer tid siden siste forsøk på å fornye sertifikater enn REFRESH_THRESHOLD, hardkodet i 60 minutter.

For å se den faktiske utløpsdatoen for sertifikatene vi mottar, kjørte vi cURL-kommandoene ovenfor fra både containeren og EC2-forekomsten. Gyldighetsperioden for sertifikatet mottatt fra containeren viste seg å være mye kortere: nøyaktig 15 minutter.

Nå har alt blitt klart: for den første forespørselen mottok tjenesten vår midlertidige sertifikater. Siden de ikke var gyldige i mer enn 15 minutter, ville AWS SDK bestemme seg for å oppdatere dem på en påfølgende forespørsel. Og dette skjedde med hver forespørsel.

Hvorfor har sertifikatenes gyldighetsperiode blitt kortere?

AWS Instance Metadata er designet for å fungere med EC2-forekomster, ikke Kubernetes. På den annen side ønsket vi ikke å endre applikasjonsgrensesnittet. Til dette brukte vi KIAM - et verktøy som ved hjelp av agenter på hver Kubernetes-node lar brukere (ingeniører som distribuerer applikasjoner til en klynge) tilordne IAM-roller til beholdere i pods som om de var EC2-forekomster. KIAM avskjærer anrop til AWS Instance Metadata-tjenesten og behandler dem fra cachen sin, etter å ha mottatt dem tidligere fra AWS. Fra et applikasjonssynspunkt endres ingenting.

KIAM leverer korttidssertifikater til pods. Dette er fornuftig med tanke på at gjennomsnittlig levetid for en pod er kortere enn for en EC2-forekomst. Standard gyldighetsperiode for sertifikater lik de samme 15 minuttene.

Som et resultat, hvis du legger begge standardverdiene oppå hverandre, oppstår det et problem. Hvert sertifikat gitt til en søknad utløper etter 15 minutter. AWS Java SDK tvinger imidlertid frem en fornyelse av ethvert sertifikat som har mindre enn 15 minutter igjen før utløpsdatoen.

Som et resultat blir det midlertidige sertifikatet tvunget til å fornyes med hver forespørsel, noe som innebærer et par anrop til AWS API og forårsaker en betydelig økning i latens. I AWS Java SDK fant vi funksjon forespørsel, som nevner et lignende problem.

Løsningen viste seg å være enkel. Vi rekonfigurerte ganske enkelt KIAM til å be om sertifikater med lengre gyldighetsperiode. Når dette skjedde, begynte forespørsler å strømme uten deltakelse fra AWS Metadata-tjenesten, og ventetiden falt til enda lavere nivåer enn i EC2.

Funn

Basert på vår erfaring med migreringer, er en av de vanligste kildene til problemer ikke feil i Kubernetes eller andre elementer på plattformen. Den tar heller ikke opp noen grunnleggende feil i mikrotjenestene vi porterer. Problemer oppstår ofte rett og slett fordi vi setter sammen ulike elementer.

Vi blander sammen komplekse systemer som aldri har samhandlet med hverandre før, og forventer at de sammen vil danne et enkelt, større system. Akk, jo flere elementer, jo mer rom for feil, jo høyere er entropien.

I vårt tilfelle var ikke den høye latensen et resultat av feil eller dårlige beslutninger i Kubernetes, KIAM, AWS Java SDK eller mikrotjenesten vår. Det var resultatet av å kombinere to uavhengige standardinnstillinger: en i KIAM, den andre i AWS Java SDK. Sett hver for seg gir begge parameterne mening: den aktive sertifikatfornyelsespolicyen i AWS Java SDK, og den korte gyldighetsperioden for sertifikater i KAIM. Men når du setter dem sammen, blir resultatene uforutsigbare. To uavhengige og logiske løsninger trenger ikke være fornuftige når de kombineres.

PS fra oversetter

Du kan lære mer om arkitekturen til KIAM-verktøyet for å integrere AWS IAM med Kubernetes på denne artikkelen fra dens skapere.

Les også på bloggen vår:

Kilde: www.habr.com

Legg til en kommentar