Оновлення бази даних і zero-downtime deployment

Про оновлення систем «на льоту» без їх зупинки (zero-downtime deployment) написано чимало статей і багато аспектів цього підходу є досить очевидними. На мій погляд, найбільш складна частина деплоймента в цьому випадку — оновлення сховищ даних, у випадку якщо їх контракт (схема) змінився. Саме цей аспект я б хотів розглянути в цій статті.

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

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

  • Додавання нового поля
  • Видалення поля
  • Перейменування поля
  • Зміни типу поля
  • Перенесення поля в іншу структуру даних (наприклад, у разі денормалізації)

Додавання нового поля також як і додавання будь-якого іншого об’єкта бази даних — назад сумісна зміна і не вимагає ніяких додаткових кроків у плані реалізації zero-downtime deployment. Досить просто застосувати зміни в базі «на льоту», після чого задеплоїти нову версію коду, яка використовує нові об’єкти бази даних.

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

Решта три типи змін більш складні в плані забезпечення zero-downtime deployment. В цілому, всі вони можуть бути виконані копіюванням даних в інші поля/сутності, і видаленням «старих» після успішного завершення міграції даних: для перейменування можна скопіювати дані з старого поля в поле з новим ім’ям, після чого видалити старе поле, зміна типу даних можна зробити разом з перейменуванням і т. д. Так чи інакше, протягом деякого періоду часу, база даних повинна підтримувати як старий так і новий контракти. Є як мінімум два способи виконати такі зміни «на льоту»:

Читайте також  Голосові помічники не вчаться на тесті Тюрінга

Якщо база даних підтримує тригери

  1. Створити тригери, які копіюють дані зі старого місця в нове на будь-яку зміну/додавання і встановити їх на продакшені.
  2. Застосувати утиліту для конверсії даних, яка робить те ж саме, але для всіх записів в базі. Так як тригери вже встановлені, утиліта може не робити нічого більш складного, ніж просто «фіктивне» оновлення кожного запису (UPDATE table SET field = field …). Дуже важливий момент тут — що дію з читання даних зі старого місця і запис у нове повинно бути атомарним і захищеним від втрачених змін. В залежності від структури бази можна скористатися або песимістичним блокуванням через SELECT FOR UPDATE або його аналоги, або оптимістичним, якщо в таблиці є поле з версією запису.
  3. Після того як утиліта закінчить свою роботу (в залежності від обсягу даних і складності оновлення час виконання може обчислюватися днями) вже можна встановлювати нову версію системи, яка підтримує нову схему даних. До цього моменту всі записи в базі, що існували на момент запуску утиліти, будуть успішно сконвертированы, а нові, що з’явилися під час її роботи — також сконвертовані тригерами.
  4. Видалити тригери і все поля (або інші об’єкти бази даних, які більше не потрібні.

Якщо немає можливості використовувати тригери (як у випадку з багатьма NoSQL рішеннями)

  1. Створити і задеплоїти нову версію програми (тимчасова версія 1 на малюнку), яка завжди читає зі старого поля, але при запису в це поле оновлює як і старе, так і відповідне нове місце (на малюнку «З» — старе, «Н» — новий). Задеплоїти цю версію на всі сайти, на яких працюють екземпляри програми.
  2. Застосувати утиліту, яка копіює дані зі старого місця в нове. Як і у випадку з тригерами необхідно вжити заходів для запобігання втрачених змін.
  3. Створити і по закінченні виконання утиліти задеплоїти ще одну версію програми (тимчасова версія 2), яка читає дані з нового поля, але пише як і раніше в два місця. Цей крок необхідний, так як під час послідовного оновлення кожного з вузлів все одно буде проміжок, коли примірники попередньої версії програми, що читають старе поле, працюють одночасно з новою.
  4. Створити і після закінчення повної розгортки попередньої задеплоїти фінальну версію, яка вже ніяк не взаємодіє зі старим полем.
  5. Видалити старі поля.
Читайте також  Котики проти нейромережі. Або вибираємо і запускаємо нейромережа для розпізнавання об'єктів на Raspberry Zero

Другий підхід вимагає створення і встановлення трьох різних версій програми, що може бути дуже незручно і громіздко. Замість цього можна скористатися feature toggling — закласти логіку всіх трьох версій в одну, але перемикати режим в залежності від конфігураційного параметра, який в ідеалі можна було б перемикати «на льоту». Таким чином, замість установки кожної наступної версії достатньо буде змінювати значення параметра (і перезапускати сервіс, якщо оновлення конфигруации «на льоту» не передбачено). Після успішного завершення інсталяції фінальної версії, весь код, що відноситься до забезпечення міграції даних, повинен бути повністю видалений з робочої гілки, нехай він навіть і буде «жити» на продакшені до наступного оновлення системи.

Нескладно помітити, що забезпечення zero downtime при оновленні системи — процедура громіздка і тендітна, тому з неї має сенсу морочитися, тільки якщо є відповідна вимога від бізнесу. Але навіть якщо вимоги щодо доступності системи досить низькі (наприклад, 99% в рік і вікно запланованого оновлення системи — доба), конверсія даних, необхідна для установки нової версії, може все одно зайняти більше. Тому, потрібно бути заздалегідь готовим до застосування подібних рішень, якщо передбачається зберігання великих обсягів даних.

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

Степан Лютий

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

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *