“Kubernetes je povečal zakasnitev za 10-krat”: kdo je kriv za to?

Opomba. prevod: Ta članek, ki ga je napisal Galo Navarro, ki zaseda položaj glavnega programskega inženirja v evropskem podjetju Adevinta, je fascinantna in poučna »raziskava« na področju delovanja infrastrukture. Njegov izvirni naslov je bil v prevodu nekoliko razširjen iz razloga, ki ga avtor pojasnjuje na samem začetku.

“Kubernetes je povečal zakasnitev za 10-krat”: kdo je kriv za to?

Opomba avtorja: Izgleda kot ta objava pritegnili veliko več pozornosti, kot je bilo pričakovano. Še vedno dobivam jezne komentarje, da je naslov članka zavajajoč in da so nekateri bralci žalostni. Razumem razloge za to, kar se dogaja, zato vam želim, kljub tveganju, da bi uničil celotno spletko, takoj povedati, o čem govori ta članek. Nenavadna stvar, ki sem jo opazil, ko ekipe migrirajo na Kubernetes, je, da kadar koli se pojavi težava (kot je povečana zakasnitev po selitvi), je prva stvar, ki jo obtožijo Kubernetes, potem pa se izkaže, da orkestrator v resnici ne krivda. Ta članek govori o enem takem primeru. Njegovo ime ponavlja vzklik enega od naših razvijalcev (kasneje boste videli, da Kubernetes nima nič s tem). Tukaj ne boste našli nobenih presenetljivih razkritij o Kubernetesu, lahko pa pričakujete nekaj dobrih lekcij o kompleksnih sistemih.

Pred nekaj tedni je moja ekipa selila eno samo mikrostoritev na osrednjo platformo, ki je vključevala CI/CD, izvajalno okolje na osnovi Kubernetesa, meritve in druge dobrote. Selitev je bila poskusne narave: nameravali smo jo vzeti za osnovo in v naslednjih mesecih prenesti še približno 150 storitev. Vsi so odgovorni za delovanje nekaterih največjih spletnih platform v Španiji (Infojobs, Fotocasa itd.).

Ko smo aplikacijo namestili v Kubernetes in vanjo preusmerili nekaj prometa, nas je čakalo alarmantno presenečenje. Zamuda (latenca) zahteve v Kubernetesu so bile 10-krat večje kot v EC2. Na splošno je bilo treba bodisi najti rešitev za to težavo bodisi opustiti selitev mikrostoritve (in po možnosti celotnega projekta).

Zakaj je zakasnitev v Kubernetesu toliko večja kot v EC2?

Da bi našli ozko grlo, smo zbrali meritve vzdolž celotne poti zahteve. Naša arhitektura je preprosta: prehod API (Zuul) posreduje zahteve instancam mikrostoritev v EC2 ali Kubernetes. V Kubernetesu uporabljamo NGINX Ingress Controller, ozadja pa so običajni predmeti, kot je Deployment z aplikacijo JVM na platformi Spring.

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

Zdelo se je, da je težava povezana z začetno zakasnitvijo v ozadju (območje problema na grafu sem označil kot "xx"). Na EC2 je odziv aplikacije trajal približno 20 ms. V Kubernetesu se je zakasnitev povečala na 100-200 ms.

Hitro smo zavrnili verjetne osumljence, povezane s spremembo izvajalnega časa. Različica JVM ostaja enaka. Tudi težave s kontejneriranjem niso imele nobene zveze s tem: aplikacija je že uspešno delovala v kontejnerjih na EC2. Nalaganje? Vendar smo opazili visoke zakasnitve tudi pri 1 zahtevi na sekundo. Premore za odvoz smeti bi lahko tudi zanemarili.

Eden od naših skrbnikov Kubernetes se je spraševal, ali ima aplikacija zunanje odvisnosti, ker so poizvedbe DNS v preteklosti povzročale podobne težave.

Hipoteza 1: Ločljivost imen DNS

Za vsako zahtevo naša aplikacija enkrat do trikrat dostopa do primerka AWS Elasticsearch v domeni, kot je elastic.spain.adevinta.com. Znotraj naših zabojnikov obstaja lupina, da lahko preverimo, ali iskanje domene dejansko traja dolgo.

Poizvedbe DNS iz vsebnika:

[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

Podobne zahteve iz enega od primerkov EC2, kjer se izvaja aplikacija:

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

Glede na to, da je iskanje trajalo približno 30 ms, je postalo jasno, da je razrešitev DNS pri dostopu do Elasticsearch res prispevala k povečanju zakasnitve.

Vendar je bilo to čudno iz dveh razlogov:

  1. Imamo že ogromno aplikacij Kubernetes, ki komunicirajo z viri AWS, ne da bi trpele zaradi visoke zakasnitve. Ne glede na razlog, se nanaša posebej na ta primer.
  2. Vemo, da JVM izvaja predpomnjenje DNS v pomnilniku. Na naših slikah je vrednost TTL zapisana v $JAVA_HOME/jre/lib/security/java.security in nastavite na 10 sekund: networkaddress.cache.ttl = 10. Z drugimi besedami, JVM bi moral predpomniti vse poizvedbe DNS za 10 sekund.

Za potrditev prve hipoteze smo se odločili, da za nekaj časa prenehamo klicati DNS in preverimo, ali je težava izginila. Najprej smo se odločili, da ponovno konfiguriramo aplikacijo, tako da bo komunicirala neposredno z Elasticsearch prek naslova IP in ne prek imena domene. To bi zahtevalo spremembe kode in novo uvedbo, zato smo domeno preprosto preslikali na njen naslov IP /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Vsebnik je zdaj prejel IP skoraj takoj. To je povzročilo nekaj izboljšav, vendar smo bili le nekoliko bližje pričakovanim stopnjam zakasnitve. Čeprav je reševanje DNS trajalo dolgo, se nam pravi razlog še vedno izmika.

Diagnostika prek omrežja

Odločili smo se za analizo prometa iz zabojnika z uporabo tcpdumpda vidite, kaj točno se dogaja v omrežju:

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

Nato smo poslali več zahtev in prenesli njihov zajem (kubectl cp my-service:/capture.pcap capture.pcap) za nadaljnjo analizo v Wireshark.

Glede poizvedb DNS ni bilo nič sumljivega (razen ene malenkosti, o kateri bom govoril pozneje). Vendar so bile nekatere nenavadnosti v načinu, kako je naša služba obravnavala vsako zahtevo. Spodaj je posnetek zaslona posnetka, ki prikazuje, da je bila zahteva sprejeta, preden se začne odgovor:

“Kubernetes je povečal zakasnitev za 10-krat”: kdo je kriv za to?

Številke paketov so prikazane v prvem stolpcu. Zaradi jasnosti sem barvno kodiral različne tokove TCP.

Zeleni tok, ki se začne s paketom 328, prikazuje, kako je odjemalec (172.17.22.150) vzpostavil povezavo TCP z vsebnikom (172.17.36.147). Po začetnem rokovanju (328-330) je prinesel paket 331 HTTP GET /v1/.. — dohodna zahteva naši storitvi. Celoten proces je trajal 1 ms.

Sivi tok (iz paketa 339) kaže, da je naša storitev poslala zahtevo HTTP instanci Elasticsearch (ni rokovanja TCP, ker uporablja obstoječo povezavo). To je trajalo 18 ms.

Zaenkrat je vse v redu in časi približno ustrezajo pričakovanim zakasnitvam (20-30 ms merjeno od odjemalca).

Vendar pa modri del traja 86 ms. Kaj se dogaja v njem? S paketom 333 je naša storitev poslala zahtevo HTTP GET /latest/meta-data/iam/security-credentials, takoj za njo pa prek iste povezave TCP še ena zahteva GET za /latest/meta-data/iam/security-credentials/arn:...

Ugotovili smo, da se to ponavlja z vsako zahtevo v celotnem sledenju. DNS ločljivost je v naših kontejnerjih res nekoliko počasnejša (razlaga tega pojava je precej zanimiva, vendar jo bom prihranil za poseben članek). Izkazalo se je, da so vzrok za dolge zamude klici storitve AWS Instance Metadata ob vsaki zahtevi.

Hipoteza 2: nepotrebni klici v AWS

Obe končni točki pripadata API za metapodatke primerka AWS. Naša mikrostoritev uporablja to storitev med izvajanjem Elasticsearch. Oba klica sta del osnovnega postopka avtorizacije. Končna točka, do katere se dostopa ob prvi zahtevi, izda vlogo IAM, povezano s primerkom.

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

Druga zahteva zahteva drugo končno točko za začasna dovoljenja za ta primer:

/ # 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"
}

Naročnik jih lahko uporablja kratek čas in mora občasno pridobiti nove certifikate (preden so Expiration). Model je preprost: AWS zaradi varnostnih razlogov pogosto rotira začasne ključe, vendar jih lahko odjemalci shranijo v predpomnilnik za nekaj minut, da nadomestijo poslabšanje zmogljivosti, povezano s pridobivanjem novih potrdil.

