„Kubernetes zvýšil latenci 10krát“: kdo za to může?

Poznámka. přel.: Tento článek, který napsal Galo Navarro, který zastává pozici hlavního softwarového inženýra v evropské společnosti Adevinta, je fascinujícím a poučným „vyšetřováním“ v oblasti provozu infrastruktury. Její původní název byl v překladu mírně rozšířen z důvodu, který autor vysvětluje hned na začátku.

„Kubernetes zvýšil latenci 10krát“: kdo za to může?

Poznámka od autora: Vypadá jako tento příspěvek přitahoval mnohem více pozornosti, než se očekávalo. Stále mě zlobí komentáře, že název článku je zavádějící a že někteří čtenáři jsou z toho smutní. Rozumím důvodům toho, co se děje, a proto i přes riziko zmaření celé intriky vám chci okamžitě říct, o čem je tento článek. Zajímavá věc, kterou jsem viděl, když týmy migrují na Kubernetes, je, že kdykoli se objeví problém (například zvýšená latence po migraci), první věc, která je obviňována, je Kubernetes, ale pak se ukáže, že orchestrátor ve skutečnosti není obviňovat. O jednom takovém případě vypráví tento článek. Jeho jméno opakuje zvolání jednoho z našich vývojářů (později uvidíte, že Kubernetes s tím nemá nic společného). Nenajdete zde žádná překvapivá odhalení o Kubernetes, ale můžete očekávat pár dobrých lekcí o složitých systémech.

Před pár týdny můj tým migroval jednu mikroslužbu na základní platformu, která zahrnovala CI/CD, běhové prostředí založené na Kubernetes, metriky a další vychytávky. Stěhování bylo zkušebního charakteru: plánovali jsme jej vzít jako základ a v následujících měsících převést přibližně 150 dalších služeb. Všichni jsou zodpovědní za provoz některých z největších online platforem ve Španělsku (Infojobs, Fotocasa atd.).

Poté, co jsme aplikaci nasadili do Kubernetes a přesměrovali do ní část provozu, nás čekalo alarmující překvapení. Zpoždění (latence) požadavky v Kubernetes byly 10krát vyšší než v EC2. Obecně bylo nutné buď najít řešení tohoto problému, nebo upustit od migrace mikroslužby (a případně celého projektu).

Proč je latence v Kubernetes mnohem vyšší než v EC2?

Abychom našli úzké místo, shromáždili jsme metriky podél celé cesty požadavku. Naše architektura je jednoduchá: brána API (Zuul) odesílá požadavky na instance mikroslužeb v EC2 nebo Kubernetes. V Kubernetes používáme NGINX Ingress Controller a backendy jsou běžné objekty jako Rozvinutí s aplikací JVM na platformě Spring.

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

Zdálo se, že problém souvisí s počáteční latencí v backendu (problémovou oblast jsem v grafu označil jako „xx“). Na EC2 trvala odezva aplikace asi 20 ms. V Kubernetes se latence zvýšila na 100-200 ms.

Rychle jsme propustili pravděpodobné podezřelé související se změnou běhového prostředí. Verze JVM zůstává stejná. Problémy s kontejnerizací s tím také neměly nic společného: aplikace již úspěšně běžela v kontejnerech na EC2. Načítání? Ale pozorovali jsme vysoké latence i při 1 požadavku za sekundu. Přestávky na odvoz odpadu by také mohly být opomenuty.

Jednoho z našich správců Kubernetes zajímalo, zda má aplikace externí závislosti, protože dotazy DNS způsobovaly v minulosti podobné problémy.

Hypotéza 1: Překlad DNS jmen

Pro každý požadavek naše aplikace přistupuje k instanci AWS Elasticsearch jednou až třikrát v doméně, jako je elastic.spain.adevinta.com. Uvnitř našich kontejnerů je tam skořápka, takže můžeme zkontrolovat, zda hledání domény skutečně trvá dlouho.

DNS dotazy z kontejneru:

[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

Podobné požadavky z jedné z instancí EC2, kde je aplikace spuštěna:

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

Vzhledem k tomu, že vyhledávání trvalo asi 30 ms, bylo jasné, že rozlišení DNS při přístupu k Elasticsearch skutečně přispívá ke zvýšení latence.

Bylo to však divné ze dvou důvodů:

  1. Již máme spoustu aplikací Kubernetes, které komunikují se zdroji AWS, aniž by trpěly vysokou latencí. Ať už je důvod jakýkoli, týká se konkrétně tohoto případu.
  2. Víme, že JVM provádí in-memory DNS cache. Na našich obrázcích je zapsána hodnota TTL $JAVA_HOME/jre/lib/security/java.security a nastavte na 10 sekund: networkaddress.cache.ttl = 10. Jinými slovy, JVM by měl ukládat do mezipaměti všechny dotazy DNS po dobu 10 sekund.

Abychom potvrdili první hypotézu, rozhodli jsme se na chvíli přestat volat DNS a zjistit, zda problém zmizel. Nejprve jsme se rozhodli překonfigurovat aplikaci tak, aby komunikovala přímo s Elasticsearch podle IP adresy, nikoli prostřednictvím názvu domény. To by vyžadovalo změny kódu a nové nasazení, takže jsme doménu jednoduše namapovali na její IP adresu /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Nyní kontejner obdržel IP téměř okamžitě. To vedlo k určitému zlepšení, ale byli jsme jen o něco blíže očekávaným úrovním latence. Přestože rozlišení DNS trvalo dlouho, skutečný důvod nám stále unikal.

Diagnostika přes síť

Rozhodli jsme se analyzovat provoz z kontejneru pomocí tcpdumpabyste viděli, co se přesně děje v síti:

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

Poté jsme odeslali několik žádostí a stáhli jejich zachycení (kubectl cp my-service:/capture.pcap capture.pcap) pro další analýzu v Wireshark.

Na DNS dotazech nebylo nic podezřelého (až na jednu maličkost, o které budu mluvit později). Ve způsobu, jakým naše služba vyřizovala každou žádost, však byly určité zvláštnosti. Níže je snímek obrazovky zachycení ukazující přijetí požadavku před zahájením odpovědi:

„Kubernetes zvýšil latenci 10krát“: kdo za to může?

Čísla balíků jsou uvedena v prvním sloupci. Pro přehlednost jsem barevně odlišil různé TCP streamy.

Zelený proud začínající paketem 328 ukazuje, jak klient (172.17.22.150) navázal TCP spojení s kontejnerem (172.17.36.147). Po úvodním podání ruky (328-330) přinesl balíček 331 HTTP GET /v1/.. — příchozí požadavek na naši službu. Celý proces trval 1 ms.

Šedý proud (z paketu 339) ukazuje, že naše služba odeslala požadavek HTTP instanci Elasticsearch (neprobíhá žádný TCP handshake, protože používá existující připojení). To trvalo 18 ms.

Zatím je vše v pořádku a časy zhruba odpovídají očekávaným zpožděním (20-30 ms při měření od klienta).

Modrý úsek však trvá 86 ms. Co se v něm děje? S paketem 333 naše služba odeslala požadavek HTTP GET na /latest/meta-data/iam/security-credentialsa hned po něm přes stejné TCP spojení další požadavek GET na /latest/meta-data/iam/security-credentials/arn:...

Zjistili jsme, že se to opakuje s každým požadavkem v průběhu trasování. Překlad DNS je v našich kontejnerech skutečně o něco pomalejší (vysvětlení tohoto jevu je docela zajímavé, ale nechám si ho na samostatný článek). Ukázalo se, že příčinou dlouhých zpoždění bylo volání služby AWS Instance Metadata na každý požadavek.

Hypotéza 2: zbytečná volání do AWS

Oba koncové body patří AWS Instance Metadata API. Naše mikroslužba používá tuto službu při spuštění Elasticsearch. Obě volání jsou součástí základního autorizačního procesu. Koncový bod, ke kterému se přistupuje při prvním požadavku, vydá roli IAM přidruženou k instanci.

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

Druhý požadavek žádá druhý koncový bod o dočasná oprávnění pro tuto instanci:

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

Klient je může používat po krátkou dobu a musí pravidelně získávat nové certifikáty (dříve než jsou Expiration). Model je jednoduchý: AWS z bezpečnostních důvodů často otáčí dočasné klíče, ale klienti je mohou několik minut ukládat do mezipaměti, aby kompenzovali snížení výkonu spojené se získáváním nových certifikátů.

AWS Java SDK by měla převzít odpovědnost za organizaci tohoto procesu, ale z nějakého důvodu se tak neděje.

Po hledání problémů na GitHubu jsme narazili na problém #1921. Pomohla nám určit směr, kterým dále „kopat“.

Sada AWS SDK aktualizuje certifikáty, když nastane jedna z následujících podmínek:

  • Datum vypršení platnosti (Expiration) Spadají do EXPIRATION_THRESHOLD, pevně zakódované na 15 minut.
  • Od posledního pokusu o obnovení certifikátů uplynulo více času než REFRESH_THRESHOLD, pevně zakódováno po dobu 60 minut.

Abychom viděli skutečné datum vypršení platnosti certifikátů, které dostáváme, spustili jsme výše uvedené příkazy cURL z kontejneru i instance EC2. Doba platnosti certifikátu přijatého z kontejneru se ukázala být mnohem kratší: přesně 15 minut.

Nyní je vše jasné: na první žádost naše služba obdržela dočasné certifikáty. Vzhledem k tomu, že nebyly platné déle než 15 minut, AWS SDK se rozhodlo je aktualizovat na další žádost. A to se stalo s každou žádostí.

Proč se doba platnosti certifikátů zkrátila?

Metadata instance AWS jsou navržena tak, aby fungovala s instancemi EC2, nikoli s Kubernetes. Na druhou stranu jsme nechtěli měnit rozhraní aplikace. K tomu jsme použili KIAM - nástroj, který pomocí agentů na každém uzlu Kubernetes umožňuje uživatelům (inženýrům nasazujícím aplikace do clusteru) přiřazovat role IAM kontejnerům v podech, jako by to byly instance EC2. KIAM zachycuje volání služby AWS Instance Metadata a zpracovává je ze své mezipaměti poté, co je předtím přijal od AWS. Z aplikačního hlediska se nic nemění.

KIAM dodává podům krátkodobé certifikáty. To dává smysl vzhledem k tomu, že průměrná životnost modulu je kratší než u instance EC2. Výchozí doba platnosti certifikátů rovných 15 minut.

V důsledku toho, pokud překryjete obě výchozí hodnoty na sebe, nastává problém. Platnost každého certifikátu poskytnutého aplikaci vyprší po 15 minutách. AWS Java SDK si však vynutí obnovení jakéhokoli certifikátu, kterému do vypršení platnosti zbývá méně než 15 minut.

V důsledku toho je dočasný certifikát nucen být obnovován s každou žádostí, což znamená několik volání do AWS API a způsobuje výrazné zvýšení latence. V AWS Java SDK jsme našli budoucí žádost, který zmiňuje podobný problém.

Řešení se ukázalo jako jednoduché. Jednoduše jsme překonfigurovali KIAM tak, aby vyžadoval certifikáty s delší dobou platnosti. Jakmile se tak stalo, požadavky začaly proudit bez účasti služby AWS Metadata a latence klesla na ještě nižší úrovně než v EC2.

Závěry

Na základě našich zkušeností s migracemi nejsou jedním z nejčastějších zdrojů problémů chyby v Kubernetes nebo jiných prvcích platformy. Neřeší ani žádné zásadní nedostatky v mikroslužbách, které přenášíme. Problémy často vznikají jednoduše proto, že dáváme dohromady různé prvky.

Mícháme dohromady komplexní systémy, které spolu nikdy předtím neinteragovaly, a očekáváme, že společně vytvoří jediný, větší systém. Bohužel, čím více prvků, tím větší prostor pro chyby, tím vyšší entropie.

V našem případě nebyla vysoká latence výsledkem chyb nebo špatných rozhodnutí v Kubernetes, KIAM, AWS Java SDK nebo v naší mikroslužbě. Byl výsledkem kombinace dvou nezávislých výchozích nastavení: jedno v KIAM, druhé v AWS Java SDK. Samostatně oba parametry dávají smysl: aktivní politika obnovy certifikátu v AWS Java SDK a krátká doba platnosti certifikátů v KAIM. Ale když je dáte dohromady, výsledky se stanou nepředvídatelnými. Dvě nezávislá a logická řešení nemusí dávat smysl, když se spojí.

PS od překladatele

Další informace o architektuře nástroje KIAM pro integraci AWS IAM s Kubernetes naleznete na tento článek od jeho tvůrců.

Přečtěte si také na našem blogu:

Zdroj: www.habr.com

Přidat komentář