Дев'ять порад щодо підвищення продуктивності Kubernetes

Дев'ять порад щодо підвищення продуктивності Kubernetes

Всім привіт! Мене звуть Олег Сидоренков, я працюю в компанії «ДомКлік» керівником команди інфраструктури. Ми експлуатуємо «Кубік» у проді вже більше трьох років, і за цей час пережили з ним багато різних цікавих моментів. Сьогодні я розповім вам, як за правильного підходу можна вичавити з «ванільного» Kubernetes ще більше продуктивності для вашого кластера. Ready steady go!

Всі ви чудово знаєте, що Kubernetes - це масштабована система з відкритим кодом для оркестрації контейнерами; ну, або 5 бінар, які творять магію, керуючи життєвим циклом ваших мікросервісів в серверному середовищі. Крім того, це досить гнучкий інструмент, який можна збирати як конструктор Lego для максимальної кастомізації під різні завдання.

І начебто все добре: закидай серваки в кластер, як дровця в топку, і горя не знай. Але якщо ти за екологію, то замислишся: «Як я можу підтримувати вогонь у грубці та ліс пошкодувати?». Іншими словами, як знайти способи покращення інфраструктури та зниження витрат.

1. Слідкуйте за ресурсами команд та програм

Дев'ять порад щодо підвищення продуктивності Kubernetes

Один із найбанальніших, але дієвих методів — введення requests/limits. Розділяйте програми по неймспейсах, а неймспейси за командами розробки. Задавайте додатку перед деплом значення споживання процесорного часу, пам'яті, ефемерного сховища.

resources:
   requests:
     memory: 2Gi
     cpu: 250m
   limits:
     memory: 4Gi
     cpu: 500m

Досвідченим шляхом ми дійшли висновку: не варто роздмухувати реквести від лімітів більше, ніж удвічі. Обсяг кластера розраховується виходячи з реквестів, і якщо ви задаватимете додаткам різницю в ресурсах, наприклад, у 5-10 разів, то уявіть, що станеться з вашою нодою, коли вона заповниться подами і різко отримає навантаження. Нічого доброго. Як мінімум, тротлінг, а як максимум, попрощаєтеся з воркером і отримаєте циклічне навантаження на інші ноди після того, як піди почнуть переїжджати.

Крім того, за допомогою limitranges Ви можете на старті задати для контейнера значення за ресурсами - мінімальні, максимальні та за замовчуванням:

➜  ~ kubectl describe limitranges --namespace ops
Name:       limit-range
Namespace:  ops
Type        Resource           Min   Max   Default Request  Default Limit  Max Limit/Request Ratio
----        --------           ---   ---   ---------------  -------------  -----------------------
Container   cpu                50m   10    100m             100m           2
Container   ephemeral-storage  12Mi  8Gi   128Mi            4Gi            -
Container   memory             64Mi  40Gi  128Mi            128Mi          2

Не забудьте обмежити ресурси неймспейсу, щоб одна команда не змогла забрати всі ресурси кластера:

➜  ~ kubectl describe resourcequotas --namespace ops
Name:                   resource-quota
Namespace:              ops
Resource                Used          Hard
--------                ----          ----
limits.cpu              77250m        80
limits.memory           124814367488  150Gi
pods                    31            45
requests.cpu            53850m        80
requests.memory         75613234944   150Gi
services                26            50
services.loadbalancers  0             0
services.nodeports      0             0

Як видно з опису resourcequotasЯкщо команда ops захоче розгорнути поди, які будуть споживати ще 10 cpu, то планувальник не дасть це зробити і видасть помилку:

Error creating: pods "nginx-proxy-9967d8d78-nh4fs" is forbidden: exceeded quota: resource-quota, requested: limits.cpu=5,requests.cpu=5, used: limits.cpu=77250m,requests.cpu=53850m, limited: limits.cpu=10,requests.cpu=10

Для вирішення подібного завдання можна написати інструмент, наприклад, як цей, що вміє зберігати та комітити стан ресурсів команд.

