Блог ·
Чому 200 від Proxmox не є результатом
Proxmox відповідає на запит клонування ідентифікатором UPID. Як читати той ідентифікатор, чесно опитувати його і відрізняти завершене завдання від успішного.
Автор: Provibr
Ви надсилаєте POST із запитом клонування на вузол Proxmox. У відповідь ви отримуєте HTTP 200 і рядок, схожий на шум у лінії. У цю мить правдива рівно одна річ: запит прийнято. Жодного диска не скопійовано, жодної машини не існує, і водночас ніщо ще не впало.
Майже кожен змінювальний виклик в API Proxmox працює саме так, і це цілком розумний задум для операцій, що тривають хвилинами. Проблемою це стає лише тоді, коли код, який викликає, трактує 200 як результат, а це типова поведінка кожного HTTP-клієнта, будь-коли написаного.
Ось як ми читаємо ці завдання і які частини виявилися гострішими, ніж мають вигляд.
Цей рядок є ідентифікатором, і він має структуру
Те, що повертається, є UPID: розділений двокрапками ідентифікатор, який називає одне завдання, що виконується. Він несе, серед інших полів, вузол, на якому завдання виконується, тип завдання, обʼєкт, над яким воно працює, і користувача, який його запустив.
Три з них є несучими.
Вузол це той, у кого треба питати статус. Завдання належить вузлові, який його виконує, а питання в масштабі кластера є іншим питанням. Беріть вузол з ідентифікатора, а не з тієї змінної, яка випадково опинилася у вас в області видимості, бо ці двоє можуть різнитися, і випадок, коли вони різняться, є рівно тим, якого ви не тестували.
Тип каже вам, що насправді було запущено, а це не завжди те, що ви просили за назвою. Резервне копіювання, запущене через endpoint вузла, повертається з типом завдання vzdump, а не з чимось у формі гостя.
Ціль є ідентифікатором гостя для гостьових операцій, і вона може бути порожньою. Це не пошкодження. Завдання цілком законно може стосуватися вузла, а не гостя, тож код, який розбирає це поле, має повертати «немає гостя», а не кидати виняток чи підставляти нуль.
Наш розбирач свідомо вузький у тому, що він відхиляє: він вимагає префікс, вимагає щонайменше вісім розділених двокрапками полів і вимагає, щоб вузол і тип були непорожніми. Усьому іншому дозволено бути відсутнім, бо формат має далі працювати для типів завдань, яких ми ніколи не бачили.
Завершене не є тим самим, що успішне
Спитайте вузол про статус завдання, і ви отримаєте два поля, що відповідають на два різні питання. Одне каже, чи завдання досі виконується, чи зупинилося. Друге, наявне лише після зупинки, каже OK або несе рядок помилки.
Записати це як два окремі предикати замість одного не є педантизмом. Єдиний isDone(), що їх змішує, породжує ваду, за якої невдале клонування звітується як завершене клонування, а машина, якої не існує, отримує рядок у вашій базі даних. У нас дві функції: одна питає, чи воно зупинилося, друга питає, чи воно зупинилося і чи статус виходу є рівно OK. Усе інше є невдачею, включно зі статусами виходу, яких ми ніколи не бачили, бо статус, якого ми не впізнаємо, не є дозволом припускати успіх.
Де живе завдання, є частиною API, а не дрібницею
Цього нас навчило резервне копіювання. Природно очікувати, що гостьова операція висітиме на гостеві: щось під власним шляхом віртуальної машини. Такого endpoint немає. Резервне копіювання запускається на вузлі, з ідентифікатором гостя як параметром.
Це не примха, яку слід обійти. Вона змінює те, що вам треба знати перед стартом: який вузол, яке сховище на тому вузлі і чи тому сховищу взагалі дозволено тримати резервні копії. Сховище, яке приймає лише образи дисків, радо прийме ваш запит і впаде всередині завдання, а до клієнта це випливе як повідомлення про помилку від програми, про яку він ніколи не чув. Тож ми фільтруємо перелік цілей за тим, що сховище оголошує як свій припустимий вміст, і викидаємо будь-яке сховище, яке в переліку є, але не відповідає, перш ніж пропонувати комусь вибір.
Те саме правило у зворотному напрямку діє для розміщення диска. Та сама ідея, протилежний тип вмісту, і його варто тримати як два явні фільтри, а не один розумний спільний.
Опитування є ритмом, а не інтервалом
Поки завдання виконується, питати доводиться повторно. Наївні варіанти обидва погані: опитуйте швидко без кінця, і ви годину гамселите чужий API керування; опитуйте повільно, і кожна швидка операція здається млявою.
Наш компроміс це три швидкі спроби з інтервалом в одну секунду, а далі кожні три секунди. Запуск машини завершується всередині швидкого вікна і відчувається миттєвим. Годинне резервне копіювання коштує одного запиту кожні три секунди, а це ніщо.
Цикл завершується рівно трьома способами, і всі три є явними: завдання звітує, що зупинилося, спливає дедлайн команди або скасовується вся команда. Четвертої гілки немає, як немає і шляху, на якому цикл просто триває далі, а це властивість, яку варто влаштувати навмисно, а не сподіватися на неї. Що дедлайн зупиняє і чого не зупиняє, є окремою темою; тут досить сказати, що цикл опитування мусить мати спосіб програти.
Більшість завдань не скаже вам, як далеко вони просунулися
Це та частина, що розчаровує кожного, хто будує смужку поступу. Статус завдання дає вам «виконується» або «зупинено». Він не дає вам відсотка.
Деякі завдання пишуть поступ у свій журнал, текстом. Клонування видає рядки з кількістю переданого і відсотком у дужках. Тож для таких ми читаємо хвіст журналу і беремо останній відсоток, який у ньому знаходимо. Виколупувати відсоток з рядка журналу не елегантно, і це єдине місце, де це число існує.
Для всього іншого ми показуємо оцінку, і тут задум важить більше за формулу. Наша асимптотично наближається до девʼяноста відсотків, виходячи з очікуваної тривалості, тож смужка завжди рухається і ніколи не доходить до кінця. Смужка, що стоїть на 100 відсотках, поки нічого не відбувається, гірша за відсутність смужки, бо вона каже тому, хто дивиться, що ви закінчили, а потім сама собі суперечить.
Очікувана тривалість, що живить цю криву, є параметром форми, а не дедлайном. Наша каже, що резервне копіювання зазвичай триває близько десяти хвилин, а відновлення близько двадцяти, з міркування, що відновлення читає архів і пише диски, а отже переміщує приблизно вдвічі більше байтів. Якщо копіювання триває довше за оцінку, нічого не ламається; крива просто чемно лишається під лінією.
Помилка міститься в журналі, а не в статусі
Коли завдання падає, статус виходу часто майже непридатний. «unable to create VM» є категорією, а не причиною. Причина міститься в останніх кількох рядках журналу завдання.
Тож звіт про невдачу збирається так: статус виходу плюс хвіст журналу з відфільтрованими рядками поступу, обмежений кількома рядками і кількома сотнями символів. Фільтрування рядків поступу важить більше, ніж звучить, бо клонування, що падає, може закінчуватися двадцятьма рядками відсотків передавання, а справжня причина буде над ними.
Одне правило щодо цього тексту, якого ми суворо дотримуємося: це цитата. Це те, що сказав гіпервізор, і воно потрапляє в наші записи й на екран без змін. Ми його не перекладаємо, не причісуємо і не переказуємо привітнішими словами. Речення навколо нього наше і перекладається для читача; процитована частина лишається рівно такою, як її сказав інший бік, бо це єдиний рядок в усьому обміні, який інженер підтримки може знайти пошуком.
Не вплітайте власних слів у речення машини
Останній урок є тим, якого ми не очікували отримати від опитувача завдань.
Наш цикл очікування колись брав вільнотекстову позначку операції і вплітав її у три речення: щось виконується, щось завершено, щось не вдалося. Це працювало, аж поки хтось не помітив, що кілька тих позначок написано мовою нашого офісу, а не англійською. Клієнти читали наполовину перекладені речення у своїх смужках поступу, а коли операція падала, те наполовину перекладене речення назавжди зберігалося в полі помилки.
Виправлення полягало не в тому, щоб поправити позначки. Воно полягало в тому, щоб прибрати саму можливість: операція тепер є перелічуванням, і кожен випадок несе ідентифікатори для «виконується», «завершено» і «не вдалося», а не слова. Не лишилося тексту, який можна зробити неправильно, а речення будується мовою читача аж наприкінці. Тримати ці каталоги чесними виявляється окремою дисципліною.
Контрольний список
Для будь-якого API, що відповідає на зміну ідентифікатором, а не результатом:
- Розберіть ідентифікатор, а не просто зберігайте його. Він каже вам, де питати, і це «де» не завжди збігається з тим, де питали ви.
- Відокремте завершене від успішного, у системі типів, якщо можете.
- Трактуйте невпізнаний кінцевий стан як невдачу, ніколи як успіх.
- Дайте циклу опитування ритм і гарантований спосіб завершитися.
- Показуйте поступ, який ви справді маєте, і чесну оцінку, яку неможливо сплутати з наявним у вас поступом.
- Цитуйте інший бік дослівно в помилках і будуйте власне речення навколо цитати, а не всередині неї.
Жодне з цього не є складним. Усі шість є тим, що ми спершу зробили неправильно.