10 тыповых памылак пры выкарыстанні Kubernetes

Заўв. перав.: аўтары гэтага артыкула - інжынеры з невялікай чэшскай кампаніі pipetail. Ім атрымалася сабраць выдатны спіс з [месцамі банальных, але ўсё яшчэ] гэтак актуальных праблем і памылак, злучаных з эксплуатацыяй кластараў Kubernetes.

10 тыповых памылак пры выкарыстанні Kubernetes

За гады выкарыстання Kubernetes нам давялося папрацаваць з вялікай колькасцю кластараў (як кіраваных, так і некіравальных – на GCP, AWS і Azure). З часам мы сталі заўважаць, што некаторыя памылкі ўвесь час паўтараюцца. Аднак у гэтым няма нічога ганебнага: мы самі здзейснілі большасць з іх!

У артыкуле сабраны найболей распаўсюджаныя памылкі, а таксама згадана пра тое, як іх выпраўляць.

1. Рэсурсы: запыты і ліміты

Гэты пункт вызначана заслугоўвае самай пільнай увагі і першага месца ў спісе.

CPU request звычайна альбо наогул не зададзены, альбо мае вельмі нізкае значэнне (каб размясціць як мага больш pod'аў на кожным вузле). Такім чынам, вузлы аказваюцца перагружаныя. Падчас высокай нагрузкі працэсарныя магутнасці вузла цалкам задзейнічаны і канкрэтная працоўная нагрузка атрымлівае толькі тое, што "запытала" шляхам тратлінгу CPU. Гэта прыводзіць да павышэння затрымак у дадатку, таймаўтам і іншым непрыемным наступстваў. (Больш падрабязна пра гэта чытайце ў іншым нашым нядаўнім перакладзе: «CPU-ліміты і агрэсіўны тротлінг у Kubernetes» - заўв. перав.)

BestEffort (вельмі ня рэкамендуецца):

resources: {}

Экстрэмальна нізкі запыт CPU (вельмі ня рэкамендуецца):

   resources:
      Requests:
        cpu: "1m"

З іншага боку, наяўнасць ліміту CPU можа прыводзіць да неабгрунтаванага пропуску тактаў pod'амі, нават калі працэсар вузла загружаны не цалкам. Ізноў жа, гэта можа прывесці да павелічэння затрымак. Працягваюцца спрэчкі вакол параметра CPU CFS quota у ядры Linux і тротлінга CPU у залежнасці ад усталяваных лімітаў, а таксама адключэнні квоты CFS… Нажаль, ліміты CPU могуць выклікаць больш праблем, чым здольныя вырашыць. Больш падрабязна пра гэта можна даведацца па спасылцы ніжэй.

Празмернае вылучэнне (overcommiting) памяці можа прывесці да больш маштабных праблем. Дасягненне мяжы CPU цягне за сабой пропуск тактаў, у той час як дасягненне мяжы па памяці цягне за сабой "забойства" pod'а. Назіралі калі-небудзь OOMkill? Так, гаворка ідзе менавіта аб ім.

Жадаеце звесці да мінімуму верагоднасць гэтай падзеі? Не размяркоўвайце празмерныя аб'ёмы памяці і выкарыстоўвайце Guaranteed QoS (Quality of Service), усталёўваючы memory request роўным ліміту (як у прыкладзе ніжэй). Больш падрабязна пра гэта чытайце ў прэзентацыі Henning Jacobs (вядучы інжынер Zalando).

Burstable (больш высокая верагоднасць атрымаць OOMkilled):

   resources:
      requests:
        memory: "128Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: 2

Гарантаваны:

   resources:
      requests:
        memory: "128Mi"
        cpu: 2
      limits:
        memory: "128Mi"
        cpu: 2

Што патэнцыйна дапаможа пры наладзе рэсурсаў?

З дапамогай metrics-server можна паглядзець бягучае спажыванне рэсурсаў CPU і выкарыстанне памяці pod'амі (і кантэйнерамі ўсярэдзіне іх). Хутчэй за ўсё, вы ўжо ім карыстаецеся. Проста выканайце наступныя каманды:

kubectl top pods
kubectl top pods --containers
kubectl top nodes

Аднак яны паказваюць толькі бягучае выкарыстанне. З ім можна атрымаць прыблізнае ўяўленне аб парадку велічынь, але ў канчатковым выніку спатрэбіцца гісторыя змены метрык у часе (каб адказаць на такія пытанні, як: "Якая была пікавая нагрузка на CPU?", "Які была нагрузка ўчора раніцай?" — і г.д.). Для гэтага можна выкарыстоўваць Праметэй, DataDog і іншыя інструменты. Яны проста атрымліваюць метрыкі з metrics-server і захоўваюць іх, а карыстач можа запытваць іх і будаваць адпаведныя графікі.

VerticalPodAutoscaler дазваляе аўтаматызаваць гэты працэс. Ён адсочвае гісторыю выкарыстання працэсара і памяці і настройвае новыя request'ы і limit'ы на аснове гэтай інфармацыі.

Эфектыўнае выкарыстанне вылічальных магутнасцяў - няпростая задача. Гэта ўсё роўна што ўвесь час гуляць у тэтрыс. Калі вы занадта шмат плаціце за вылічальныя магутнасці пры нізкім сярэднім спажыванні (скажам, ~10%), рэкамендуем звярнуць увагу на прадукты, заснаваныя на AWS Fargate ці Virtual Kubelet. Яны пабудаваны на мадэлі білінгу serverless/pay-per-usage, што ў такіх умовах можа аказацца танней.

2. Liveness і readiness probes

Па змаўчанні праверкі стану liveness і readiness у Kubernetes не ўключаны. І часам іх забываюць уключыць…

Але як яшчэ можна ініцыяваць перазапуск сэрвісу ў выпадку неадхільнай памылкі? І як балансавальнік нагрузкі даведаецца, што нейкі pod гатовы прымаць трафік? Ці што ён здольны апрацаваць больш трафіку?

Часта гэтыя спробы блытаюць паміж сабой:

  • Жвавасць - Праверка "жывучасці", якая перазапускае pod пры няўдалым завяршэнні;
  • Гатоўнасць - Праверка гатоўнасці, яна пры няўдачы адключае pod ад сэрвісу Kubernetes (гэта можна праверыць з дапамогай kubectl get endpoints) і трафік на яго не паступае да таго часу, пакуль чарговая праверка не завершыцца паспяхова.

Абедзве гэтыя праверкі ВЫКАНВАЮЦЦА НА ПРАЦЯГУ ЎСЯГО ЖЫЦЦЁВАГА ЦЫКЛУ POD'А. Гэта вельмі важна.

Распаўсюджана памылка, што readiness-пробы запускаюцца толькі на старце, каб балансавальнік мог даведацца, што pod гатовы (Ready) і можа прыступіць да апрацоўкі трафіку. Аднак гэта толькі адзін з варыянтаў іх ужывання.

Іншы магчымасць даведацца, што трафік на pod празмеру вялікі і перагружае яго (або pod праводзіць рэсурсаёмістыя вылічэнні). У гэтым выпадку readiness-праверка дапамагае зменшыць нагрузку на pod і «астудзіць» яго. Паспяховае завяршэнне readiness-праверкі ў будучыні дазваляе зноў павысіць нагрузку на pod. У гэтым выпадку (пры няўдачы readiness-пробы) правал liveness-праверкі быў бы вельмі контрпрадуктыўным. Навошта перазапускаць pod, які здаровы і працуе з усіх сіл?

Таму ў некаторых выпадках поўная адсутнасць праверак лепш, чым іх уключэнне з няправільна настроенымі параметрамі. Як было сказана вышэй, калі liveness-праверка капіюе readiness-праверку, то вы ў вялікай бядзе. Магчымы варыянт - наладзіць толькі readiness-тэст, А небяспечны liveness пакінуць у баку.

Абодва тыпу праверак не павінны завяршацца няўдачай пры падзенні агульных залежнасцяў, інакш гэта прывядзе да каскаднай (лавінападобнай) адмовы ўсіх pod'аў. Іншымі словамі, не шкодзіце самому сабе.

3. LoadBalancer для кожнага HTTP-сэрвісу

Хутчэй за ўсё, у вас у кластары ёсць HTTP-сэрвісы, якія вы хацелі б пракінуць у навакольны свет.

Калі адкрыць сэрвіс як type: LoadBalancer, яго кантролер (у залежнасці ад пастаўшчыка паслуг) будзе прадастаўляць і ўзгадняць знешні LoadBalancer (не абавязкова які працуе на L7, хутчэй нават на L4), і гэта можа адбіцца на кошце (вонкавы статычны адрас IPv4, вылічальныя магутнасці, пасекундная тарыфікацыя) з-за неабходнасці стварэння вялікай колькасці падобных рэсурсаў.

У дадзеным выпадку значна лагічна выкарыстоўваць адзін вонкавы балансавальнік нагрузкі, адчыняючы сэрвісы як type: NodePort. Або, што яшчэ лепш, разгарнуць нешта накшталт nginx-ingress-controller (Або траефік), які выступіць адзіным NodePort endpoint'ам, звязаным з вонкавым балансавальнікам нагрузкі, і будзе маршрутызаваць трафік у кластары з дапамогай ўваходжанне-рэсурсаў Kubernetes.

Іншыя ўнутрыкластарныя (мікра)сэрвісы, якія ўзаемадзейнічаюць адзін з адным, могуць «мець зносіны» з дапамогай сэрвісаў тыпу ClusterIP і убудаванага механізму выяўлення сэрвісаў праз DNS. Толькі не выкарыстоўвайце іх публічныя DNS/IP, бо гэта можа паўплываць на затрымку і прывесці да росту кошту хмарных паслуг.

4. Аўтамаштабаванне кластара без уліку яго асаблівасцяў

Пры даданні вузлоў у кластар і іх выдаленні з яго не варта спадзявацца на некаторыя базавыя метрыкі накшталт выкарыстанні CPU на гэтых вузлах. Планаванне pod'а павінна рабіцца з улікам мноства абмежаванняў, такіх як affinity pod'ов/вузлоў, taints і tolerations, запыты рэсурсаў, QoS і г.д. Выкарыстанне вонкавага autoscaler'а, не які ўлічвае гэтыя нюансы, можа прывесці да праблем.

Уявіце, што нейкі pod павінен быць запланаваны, але ўсе даступныя магутнасці CPU запытаны/разабраны і pod захрасае ў стане Pending. Вонкавы autoscaler бачыць сярэднюю бягучую загрузку CPU (а не запытаную) і не ініцыюе пашырэнне (scale-out) - не дадае яшчэ адзін вузел. У выніку гэты pod не будзе запланаваны.

Пры гэтым зваротнае маштабаванне (scale-in) - выдаленне вузла з кластара - заўсёды складаней рэалізаваць. Прадстаўце, што ў вас stateful pod (з падлучаным сталым сховішчам). Persistent-тома звычайна належаць да вызначанай зоне даступнасці і не рэпліцыруюцца ў рэгіёне. Такім чынам, калі вонкавы autoscaler выдаліць вузел з гэтым pod'ом, то планавальнік не зможа запланаваць дадзены pod на іншы вузел, бо гэта можна зрабіць толькі ў той зоне даступнасці, дзе знаходзіцца сталае сховішча. Pod завісне ў стане Pending.

У Kubernetes-супольнасці вялікай папулярнасцю карыстаецца cluster-autoscaler. Ён працуе ў кластары, падтрымлівае API ад асноўных пастаўшчыкоў хмарных паслуг, улічвае ўсе абмежаванні і ўмее маштабавацца ў вышэйпералічаных выпадках. Ён таксама здольны выконваць scale-in пры захаванні ўсіх устаноўленых абмежаванняў, тым самым эканомячы грошы (якія інакш былі б патрачаны на незапатрабаваныя магутнасці).

5. Пагарджанне магчымасцямі IAM/RBAC

Сцеражыцеся выкарыстоўваць IAM-карыстальнікаў з пастаяннымі сакрэтамі для машын і прыкладанняў. Арганізуйце часовы доступ, выкарыстоўваючы ролі і ўліковыя запісы службаў (service accounts).

Мы часта сутыкаемся з тым, што ключы доступу (і сакрэты) аказваюцца за'hardcode'ны ў канфігурацыі прыкладання, а таксама з грэбаваннем ратацыяй сакрэтаў нягледзячы на ​​наяўны доступ да Cloud IAM. Выкарыстоўвайце ролі IAM і ўліковыя запісы службаў замест карыстальнікаў, дзе гэта дарэчы.

10 тыповых памылак пры выкарыстанні Kubernetes

Забудзьцеся пра kube2iam і пераходзіце адразу да роляў IAM для service accounts (як гэта апісваецца ў аднайменнай нататцы Štěpán Vraný):

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-role
  name: my-serviceaccount
  namespace: default

Адна анатацыя. Не так ужо складана, праўда?

Акрамя таго, не надзяляйце service accounts і профілі інстансаў прывілеямі admin и cluster-admin, калі яны ў гэтым не маюць патрэбы. Гэта рэалізаваць крыху складаней, асабліва ў RBAC K8s, але вызначана варта выдаткоўваных намаганняў.

6. Не належце на аўтаматычнае anti-affinity для pod'ов

Уявіце, што ў вас тры рэплікі некаторага deployment'а на вузле. Вузел падае, а разам з ім і ўсе рэплікі. Непрыемная сітуацыя, так? Але чаму ўсе рэплікі былі на адным вузле? Няўжо Kubernetes не павінен забяспечваць высокую даступнасць (HA)?!

Нажаль, планавальнік Kubernetes па сваёй ініцыятыве не выконвае правілы паасобнага існавання. (anti-affinity) для pod'аў. Іх неабходна відавочна прапісаць:

// опущено для краткости
      labels:
        app: zk
// опущено для краткости
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"

Вось і ўсё. Цяпер pod'ы будуць планавацца на розныя вузлы (гэта ўмова правяраецца толькі падчас планавання, але не іх працы - адсюль і requiredDuringSchedulingIgnoredDuringExecution).

Тут мы гаворым аб podAntiAffinity на розных вузлах: topologyKey: "kubernetes.io/hostname", - А не аб розных зонах даступнасці. Каб рэалізаваць паўнавартасную HA, давядзецца капнуць глыбей у гэтую тэму.

7. Ігнараванне PodDisruptionBudget'аў

Уявіце, што ў вас production-нагрузка ў кластары Kubernetes. Перыядычна вузлы і сам кластар даводзіцца абнаўляць (ці выводзіць з эксплуатацыі). PodDisruptionBudget (PDB) – гэта нешта накшталт гарантыйнай дамовы аб абслугоўванні паміж адміністратарамі кластара і карыстачамі.

PDB дазваляе пазбегнуць перабояў у працы сэрвісаў, выкліканых недахопам вузлоў:

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: zookeeper

У гэтым прыкладзе вы, як карыстач кластара, заяўляеце адміністратарам: "Гэй, у мяне ёсць сэрвіс zookeeper, і незалежна ад таго, што вы робіце, я б жадаў, каб прынамсі 2 рэплікі гэтага сэрвісу заўсёды былі даступныя".

Падрабязней пра гэта можна пачытаць тут.

8. Некалькі карыстальнікаў або асяродкаў у агульным кластары

Прасторы імёнаў Kubernetes (namespaces) не забяспечваюць моцную ізаляцыю.

Распаўсюджана памылка, што калі разгарнуць не-prod-нагрузку ў адну прастору імёнаў, а prod-нагрузку ў іншую, то яны ніяк не будуць уплываць адзін на аднаго… Зрэшты, некаторы ўзровень ізаляцыі можна дасягнуць з дапамогай запытаў/абмежаванняў рэсурсаў, усталёўкі квот, заданні priorityClass'ов. Нейкую «фізічную» ізаляцыю ў data plane забяспечваюць affinities, tolerations, taints (ці nodeselectors), аднак падобны падзел даволі складана рэалізаваць.

Тым, каму неабходна сумяшчаць абодва тыпы працоўных нагрузак у адным кластары, давядзецца мірыцца са складанасцю. Калі ж такой патрэбнасці няма, і вам па кішэні завесці яшчэ адзін кластар (скажам, у публічным воблаку), то лепш так і зрабіць. Гэта дазволіць дасягнуць значна больш высокага ўзроўню ізаляцыі.

9. externalTrafficPolicy: Cluster

Вельмі часта мы назіраем, што ўвесь трафік унутр кластара паступае праз сэрвіс тыпу NodePort, для якога па змаўчанні ўсталявана палітыка. externalTrafficPolicy: Cluster. Гэта азначае, што NodePort адкрыты на кожным вузле ў кластары, і можна выкарыстоўваць любы з іх для ўзаемадзеяння з патрэбным сэрвісам (наборам pod'аў).

10 тыповых памылак пры выкарыстанні Kubernetes

Пры гэтым рэальныя pod'ы, злучаныя з вышэйзгаданым NodePort-сэрвісам, звычайна маюцца толькі на неком падмностве гэтых вузлоў. Іншымі словамі, калі я падключуся да вузла, на якім няма патрэбнага pod'а, ён будзе перанакіроўваць трафік на іншы вузел, дадаючы транзітны ўчастак (hop) і павялічваючы затрымку (калі вузлы знаходзяцца ў розных зонах даступнасці/дата-цэнтрах, затрымка можа апынуцца даволі высокай; акрамя таго, вырастуць выдаткі на egress-трафік).

З іншага боку, калі для нейкага сэрвісу Kubernetes зададзена палітыка externalTrafficPolicy: Local, то NodePort адчыняецца толькі на тых вузлах, дзе фактычна запушчаныя патрэбныя pod'ы. Пры выкарыстанні знешняга балансавальніка нагрузак, правяраючага стану (healthchecking) endpoint'аў (як гэта робіць AWS ELB), ён будзе адпраўляць трафік толькі на патрэбныя вузлы, Што спрыяльна адаб'ецца на затрымках, вылічальных патрэбах, рахунках за egress (ды і разумны сэнс дыктуе тое ж самае).

Высокая верагоднасць, што вы ўжо карыстаецеся нешта накшталт траефік або nginx-ingress-controller у якасці канчатковай NodePort-кропкі (ці LoadBalancer, які таксама выкарыстоўвае NodePort) для маршрутызацыі HTTP ingress-трафіку, і ўстаноўка гэтай опцыі можа значна знізіць затрымку пры падобных запытах.

В гэтай публікацыі можна больш падрабязна даведацца аб externalTrafficPolicy, яе перавагах і недахопах.

10. Не прывязвайцеся да кластараў і не марнатраўце control plane

Раней серверы было прынята зваць імёнамі ўласнымі: Антон, HAL9000 і Colossus… Сёння на змену ім прыйшлі выпадкова згенераваныя ідэнтыфікатары. Аднак звычка засталася, і зараз уласныя імёны дастаюцца кластарам.

Тыповая гісторыя (заснаваная на рэальных падзеях): усё пачыналася з доказу канцэпцыі, таму кластар насіў ганаровае імя тэставанне… Прайшлі гады, і ён ДА СЭТАГА часу выкарыстоўваецца ў production, і ўсе баяцца да яго дакрануцца.

Няма нічога пацешнага ў тым, што кластары ператвараюцца ў гадаванцаў, таму рэкамендуемы перыядычна выдаляць іх, адначасна практыкуючыся ў аднаўленні пасля збояў (у гэтым дапаможа chaos engineering - заўв. перав.). Акрамя таго, не перашкодзіць заняцца і кіравальным пластом (control plane). Боязь дакрануцца да яго - не вельмі добры знак. І г.д. мёртвы? Хлопцы, вы ўліплі па-сапраўднаму!

З іншага боку, не варта захапляцца маніпуляцыямі з ім. З часам кіруючы пласт можа стаць павольным. Хутчэй за ўсё, гэта злучана з вялікай колькасцю ствараных аб'ектаў без іх ратацыі (звычайная сітуацыя пры выкарыстанні Helm з наладамі па змаўчанні, з-за чаго не абнаўляецца яго стан у configmap'ах/сакрэтах - як вынік, у кіравальным пласце запасяцца тысячы аб'ектаў) або з пастаянным рэдагаваннем аб'ектаў kube-api (для аўтаматычнага маштабавання, для CI/CD, для маніторынгу, логі падзей, кантролеры і г.д.).

Акрамя таго, рэкамендуемы праверыць дамовы SLA / SLO з пастаўшчыком managed Kubernetes і звярнуць увагу на гарантыі. Вендар можа гарантаваць даступнасць кіраўніка пласта (або яго субкампанентаў), але не p99-затрымку запытаў, якія вы яму дасылаеце. Іншымі словамі, можна ўвесці kubectl get nodes, а адказ атрымаць толькі праз 10 хвілін, і гэта не будзе з'яўляцца парушэннем умоў пагаднення аб абслугоўванні.

11. Бонус: выкарыстанне тэга latest

А вось гэта ўжо класіка. У апошні час мы сустракаемся з падобнай тэхнікай не так часта, паколькі шматлікія, навучаныя горкім досведам, перасталі выкарыстоўваць тэг :latest і пачалі замацоўваць (pin) версіі. Ура!

ECR падтрымлівае нязменнасць тэгаў вобразаў; рэкамендуемы азнаёміцца ​​з гэтай характэрнай асаблівасцю.

Рэзюмэ

Не чакайце, што ўсё запрацуе па ўзмаху рукі: Kubernetes - гэта не панацэя. Дрэннае дадатак застанецца такім нават у Kubernetes (і, магчыма, стане яшчэ горш). Бестурботнасць прывядзе да залішняй складанасці, павольнай і напружанай працы кіраўніка пласта. Акрамя таго, вы рызыкуеце застацца без стратэгіі аварыйнага аднаўлення. Не разлічвайце, што Kubernetes са скрынкі возьме на сябе забеспячэнне ізаляцыі і высокай даступнасці. Выдаткуйце некаторы час на тое, каб зрабіць сваё прыкладанне па-сапраўднаму cloud native.

Пазнаёміцца ​​з няўдалым вопытам розных каманд можна ў гэтай падборцы гісторый ад Henning Jacobs.

Жадаючыя дапоўніць спіс памылак, прыведзены ў гэтым артыкуле, могуць звязацца з намі ў Twitter (@MarekBartik, @MstrsObserver).

PS ад перакладчыка

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

Крыніца: habr.com

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