"Kubernetes павялічыў затрымку ў 10 разоў": хто ж у гэтым вінаваты?

Заўв. перав.: Гэты артыкул, напісаны Galo Navarro, што займае пасаду Principal Software Engineer у еўрапейскай кампаніі Adevinta, - займальнае і павучальнае "расследаванне" ў галіне эксплуатацыі інфраструктуры. Яе арыгінальная назва была крыху дапоўнена ў перакладзе па прычыне, якую тлумачыць аўтар у самым пачатку.

"Kubernetes павялічыў затрымку ў 10 разоў": хто ж у гэтым вінаваты?

Заўвага ад аўтара: Падобна, гэтая публікацыя прыцягнула значна больш увагі, чым чакалася. Я да гэтага часу атрымліваю гнеўныя каментары аб тым, што назва артыкула ўводзіць у зман і што некаторыя чытачы засмучаныя. Я разумею прычыны таго, што адбываецца, таму, нягледзячы на ​​рызыку сарваць усю інтрыгу, хачу адразу расказаць, пра што гэты артыкул. Пры пераходзе каманд на Kubernetes я назіраю цікавую рэч: кожны раз, калі ўзнікае праблема (напрыклад, рост затрымак пасля міграцыі), перш за ўсё абвінавачваюць Kubernetes, аднак потым аказваецца, што аркестратар, увогуле, не вінаваты. Гэты артыкул апавядае пра адзін з такіх выпадкаў. Яе назва паўтарае вокліч аднаго з нашых распрацоўшчыкаў (потым вы пераканаецеся, што Kubernetes тут зусім ні пры чым). У ёй вы не знойдзеце нечаканых адкрыццяў аб Kubernetes, але можаце разлічваць на пару добрых урокаў аб складаных сістэмах.

Пару тыдняў таму мая каманда займалася міграцыяй аднаго мікрасэрвісу на асноўную платформу, якая ўключае CI/CD, працоўнае асяроддзе на аснове Kubernetes, метрыкі і іншыя карыснасці. Пераезд насіў выпрабавальны характар: мы планавалі ўзяць яго за аснову і перанесці яшчэ прыкладна 150 сэрвісаў у найбліжэйшыя месяцы. Усе яны адказваюць за працу некаторых з найбуйных анлайн-пляцовак Іспаніі (Infojobs, Fotocasa і інш.).

Пасля таго, як мы разгарнулі прыкладанне ў Kubernetes і перанакіравалі на яго частку трафіку, нас чакаў трывожны сюрпрыз. Затрымка (latency) запытаў у Kubernetes была ў 10 разоў вышэй, чым у EC2. Увогуле, было неабходна або шукаць вырашэнне гэтай праблемы, або адмаўляцца ад міграцыі мікрасэрвісу (і, магчыма, ад усяго праекта).

Чаму ў Kubernetes затрымка настолькі вышэйшая, чым у EC2?

Каб знайсці вузкае месца, мы сабралі метрыкі на ўсім шляху запыту. Наша архітэктура простая: API-шлюз (Zuul) праксіруе запыты да экзэмпляраў мікрасэрвісу ў EC2 або Kubernetes. У Kubernetes мы выкарыстоўваем NGINX Ingress Сontroller, а бэкэнды ўяўляюць сабой звычайныя аб'екты тыпу разгортванне з JVM-дадаткам на платформе Spring.

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

Здавалася, што праблема злучана з затрымкай на пачатковым этапе працы ў бэкендзе (я пазначыў праблемны ўчастак на графіцы як «хх»). У EC2 адказ дадатку займаў каля 20 мс. У Kubernetes затрымка ўзрастала да 100-200 мс.

Мы хутка адкінулі верагодных падазраваных, звязаных са зменай асяроддзя выканання. Версія JVM засталася ранейшай. Праблемы кантэйнерызацыі таксама былі ні пры чым: прыкладанне ўжо паспяхова працавала ў кантэйнерах у EC2. Загрузка? Але мы назіралі высокія затрымкі нават пры 1 запыце за секунду. Паўзамі на зборку смецця таксама можна было занядбаць.