AWS Java SDK bi moral prevzeti odgovornost za organizacijo tega procesa, vendar se to iz nekega razloga ne zgodi.

Po iskanju težav na GitHubu smo naleteli na težavo #1921. Pomagala nam je določiti smer, v katero bomo »kopali« naprej.

AWS SDK posodobi potrdila, ko nastopi eden od naslednjih pogojev:

  • Rok uporabnosti (Expiration) Padec v EXPIRATION_THRESHOLD, kodirano na 15 minut.
  • Od zadnjega poskusa podaljšanja certifikatov je minilo več časa kot REFRESH_THRESHOLD, trdo kodiran za 60 minut.

Da bi videli dejanski datum poteka potrdil, ki jih prejmemo, smo zagnali zgornje ukaze cURL iz vsebnika in primerka EC2. Izkazalo se je, da je čas veljavnosti potrdila, prejetega iz zabojnika, precej krajši: točno 15 minut.

Zdaj je vse postalo jasno: za prvo zahtevo je naša služba prejela začasna potrdila. Ker niso bili veljavni več kot 15 minut, bi se AWS SDK odločil, da jih posodobi ob naslednji zahtevi. In to se je zgodilo z vsako prošnjo.

Zakaj se je rok veljavnosti certifikatov skrajšal?

Metapodatki o primerku AWS so zasnovani za delo s primerki EC2, ne s Kubernetesom. Po drugi strani pa nismo želeli spremeniti vmesnika aplikacije. Za to smo uporabili KIAM - orodje, ki z uporabo agentov na vsakem vozlišču Kubernetes omogoča uporabnikom (inženirjem, ki uvajajo aplikacije v gručo), da dodelijo vloge IAM vsebnikom v podih, kot da bi bili primerki EC2. KIAM prestreže klice v storitev metapodatkov primerkov AWS in jih obdela iz svojega predpomnilnika, potem ko jih je prej prejel od AWS. Z vidika uporabe se nič ne spremeni.

KIAM dobavlja kratkoročna potrdila pods. To je smiselno glede na to, da je povprečna življenjska doba stroka krajša kot pri primerku EC2. Privzeto obdobje veljavnosti certifikatov enako enakih 15 minut.

Posledično se pojavi težava, če prekrijete obe privzeti vrednosti eno na drugo. Vsako potrdilo, posredovano aplikaciji, poteče po 15 minutah. Vendar AWS Java SDK prisili obnovitev katerega koli potrdila, ki ima do datuma poteka manj kot 15 minut.

Posledično je začasno potrdilo prisiljeno obnoviti z vsako zahtevo, kar vključuje nekaj klicev v API AWS in povzroči znatno povečanje zakasnitve. V AWS Java SDK smo našli zahteva za funkcijo, ki omenja podobno težavo.

Rešitev se je izkazala za preprosto. KIAM smo enostavno preoblikovali tako, da zahteva certifikate z daljšo veljavnostjo. Ko se je to zgodilo, so zahteve začele teči brez sodelovanja storitve metapodatkov AWS, zakasnitev pa je padla na celo nižje ravni kot v EC2.

Ugotovitve

Na podlagi naših izkušenj s selitvami eden najpogostejših virov težav niso hrošči v Kubernetesu ali drugih elementih platforme. Prav tako ne odpravlja temeljnih napak v mikrostoritvah, ki jih prenašamo. Težave pogosto nastanejo preprosto zato, ker sestavljamo različne elemente.

Mešamo kompleksne sisteme, ki še nikoli niso sodelovali drug z drugim, in pričakujemo, da bodo skupaj tvorili en sam, večji sistem. Žal, več elementov, več prostora za napake, večja je entropija.

V našem primeru visoka zakasnitev ni bila posledica napak ali slabih odločitev v Kubernetes, KIAM, AWS Java SDK ali naši mikrostoritvi. Bil je rezultat združevanja dveh neodvisnih privzetih nastavitev: ene v KIAM, druge v AWS Java SDK. Gledano ločeno sta oba parametra smiselna: aktivna politika obnavljanja potrdil v AWS Java SDK in kratko obdobje veljavnosti potrdil v KAIM. Ko pa jih sestavite skupaj, postanejo rezultati nepredvidljivi. Ni nujno, da sta dve neodvisni in logični rešitvi smiselni v kombinaciji.

PS od prevajalca

Več o arhitekturi pripomočka KIAM za integracijo AWS IAM s Kubernetesom lahko izveste na ta članek od njenih ustvarjalcev.

Preberite tudi na našem blogu:

Vir: www.habr.com

Dodaj komentar