"Kubernetes lisäsi latenssia 10 kertaa": kuka on syypää tähän?

Huomautus. käännös: Tämä artikkeli, jonka on kirjoittanut Galo Navarro, joka työskentelee pääohjelmistosuunnittelijana eurooppalaisessa Adevinta-yrityksessä, on kiehtova ja opettavainen "tutkimus" infrastruktuuritoimintojen alalla. Sen alkuperäistä otsikkoa on hieman laajennettu käännöksessä syystä, jonka kirjoittaja selittää heti alussa.

"Kubernetes lisäsi latenssia 10 kertaa": kuka on syypää tähän?

Huomautus kirjoittajalta: Näyttää tältä viestiltä houkutteli odotettua enemmän huomiota. Saan edelleen vihaisia ​​kommentteja siitä, että artikkelin otsikko on harhaanjohtava ja että jotkut lukijat ovat surullisia. Ymmärrän tapahtumien syyt, joten huolimatta koko juonittelun pilaamisen vaarasta haluan heti kertoa sinulle, mistä tämä artikkeli kertoo. Omituinen asia, jonka olen nähnyt tiimien siirtyessä Kubernetesiin, on se, että aina kun ilmenee ongelmia (kuten lisääntynyt latenssi siirron jälkeen), ensimmäinen asia, josta syytetään, on Kubernetes, mutta sitten käy ilmi, että orkesteri ei todellakaan ole syyttää. Tämä artikkeli kertoo yhdestä tällaisesta tapauksesta. Sen nimi toistaa yhden kehittäjämme huudon (myöhemmin näet, ettei Kubernetesilla ole mitään tekemistä sen kanssa). Et löydä täältä yllättäviä paljastuksia Kubernetesista, mutta voit odottaa pari hyvää oppituntia monimutkaisista järjestelmistä.

Pari viikkoa sitten tiimini oli siirtämässä yksittäistä mikropalvelua ydinalustalle, joka sisälsi CI/CD:n, Kubernetes-pohjaisen ajonajan, mittaustuloksia ja muita herkkuja. Muutto oli kokeiluluonteinen: aioimme ottaa sen pohjaksi ja siirtää tulevina kuukausina noin 150 palvelua lisää. He kaikki vastaavat joidenkin Espanjan suurimpien verkkoalustojen (Infojobs, Fotocasa jne.) toiminnasta.

Kun otimme sovelluksen käyttöön Kubernetesiin ja ohjasimme liikennettä siihen, meitä odotti hälyttävä yllätys. Viive (viive) pyynnöt Kubernetesissa olivat 10 kertaa suuremmat kuin EC2:ssa. Yleensä oli tarpeen joko löytää ratkaisu tähän ongelmaan tai luopua mikropalvelun (ja mahdollisesti koko projektin) migraatiosta.

Miksi latenssi on niin paljon korkeampi Kubernetesissa kuin EC2:ssa?

Pullonkaulan löytämiseksi keräsimme mittareita koko pyyntöpolun varrelta. Arkkitehtuurimme on yksinkertainen: API-yhdyskäytävä (Zuul) välittää pyynnöt mikropalveluesiintymiin EC2:ssa tai Kubernetesissa. Kubernetesissa käytämme NGINX Ingress Controlleria ja taustat ovat tavallisia objekteja, kuten käyttöönoton JVM-sovelluksella Spring-alustalla.

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

Ongelma näytti liittyvän taustan alkuperäiseen latenssiin (merkitsin kaavioon ongelma-alueeksi "xx"). EC2:lla sovelluksen vastaus kesti noin 20 ms. Kubernetesissa latenssi nousi 100-200 ms:iin.

Hylkäsimme nopeasti suoritusajan muutokseen liittyvät todennäköiset epäillyt. JVM-versio pysyy samana. Myöskään konttien käsittelyongelmilla ei ollut mitään tekemistä asian kanssa: sovellus toimi jo onnistuneesti EC2:n konteissa. Ladataan? Mutta havaitsimme korkeita viiveitä jopa yhdellä pyynnöllä sekunnissa. Myös jätteiden keräämisen taukoja voitaisiin jättää huomiotta.