Адзін з нашых адміністратараў Kubernetes пацікавіўся, ці няма ў дадатку знешніх залежнасцяў, паколькі ў мінулым запыты да DNS выклікалі падобныя праблемы.

Гіпотэза 1: дазвол імёнаў DNS

Пры кожным запыце наша дадатак ад аднаго да трох разоў звяртаецца да асобніка AWS Elasticsearch у дамене накшталт elastic.spain.adevinta.com. Унутры кантэйнераў у нас маецца shell, таму мы можам праверыць, ці сапраўды пошук дамена займае працяглы час.

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 мс, стала ясна, што дазвол DNS пры звароце да Elasticsearch сапраўды робіць уклад у ўзрастанне затрымкі.

Аднак гэта было дзіўна па дзвюх прычынах:

  1. У нас ужо ёсць маса прыкладанняў у Kubernetes, якія ўзаемадзейнічаюць з рэсурсамі AWS, але не пакутуюць ад вялікіх затрымак. Які б ні была прычына, яна мае дачыненьне канкрэтна да гэтага выпадку.
  2. Мы ведаем, што JVM ажыццяўляе in-memory-кэшаванне 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

Цяпер кантэйнер атрымліваў IP амаль імгненна. Гэта прывяло да некаторага паляпшэння, але мы толькі злёгку наблізіліся да чаканага ўзроўню затрымкі. Хоць дазвол DNS займала шмат часу, сапраўдная прычына па-ранейшаму выслізгвала ад нас.

Дыягностыка з дапамогай сеткі

Мы вырашылі прааналізаваць трафік з кантэйнера з дапамогай tcpdump, каб прасачыць, што менавіта адбываецца ў сетцы:

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

Затым мы даслалі некалькі запытаў і спампавалі іх capture (kubectl cp my-service:/capture.pcap capture.pcap) для далейшага аналізу ў Wireshark.

У DNS-запытах не было нічога падазронага (акрамя адной дробязі, пра якую я раскажу пазней). Але былі пэўныя дзівацтвы ў тым, як наш сэрвіс апрацоўваў кожны запыт. Ніжэй прыведзены скрыншот capture'а, які паказвае прыняцце запыту да пачатку адказу:

"Kubernetes павялічыў затрымку ў 10 разоў": хто ж у гэтым вінаваты?

Нумары пакетаў прыведзены ў першым слупку. Для яснасці я вылучыў колерам розныя плыні TCP.

Зялёны струмень, які пачынаецца з 328-го пакета, паказвае, як кліент (172.17.22.150) усталяваў TCP-злучэнне з кантэйнерам (172.17.36.147). Пасля першаснага поціску рукі (328-330), пакет 331 прынёс HTTP GET /v1/.. - уваходны запыт да нашага сэрвісу. Увесь працэс заняў 1 мс.

Шэры струмень (з пакета 339) паказвае, што наш сэрвіс паслаў HTTP-запыт да асобніка Elasticsearch (TCP-поціск рукі адсутнічае, паколькі выкарыстоўваецца ўжо наяўнае злучэнне). На гэта спатрэбілася 18 мс.

Пакуль усё нармальна, і часы прыкладна адпавядаюць чаканым затрымкам (20-30 мс пры замерах з кліента).

Аднак сіняя секцыя займае 86 мс. Што ў ёй адбываецца? З пакетам 333 наш сэрвіс даслаў HTTP GET-запыт на /latest/meta-data/iam/security-credentials, а адразу пасля яго, па тым жа TCP-злучэнні, яшчэ адзін GET-запыт на /latest/meta-data/iam/security-credentials/arn:...

Мы выявілі, што гэта паўтараецца з кожным запытам ва ўсёй трасіроўцы. Дазвол DNS сапраўды крыху павольней у нашых кантэйнерах (тлумачэнне гэтага феномену вельмі цікава, але я зберагу яго для асобнага артыкула). Аказалася, што прычынай вялікіх затрымак з'яўляюцца звароты да сэрвісу AWS Instance Metadata пры кожным запыце.

