П'ять промахів при розгортанні першої програми на Kubernetes

П'ять промахів при розгортанні першої програми на KubernetesFail by Aris-Dreamer

Багато хто вважає, що достатньо перенести додаток на Kubernetes (або за допомогою Helm або вручну) — і буде щастя. Але не все так просто.

Команда Mail.ru Cloud Solutions переклала статтю DevOps-інженера Джуліана Гінді. Він розповідає, з яким підводним камінням його компанія зіткнулася в процесі міграції, щоб ви не наступали на ті ж граблі.

Крок перший: налаштування запитів пода та лімітів

Почнемо з налаштування чистого оточення, в якому працюватимуть наші поди. Kubernetes чудово справляється з плануванням подів та обробкою станів відмови. Але виявилося, що планувальник іноді не може розмістити під, якщо важко оцінити, скільки ресурсів йому потрібно для успішної роботи. Саме тут спливають запити на ресурси та ліміти. Ведеться багато суперечок щодо найкращого підходу до настроювання запитів та лімітів. Іноді здається, що це справді швидше мистецтво, ніж наука. Ось наш підхід.

Запити пода (pod requests) - це основне значення, яке використовується планувальником для оптимального розміщення пода.

З документації Kubernetes: на етапі фільтрації визначається набір вузлів, де можна запланувати під. Наприклад, фільтр PodFitsResources перевіряє, чи достатньо ресурсів для задоволення конкретних запитів подання на ресурси.

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

У цьому випадку планувальник часто «видавлював» поди і не міг повторно їх запланувати через те, що площина управління поняття не мала скільки ресурсів потрібно додатку, адже це ключовий компонент алгоритму планування.

Ліміти пода (pod limits) - Це більш чітке обмеження для подавання. Воно є максимальним обсягом ресурсів, який кластер виділить контейнеру.

Знову ж таки, з офіційної документації: якщо для контейнера встановлено ліміт пам'яті 4 ГіБ, то kubelet (і середовище виконання контейнера) введе його примусово. Середовище виконання не дозволяє контейнеру використовувати більше заданого ліміту ресурсів. Наприклад, коли процес у контейнері намагається використати більше допустимого об'єму пам'яті, ядро ​​системи завершує цей процес з помилкою out of memory (OOM).

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

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