Yksi Kubernetes-järjestelmänvalvojistamme ihmetteli, oliko sovelluksella ulkoisia riippuvuuksia, koska DNS-kyselyt olivat aiheuttaneet samanlaisia ​​ongelmia aiemmin.

Hypoteesi 1: DNS-nimen ratkaisu

Kutakin pyyntöä kohden sovelluksemme käyttää AWS Elasticsearch -esiintymää yhdestä kolmeen kertaa verkkotunnuksessa, kuten elastic.spain.adevinta.com. Konteissamme siellä on kuori, jotta voimme tarkistaa, kestääkö verkkotunnuksen etsiminen todella kauan.

DNS-kyselyt säilöstä:

[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

Samankaltaisia ​​pyyntöjä yhdeltä EC2-esiintymästä, jossa sovellus on käynnissä:

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

Ottaen huomioon, että haku kesti noin 30 ms, kävi selväksi, että DNS-resoluutio Elasticsearchia käytettäessä todellakin myötävaikutti latenssin kasvuun.

Tämä oli kuitenkin outoa kahdesta syystä:

  1. Meillä on jo paljon Kubernetes-sovelluksia, jotka ovat vuorovaikutuksessa AWS-resurssien kanssa kärsimättä korkeasta latenssista. Oli syy mikä tahansa, se liittyy nimenomaan tähän tapaukseen.
  2. Tiedämme, että JVM suorittaa DNS-välimuistin. Kuvissamme TTL-arvo on kirjoitettu $JAVA_HOME/jre/lib/security/java.security ja aseta 10 sekuntiin: networkaddress.cache.ttl = 10. Toisin sanoen JVM:n tulisi tallentaa kaikki DNS-kyselyt välimuistiin 10 sekunnin ajan.

Vahvistaaksemme ensimmäisen hypoteesin päätimme lopettaa DNS-soiton hetkeksi ja katsoa, ​​hävisikö ongelma. Ensin päätimme määrittää sovelluksen uudelleen siten, että se kommunikoi suoraan Elasticsearchin kanssa IP-osoitteen kautta verkkotunnuksen sijaan. Tämä vaatisi koodin muutoksia ja uutta käyttöönottoa, joten yhdistämme verkkotunnuksen sen IP-osoitteeseen /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Nyt kontti sai IP-osoitteen melkein välittömästi. Tämä johti jonkin verran parannukseen, mutta olimme vain hieman lähempänä odotettua latenssitasoa. Vaikka DNS-selvitys kesti kauan, todellinen syy jäi silti meiltä.

Diagnostiikka verkon kautta

Päätimme analysoida kontin liikennettä käyttämällä tcpdumpnähdäksesi mitä verkossa tarkalleen tapahtuu:

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

Lähetimme sitten useita pyyntöjä ja latasimme niiden kaappauksen (kubectl cp my-service:/capture.pcap capture.pcap) lisäanalyysiä varten Wireshark.

DNS-kyselyissä ei ollut mitään epäilyttävää (paitsi yksi pieni asia, josta puhun myöhemmin). Mutta tavassa, jolla palvelumme käsitteli jokaista pyyntöä, oli tiettyjä omituisuuksia. Alla on kuvakaappaus kaappauksesta, joka näyttää pyynnön hyväksymisen ennen vastauksen alkamista:

"Kubernetes lisäsi latenssia 10 kertaa": kuka on syypää tähän?

Pakkausnumerot näkyvät ensimmäisessä sarakkeessa. Selvyyden vuoksi olen värikoodannut eri TCP-virrat.

Vihreä stream, joka alkaa paketista 328, näyttää kuinka asiakas (172.17.22.150) loi TCP-yhteyden säiliöön (172.17.36.147). Ensimmäisen kättelyn (328-330) jälkeen paketti 331 tuotiin HTTP GET /v1/.. - palvelullemme saapuva pyyntö. Koko prosessi kesti 1 ms.

Harmaa virta (paketista 339) osoittaa, että palvelumme lähetti HTTP-pyynnön Elasticsearch-instanssiin (ei ole TCP-kättelyä, koska se käyttää olemassa olevaa yhteyttä). Tämä kesti 18 ms.

Toistaiseksi kaikki on hyvin ja ajat vastaavat suunnilleen odotettuja viiveitä (20-30 ms asiakkaalta mitattuna).

Sininen osa kestää kuitenkin 86 ms. Mitä siinä tapahtuu? Paketilla 333 palvelumme lähetti HTTP GET -pyynnön osoitteeseen /latest/meta-data/iam/security-credentials, ja heti sen jälkeen saman TCP-yhteyden kautta toinen GET-pyyntö /latest/meta-data/iam/security-credentials/arn:...

Huomasimme, että tämä toistui jokaisella pyynnöllä koko jäljen ajan. DNS-resoluutio on todellakin hieman hitaampaa säilöissämme (selitys tälle ilmiölle on varsin mielenkiintoinen, mutta säästän sen erillistä artikkelia varten). Kävi ilmi, että pitkien viiveiden syynä olivat puhelut AWS Instance Metadata -palveluun jokaisessa pyynnössä.

Hypoteesi 2: tarpeettomat kutsut AWS:lle

Molemmat päätepisteet kuuluvat AWS Instance Metadata API. Mikropalvelumme käyttää tätä palvelua suorittaessaan Elasticsearchia. Molemmat puhelut ovat osa perusvaltuutusprosessia. Päätepiste, jota käytetään ensimmäisessä pyynnössä, antaa ilmentymään liittyvän IAM-roolin.

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

Toinen pyyntö pyytää toiselta päätepisteeltä väliaikaisia ​​käyttöoikeuksia tälle ilmentymälle:

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

Asiakas voi käyttää niitä lyhyen aikaa ja hänen on hankittava säännöllisesti uusia varmenteita (ennen kuin ne ovat Expiration). Malli on yksinkertainen: AWS kiertää väliaikaisia ​​avaimia usein turvallisuussyistä, mutta asiakkaat voivat tallentaa ne välimuistiin muutaman minuutin ajan kompensoidakseen uusien varmenteiden saamiseen liittyvän suorituskykyrangaistuksen.

AWS Java SDK:n pitäisi ottaa vastuu tämän prosessin järjestämisestä, mutta jostain syystä näin ei tapahdu.

Kun etsimme ongelmia GitHubista, törmäsimme ongelmaan #1921. Hän auttoi meitä määrittämään suunnan, johon "kaivaa" edelleen.

AWS SDK päivittää varmenteet, kun jokin seuraavista ehdoista täyttyy:

  • Päättymispäivä (Expiration) Pudota EXPIRATION_THRESHOLD, kovakoodattu 15 minuuttiin.
  • Edellisestä varmenteiden uusimisyrityksestä on kulunut enemmän aikaa REFRESH_THRESHOLD, kovakoodattu 60 minuuttia.

Nähdäksemme vastaanottamiemme varmenteiden todellisen vanhenemispäivän suoritimme yllä olevat cURL-komennot sekä säilöstä että EC2-esiintymästä. Kontista saadun todistuksen voimassaoloaika osoittautui huomattavasti lyhyemmäksi: tasan 15 minuuttia.

Nyt kaikki on käynyt selväksi: ensimmäisestä pyynnöstä palvelumme sai väliaikaiset varmenteet. Koska ne eivät olleet voimassa yli 15 minuuttia, AWS SDK päättää päivittää ne myöhemmän pyynnön yhteydessä. Ja tämä tapahtui jokaisen pyynnöstä.

Miksi todistusten voimassaoloaika on lyhentynyt?

AWS-ilmentymien metatiedot on suunniteltu toimimaan EC2-esiintymien, ei Kubernetesin, kanssa. Toisaalta emme halunneet muuttaa sovelluksen käyttöliittymää. Tätä varten käytimme KIAM - Työkalu, joka käyttää agentteja kussakin Kubernetes-solmussa ja antaa käyttäjien (insinöörit, jotka ottavat käyttöön sovelluksia klusteriin) määrittää IAM-rooleja säiliöissä oleville säiliöille ikään kuin ne olisivat EC2-esiintymiä. KIAM sieppaa puhelut AWS Instance Metadata -palveluun ja käsittelee ne välimuististaan ​​saatuaan ne aiemmin AWS:ltä. Sovelluksen näkökulmasta mikään ei muutu.

KIAM toimittaa paloille lyhytaikaisia ​​sertifikaatteja. Tämä on järkevää, kun otetaan huomioon, että podin keskimääräinen käyttöikä on lyhyempi kuin EC2-esiintymän. Varmenteiden oletus voimassaoloaika sama 15 minuuttia.

Tämän seurauksena, jos asetat molemmat oletusarvot päällekkäin, syntyy ongelma. Jokainen hakemukseen annettu varmenne vanhenee 15 minuutin kuluttua. AWS Java SDK kuitenkin pakottaa uusimaan kaikki varmenteet, joiden vanhenemispäivää on jäljellä alle 15 minuuttia.

Tämän seurauksena väliaikainen varmenne pakotetaan uusimaan jokaisen pyynnön yhteydessä, mikä aiheuttaa pari kutsua AWS API:lle ja lisää merkittävästi viivettä. Löysimme AWS Java SDK:ssa ominaisuuspyyntö, jossa mainitaan samanlainen ongelma.

Ratkaisu osoittautui yksinkertaiseksi. Me yksinkertaisesti määritimme KIAM:n uudelleen pyytämään varmenteita, joilla on pidempi voimassaoloaika. Kun tämä tapahtui, pyyntöjä alkoi virrata ilman AWS Metadata -palvelun osallistumista, ja viive putosi vielä alemmalle tasolle kuin EC2:ssa.

Tulokset

Migraatioista saatujen kokemustemme perusteella yksi yleisimmistä ongelmien lähteistä ei ole Kubernetesin tai muiden alustan elementtien bugit. Se ei myöskään korjaa mitään perustavanlaatuisia puutteita siirretyissä mikropalveluissa. Ongelmia syntyy usein vain siksi, että yhdistämme eri elementtejä.

Yhdistelemme monimutkaisia ​​järjestelmiä, jotka eivät ole koskaan olleet vuorovaikutuksessa toistensa kanssa, odottaen, että ne yhdessä muodostavat yhden, suuremman järjestelmän. Valitettavasti mitä enemmän elementtejä, sitä enemmän tilaa virheille, sitä suurempi entropia.

Meidän tapauksessamme korkea latenssi ei johtunut Kubernetesin, KIAM:n, AWS Java SDK:n tai mikropalvelumme virheistä tai huonoista päätöksistä. Se oli tulosta kahden itsenäisen oletusasetuksen yhdistämisestä: toinen KIAM:ssa ja toinen AWS Java SDK:ssa. Erikseen otettuna molemmat parametrit ovat järkeviä: aktiivinen varmenteen uusimiskäytäntö AWS Java SDK:ssa ja varmenteiden lyhyt voimassaoloaika KAIMissa. Mutta kun ne yhdistetään, tuloksista tulee arvaamattomia. Kahden itsenäisen ja loogisen ratkaisun ei tarvitse olla järkeä yhdistettyinä.

PS kääntäjältä

Voit oppia lisää KIAM-apuohjelman arkkitehtuurista AWS IAM:n integroimiseksi Kubernetesiin osoitteessa tässä artikkelissa sen tekijöiltä.

Lue myös blogistamme:

Lähde: will.com

Lisää kommentti