2. Підбирайте оптимальне файлове сховище

Дев'ять порад щодо підвищення продуктивності Kubernetes

Тут я хотів би торкнутися теми персистентних томів і дискової підсистеми worker-од Kubernetes. Я сподіваюся, що ніхто не використовує "Куб" на HDD у проді, але часом і звичайного SSD вже стає мало. Ми стикалися з такою проблемою, що логи вбивали диск за операціями вводу-виводу, і тут варіантів рішення не дуже багато:

  • Використовувати високопродуктивні SSD або переходити на NVMe (якщо ви самі розпоряджаєтесь своїм залізом).

  • Зменшувати рівень журналування.

  • Робити «розумне» балансування подів, які ґвалтують диск (podAntiAffinity).

Скрин вище показує, що відбувається під nginx-ingress-controller з диском, коли включено журнал access_logs (~12 тис. журналів/сек.). Такий стан, звичайно, може призводити до деградації всіх програм на цій ноді.

Що стосується PV, на жаль, я не випробував все види Persistent Volumes. Використовуйте найкращий варіант, який підходить саме вам. У нас історично так склалося, що невелика частина сервісів потребує RWX-томів, і давним-давно під це завдання стали використовувати NFS-хранилку. Дешево та… вистачає. Звичайно, ми з ним наїлися гівна — будь здоровий, але навчилися його тюнити, і голова більше не болить. А якщо можливо, переходьте на об'єктне сховище S3.

3. Збирайте оптимізовані образи

Дев'ять порад щодо підвищення продуктивності Kubernetes

Найкраще використовувати оптимізовані під контейнери образи, щоб Kubernetes міг швидше діставати їх та ефективніше виконувати. 

Оптимізованість означає, що образи:

  • містять лише одну програму або виконують тільки одну функцію;

  • невеликого розміру, тому що великі образи гірше передаються через мережу;

  • мають кінцеві точки для перевірки працездатності та готовності, за допомогою яких Kubernetes може робити якісь дії у разі простоїв;

  • використовують дружні до контейнерів операційні системи (на зразок Alpine або CoreOS), які більш стійкі до помилок конфігурування;

  • використовують багатоетапні складання, щоб ви могли розгортати тільки скомпіловані програми, а не супутні вихідні.

Є багато інструментів та сервісів, що дозволяють перевірити та оптимізувати образи на льоту. Важливо завжди підтримувати їх у актуальному стані та перевіреними на безпеку. У результаті ви отримуєте:

  1. Зниження навантаження на весь кластер.

  2. Зменшення часу запуску контейнера.

  3. Найменший обсяг всього вашого Docker registry.

4. Використовуйте кеш ДНР

Дев'ять порад щодо підвищення продуктивності Kubernetes

Якщо говорити про високі навантаження, то без тюнінгу DNS-системи кластера жити досить паршиво. Колись давно розробники Kubernetes підтримували своє рішення kube-dns. Воно було впроваджено і в нас, але ця софтина особливо не тюніла і не видавала необхідну продуктивність, хоча, начебто, завдання просте. Потім з'явився coredns, на який ми перейшли і горя не знали, згодом він став DNS-сервісом за замовчуванням в K8s. У якийсь момент ми доросли до 40 тис. rps до DNS-системи, і цього рішення теж не вистачало. Але, завдяки щасливому випадку, вийшов Nodelocaldns, він же node local cache, він же NodeLocal DNSCache.

Чому ми це використовуємо? У ядрі Linux є баг, який при множинному зверненні через conntrack NAT UDP призводить до стану гонки за запис в conntrack-таблиці, і частина трафіку через NAT втрачається (кожен похід через Service — це NAT). Nodelocaldns вирішує цю проблему шляхом позбавлення NAT і апгрейду підключення до TCP до апстрімових DNS, а також локальним кешуванням DNS-запитів до апстрімів (включаючи короткий 5-секундний негативний кеш).

