Як насправді працює планувальник Kubernetes

На цьому тижні мені стали відомі подробиці про те, як працює планувальник Kubernetes, і я хочу поділитися ними з тими, хто готовий поринути в нетрі організації того, як це насправді працює.
Додатково зазначу, що цей випадок став наочною ілюстрацією того, як без необхідності у чиїйсь допомоги перейти від стану «Не маю поняття, як ця система навіть спроектована» до «Окей, думаю, що мені зрозумілі базові архітектурні рішення і чим вони обумовлені» .
Сподіваюся, цей невеликий потік свідомості виявиться для когось корисним. Під час вивчення даної теми мені найбільше знадобився документ Writing Controllers із чудової документації Kubernetes для розробників.
Навіщо планувальник?
Планувальник Kubernetes відповідає за призначення вузлів подам (pods). Суть його роботи зводиться до наступного:
- Ви створюєте під.
- Планувальник зауважує, що у нового пода немає призначеного йому вузла.
- Планувальник призначає поду вузол.
У Kubernetes застосовується ідея контролера. Робота контролера полягає в наступному:
- подивитися на стан системи;
- помітити, де актуальний стан відповідає бажаному (наприклад, «цьому поду має бути призначений вузол»);
- повторити.
У загальному вигляді роботупланувальника можна представити як такий цикл:
Однак працює все не зовсім так
Але на цьому тижні ми збільшували навантаження на кластер Kubernetes і зіткнулися із проблемою.
Іноді під назавжди "застрявав" у стані Pending (коли вузол не призначений на під). При перезавантаженні планувальника під виходив із цього стану (ось тикет).
Така поведінка не сходилася з моєю внутрішньою моделлю того, як працює планувальник Kubernetes: якщо очікує призначення вузла, то планувальник повинен виявити це і призначити вузол. Планувальник не повинен перезапускатись для цього!
Настав час звернутися до коду. І ось що мені вдалося з'ясувати як завжди, можливо, що тут є помилки, т.к. все досить складно, але в вивчення пішов лише тиждень.
Як працює планувальник: швидкий огляд коду
Почнемо із scheduler.go. (Об'єднання всіх потрібних файлів доступне тут - для зручності навігації по вмісту.)
Основний цикл планувальника (на момент комміту e4551d50e5) виглядає так:
… що означає: «Вічно запускай sched.scheduleOne». А що там відбувається?
Окей, а що робить NextPod()? Звідки ростуть ноги?
Окей, все досить просто! Є черга з подів (podQueue), і наступні поди приходять із неї.
Але як поди потрапляють у цю чергу? Ось відповідний код:
Тобто існує обробник події, який при додаванні нового пода додає його в чергу.
Як працює планувальник: простою мовою
Тепер, коли ми пройшлися за кодом, можна підбити підсумки:
- На самому початку кожен під, якому буде потрібно планувальник, поміщається в чергу.
- Коли створюються нові поди, вони також додаються до черги.
- Планувальник постійно бере поди зчерги та здійснює для них планування.
- От і все!
Звичайно, насправді планувальник розумніший: якщо під не потрапив до планувальника, у загальному випадку викликається обробник помилки на кшталт цього:
Виклик функції sched.config.Error знову додає в чергу, тому для нього все-таки буде зроблено повторну спробу обробки.
Зачекайте. Чому ж тоді «застряг» наш під?
Чому планувальник спроектовано так?
Думаю, що надійніша архітектура виглядає так:
То чому ж замість такого підходу ми бачимо всі ці складнощі з кешами, запитами, зворотними викликами? Дивлячись на історію, приходиш до думки, що основна причина у продуктивності. Приклади – це оновлення про масштабованість у Kubernetes 1.6 і ця публікація CoreOS про покращення продуктивності планувальника Kubernetes. В останній йдеться про скорочення часу планування для 30 тисяч подів(на 1 тисячі вузлів - прим. Перев.)з 2+ годин до менше 10 хвилин. 2 години – це досить довго, а продуктивність важлива!
Стало ясно, що опитувати всі 30 тисяч подів вашої системи щоразу при плануванні для нового поду — це занадто довго, тому справді доводиться придумати складніший механізм.
Що насправді використовує планувальник: informers у Kubernetes
Хочу сказати ще про один момент, який здається дуже важливим дляархітектури всіх контролерів Kubernetes Це ідея інформаторів (informers). На щастя, є документація, яка перебуває вуглецем «kubernetes informer».
Цей вкрай корисний документ називається Writing Controllers і розповідає про дизайн для тих, хто пише свій контролер (на зразок планувальника або згаданого контролера cronjob). Чудово!
Якби цей документ знайшовся насамперед, думаю, що розуміння того, що відбувається, прийшло б трохи швидше.
Отже, інформатори! Ось що говорить документація:
Використовуйте SharedInformers. SharedInformers пропонують хуки для отримання повідомлень про додавання, зміну або видалення конкретного ресурсу. Також вони пропонують зручні функції для доступу до кешів, що розділяються, і для визначення, де кеш застосовний.
Коли контролер запускається, він створює informer (наприклад pod informer ), який відповідає за:
- виведення всіх подів (насамперед);
- повідомлення про зміни.
Повторне приміщення у чергу
Для надійного повторного приміщення у чергу виносите помилки на верхній рівень. Для простої реалізації з розумним відкатом є workqueue.RateLimitingInterface.
Головна функція контролера повинна повертати помилку, коли потрібне повторне приміщення в чергу. Коли його немає, використовуйте utilruntime.HandleError та повертайте nil . Це значно спрощує вивчення випадків обробки помилок та гарантує, що контролер нічого не втратить, коли ценеобхідно.
Виглядає як хороша порада: коректно обробити всі помилки може бути нелегко, тому важливо наявність простого способу, що гарантує, що рецензенти коду побачать, чи помилки обробляються коректно. Кльово!
Необхідно «синхронізувати» своїх інформаторів (чи не так?)
І остання цікава деталь під час мого розслідування.
У informers використовується концепція "синхронізації" (sync). Вона трохи схожа на рестарт програми: ви отримуєте список усіх ресурсів, за якими спостерігаєте, тому можете перевірити, що все дійсно гаразд. Ось що те саме керівництво говорить про синхронізацію:
Watches та Informers «синхронізуватимуться». Періодично вони доставляють вашому методу Update кожен відповідний об'єкт у кластері. Добре для випадків, коли може знадобитися виконати додаткову дію з об'єктом, хоча це може бути не завжди.
У випадках, коли ви впевнені, що повторне розміщення елементів не потрібно і нових змін немає, можна порівняти версію ресурсу у нового і старого об'єктів. Якщо вони ідентичні, ви можете пропустити повторне приміщення в чергу. Будьте обережні. Якщо повторне розміщення елемента буде пропущено за будь-яких помилок, він може загубитися (ніколи не потрапити в чергу повторно).
Простіше кажучи, «необхідно синхронізувати; якщо ви не синхронізуєте, можете зіткнутися із ситуацією, коли елемент втрачено, а нову спробу приміщення в чергу не буде зроблено». Саме це і сталося у нашому випадку!
Планувальник Kubernetes не синхронізується повторно
Отже, після знайомства з концепцією синхронізації… приходиш до висновку, що, схоже, планувальник Kubernetes її ніколи не виконує? У цьому коді все виглядає саме так:
@brendandburns - що тут планується виправити? Я дійсно проти таких маленьких періодів повторної синхронізації, тому що вони значно позначаться на продуктивності.
Згоден з @wojtek-t. Якщо resync взагалі будь-коли і може вирішити проблему, це означає, що десь у коді є баг, який ми намагаємося сховати. Не думаю, що resync – правильне рішення.
Виходить, що мейнтейнери проекту вирішили не виконувати повторну синхронізацію, тому що краще, щоб баги, закладені в коді, спливали та виправлялися, а не ховалися за допомогою resync.
Поради щодо читання коду
Наскільки мені відомо, ніде не описано реальну роботу планувальника Kubernetes зсередини (як і багато інших речей!).
Ось пара прийомів, які допомогли мені під час читання потрібного коду:
-
Поєднайте все потрібне у великий файл. Вище вже написано про це, але дійсно: переходити між викликами функцій стало набагато простіше в порівнянні з перемиканням між файлами, особливо коли ще не знаєш, як все повністю організовано.
Працювати з Kubernetes досить здорово!
Kubernetes – по-справжньому складне програмне забезпечення. Навіть для того, щоб отримати працюючий кластер, потрібно налаштувати як мінімум 6 різних компонентів: api server, scheduler, controller manager, container networking на кшталт flannel, kube-proxy, kubelet. Тому (якщо ви хочете розуміти програмнезабезпечення, яке запускаєте, як і я) необхідно розуміти, що всі ці компоненти роблять, як вони взаємодіють один з одним і як налаштувати кожну з їхніх 50 трлн можливостей для отримання того, що потрібно.
Тим не менш, документація досить хороша, а коли щось недостатньо документовано, код дуже простий для читання і pull requests, схоже, дійсно рецензуються.
Мені довелося по-справжньому і звичайнісінького практикувати принцип «читай документацію і, якщо її немає, то читай код». Але в будь-якому випадку це чудова навичка, щоб стати краще!