Kubernetes.Ресурсы.Память (Memory) - теория. часть13

Thank you for reading this post, don't forget to subscribe!

Что­бы Kubernetes смог мак­си­маль­но эффек­тив­но исполь­зо­вать доступ­ную инфра­струк­ту­ру и кор­рект­но выде­лить ресур­сы, необ­хо­ди­мые для рабо­ты ваше­го при­ло­же­ния, вам сле­ду­ет ука­зать тре­бо­ва­ния в ресур­сам каж­до­го кон­тей­не­ра. В дан­ный момент есть воз­мож­ность зада­вать два типа тре­бо­ва­ний (requests и limits) для двух типов ресур­сов - памя­ти (memory) и про­цес­со­ра (CPU). В дан­ной ста­тье рас­смот­рим requests и limits при­ме­ни­тель­но к памяти.

При опи­са­нии пода (Pods) для каж­до­го из его кон­тей­не­ров могут быть зада­ны тре­бо­ва­ния к ресур­сам в сле­ду­ю­щем формате:

Тип тре­бо­ва­ний requests исполь­зу­ет­ся в Kubernetes пла­ни­ров­щи­ком для кор­рект­но­го раз­ме­ще­ния и запус­ка подов в суще­ству­ю­щей инфра­струк­ту­ре, как бы гово­ря “запу­сти кон­тей­не­ры дан­но­го пода там, где есть доста­точ­ное коли­че­ство запро­щен­ных ресур­сов”. Limits - жест­кое огра­ни­че­ние ресур­сов, доступ­ных кон­тей­не­ру и сре­де его выпол­не­ния (тут и далее име­ет­ся в виду Docker container runtime). Пре­вы­ше­ние ука­зан­ных лими­тов (limits) ресур­сов зача­стую при­во­дит к тро­тт­лин­гу или оста­нов­ке (termination) контейнера.

Если зна­че­ние requests для кон­тей­не­ра не ука­за­но, то по умол­ча­нию будет исполь­зо­вать­ся зна­че­ние уста­нов­лен­ное в limits. Если же не ука­за­но зна­че­ние limits, то по умол­ча­нию это зна­че­ние будет рав­но 0 (если верить доку­мен­та­ции - неогра­ни­чен­но. На самом деле огра­ни­чи­ва­ет­ся ресур­са­ми узла, на кото­ром запус­ка­ет­ся под).

Воз­ни­ка­ет вопрос, сто­ит ли ука­зы­вать зна­че­ния limits боль­ше, чем requests? Если ваше при­ло­же­ние ста­биль­но исполь­зу­ет пред­ска­зу­е­мый объ­ем опе­ра­тив­ной памя­ти, то уста­нав­ли­вать раз­ные зна­че­ния пара­мет­ров requests и limits для памя­ти нет смыс­ла. В слу­чае с CPU, раз­ни­ца меж­ду задан­ны­ми зна­че­ни­я­ми requests и limits может не уста­нав­ли­вать­ся (при усло­вии, что эти же ресур­сы не исполь­зу­ют­ся дру­ги­ми кон­тей­не­ра­ми и их не нуж­но “делить”).

Если вы нови­чок в Kubernetes, для нача­ла луч­ше все­го исполь­зо­вать зна­че­ния limits точ­но такие же как и requests - это обес­пе­чит так назы­ва­е­мый “гаран­ти­ро­ван­ный класс каче­ства сер­ви­са” (Guaranteed QoS class, об этих клас­сах чуть ниже). С дру­гой сто­ро­ны, класс Burstable QoS потен­ци­аль­но поз­во­ля­ет более эффек­тив­но исполь­зо­вать ресур­сы инфра­струк­ту­ры, прав­да, за счет боль­шей непред­ска­зу­е­мо­сти - напри­мер, рост CPU-latency может повли­ять на осталь­ные поды/контейнеры, запу­щен­ные на том же рабо­чем узле (ноде).

В Kubernetes QoS клас­сы исполь­зу­ют­ся в соот­вет­ствии с нали­чи­ем и кон­фи­гу­ра­ци­ей requests и limits (деталь­ное опи­са­ние):

  • если для всех кон­тей­не­ров пода уста­нов­ле­ны отлич­ные от 0 requests и limits для всех типов ресур­сов, и эти зна­че­ния рав­ны, то под будет при­над­ле­жать к клас­су Guaranteed;
  • если для одно­го или несколь­ких кон­тей­не­ра пода уста­нов­ле­ны отлич­ные от 0 requests и limits для одно­го или всех типов ресур­сов и эти зна­че­ния не рав­ны, то под будет при­над­ле­жать к клас­су Burstable;
  • если для всех кон­тей­не­ров пода не уста­нов­ле­ны зна­че­ния requests и limits для всех типов ресур­сов, то поду будет при­сво­ен класс Best-Effort.

Поды клас­са Best-Effort обла­да­ют наи­мень­шим при­о­ри­те­том. Они могут исполь­зо­вать любое коли­че­ство сво­бод­ной памя­ти, доступ­ное на рабо­чем узле, но будут оста­нов­ле­ны в первую оче­редь, если систе­ма испы­ты­ва­ет недо­ста­ток памя­ти (under memory pressure). Поды клас­са Burstable обыч­но име­ют неко­то­рое гаран­ти­ро­ван­ное коли­че­ство ресур­сов (бла­го­да­ря requests), но могут исполь­зо­вать боль­ше ресур­сов (если такие доступ­ны). Если систе­ма испы­ты­ва­ет недо­ста­ток памя­ти (и оста­нов­ка подов с клас­сом Best-Effort не помог­ла), то поды дан­но­го клас­са, кото­рые пре­вы­си­ли зна­че­ние задан­ное в requests будут оста­нов­ле­ны. Класс Guaranteed обла­да­ет мак­си­маль­ным при­о­ри­те­том, и поды дан­но­го клас­са будут оста­нов­ле­ны толь­ко если они исполь­зу­ют боль­ше ресур­сов, чем уста­нов­ле­но в limits.

Итак, что же озна­ча­ет память (memory) в дан­ном кон­тек­сте? В нашем слу­чае, это общее зна­че­ние раз­ме­ра стра­ниц памя­ти (Resident set size, RSS) и исполь­зо­ва­ния кэша стра­ниц (page cache) контейнерами.

При­ме­ча­ние. В “чистом” docker'е в это зна­че­ние так­же вхо­дит своп (swap), кото­рый преду­смот­ри­тель­но отклю­чен в Kubernetes.

RSS - раз­мер стра­ниц памя­ти, выде­лен­ных про­цес­су опе­ра­ци­он­ной систе­мой и в насто­я­щее вре­мя нахо­дя­щих­ся в ОЗУ. Напри­мер, для Java про­цес­са это heap (куча), non-heap (стек) память, оff-heap (она же native memory) и т. д.

Кэш стра­ниц - ино­гда так­же назы­ва­е­мый дис­ко­вый кэшем, исполь­зу­ет­ся для кеши­ро­ва­ния бло­ков с HDD/SSD. Все опе­ра­ции ввода/вывода обыч­но про­ис­хо­дят через этот кэш (из сооб­ра­же­ний про­из­во­ди­тель­но­сти). Чем боль­ше дан­ных читает/записывает ваше при­ло­же­ние на диск, тем боль­ший объ­ем памя­ти необ­хо­дим для кэша стра­ниц. Ядро будет исполь­зо­вать доступ­ную память для кэша стра­ниц, но будет осво­бож­дать ее, если память пона­до­бит­ся в дру­гом месте/процессе - таким обра­зом про­из­во­ди­тель­ность ваше­го при­ло­же­ния может сни­жать­ся при недо­ста­точ­ном объ­е­ме опе­ра­тив­ной памяти.

Исхо­дя из доку­мен­та­ции docker, мож­но ска­зать, что раз­мер кэша стра­ниц, исполь­зу­е­мых кон­тей­не­ром, может силь­но отли­чать­ся в зави­си­мо­сти от того, могут ли неко­то­рые фай­лы “поде­ле­ны” меж­ду несколь­ки­ми кон­тей­не­ра­ми, запу­щен­ны­ми на одном рабо­чем узле (дости­га­ет­ся бла­го­да­ря overlayfs storage driver).

Зна­че­ния пара­мет­ров requests и limits изме­ря­ют­ся в бай­тах, одна­ко мож­но исполь­зо­вать и суф­фик­сы. К при­ме­ру, настрой­ка памя­ти JVM Xmx1g (1024³ bytes) будет соот­вет­ство­вать 1Gi в спе­ци­фи­ка­ции контейнера.

Огра­ни­че­ния по памя­ти (limits) с точ­ки зре­ния Kubernetes счи­та­ют­ся “несжи­ма­е­мы­ми” (non-compressible), сле­до­ва­тель­но при пре­вы­ше­нии этих огра­ни­че­ний тро­тт­линг невоз­мо­жен - ядро будет агрес­сив­но очи­щать кэш стра­ниц (для осво­бож­де­ния ресур­сов / дости­же­ния жела­е­мо­го состо­я­ния рабо­че­го узла) и кон­тей­не­ры к кон­це кон­цов могут быть оста­нов­ле­ны (пре­рва­ны) хоро­шо извест­ным Linux Out of Memory (OOM) Killer.

Для хоро­шей настрой­ки при­ло­же­ния часто при­хо­дит­ся эмпи­ри­че­ским путем под­би­рать необ­хо­ди­мые зна­че­ния requests и limits и менять их на про­тя­же­нии все­го жиз­нен­но­го цик­ла при­ло­же­ния, поэто­му не сто­ит пре­не­бре­гать сбо­ром мет­рик, мони­то­рин­гом и опо­ве­ще­ни­ем о исполь­зо­ва­нии ресурсов.