На жаль, я не можу дати конкретних вказівок, які значення встановлювати, але ми самі дотримуємося наступних правил:

  1. Використовуючи інструмент навантажувального тестування, моделюємо базовий рівень трафіку та спостерігаємо за використанням ресурсів пода (пам'яті та процесора).
  2. Встановлюємо запити на довільно низьке значення (з обмеженням ресурсів приблизно в 5 разів більше значення запитів) і спостерігаємо. Коли запити на надто низькому рівні, процес не може розпочатися, що часто викликає загадкові помилки часу виконання Go.

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

Уявіть ситуацію, коли у вас легкий веб-сервер з дуже високим обмеженням ресурсів, наприклад 4 ГБ пам'яті. Ймовірно, цей процес доведеться масштабувати горизонтально і кожен новий модуль доведеться планувати на вузлі з доступним обсягом пам'яті щонайменше 4 ГБ. Якщо такого вузла немає, кластер повинен ввести новий вузол для обробки цього пода, що може зайняти деякий час. Важливо досягти мінімальної різниці між запитами ресурсів та лімітами, щоб забезпечити швидке та плавне масштабування.

Крок другий: налаштування тестів Liveness та Readiness

Це ще одна тонка тема, яка часто обговорюється у спільноті Kubernetes. Важливо добре розумітися на тестах життєздатності (Liveness) та готовності (Readiness), оскільки вони забезпечують механізм сталої роботи програмного забезпечення та мінімізують час простою. Однак вони можуть завдати серйозного удару по продуктивності вашої програми, якщо не налаштовані правильно. Нижче наводиться короткий виклад, що собою представляють обидві проби.

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

Проби Liveness повинні бути дешевими, тобто не споживати багато ресурсів, тому що вони часто запускаються і повинні інформувати Kubernetes, що програма запущена.

Якщо ви встановите параметр для запуску кожну секунду, це додасть 1 запит на секунду, так що прийміть до уваги, що для обробки цього трафіку знадобляться додаткові ресурси.

У нас в компанії тести Liveness перевіряють основні компоненти програми навіть якщо дані (наприклад, з віддаленої бази даних або кешу) не повністю доступні.

Ми налаштували в додатках кінцеву точку «працездатності», яка просто повертає код відповіді 200. Це показник того, що процес запущений та здатний обробляти запити (але ще не трафік).

проба Готовність вказує, чи готовий контейнер для обслуговування запитів. Якщо проба готовності виходить з ладу, контролер кінцевих точок видаляє IP-адресу пода з кінцевих точок усіх служб, що відповідають поду. Це також йдеться у документації Kubernetes.

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

У співтоваристві ведеться багато суперечок, чи слід звертатися безпосередньо до бази даних. Враховуючи накладні витрати (перевірки виконуються часто, але їх можна регулювати), ми вирішили, що для деяких програм готовність обслуговувати трафік зараховується лише після перевірки того, що з бази даних повертаються записи. Добре продумані проби готовності забезпечили більш високий рівень доступності та усунули простої під час розгортання.

Якщо ви вирішите робити запит до бази даних для перевірки готовності програми, переконайтеся, що він обходиться якомога дешевше. Візьмемо такий запит:

SELECT small_item FROM table LIMIT 1

Ось приклад, як ми налаштовуємо ці два значення в Kubernetes:

livenessProbe: 
 httpGet:   
   path: /api/liveness    
   port: http 
readinessProbe:  
 httpGet:    
   path: /api/readiness    
   port: http  periodSeconds: 2

Можна додати деякі додаткові параметри конфігурації:

  • initialDelaySeconds - Скільки секунд пройде між запуском контейнера і початком запуску проб.
  • periodSeconds - Інтервал очікування між запусками проб.
  • timeoutSeconds - Кількість секунд, після яких під вважається аварійним. Звичайний тайм-аут.
  • failureThreshold - кількість відмов тестів, перш ніж у під буде відправлено сигнал перезапуску.
  • successThreshold - кількість успішних проб, перш ніж під переходить у стан готовності (після збою, коли під запускається або відновлюється).

Крок третій: налаштування дефолтних мережевих політик

У Kubernetes «плоска» мережева топографія, за умовчанням всі поди взаємодіють один з одним безпосередньо. У деяких випадках це небажано.

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

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

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:  
 name: default-deny-ingress
spec:  
 podSelector: {}  
 policyTypes:  
   - Ingress

Візуалізація цієї конфігурації:

П'ять промахів при розгортанні першої програми на Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Більш детально тут.

Крок четвертий: нестандартна поведінка за допомогою хуків та init-контейнерів

Однією з головних завдань було забезпечення деплоїв у Kubernetes без простою для розробників. Це важко через те, що існує безліч варіантів завершення роботи додатків та звільнення ними використаних ресурсів.

Особливі труднощі виникли з Nginx. Ми помітили, що при послідовному розгортанні цих подів активні з'єднання переривалися до успішного завершення.

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

lifecycle: 
 preStop:
   exec:
     command: ["/usr/local/bin/nginx-killer.sh"]

А от nginx-killer.sh:

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
   echo "Waiting while shutting down nginx..."
   sleep 10
done

Ще одна надзвичайно корисна парадигма – використання init-контейнерів для обробки запуску конкретних програм. Це особливо корисно у випадку, якщо у вас є ресурсомісткий процес міграції бази даних, який слід запустити до запуску програми. Для цього процесу ви також можете вказати вищий ліміт ресурсів, не встановлюючи такий ліміт для основної програми.

Іншою поширеною схемою є доступ до секретів в init-контейнері, який надає ці облікові дані головному модулю, що запобігає несанкціонованому доступу до секретів із самого основного модуля програми.

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

Крок п'ятий: налаштування ядра

Насамкінець розповімо про більш просунуту техніку.

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

Однак, Kubernetes дозволяє запустити привілейований контейнер, який змінює параметри ядра тільки для конкретного пода. Ось що ми використовували для зміни максимальної кількості відкритих з'єднань:

initContainers:
  - name: sysctl
     image: alpine:3.10
     securityContext:
         privileged: true
      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

Це просунута техніка, яка часто не потрібна. Але якщо ваш додаток ледве справляється з великим навантаженням, можете спробувати налаштувати деякі з цих параметрів. Більш детальна інформація про цей процес та налаштування різних значень – як завжди в офіційній документації.

На закінчення

Хоча Kubernetes може здатися готовим рішенням з коробки, для безперебійної роботи додатків необхідно зробити кілька ключових кроків.

Протягом усієї міграції на Kubernetes важливо слідувати «циклу навантажувального тестування»: запускаєте програму, тестуєте її під навантаженням, спостерігаєте за метриками та поведінкою при масштабуванні, налаштовуєте конфігурацію на основі цих даних, потім знову повторюєте цей цикл.

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

Завжди ставте собі такі питання:

  1. Скільки ресурсів споживають програми та як зміниться цей обсяг?
  2. Якими є реальні вимоги до масштабування? Скільки трафіку в середньому буде обробляти програму? А як щодо пікового трафіку?
  3. Як часто потрібне сервісу горизонтальне масштабування? Як швидко потрібно вводити в дію нові поди, щоб приймати трафік?
  4. Наскільки коректно завершується робота подів? Чи це потрібно взагалі? Чи можна розгортатися без даунтайму?
  5. Як мінімізувати ризики для безпеки та обмежити збитки від будь-яких скомпрометованих подів? Чи є у якихось сервісів дозволи чи доступи, які їм не потрібні?

Kubernetes надає неймовірну платформу, яка дозволяє використовувати найкращі практики для розгортання тисяч сервісів у кластері. Проте всі програми різні. Іноді використання вимагає трохи більше роботи.

На щастя, Kubernetes надає необхідні налаштування для досягнення всіх технічних цілей. Використовуючи комбінацію запитів ресурсів та лімітів, проб Liveness та Readiness, init-контейнерів, мережевих політик та нестандартного налаштування ядра, ви можете досягти високої продуктивності поряд з відмовостійкістю та швидкою масштабованістю.

Що ще почитати:

  1. Найкращі практики та рекомендації для запуску контейнерів та Kubernetes у виробничих середовищах.
  2. 90+ корисних інструментів для Kubernetes: розгортання, керування, моніторинг, безпека та не тільки.
  3. Наш канал Навколо Kubernetes у Телеграмі.

Джерело: habr.com

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