Деплой у Kubernetes: Оптимізація та Стратегії
Привіт! Сьогодні хочу поговорити з вами про деплой.
За даними, наведеними в Google SRE book, до 70% проблем виникає внаслідок змін в уже працюючих системах. Якщо у вас добре спроектоване та написане застосування і стабільна, налагоджена інфраструктура, саме деплой є вузьким місцем, яке можна покращити. Для мінімізації ризиків Google SRE BOOK рекомендує використовувати поступові викати, швидко і точно аналізувати проблеми, а в разі необхідності швидко відкотитися на попередню версію.
Ця стаття про деплой у Kubernetes, оскільки це найпопулярніша інфраструктурна платформа, яка вже має багато можливостей для побудови відмовостійких викатів. А те, чого не вистачає «з коробки», покривається можливостями інструментів розвиненої екосистеми.
Але почати потрібно не з можливостей Kubernetes, а з архітектурних особливостей, які повинні бути реалізовані при створенні застосувань, призначених для роботи в ній.
Фактор 9
У The Twelve-Factor App в якості 9-го фактора вказується утилізованість (Disposability). З наступною формулюванням:
«максимізуйте надійність за допомогою швидкого запуску та коректного завершення роботи (застосування)».
Ми повинні пам’ятати, що:
- екземпляри застосування ефемерні;
- старт і зупинка — нормальні операції;
- старт і зупинка повинні бути швидкими та надійними.
А ще — що застосування зависають і падають, ноди підвішуються, поди евіктуються, до них приходить OOMKiller, і це норма, в якій ми живемо.
Тому перше, про що ми повинні поговорити в цьому контексті, це про те, як Kubernetes запускає і зупиняє POD’и із застосуваннями і як можна оптимізувати цей процес.
Оптимізація запуску пода
Як Kubernetes запускає і зупиняє POD’и? Ці питання чудово розкриті в книзі Марко Лукша Kubernetes in Action. Але поки не будемо заглиблюватися у фоліант і обговоримо питання на достатньому для розуміння рівні.
На початку відбудеться певна подія (наприклад, створення нового ReplicaSet), на основі якої Kubernetes Controller Manager сформує завдання на генерацію POD’а і через Kubernetes API запише його в ETCD. Далі його підхопить Scheduler, і POD перейде в Pending (тобто для нього буде шукатися node — фізичний сервер для розгортання). Коли node знайдена, POD переходить у статус PodScheduled.
Далі послідовно запускаються init-контейнери, після чого POD отримує статус initialized. Потім запускаються робочі контейнери, проходять дві проби — Startup і Readiness, після чого POD переходить у статус Ready і на нього починає йти мережевий трафік.
Процес зрозумілий і знайомий, сподіваюся, всім DevOps`ам. Що можна прискорити в цій схемі?
Init-контейнери
Init-контейнери запускаються по черзі, що створює втрату часу при очікуванні одним контейнером іншого. Тому їх кількість потрібно мінімізувати. Використовуйте init-контейнери тільки тоді, коли це дійсно необхідно.
Кейс з практики
Ми з командою налагоджували запуск сценаріїв популярного ETL-шедулера AirFlow. Він запускає новий контейнер на кожен крок ETL-пайплайна. Ми досягли прискорення запуску кроків пайплайна на 20 секунд, прибравши єдиний init-контейнер, відповідальний за створення Kerberos ticket для доступу до зовнішніх сервісів. Справа в тому, що цей квиток був потрібен в дуже обмеженій кількості кроків, і прибравши його з init-контейнера в код сценарію, ми значно прискорили запуск всіх інших кроків.
Процедура ініціалізації
Хороші проби — питання екзистенційне. На пробах ми економити не можемо, але можемо прискорити процедуру ініціалізації.
Перша можлива проблема — це прогін міграцій в init-процедурі. Дуже погана, але, на жаль, дуже часта практика. Міграції ми повинні робити один раз при старті деплоя нової версії.
Далі можливо якесь скачування асетів, встановлення пакетів в контейнері під час запуску, коли хтось полінувався зробити щось на етапі збірки, і потім воно нам заважає.
І останній момент — JIT-компіляція. Оцініть, чи так вона потрібна і які бонуси ви отримуєте в обмін на затримку старту застосування (особливо в режимі обмеженого споживання ресурсів процесора).
Оптимізація зупинки пода
Зупинка пода важливіша, ніж його старт. У динамічному середовищі, де навантаження постійно змінюється, здатність до швидкої і коректної зупинки мінімізує витрати і підвищує надійність системи.
Зупинка процесу в контейнері пода здійснюється наступним чином. Процесу посилається сигнал SIGTERM, далі слідує grace period (за замовчуванням — 30 секунд), протягом якого процес повинен завершити роботу. Якщо цього не відбувається, процес зупиняється примусово за допомогою SIGKILL. Що може піти не так? Перш за все обробка SIGTERM.
Не чекаємо SIGKILL
Якщо ваше застосування не обробляє SIGTERM, може прийти SIGKILL і зробити боляче.
Щоб цього не сталося, застосування, отримавши SIGTERM, повинно завершитися коректно: обробити всі запити і повернути результат, закрити сесії, видалити підписки на черги, закрити з’єднання з базами даних… Загалом, зробити так, щоб його зупинка пройшла непомітно для інших учасників обробки навантажень.
На самому ділі це неочевидний момент, оскільки не всі середовища виконання і фреймворки реалізують таку поведінку за замовчуванням. Наприклад, у нас є Java з Spring Boot. Цей зв’язок, отримавши SIGTERM, просто вб’є все, що знаходиться всередині Java-машини. Але в Spring Boot нещодавно з’явилася опція server.shutdown=graceful.
Виставивши її, ми можемо перехопити SIGTERM і коректно його обробити. Тобто потрібно знати ваш runtime, потрібно знати ваші фреймворки, щоб коректно робити зупинку застосування в POD`і.
pre-stop hooks
Тепер про сигнали в Linux. Ось деякі, присвячені зупинці процесів:
SIGINT — інтерактивне завершення процесу.
Наприклад, якщо процес працює в GUI-режимі, то на цей сигнал він може видати вікно «Ви точно хочете завершити роботу?» і проігнорувати його, якщо ви виберете «Ні».
SIGTERM — штатне завершення процесу. Його ми можемо перехопити і обробити.
SIGQUIT — швидке завершення процесу. Трохи більш наполегливе вимога завершити процес, але ми також можемо його перехопити.
SIGKILL — негайне безумовне завершення процесу (перехоплення неможливо).
Але не всі дотримуються цих угод. Наприклад, в Nginx штатне завершення (graceful shutdown) процесу це SIGQUIT.
Те ж саме поведінка у PHP-FPM.
Щоб коректно зупинити ці сервіси в Kubernetes, ми можемо використовувати механізм pre-stop hooks.
Для Nginx ми можемо використовувати nginx cli для зупинки (до речі, в останніх офіційних іміджах Nginx проблема з SIGTERM вирішена і описаний workaround можна не застосовувати).
Для зупинки PHP-FPM ми маємо можливість за допомогою утиліти kill послати сигнал SIGQUIT в PID #1, але цього може бути недостатньо.
PHP-FPM працює за моделлю, в якій є master-процес, що виконує оркестрацію, і child-процеси, що виконують обробку запитів, і без налаштування process_control_timeout майстер просто вб’є воркери, не чекаючи, коли вони виконають поточні завдання.
Таким чином, потрібно знати особливості не тільки runtime і фреймворків, а й базового ПО.
Видалення Endpoint
Важливий момент в процесі зупинки POD’а — видалення Endpoint’а. Вони, як правило, породжуються за допомогою kubernetes service. Об’єкт service виконує в Kubernetes безліч функцій. У даному випадку нас цікавлять дві: Service Discovery і маршрутизація.
Service Discovery — це коли service шукає POD’и на основі переданих йому міток і робить ендпоінти (Service -> Label -> POD -> Endpoint).
Тобто ендпоінти — це POD’и, які відповідають міткам (labels) сервісу і пройшли Readiness-пробу. Їх ми можемо подивитися за допомогою команди kubectl get endpoints.
Видалення ендпоінта відбувається одночасно з зупинкою POD’а.
І тут важливо розуміти, як цей процес відбувається в розрізі кластера:
Коли на Control Plane з’являється завдання на видалення POD’а, в ETCD записується і завдання на видалення ендпоінта. На ноді кластера працює Kubelet, який періодично опитує Kubernetes API, завантажує конфігурацію своєї ноди і намагається її застосувати.
Він через Container Runtime Interface посилає в POD’и сигнали зупинки процесів і зупиняє контейнери. Крім Kubelet на ноді працює процес kube-proxy, який через CNI-плагін видаляє ендпоінт. Як правило, endpoint робиться за допомогою правил iptables з використанням conntrack, тому при видаленні правил поточні мережеві з’єднання не розриваються до їх завершення.
Проблема в тому, що ендпоінти — це cluster-wide-сутність, вони фактично є на кожній ноді кластера, і видалення відбувається навіть на нодах, на яких немає контейнерів, що належать зупиняємому POD’у:
Але в основі роботи кластера лежить PULL-основна схема, тому деякі ноди можуть затриматися з оновленням конфігурації / видаленням ендпоінта:
Крім того, в кластері може працювати додаткове мережеве ПЗ на зразок Ingress або Service mesh, яке також стежить за конфігурацією кластера, станом POD`в, ендпоінтів і змінює свою конфігурацію в залежності від того, що виявлено.
Тобто ми повинні розуміти, що трафік на POD може приходити навіть після отримання SIGTERM, і застосування повинно вміти обробляти цей трафік. Якщо наше застосування цього не робить, ми, наприклад, можемо додати затримку в pre-stop-хуки:
Але це збільшує час зупинки, а значить, збільшує споживання ресурсів.
Якщо ми розробляємо застосування самостійно, правильніше за все буде реалізувати стратегію обробки трафіку при зупинці на рівні коду.
Варіанти: або обробляти запити, сподіваючись встигнути в grace-період, або повертати помилку обробки запиту. Тобто краще отримати помилку і швидко зробити retry, ніж чекати, коли застосування просто закінчить «слухати» мережу, — в цьому випадку ми відвалимося по таймауту, і все одно доведеться зробити retry, просто набагато пізніше.
Резюмуючи все вищесказане: швидкий старт і коректна зупинка POD’ів в Kubernetes — це та база, на якій робиться не тільки відмовостійкий викат, але і нормальна, стабільна експлуатація застосувань.
Стратегії деплоя
Перш ніж обговорювати стратегії деплоя, варто домовитися про критерії, за якими ми можемо їх порівнювати. З практичної точки зору можна виділити наступні.
- Толерантність до збоїв і простоїв — ключовий показник і тема цієї статті.
- Консистентність. В рамках деяких стратегій деплоя можуть одночасно працювати кілька версій застосування, між якими користувацький трафік може розподілятися в тому числі випадковим чином. Якщо у версій різні функції, це здатне привести до неприємних колізій.
- Ресурсоємність. Кількість обладнання, яке потрібно для реалізації тієї чи іншої стратегії.
- Простота відкату. Можливість швидко повернутися до попередньої версії, якщо щось пішло не так.
- «Прогрів» інфраструктури. Оточення нашого застосування може містити:
• кеші, які можуть скидатися при деплої;
• агрегатори з’єднань, які можуть ламатися під час деплоя;
• автоскейлери, які можуть працювати на основі метрик, просто не встигли накопичитися для нової версії;
• тривалі з’єднання, що вимагають переустановки при зміні версій.
Є таке поняття — «метастабільний стан», коли система при одних і тих же зовнішніх умовах може бути як стабільна, так і нестабільна, і переходити з одного стану в інше на основі якихось внутрішніх збурень.
Класичний приклад метастабільного стану — тривалі з’єднання (наприклад, websocket). Вони вимагають досить мало ресурсів на підтримку, але установка з’єднання — процес з декількох ресурсоємних дій, в яких можуть брати участь безліч пов’язаних компонентів: потрібно встановити зашифрований канал, провести аутентифікацію клієнта, вибрати відповідний інстанс і прив’язати до нього з’єднання. І якщо через якесь внутрішнє збурення (наприклад, необережного деплоя) буде потрібно масова переустановка тривалих з’єднань, система може впасти і самостійно вже не піднятися.
Ці нюанси необхідно враховувати при проектуванні процедури деплоя.
Вибір стратегії деплоя
Найпопулярнішими стратегіями деплоя є:
- Recreate,
- Rolling,
- Blue/green,
- Canary,
- Dark (A/B).
З коробки в k8s є Recreate, RollingUpdate і частково canary. Два основних контролера, за допомогою яких ми розгортаємо застосування в k8s, — це Deployment для stateless і Statefulset для stateful-застосувань. Налаштування стратегій деплоя трохи різняться:
• Deployment — .spec.strategy:
Recreate
RollingUpdate
• Statefulset — .spec.updateStrategy:
OnDelete
RollingUpdate
В Stetefulset стратегія recreate можлива тільки при ручному видаленні POD’ів, але якщо абстрагуватися від контролерів і розгортати безпосередньо типи застосувань, то деплой stateful-застосувань дуже сильно залежить від принципів їх роботи з даними: вимог до консистентності даних, реалізації реплікації, шардінгу, читання і запису і, як правило, не зводиться до простого вибору стратегії викату в налаштуваннях.
Але stateful у k8s — це не найпоширеніший випадок (і найчастіше це завдання вирішується за допомогою патерну kubernetes operator), тому сфокусуємося на роботі зі stateless за допомогою контролера з типом Deployment.
Recreate
При recreate у нас є стара версія застосування:
ми її вимикаємо:
і на її місці запускаємо нову:
Для її налаштування ми повинні виставити Recreate в ключ .spec.strategy.type:
За критеріями оцінки у нас виходить ось що.
- Толерантність до збоїв і простоїв — погана: Recreate — це гарантований простій.
- Консистентність — хороша: в один час працює тільки одна версія застосування.
- Ресурсоємність — низька: для реалізації цієї стратегії нове обладнання не потрібно.
- «Прогрів» інфраструктури — з очевидних причин проблемне місце.
- Простота відкату — неоднозначна. Викат нової версії — це часто не просто зміна версії докер-іміджа в подах застосування. Нерідко разом з нею йде зміна схеми даних (накат міграцій), і коли ми дотримуємося стратегії recreate, у нас немає стимулу робити схеми даних назад сумісними. Тому в разі відкату стара версія застосування може просто не запрацювати на новій схемі, а добре організований відкат міграцій зустрічається досить рідко. Тобто відкат для Recreate потрібно продумувати окремо, з урахуванням специфіки застосування.
Коли варто використовувати Recreate.
- Потрібна строга консистентність. Наприклад, якщо наше застосування — валідатор криптовалюти, і дублювання підтвердження в blockchain для нас набагато гірше, ніж невеликий простій.
- Є технологічне вікно, в якому ми можемо робити з нашою інфраструктурою все, що завгодно. В цьому випадку recreate — це просто, дешево і сердито.
- Застосування не обробляє вхідні запити. Наприклад, коли в нашому застосуванні є асинхронні воркери, що працюють за PULL-моделлю, і ми можемо оновитися швидше, ніж настане SLA за часом обробки черги завдань.
Rolling
При RollingUpdate ми поступово відключаємо старі екземпляри застосування:
На їх місце викочуємо нові:
При цьому вхідний трафік беруть на себе працюючі екземпляри:
І так поступово переводимо всі робочі інстанси нашого застосування на нову версію:
Для налаштування цієї стратегії потрібно виставити в ключ .spec.strategy.type значення RollingUpdate.
Крім цього, ми можемо змінити параметри
maxUnavailable
— кількість POD`ів, які можна відключити при оновленні;maxSurge
— кількість POD`ів, які можна запустити додатково.
Комбінуючи ці значення, ми можемо або піднімати нові POD`и і потім вимикати старі, або вимикати старі, а на їх місці запускати нові, або поєднувати цю поведінку, чітко керуючи потребою в додаткових ресурсах і потужностями, що обслуговують вхідний трафік.
Для прогріву інфраструктури можна використовувати параметр minReadySeconds — проміжок часу, після якого POD вважається працюючим. А для відкату на стару версію у разі появи проблем можна виставити progressDeadlineSeconds — якщо процес оновлення не завершиться за вказаний час, буде здійснено автоматичний відкат.
Але, як правило, в релізі ми катимо одночасно кілька об’єктів, і відкат одного Deployment буває недостатнім (наприклад, в релізі може міститися оновлення Deployment і ConfigMap, який містить конфігурацію для нової версії). Тому варто використовувати інші інструменти. Наприклад, helm package manager з ключами timeout і atomic дозволяє відкотити реліз цілком.
Тепер оцінюємо Rollingupdate з точки зору сформульованих раніше критеріїв.
- Толерантність до збоїв і простоїв — потенційно хороша.
- Консистентність — погана: при викаті одночасно працюють дві версії застосування, між якими за замовчуванням трафік балансується у випадковому порядку.
- Ресурсоємність — неоднозначна. Можна реалізувати сценарії як з потребою в додатковому обладнанні, так і без нього.
- Простота відкату — висока.
- «Прогрів» інфраструктури — хороший, з можливістю керувати процесом прогріву «з коробки».
Як поліпшити консистентність при Rolling Update?
Є кілька способів поліпшити консистентність запитів при rolling update.
Версіонування / зворотна сумісність API. Якщо ваше застосування надає API, ви можете його версіонувати: старі клієнти йдуть на api/v1, нові на api/v2, і таким чином колізій не відбувається.
Session Affinity. Можна реалізувати прив’язку клієнт-бекенд. Наприклад, за допомогою ingress-nginx:
Після того як виставлені ці анотації, ingress починає роздавати клієнтам cookie, на підставі яких зв’язує клієнта з бекендом, виключаючи переброс трафіку клієнта між різними POD`ами під час деплоя.
Чим це може бути загрожує? Припустимо, є стара версія застосування з ідеально збалансованим трафіком.
Після перекату першого екземпляра балансер за алгоритмом round robin розподіляє трафік між працюючими екземплярами:
Після перекату другого екземпляра ця процедура повторюється для трафіку, прив’язаного до нього, і створюється значний дисбаланс.
Після перекату третього екземпляра і перерозподілу його трафіку ситуація може виглядати так:
Як можна помітити, трафік в процесі деплоя на деякі екземпляри застосування може збільшитися більш ніж в два рази. Це потрібно враховувати при проектуванні деплоя застосувань з тривалими з’єднаннями і або закладати резерв по ресурсах, або використовувати «розумні» балансувальники, що вміють працювати в залежності від завантаженості бекендів.
- Feature toggles / feature flags. Ця техніка прийшла з trunk-based development — методики розробки через git, в якій використовуються екстремально короткоживучі гілки. День-два — і розробник зобов’язаний змержити свою гілку з основною. Але фічу дуже складно реалізувати за цей час, тому в систему вводять певний перемикач, здатний включати або відключати її. Використовуючи feature toggles, можна спокійно викотити нову версію застосування, а потім включити новий функціонал, що доставляється з релізом.
Коли варто використовувати RollingUpdate.
- Проблема з консистентністю деплоя не стоїть або вирішена.
- Потрібно мінімізувати простій: RollingUpdate архітектурно одна з найбільш толерантних до простоїв стратегій.
- Частий релізний цикл: толерантність до збоїв, широкі можливості автоматизації і тонке налаштування викату дозволяють деплоїти нові версії під навантаженням, без технологічних вікон.
Kubernetes update strategies out of the box
Резюмуючи розповідь про стратегії викату, доступні в k8s «з коробки», хочу зауважити, що можливо їх одночасне використання для різних компонентів застосування в рамках одного релізу.
Часто застосування будуються за модульною архітектурою, і окремі компоненти (наприклад, фронт і бек) можна оновлювати, використовуючи RollingUpdate, pull-воркери — використовуючи Recreate. Тобто знання про внутрішній устрій застосування і взаємозв’язки його компонентів — абсолютно необхідна річ для побудови нормального викату.
Blue/Green
Blue/Green на перший погляд виглядає досить просто. Є працююча версія застосування, поруч з нею запускається нова:
Потім ми просто перемикаємо трафік:
Якщо все влаштовує, зупиняємо стару версію застосування і живемо на новій:
Оцінюємо.
- Толерантність до збоїв і простоїв — не все так просто, є нюанс, про який поговоримо далі.
- Консистентність — хороша. Трафік пускаємо на строго певну версію застосування.
- Ресурсоємність — висока. Для реалізації цієї стратегії потрібні подвійні потужності.
- «Прогрів» інфраструктури — потенційно поганий.
- Простота відкату — екстремальна. Просто повертаємо трафік на стару версію.
Тонкощі blue/green
Сумісність схем даних. Оскільки при blue/green одночасно працюють дві версії застосування, схема даних в БД повинна підходити обом.
Вихідні запити від нової версії. Крім вхідного трафіку, наші застосування самі можуть робити якісь запити. Як описувалося раніше, в архітектурі застосування можуть бути асинхронні pull-воркери, підписуються на черги і беруть звідти завдання, і в разі blue/green воркери нової версії будуть «підкрадати» завдання у старої.
Деплой нової версії / зупинка старої. У класичному підході, який дістався нам від епохи серверів bare metal, ми робимо неймспейс blue, неймспейс green і поперемінно деплоїмо то в один, то в інший. І якщо ми не обкладемо цей процес певним рівнем захисної автоматики, то випадково «передеплоїти» робочу версію застосування — питання часу. Уникнути цього можна, змінивши підхід до найменування неймспейсів: в k8s ми можемо створювати їх з нуля, використовуючи (наприклад) ім’я версії застосування в якості суфікса.
Перемикання трафіку. Є десятки способів перемикання трафіку між версіями застосування при реалізації blue/green:
- можна міняти запис в DNS (і це насправді не найкращий спосіб через загальну архітектурну інертність DNS);
- якщо наше застосування працює в хмарі, ми можемо використовувати хмарні балансувальники навантаження;
- в екосистемі k8s для перемикання трафіку можна використовувати Kubernetes service, Ingress-контролери, API Gateway, Service mesh.
Але головне, про що потрібно пам’ятати, проектуючи методику перемикання трафіку, — вона повинна бути коректною. З’єднання на стару версію застосування повинні обробитися правильно: отримати результат і тільки після цього закритися.
Поясню це на прикладі. Припустимо, у нас є два неймспейса. У старому є ингресс, на який йде весь трафік:
Якщо ми в рамках перемикання трафіку просто видалимо ингресс в старому неймспейсі і створимо в новому, з великою ймовірністю запити до старого неймспейсу будуть розірвані і завершаться з помилкою:
Уникнути цього можна, описавши додатковий «канарейковий» ингресс:
Описавши анотації на новому ингресі, ми направимо в нього 100% нових з’єднань:
nginx.ingress.kubernetes.io/canary: "true" nginx.ingress.kubernetes.io/canary-weight: "100"
А коли трафік на старий ингресс завершиться, можемо видалити старий ингресс і анотації з нового.
Коли варто використовувати blue/green
- Якщо у вас хмари, і подвоєння інфраструктури на час деплоя коштує відносно дешево.
- Рідкісний релізний цикл: стратегія blue/green вимагає певної «роздумливості».
- Коли потрібен легкий і швидкий відкат на стару версію.
Canary і A/B
На перший погляд Canary і A/B виглядають досить схоже: здійснюється поступова заміна частини екземплярів застосування на нові версії і на них направляється трафік.
Якщо нас все влаштовує, повністю переходимо на нову версію:
Все це дуже сильно нагадує Rolling update. На технологічному рівні canary від A/B, як правило, відрізняється маршрутизацією трафіку. При A/B трафік маршрутизується на клієнті (або на основі клієнтів), при canary найчастіше йде ймовірнісний розподіл трафіку на сервісі. Різниця в тому, що, на відміну від Rolling update, canary і A/B — це перш за все тести.
A/B — тест нової функціональності на фокус-групі реальних користувачів. Тут основну роль відіграє моніторинг з метриками користувацької задоволеності.
Canary — тест нової версії застосування на реальному трафіку. Тут важливий моніторинг стабільності та продуктивності. Canary від rolling update відрізняється якраз повнотою метрик і перемиканням екземплярів на нову версію в залежності від даних моніторингу.
Якщо порівнювати за критеріями оцінки, то ресурсоємність у обох стратегій буде неоднозначною — ми можемо реалізувати процес як з необхідністю в додатковому обладнанні, так і без.
Плюс у canary будуть проблеми з консистентністю, оскільки найчастіше там реалізується ймовірнісний розподіл трафіку.
При реалізації canary і A/B є ряд нюансів, які варто обговорити докладніше. Це управління трафіком і масштабування версій. Давайте їх розберемо.
Управління трафіком при A/B і Canary
Можна виділити такі можливості управління трафіком:
- через масштабування версій;
- на основі ваг;
- на основі з’єднань.
Варіант через масштабування версій наведено в документації kubernetes і є класичним прикладом реалізації Canary в k8s «з коробки».
Припустимо, у нас описано два об’єкти deployment з різними іменами і версіями іміджів, але з однаковими мітками (labels).
Якщо ми опишемо Kubernetes service, що шукає POD`и за вказаними мітками, трафік через цей сервіс розподілятиметься рівномірно на всі знайдені поди. Таким чином, змінюючи кількість реплік в кожному з deployment, можна керувати трафіком. Але цей випадок хороший саме для прикладу і насправді надає занадто мало можливостей. Як правило, в реальних проєктах використовуються більш потужні і функціональні рішення на кшталт ingress-контролерів, service mesh, API Gateway.
В принципі, незважаючи на безліч інструментів, реалізації побудовані за загальними принципами. Розберемо їх на прикладі ingress nginx.
Балансування на основі ваг ми розглядали, коли говорили про Blue/Green:
Створивши додатковий ингресс-ресурс з анотаціямиnginx.ingress.kubernetes.io/canary: "true"
Для оголошення canary-ингрессаnginx.ingress.kubernetes.io/canary-weight: "30"
Для визначення розміру трафіку, спрямованого на новий ингрессnginx.ingress.kubernetes.io/canary-total: "100"
Для визначення загальної кількості трафіку ми направимо 30% трафіку на нову версію застосування. Це рішення застосовується, як правило, для стратегії типу canary.
Для стратегії A/B ми можемо робити балансування на основі аналізу того, що приходить до нас у з’єднаннях. Це можуть бути headers або cookie.
Для включення маршрутизації цього типу точно також потрібно описати ингресс з анотацієюnginx.ingress.kubernetes.io/canary: "true"
Для використання маршрутизації по заголовках потрібно описати анотаціїnginx.ingress.kubernetes.io/canary-by-header
— з ім’ям заголовка.
nginx.ingress.kubernetes.io/canary-by-header-value
— зі значенням, яке повинен прийняти заголовок:
Для маршрутизації по cookie ми повинні записати в анотації тільки ім’я cookienginx.ingress.kubernetes.io/canary-by-cookie
(вміст cookie нас не цікавить):
Вибір, в яких випадках використовувати ці способи, залежить від особливостей бізнес-процесів. Можливо, є якась група «улюблених користувачів», клієнти, яких ми після аутентифікації просимо давати відповідний заголовок для доступу до нової версії, або ми можемо роздавати «печеньки» випадковим користувачам і таким чином запрошувати їх до тестування нової версії ПЗ.
Масштабування версій при A/B і Canary
Оскільки бюджет на інфраструктуру, як правило, обмежений, плюс для чистоти експерименту необхідно дотримуватися пропорцій між вхідним трафіком і ресурсами версії застосування, на які він спрямований, управління масштабуванням екземплярів версій досить важливо при реалізації A/B і canary. Масштабування версій повинно встигати за перемиканням трафіку.
Хорошою практикою тут є використання автоскейлерів (autoscaler): HPA, вбудований в k8s, якщо можна спиратися на стандартні метрики споживання CPU/RAM, або KEDA або Karpenter, якщо можна виділити більш складні метрики.
Резюме
Сумуючи все вищесказане, відзначимо, що в Kubernetes є все необхідне для побудови відмовостійких викатів. Базові стратегії викату — Rolling і Recreate — реалізовані «з коробки» і чудово працюють. Але якщо вам їх не вистачає, цілком можна реалізувати щось більш підходяще за допомогою екосистеми, що склалася навколо Kubernetes.
Якщо ви не готові самі збирати пазл з екосистеми компонентів, можете використовувати вже готові рішення. Такі як Argo Rollouts або Flagger. У них вже реалізовані механізми управління трафіком і масштабування версій в залежності від метрик моніторингу.
Наприклад, Flagger реалізує Canary, A/B, Blue/Green «з коробки», вміє перемикати трафік за допомогою більшості найпопулярніших інструментів для балансування і працювати з більшістю топових систем моніторингу.
Ось і все. Якщо ви знайшли цю статтю заслуговуючою на увагу, а також якщо вам цікаве порівняння Argo Rollouts і Flagger — напишіть про це в коментарях, і я обов’язково зроблю його в наступних статтях.
FAQ
1. Що таке деплой в Kubernetes? Деплой в Kubernetes — це процес розгортання нової версії застосування в кластері Kubernetes з мінімальними перервами в його роботі.
2. Які основні стратегії деплоя існують в Kubernetes? Основні стратегії деплоя включають Recreate, RollingUpdate, Blue/Green, Canary та A/B.
3. Як оптимізувати запуск пода в Kubernetes? Для оптимізації запуску пода слід мінімізувати кількість init-контейнерів, уникати запуску міграцій в init-процедурі та оцінити необхідність JIT-компіляції.
4. Що таке pre-stop hooks і як вони використовуються? Pre-stop hooks — це механізм, який дозволяє виконати певні дії перед зупинкою пода. Вони використовуються для коректної зупинки сервісів, таких як Nginx або PHP-FPM.
5. Як забезпечити толерантність до збоїв при деплої? Толерантність до збоїв забезпечується за допомогою правильної стратегії деплоя, такої як RollingUpdate або Blue/Green, а також використанням автоскейлерів для масштабування версій застосування.