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-проби запускаються тільки на старті, щоб балансувальник міг дізнатися, що під готовий (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 (або траефік), який виступить єдиним Порт вузла endpoint'ом, пов'язаним із зовнішнім балансувальником навантаження, і маршрутизуватиме трафік у кластері за допомогою проникнення-ресурсів Kubernetes.

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

4. Автомасштабування кластера без урахування його особливостей

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

Уявіть, що якийсь під повинен бути запланований, але всі доступні потужності 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. Це означає, що Порт вузла відкритий на кожному вузлі в кластері, і можна використовувати будь-який із них для взаємодії з потрібним сервісом (набором 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. Не прив'язуйтесь до кластерів і не зловживайте.

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

Типова історія (заснована на реальних подіях): все починалося з доказів концепції, тому кластер носив горде ім'я Тестування… Минули роки, і він ДО СЬОГОДНІ використовується в production, і всі бояться до нього доторкнутися.

Немає нічого кумедного в тому, що кластери перетворюються на вихованців, тому рекомендуємо періодично видаляти їх, попутно практикуючись у відновлення після збоїв (у цьому допоможе інженерія хаосу - прим. перев.). Крім того, не завадить зайнятися і керуючим шаром (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

Додати коментар або відгук