Гіпотэза 2: лішнія звароты да AWS

Абодва endpoint'а належаць AWS Instance Metadata API. Наш мікрасэрвіс выкарыстоўвае гэты сэрвіс падчас працы з Elasticsearch. Абодва выкліку з'яўляюцца часткай базавага працэсу аўтарызацыі. Endpoint, да якога адбываецца зварот пры першым запыце, выдае ролю IAM, злучаную з асобнікам.

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

Другі запыт звяртаецца да другога endpoint'у за часовымі паўнамоцтвамі для дадзенага асобніка:

/ # 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 павінен узяць на сябе абавязкі па арганізацыі гэтага працэсу, аднак па нейкай прычыне гэтага не адбываецца.

Правёўшы пошук па issues на GitHub, мы сутыкнуліся з праблемай #1921. Яна дапамагла нам вызначыць напрамак, у якім трэба "капаць" далей.

AWS SDK абнаўляе сертыфікаты пры надыходзе адной з наступных умоў:

  • Тэрмін заканчэння іх дзеяння (Expiration) трапляе ў EXPIRATION_THRESHOLD, жорстка ўсталяваны ў кодзе на 15 хвілін.
  • З моманту апошняй спробы абнавіць сертыфікаты прайшло больш чакай, чым REFRESH_THRESHOLD, за'hardcode'ены на 60 хвілін.

Каб паглядзець фактычны тэрмін заканчэння атрыманых намі сертыфікатаў, мы выканалі прыведзеныя вышэй cURL-каманды з кантэйнера і з экзэмпляра EC2. Час дзеяння сертыфіката, атрыманага з кантэйнера, аказалася значна карацей: роўна 15 хвілін.

Цяпер усё стала зразумела: для першага запыту наш сэрвіс атрымліваў часовыя сертыфікаты. Паколькі тэрмін іх дзеяння не перавышаў 15 мінуць, пры наступным запыце AWS SDK вырашаў абнавіць іх. І такое адбывалася з кожным запытам.

Чаму тэрмін дзеяння сертыфікатаў стаў карацейшым?

Сэрвіс AWS Instance Metadata прызначаны для працы з асобнікамі EC2, а не Kubernetes. З іншага боку, нам не хацелася мяняць інтэрфейс прыкладанняў. Для гэтага мы скарысталіся KIAM - прыладай, які з дапамогай агентаў на кожным вузле Kubernetes дазваляе карыстачам (інжынерам, якія разгортваюць прыкладанні ў кластар) прысвойваць IAM-ролі кантэйнерам у pod'ах так, нібы яны з'яўляюцца інстансамі EC2. KIAM перахапляе выклікі да сэрвісу AWS Instance Metadata і апрацоўвае іх са свайго кэша, папярэдне атрымаўшы ад AWS. З пункту гледжання прыкладання нічога не мяняецца.

KIAM пастаўляе кароткатэрміновыя сертыфікаты pod'ам. Гэта разумна, калі ўлічыць, што сярэдняя працягласць існавання pod'а менш, чым асобніка EC2. Па змаўчанні тэрмін дзеяння сертыфікатаў роўны тым жа 15 хвілінам.

У выніку, калі накласці абодва значэнні па змаўчанні сябар на сябра, узнікае праблема. Кожны сертыфікат, прадстаўлены з дадаткам, заканчваецца праз 15 хвілін. Пры гэтым AWS Java SDK прымусова абнаўляе любы сертыфікат, да тэрміна заканчэння дзеяння якога застаецца менш за 15 хвілін.

У выніку, часовы сертыфікат прымусова абнаўляецца з кожным запытам, што цягне за сабой пары зваротаў да API AWS і прыводзіць да значнага павелічэння затрымкі. У 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 можна ў гэтым артыкуле ад яе стваральнікаў.

А ў нашым блогу таксама чытайце:

Крыніца: habr.com

Дадаць каментар