„Kubernetes ја зголеми латентноста за 10 пати“: кој е виновен за ова?

Забелешка. превод.: Оваа статија, напишана од Гало Наваро, кој ја има позицијата главен софтверски инженер во европската компанија Адевинта, е фасцинантна и поучна „истрага“ на полето на инфраструктурните операции. Неговиот оригинален наслов беше малку проширен во преводот од причина што авторот ја објаснува на самиот почеток.

„Kubernetes ја зголеми латентноста за 10 пати“: кој е виновен за ова?

Белешка од авторот: Изгледа како овој пост привлечени многу повеќе внимание од очекуваното. Сè уште добивам лути коментари дека насловот на статијата е погрешен и дека некои читатели се тажни. Ги разбирам причините за тоа што се случува, затоа, и покрај ризикот да ја уништам целата интрига, сакам веднаш да ви кажам за што е оваа статија. Една љубопитна работа што ја видов додека тимовите мигрираат во Kubernetes е дека секогаш кога ќе се појави проблем (како зголемена латентност по миграцијата), првото нешто што се обвинува е Kubernetes, но потоа излегува дека оркестраторот навистина не Вина. Оваа статија раскажува за еден таков случај. Неговото име го повторува извикот на еден од нашите програмери (подоцна ќе видите дека Кубернетес нема никаква врска со тоа). Овде нема да најдете изненадувачки откритија за Kubernetes, но можете да очекувате неколку добри лекции за сложените системи.

Пред неколку недели, мојот тим мигрираше единствена микросервис на основна платформа која вклучуваше CI/CD, траење базирано на Кубернетес, метрика и други добрини. Овој потег беше од пробна природа: планиравме да го земеме како основа и да пренесеме уште околу 150 услуги во наредните месеци. Сите тие се одговорни за работата на некои од најголемите онлајн платформи во Шпанија (Infojobs, Fotocasa итн.).

Откако ја распоредивме апликацијата на Kubernetes и пренасочивме одреден сообраќај кон неа, не чекаше алармантно изненадување. Одложување (латентност) барањата во Kubernetes беа 10 пати повисоки отколку во EC2. Во принцип, беше неопходно или да се најде решение за овој проблем, или да се напушти миграцијата на микросервисот (и, можеби, целиот проект).

Зошто латентноста е толку многу поголема кај Кубернетс отколку во EC2?

За да го пронајдеме тесното грло, собравме метрика по целата патека на барањето. Нашата архитектура е едноставна: API gateway (Zuul) ги посредува барањата до микросервисни примероци во EC2 или Kubernetes. Во Kubernetes користиме NGINX Ingress Controller, а позадините се обични објекти како распоредување со JVM апликација на пролетната платформа.

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

Се чинеше дека проблемот е поврзан со почетната латентност во задниот дел (ја означив проблематичната област на графикот како „xx“). На EC2, одговорот на апликацијата траеше околу 20 ms. Во Кубернетес, латентноста се зголеми на 100-200 ms.

Брзо ги отфрливме веројатните осомничени поврзани со промената на времето на траење. Верзијата на JVM останува иста. Проблемите со контејнеризацијата исто така немаа никаква врска со тоа: апликацијата веќе работеше успешно во контејнерите на EC2. Се вчитува? Но, забележавме високи доцнења дури и при 1 барање во секунда. Може да се занемарат и паузите за собирање ѓубре.

Еден од нашите администратори на Кубернетс се запраша дали апликацијата има надворешни зависности бидејќи барањата за DNS предизвикуваа слични проблеми во минатото.

Хипотеза 1: Резолуција на името на DNS

За секое барање, нашата апликација пристапува до AWS Elasticsearch пример еден до три пати во домен како elastic.spain.adevinta.com. Внатре во нашите контејнери има школка, за да можеме да провериме дали пребарувањето домен навистина трае долго.

Барања за DNS од контејнерот:

[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

Слични барања од еден од примероците на EC2 каде што апликацијата работи:

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

Имајќи предвид дека пребарувањето траеше околу 30 ms, стана јасно дека резолуцијата на DNS при пристап до Elasticsearch навистина придонесува за зголемување на латентноста.

Сепак, ова беше чудно од две причини:

  1. Веќе имаме еден тон апликации на Kubernetes кои комуницираат со ресурсите на AWS без да страдаат од висока латентност. Без оглед на причината, таа се однесува конкретно на овој случај.
  2. Знаеме дека JVM прави DNS кеширање во меморијата. На нашите слики, вредноста TTL е напишана во $JAVA_HOME/jre/lib/security/java.security и поставете го на 10 секунди: networkaddress.cache.ttl = 10. Со други зборови, JVM треба да ги кешира сите DNS барања за 10 секунди.

За да ја потврдиме првата хипотеза, решивме да престанеме да повикуваме DNS некое време и да видиме дали проблемот исчезна. Прво, решивме да ја реконфигурираме апликацијата така што таа директно комуницира со Elasticsearch преку IP адреса, наместо преку име на домен. Ова ќе бара промени на кодот и ново распоредување, па едноставно го мапиравме доменот на неговата IP адреса /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Сега контејнерот доби ИП скоро веднаш. Ова резултираше со одредено подобрување, но бевме само малку поблиску до очекуваните нивоа на латентност. Иако резолуцијата на DNS траеше долго, вистинската причина сепак ни избега.

Дијагностика преку мрежа

Решивме да го анализираме сообраќајот од контејнерот користејќи tcpdumpза да видите што точно се случува на мрежата:

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

Потоа испративме неколку барања и го преземавме нивното снимање (kubectl cp my-service:/capture.pcap capture.pcap) за понатамошна анализа во Wireshark.

Немаше ништо сомнително во прашањата за DNS (освен една ситница за која ќе зборувам подоцна). Но, имаше одредени необичности во начинот на кој нашата служба постапуваше со секое барање. Подолу е скриншот од снимањето што покажува дека барањето е прифатено пред да започне одговорот:

„Kubernetes ја зголеми латентноста за 10 пати“: кој е виновен за ова?

Броевите на пакетите се прикажани во првата колона. За јасност, ги шифрирав различните текови на TCP во боја.

Зелениот тек кој започнува со пакетот 328 покажува како клиентот (172.17.22.150) воспоставил TCP врска со контејнерот (172.17.36.147). По првичното ракување (328-330), донесе пакет 331 HTTP GET /v1/.. — дојдовно барање до нашата услуга. Целиот процес траеше 1 ms.

Сивиот поток (од пакетот 339) покажува дека нашата услуга испрати HTTP барање до примерокот на Elasticsearch (нема TCP ракување бидејќи користи постоечка врска). Ова траеше 18 ms.

Досега сè е во ред, а времињата приближно одговараат на очекуваните доцнења (20-30 ms кога се мери од клиентот).

Сепак, синиот дел трае 86 ms. Што се случува во него? Со пакетот 333, нашата услуга испрати барање за HTTP GET до /latest/meta-data/iam/security-credentials, а веднаш по него, преку истата TCP конекција, уште едно барање GET до /latest/meta-data/iam/security-credentials/arn:...

Откривме дека тоа се повторува со секое барање во текот на трагата. Резолуцијата на DNS е навистина малку побавна во нашите контејнери (објаснувањето за овој феномен е доста интересно, но ќе го зачувам за посебна статија). Се испостави дека причината за долгите одложувања се повиците до услугата AWS Instance Metadata на секое барање.

Хипотеза 2: непотребни повици до AWS

И двете крајни точки припаѓаат на API за метаподатоци за пример на AWS. Нашиот микросервис ја користи оваа услуга додека работи на Elasticsearch. Двата повици се дел од основниот процес на овластување. Крајната точка до која се пристапува на првото барање ја издава улогата на IAM поврзана со инстанцата.

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

Второто барање бара од втората крајна точка привремени дозволи за овој пример:

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

Клиентот може да ги користи за краток временски период и мора периодично да добива нови сертификати (пред тие да бидат Expiration). Моделот е едноставен: AWS често ги ротира привремените клучеви од безбедносни причини, но клиентите можат да ги кешираат неколку минути за да ја компензираат казната за изведба поврзана со добивањето нови сертификати.

AWS Java SDK треба да ја преземе одговорноста за организирање на овој процес, но поради некоја причина тоа не се случува.

По пребарувањето проблеми на GitHub, наидовме на проблем # 1921. Таа ни помогна да ја одредиме насоката во која понатаму да „копаме“.

AWS SDK ги ажурира сертификатите кога ќе се појави еден од следниве услови:

  • Датум на истекување (Expiration) Паѓа во EXPIRATION_THRESHOLD, хардкодирано до 15 минути.
  • Помина повеќе време од последниот обид да се обноват сертификатите отколку REFRESH_THRESHOLD, хардкодирано 60 минути.

За да го видиме вистинскиот датум на истекување на сертификатите што ги добиваме, ги извршивме горенаведените команди cURL и од контејнерот и од примерот EC2. Се покажа дека периодот на важност на сертификатот добиен од контејнерот е многу пократок: точно 15 минути.

Сега сè стана јасно: за првото барање, нашата услуга доби привремени сертификати. Бидејќи тие не важеа повеќе од 15 минути, AWS SDK ќе одлучи да ги ажурира на следно барање. И ова се случуваше со секое барање.

Зошто рокот на важност на сертификатите стана пократок?

Метаподатоците на примерот AWS се дизајнирани да работат со EC2 примероци, а не со Kubernetes. Од друга страна, не сакавме да го промениме интерфејсот на апликацијата. За ова користевме КИАМ - алатка која, користејќи агенти на секој Kubernetes јазол, им овозможува на корисниците (инженери кои распоредуваат апликации во кластер) да доделуваат улоги на IAM на контејнерите во pods како да се примероци на EC2. KIAM пресретнува повици до услугата AWS Instance Metadata и ги обработува од нејзината кеш меморија, откако претходно ги примил од AWS. Од аспект на примена, ништо не се менува.

КИАМ обезбедува краткорочни сертификати за мешунките. Ова има смисла имајќи предвид дека просечниот животен век на мешунките е пократок од примерокот EC2. Стандарден период на важност за сертификатите еднакво на истите 15 минути.

Како резултат на тоа, ако ги преклопите двете стандардни вредности една врз друга, се јавува проблем. Секој сертификат доставен на апликација истекува по 15 минути. Сепак, AWS Java SDK принудува обновување на кој било сертификат што има помалку од 15 минути пред датумот на истекување.

Како резултат на тоа, привремениот сертификат е принуден да се обновува со секое барање, што повлекува неколку повици до AWS API и резултира со значително зголемување на латентноста. Најдовме во AWS Java SDK барање за одлики, кој споменува сличен проблем.

Решението се покажа едноставно. Едноставно го реконфигуриравме KIAM да бара сертификати со подолг период на важност. Штом се случи ова, барањата почнаа да течат без учество на услугата AWS Metadata, а латентноста падна на уште пониски нивоа отколку во EC2.

Наоди

Врз основа на нашето искуство со миграциите, еден од најчестите извори на проблеми не се грешки во Kubernetes или други елементи на платформата. Исто така, не се справува со никакви основни недостатоци во микросервисите што ги пренесуваме. Проблемите често се појавуваат едноставно затоа што спојуваме различни елементи заедно.

Ние мешаме сложени системи кои никогаш претходно не биле во интеракција еден со друг, очекувајќи дека заедно ќе формираат единствен, поголем систем. За жал, колку повеќе елементи, толку повеќе простор за грешки, толку е поголема ентропијата.

Во нашиот случај, високата латентност не беше резултат на грешки или лоши одлуки во Kubernetes, KIAM, AWS Java SDK или нашиот микросервис. Тоа беше резултат на комбинирање на две независни стандардни поставки: едната во KIAM, другата во AWS Java SDK. Земени одделно, двата параметри имаат смисла: активната политика за обновување на сертификатот во AWS Java SDK и краткиот период на важност на сертификатите во KAIM. Но, кога ќе ги соберете, резултатите стануваат непредвидливи. Две независни и логични решенија не мора да имаат смисла кога се комбинираат.

PS од преведувач

Можете да дознаете повеќе за архитектурата на алатката KIAM за интегрирање на AWS IAM со Kubernetes на овој напис од неговите креатори.

Прочитајте и на нашиот блог:

Извор: www.habr.com

Додадете коментар