5. Масштабуйте поди горизонтально та вертикально автоматично

Дев'ять порад щодо підвищення продуктивності Kubernetes

Чи можете ви з упевненістю сказати, що всі ваші мікросервіси готові до двох-триразового зростання навантаження? Як правильно виділяти ресурси своїм додаткам? Тримати запущеними пару подів понад робоче навантаження може виявитися надмірним, а тримати впритул - ризикуєте отримати простий від раптового зростання трафіку на сервіс. Золотий центр допомагають досягти закляття множення такі послуги, як Горизонтальний автоматичний масштабатор и Вертикальний модуль автоматичного масштабування.

VPA дозволяє автоматично піднімати requests/limits ваших контейнерів у поді залежно від фактичного використання. Чим він може бути корисним? Якщо у вас є поди, які з якоїсь причини не можна горизонтально відмасштабувати (що не зовсім надійно), то можете спробувати довірити зміну його ресурсів VPA. Його фішка полягає в системі рекомендацій на основі історичних та поточних даних з metric-server, тому якщо ви не хочете автоматично змінювати requests/limits, то можете просто відстежувати рекомендовані ресурси для ваших контейнерів та оптимізувати налаштування для економії процесора та пам'яті в кластері.

Дев'ять порад щодо підвищення продуктивності KubernetesЗображення взято з https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231

Планувальник у Kubernetes завжди ґрунтується на requests. Якого б значення ви туди не поставили, планувальник шукатиме відповідну ноду, виходячи з нього. Значення limits потрібні кублету для того, щоб розуміти, коли тротлити або вбивати під. І оскільки єдиний важливий параметр – значення requests, VPA працюватиме з ним. Щоразу, коли ви задаєте вертикальне масштабування програми, ви визначаєте, якими повинні бути requests. А що тоді буде із limits? Цей параметр також буде пропорційно відмасштабований.

Наприклад, ось стандартні настройки пода:

resources:
   requests:
     memory: 250Mi
     cpu: 200m
   limits:
     memory: 500Mi
     cpu: 350m

Механізм рекомендацій визначає, що додатку для нормальної роботи потрібно 300m CPU і 500Mi. Ви отримаєте такі налаштування:

resources:
   requests:
     memory: 500Mi
     cpu: 300m
   limits:
     memory: 1000Mi
     cpu: 525m

Як згадувалося вище, це пропорційне масштабування виходячи із співвідношення requests/limits у маніфесті:

  • CPU: 200m → 300m: співвідношення 1:1.75;

  • Memory: 250Mi → 500Mi: співвідношення 1:2.

Щодо HPA, то тут механізм роботи прозоріший. Виставляються граничні значення метрик, наприклад, процесора і пам'яті, і якщо середнє значення всіх реплік перевищує граничне, то додаток масштабується на +1 під доти, поки значення не впаде нижче порога, або поки не буде досягнуто максимальної кількості реплік.

Дев'ять порад щодо підвищення продуктивності KubernetesЗображення взято з https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231

Крім звичайних метрик, на зразок процесора та пам'яті, ви можете налаштувати пороги на своїх кастомних метриках з Prometheus і працювати з ними, якщо вважаєте це найбільш точним визначенням, коли слід масштабувати вашу програму. Після того, як програма стабілізується нижче заданої межі метрики, HPA почне масштабувати поди вниз до мінімальної кількості реплік або до стану, коли навантаження задовольнятиме заданий поріг.

6. Не забувайте про Node Affinity та Pod Affinity

Дев'ять порад щодо підвищення продуктивності Kubernetes

Не всі вузли працюють на однаковому устаткуванні, не всім подам потрібно виконувати додатки, що вимагають інтенсивних обчислень. Kubernetes дозволяє задавати спеціалізацію нод та подів за допомогою Node Affinity и Pod Affinity.

Якщо у вас є ноди, які підходять для операцій з інтенсивними обчисленнями, то для максимальної ефективності краще прив'язати додатки до відповідних нодів. Для цього використовуйте nodeSelector з міткою вузла.

