Блог ·
Міграція, яка ніколи не виконалася
Мігратор, який звітує про успіх, пропустивши файл, гірший за той, що падає. Як упорядкування за позначкою часу тихо губить міграції і що перевіряти.
Автор: Provibr
Мігратор схеми, який падає, це хороший день на роботі. Ви отримуєте стек викликів, номер рядка і базу даних, що досі перебуває у зрозумілому вам стані.
Поганий день це той, коли мігратор друкує, що схема актуальна, виходить з кодом нуль і помиляється. Такий день у нас був не раз, з різних причин, і найгірший з них закінчився тим, що продакшн-код читав колонку, якої не існувало.
Ось чого ми навчилися, і написано це для кожного, хто виконує міграції Postgres з каталогу файлів, яким би інструментом ті файли не було згенеровано.
Рішення ухвалюється на підставі одного числа
Більшість файлових міграторів тримають таблицю того, що вже застосовано. Наш теж: схема, таблиця, рядок на кожну міграцію з хешем і позначкою часу створення.
Помилка полягає в припущенні, що ця таблиця є множиною, до якої мігратор звертається. Це не так. Прочитайте код і знайдете запит, який вибирає рівно один рядок, найновіший за позначкою часу, а далі одне порівняння на кожен файл міграції: виконати його, якщо остання застосована позначка часу менша за позначку цього файлу.
З цього одразу випливають три речі, і жодної з них немає в чиїйсь мисленнєвій моделі.
Хеш не бере участі в рішенні. Він записується, коли міграція виконується, і більше ніколи не читається, щоб щось вирішити. Хеш у таблиці, який більше не збігається з жодним файлом на диску, зазвичай означає, що той файл відредагували після застосування, і це неохайно, але нешкідливо. Це не інструмент щось помітив.
Значення має лише найновіший рядок. Не множина застосованих файлів і не їхня кількість. Один рядок.
Упорядкування йде за позначкою часу, а не за іменем файлу. Числа на початку ваших імен файлів призначені для людей. Якщо ваш інструмент записує значення впорядкування в маніфест, істиною є те значення, а число є оздобою.
Складіть це докупи і отримаєте властивість, що коштує вам робочих днів: міграція, чия позначка часу не більша за найновішу застосовану, ніколи не виконається на тій базі даних. Ніколи. Ні на цьому розгортанні, ні на наступному, ні після десяти повторів. Помилки не буде, бо з погляду інструмента все гаразд.
Як таке справді трапляється
Це звучить як те, що не може статися з обережною командою. Ось як це сталося з нами двічі, у спосіб, що не має нічого спільного з недбалістю.
Дві гілки, те саме число. Двоє людей паралельно додають по міграції. Обидва інструменти видають наступне вільне число, обидва отримують те саме, обидва пишуть позначку часу. Ту гілку, що вливається другою, перейменовують на наступне число, і це очевидне виправлення, але не повне: якщо її позначка часу досі менша за вже застосовану, перейменування змінило імʼя файлу, а не рішення. Міграцію пропущено. Симптомом у нашому випадку був тип-перелічення, якому просто бракувало одного значення, на базі даних, де кожен інший обʼєкт був на місці й правильний.
Позначки часу, написані руками і не по порядку. Наші є круглими числами, виставленими вручну, а не справжнім годинником, що робить їх читабельними і водночас легкими для помилки. Одна міграція отримала позначку, ранішу за дві вже застосовані. Мігратор відзвітував про успіх. У продакшні це була б помилка типу на кожному без винятку рядку інтеграції, а це не те, про що ви хочете дізнатися від клієнта.
Обидва випадки мають спільну форму, яку варто назвати. Облік ведеться за значенням упорядкування, а не за ідентичністю. Журнал з ключем за ідентичністю («чи застосував я цю міграцію?») не може пропустити нічого, у якому б порядку речі не приходили. Журнал з ключем за впорядкуванням («як далеко я зайшов?») може, і це задум, спільний для більшості міграторів, бо він робить звичайний шлях читанням одного рядка.
Лічильник не є перевіркою
Після першого інциденту ми додали очевидний запобіжник: після міграції подивитися на кількість рядків у таблиці міграцій і підтвердити, що вона зросла.
Цей запобіжник необхідний і недостатній, і ми дізналися про це, спостерігаючи, як він проходить, поки потрібного нам бракувало. Один прогін міграцій підняв кількість рядків з девʼяноста восьми до ста чотирьох. Зайшло шість міграцій. Тієї, яка нас цікавила, серед них не було. Лічильник зріс, усе відзвітувало про успіх, а кожна сторінка продукту для авторизованих користувачів повертала 500 на колонці, якої не існувало.
Ніщо в лічильнику не каже вам, які міграції виконалися. Якщо ви хочете знати, чи існує обʼєкт, єдиною чесною перевіркою є спитати базу даних, чи цей обʼєкт існує: перелічити таблиці, описати таблицю, вибрати значення переліку. Ми порівнюємо список таблиць середовища, яке щойно змігрували, зі списком того, якому довіряємо.
Є різновид цієї відмови, який не має нічого спільного з позначками часу і дає той самий зелений вивід. Наш мігратор їде всередині одного з наших службових образів. Хтось перезібрав лише інший сервіс, запустив міграцію зі застарілого образу і отримав бадьоре «схема актуальна» від контейнера, який ніколи не чув про ті дві міграції. Інструмент не брехав. Він відповідав на питання про файли, які мав, а мав він старі файли.
Усе або нічого, в одній транзакції
Одну деталь реалізації нашого мігратора варто знати, бо вона змінює те, як ви відновлюєтеся: кожна очікувана міграція виконується всередині однієї транзакції, а облікова вставка для кожної з них іде всередині неї.
Це добра новина і обмеження водночас. Добра новина в тому, що відмова посеред пакета із семи лишає вас із нулем застосованих замість трьох з половиною. Немає частково змігрованого стану, про який треба міркувати, а це саме те, чого ви хочете в момент, коли міркувати вам найважче.
Обмеження в тому, що один оператор, який Postgres не може виконати всередині транзакційного блоку, потягне за собою весь пакет, і що дуже довга міграція тримає блокування протягом усього пакета, а не лише власної тривалості. Жодне з цього нас поки не вкусило. Про обидва варто прочитати до того, як вкусить.
Як полагодити одну, не погіршивши наступну
Припустімо, ви її знайшли: міграція у вашому каталозі, яку ця база даних ніколи не виконувала. Інстинкт каже полагодити маніфест, знизити або підняти позначку часу, поки інструмент її не підхопить, і запустити знову.
Не знижуйте її, а піднімайте обережно.
Причина полягає в тому самому правилі, що спричинило проблему. Якщо ви знижуєте позначку часу на міграції, яку вже застосовано десь інде, ви нічого не змусили виконатися; ви зробили файл, чия позначка тепер нижча за максимальний досягнутий рівень на тих базах даних, де він виконався, і наступна міграція, яку ви напишете з позначкою між цими двома, буде там пропущена. Ви полагодили одне середовище і звели наступну відмову в іншому.
Працює те, щоб застосувати SQL самотужки, в одній транзакції, а потім подбати, щоб облік відображав дійсність. Мігратор не зробить цього за вас, бо з його боку робити нема чого.
Що ми робимо тепер
Пʼять правил, усі дешеві, усі записані тому, що кожного з них ми одного разу не дотрималися.
- Розгортання і міграція є однією операцією, а не двома. Проміжок між ними є вікном, у якому новий код читає стару схему. Наш коштував нам адмінсторінки для кожного адміністратора і завдання білінгу, яке тихо падало цілий день на колонках, яких там ще не було.
- Збирайте все перед міграцією, а не лише той сервіс, який ви змінили. Мігратор подорожує в образі, а образ, який ви не перезібрали, містить міграції, яких ви не додавали.
- Перевіряйте значення впорядкування, а не імена файлів, щоразу, коли двоє людей працювали паралельно. Перейменування файлу не змінює того, що інструмент порівнює.
- Перевіряйте обʼєкт, а не лічильник. Після міграції, що додає колонку, спитайте про ту колонку. Це одна команда і єдина перевірка, яка не може пройти вхолосту.
- Ніколи не знижуйте значення впорядкування, яке вже десь застосовано. Це має вигляд локального виправлення, а є віддаленою міною.
Незручна нитка крізь усе це полягає в тому, що інструмент щоразу поводився правильно. Він ніколи не падав, нічого не псував, ніколи не брехав про те, що зробив. Він відповідав на вужче питання, ніж те, яке ми думали, що ставимо, і друкував речення, правдиве щодо того вужчого питання.
Це той клас відмов, проти якого варто виробляти звички, і в міграціях, і всюди. Інструмент, що падає гучно, вчить вас того, що він робить. Інструмент, що досягає успіху вузько, не вчить вас нічого до того дня, коли це матиме значення.