Припустимо, у вас дві ноди: одна з CPUType=HIGHFREQ і великою кількістю швидких ядер, інша з MemoryType=HIGHMEMORY великою кількістю пам'яті та більш високою швидкодією. Найпростіше призначити розгортання пода ноді HIGHFREQ, додавши до розділу spec такий селектор:

…
nodeSelector:
	CPUType: HIGHFREQ

Витратніший і специфічніший спосіб зробити це — використовувати nodeAffinity в полі affinity розділу spec. Є два варіанта:

  • requiredDuringSchedulingIgnoredDuringExecution: жорстке налаштування (планувальник розгортатиме поди тільки на конкретних нодах (і ніде більше));

  • preferredDuringSchedulingIgnoredDuringExecution: м'яке налаштування (планувальник спробує розгорнути на конкретних нодах, а якщо не вийде, спробує розгорнути на наступній доступній ноді).

Ви можете встановити певний синтаксис керування мітками вузлів, наприклад, In, NotIn, Exists, DoesNotExist, Gt або Lt. Однак пам'ятайте, що складні методи у довгих списках міток уповільнять прийняття рішень у критичних ситуаціях. Іншими словами, не ускладнюйте.

Як згадувалося вище, Kubernetes дозволяє задати прив'язку поточних подів. Тобто, ви можете зробити так, щоб певні поди працювали разом з іншими подами в тій же зоні доступності (актуально для хмар) або нодах.

В podAffinity поля affinity розділу spec доступні ті ж поля, що і у випадку з nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution и preferredDuringSchedulingIgnoredDuringExecution. Єдина відмінність у тому, що matchExpressions прив'яже поди до ноди, на якій вже виконується під такою міткою.

Ще Kubernetes пропонує поле podAntiAffinity, яке, навпаки, не прив'язує під до ноди з певними подами.

Щодо виразів nodeAffinity можна дати ту саму пораду: намагайтеся зберігати простоту і логічність правил, не треба намагатися перевантажити специфікацію подів складним набором правил. Дуже легко створити правило, яке не відповідатиме умовам кластера, створивши зайве навантаження на планувальник і знизивши загальну продуктивність.

7. Taints & Tolerations

Є ще один спосіб керування планувальником. Якщо у вас великий кластер із сотнями нод та тисячами мікросервісів, то дуже складно не дозволяти певним подам розміщуватись на певних нодах.

У цьому допомагає механізм taints – забороняючих правил. Наприклад, можна у певних сценаріях заборонити певним нодам запускати у себе поди. Для застосування taint до конкретного сайту потрібно використовувати опцію taint в Kubectl. Вкажіть ключ і значення, а потім taint начебто NoSchedule або NoExecute:

$ kubectl taint nodes node10 node-role.kubernetes.io/ingress=true:NoSchedule

Також варто відзначити, що механізм taint підтримує три основні ефекти: NoSchedule, NoExecute и PreferNoSchedule.

  • NoSchedule означає, що поки у специфікації пода не буде відповідного запису tolerations, він не зможе бути розгорнутий на ноді (у цьому прикладі node10).

  • PreferNoSchedule - Спрощена версія NoSchedule. У цьому випадку планувальник спробує не розподіляти поди, які не мають відповідного запису. tolerations на ноду, але це жорстке обмеження. Якщо в кластері не виявиться ресурсів, то піди почнуть розвертатися на цій ноді.

  • NoExecute — цей ефект запускає негайну евакуацію подів, які не мають відповідного запису tolerations.

Цікаво, що така поведінка може бути скасована за допомогою механізму tolerations. Це зручно, коли є заборонена нода і вам знадобилося розмістити на ній тільки інфраструктурні сервіси. Як це зробити? Дозволити тільки ті поди, для яких є підходяща толерація.

Ось як виглядатиме специфікація пода:

spec:
   tolerations:
     - key: "node-role.kubernetes.io/ingress"
        operator: "Equal"
        value: "true"
        effect: "NoSchedule"

Це не означає, що при наступному редепло під потрапить саме на цю ноду, це не механізм Node Affinity і nodeSelector. Але комбінуючи кілька фіч, ви можете досягти дуже гнучкого налаштування планувальника.

8. Налаштуйте пріоритет розгортання подов

Те, що ви налаштували прив'язку подів до нодів, не означає, що всі поди повинні оброблятися з однаковим пріоритетом. Наприклад, ви можете захотіти розгортати якісь поди раніше за інших.

Kubernetes пропонує різні способи налаштування пріоритетності подів (Pod Priority and Preemption). Налаштування складається з кількох частин: об'єкта PriorityClass та описи поля priorityClassName у специфікації пода. Розглянемо приклад:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 99999
globalDefault: false
description: "This priority class should be used for very important pods only"

Ми створюємо PriorityClass, задаємо йому ім'я, опис та значення. чим вище valueтим вище пріоритет. Значення може бути будь-яким 32-бітовим цілим числом, меншим або рівним 1 000 000 000. Вищі значення зарезервовані для критично важливих системних подів, які, як правило, не можуть бути витіснені. Витіснення відбуватиметься тільки якщо високопріоритетному поду ніде розгорнуться, тоді частина подів з певної ноди будуть евакуйовані. Якщо вам цей механізм занадто жорсткий, то можна додати опцію preemptionPolicy: Never, І тоді витіснення не буде, під стоятиме першим у черзі і чекатиме, коли планувальник знайде для нього вільні ресурси.

Далі ми створюємо під, в якому вказуємо ім'я priorityClassName:

apiVersion: v1
kind: Pod
metadata:
  name: static-web
  labels:
    role: myrole
 spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP
  priorityClassName: high-priority
          

Можна створювати скільки завгодно класів пріоритетності, хоча рекомендується не захоплюватися цим (скажімо, обмежитися низьким, середнім та високим пріоритетом).

Таким чином, у разі потреби ви зможете підвищити ефективність розгортання критичних сервісів, таких як nginx-ingress-controller, coredns тощо.

9. Оптимізуйте ETCD-кластер

Дев'ять порад щодо підвищення продуктивності Kubernetes

ETCD можна назвати мозком всього кластеру. Дуже важливо підтримувати роботу цієї БД на високому рівні, оскільки саме від неї залежить швидкість операцій у «Кубі». Досить стандартним, і в той же час непоганим рішенням триматиме кластер ETCD на майстер-нодах, щоб мати мінімальну затримку до kube-apiserver. Якщо не виходить так зробити, то розташовуйте ETCD якомога ближче, маючи хорошу пропускну здатність між учасниками. Також звертайте увагу на те, скільки нод із ETCD може випасти без шкоди для кластера

Дев'ять порад щодо підвищення продуктивності Kubernetes

Майте на увазі, що надмірне збільшення кількості учасників у кластері може підвищити відмовостійкість на шкоду продуктивності, все має бути в міру.

Якщо говорити про налаштування сервісу, то рекомендацій небагато:

  1. Мати хороше залізо, виходячи з розмірів кластера (можна почитати тут).

  2. Підкрутити кілька параметрів, якщо ви розмазали кластер між парою ДЦ або ваша мережа та диски залишають бажати кращого (можна почитати тут).

Висновок

У цій статті описані пункти, яких наша команда намагається дотримуватись. Це не покроковий опис дій, а варіанти, які можуть стати в нагоді для оптимізації накладних витрат на кластер. Зрозуміло, що кожен кластер по-своєму унікальний, і рішення щодо налаштування можуть сильно відрізнятися, тому було б цікаво отримати від вас зворотний зв'язок: як ви стежите за своїм кластером Kubernetes, за допомогою чого ви покращуєте його роботу. Діліться своїм досвідом у коментарях, буде цікаво його дізнатися.

Джерело: habr.com