Skip to content

Instantly share code, notes, and snippets.

@levonet
Last active October 17, 2021 12:14
Show Gist options
  • Save levonet/ba20ed2c2c47ee29b9bc19e99fff9e3a to your computer and use it in GitHub Desktop.
Save levonet/ba20ed2c2c47ee29b9bc19e99fff9e3a to your computer and use it in GitHub Desktop.
About another approach to database deployment.

Кочуючі дані в інфраструктурі BlaBlaCar

Цієї весни я дивився мітап Beyond Serverless and DevOps де Aviran Mordo з Wix розповідав про інфраструктуру та її бачення. Про те, що розробнику не потрібно знати про складні налаштування розгортання. Мене надзвичайно вразила ідея, що достатньо додати в проект частину коду яка буде щось писати в базу даних, і розробника не хвилює звідки база даних візьметься. За це має відповідати платформа.

Такі ідеї дуже близькі для нас. Саме тому хочу поділитись тим, як ми в Київській команді Pro Marketplace B2B BlaBlaCar перейшли від ручного створення і обслуговування баз даних (БД) до динамічного менеджменту даними без участі людини.

Єдине що тепер нам потрібно, це в описі конфігурації кожного сервісу декларативно вказати, що цьому сервісу потрібна БД або KV сховище. На цьому завершується взаємодія людини з інфраструктурою. Все інше відбудеться автоматично. Потрібні бази будуть автоматично розгорнуті під час розгортання сервісу.

Але це верхівка айсбергу, в середині закладений механізм створення нової бази для кожного розгортання. Семе цей механізм вирішує більшість викликів, пов'язаних з реалізацією такого підходу. Завдяки цьому механізму ми можемо мати необмежену кількість розгорнутих сервісів без жодних конфліктів.

На сьогодні ми маємо до 500 різних БД розгорнутих автоматично. І ми на їх підтримку не витрачаємо жодної хвилини.

Давайте розберемось, як цей механізм влаштований, який створює нову БД для кожної нової версії сервісу.

Підхід з кочуючими даними

Основна ідея в тому що сервіс декларує, що йому потрібна БД, і він її отримує під час запуску. Головна умова, що система може одночасно розгорнути будь-яку кількість версій одного сервісу в одному оточенні. При цьому існує лише одна версія на одне оточення, на яку подається основне навантаження.

В такому випадку нам потрібно для кожної версії сервісу створити окрему БД з копією даних з поточної версії, на яку подається основне навантаження. Лише таким чином ми отримаємо незалежні версії сервісів, над якими безпечно проводити будь-які дії.

Цей підхід є невід'ємною частиною процесів розгортання сервісів:

  • розгортання нової версії
  • перемикання навантаження на нову версію
  • термінове повернення на попередню версію, якщо виникли проблеми з новою версією
  • видалення старої версії

Тому, в першу чергу, роздивимось механізм переміщення даних в рамках цих етапів.

Також нам знадобляться дві змінні для збереження стану, які можна зберігати в інфраструктурному KV сховищі:

  • current — зберігає версію сервісу на яку подається основне навантаження. Простіше, основний домен на балансері дивиться на контейнери саме цієї версії сервісу.
  • restore — зберігає попередню версію сервісу яка була current до того.

Для спрощення, далі описаний механізм без прив'язки до жодної платформи чи системи розгортання.

Розгортаємо нову версію сервісу

Уявімо собі що ми маємо розгорнутий сервіс v1 який приєднується до БД, назвемо її так само v1. Сервіс v1 є поточний, тому має стан current, на нього подається навантаження.

Тепер нам потрібно розгорнути версію сервісу v2. Це відбувається наступними кроками:

  1. Зупиняємо БД v2 та видаляємо дані в тому числі на репліках. Лише в випадку коли v2 вже була розгорнута.
  2. Створюємо в томі v2 копію бази v1 яка зараз є current.
  3. Запускаємо master БД v2 над цією копією.
  4. Запускаємо скрипти міграції схеми БД. Цей крок опціональний.
  5. Розгортаємо репліки БД кроками 1-2, але замість копії бази v1 робимо копію master v2.
  6. Запускаємо сервіс v2, який приєднується до БД v2.

deploy staging

Давайте окремо розглянемо декілька кроків.

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

Четвертий крок потрібен лише для реляційних БД, але навіть з ними ми можемо оптимізувати процес, вимкнувши міграцію, якщо між версіями сервісу відсутні зміни в скриптах міграції БД.

П'ятий та шостий кроки ми можемо запустити паралельно, оскільки сервіс може не чекати запуск реплік БД.

Тепер ми маємо розгорнутий сервіс з власною БД. Такий сервіс можна розгорнути як для Pull Request в середовищі розробки, так і в виробничому середовищі після створення нового тегу.

Для середовища розробки на кожну зміну коду ми щоразу видаляємо сервіс разом з базами даних і розгортаємо версію сервісу заново.

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

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

В окремих випадках сервіс може чекати запуску найближчої репліки, там де архітектурно сервіс робить зміни в master БД, а читає з реплік.

Перемикання навантаження на нову версію

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

Перемикання в стан current відбувається з повторенням частини попередніх кроків:

  1. Зупиняємо БД v2 та видаляємо дані в тому числі на репліках.
  2. Блокуємо на запис схему БД v1 (current). Цей крок опціональний.
  3. Повторюємо кроки 2-5 з з попереднього етапу:
    1. Запускаємо master БД v2 над цією копією.
    2. Запускаємо скрипти міграції схеми БД.
    3. Розгортаємо репліки БД кроками 1-2, але замість копії бази v1 робимо копію master v2.
    4. Запускаємо сервіс v2, який приєднується до БД v2.
  4. Перемикаємо навантаження з сервісу v1 на сервіс v2. Зберігаємо інформацію про те що сервіс v2 є current, а сервіс v1 помічаємо як restore.
  5. Реєструємо БД в планувальнику резервних копій. Цей крок опціональний.

deploy current

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

Що до останнього кроку, то створення резервних копій має сенс лише для версій які мають стан current. Оскільки БД розгортаються динамічно, а велику кількість сервісів чи оточень складно контролювати, то ми маємо автоматично додати інформацію про новий сервіс в систему створення резервних копій.

Тепер навантаження подається на сервіс v2, сервіс v1 залишається в очікуванні.

Термінове повернення на попередню версію

Час від часу виникає ситуація коли проблема проявляється лише під повним навантаженням на сервіс. В такому випадку потрібно мати можливість швидко повернути навантаження на попередню версію:

  1. Знімаємо блокування з схеми БД v1 (restore). Цей крок опціональний.
  2. Перемикаємо навантаження з сервісу v2 на сервіс v1. Зберігаємо інформацію про те що сервіс v1 є current, а restore обнуляємо.

deploy restore

Таким чином ми відкотились на попередню робочу версію за мінімальну кількість кроків та часу. Підхід версіонування даних дозволяє не хвилюватись за повернення схеми даних до попередньої версії у разі її зміни.

Видалення сервісу

Коли ми впевнились, що нова версія працює, а стара вже не потрібна, ми видаляємо стару версію разом зі старою базою даних. Це можна робити як в ручну, так і придумати певну логіку для автоматизації. Видалення також відбувається певними кроками:

  1. Робимо резервне копіювання даних версії яка видаляється
  2. Видаляємо сервіс
  3. Видаляємо БД разом з даними. Обнуляємо restore, якщо він містив видалену версію.

Обов'язковою умовою видалення має бути те, що версія не є current.

Щоб убезпечити себе від втрати даних ми створюємо backup старої БД безпосередньо перед її видаленням.

Як ми до цього прийшли

У нас все відбувалось еволюційно. Ми вирішували задачі спрощення та прискорення розробки прибираючи з процесів розгортання зайві ланки.

Перше питання про БД постало коли ми автоматизували розгортання сервісу по зміні в Pull Request. До якої бази має приєднуватись сервіс? Якщо вона одна, як розробникам ділити її між собою? Якщо кожен розробник має свою, то як Pull Request дізнається про цю базу? А якщо і дізнається, як уникнути конфліктів, коли код і скрипти міграції будуть влиті в основну гілку?

Так, більшість цих питань вирішується різними процесам, але більшість з них залежать від людського фактору, або має інші проблеми. Ми відкинули ідею створення баз з снепшотів так як вони будуть завжди застарівати, через що можуть виникати конфлікти в конвеєрі розгортання сервісів в середовищі розробника. Також хотілось позбутись людського фактору трохи більше ніж повністю.

Тоді здалося очевидним, що для кожного Pull Request ми можемо копіювати БД з версії яка розгорнута з основної гілки. Далі накладати скрипти міграції на базу, після чого розгортати контейнери з побудованим сервісом.

Ми так і зробили. Всі задоволені. Кожен розробник має стільки БД, скільки має задач і це не коштує нічого з боку підтримки інфраструктури. Тепер ніщо не заважає експериментувати з власною копією БД. Крім цього зникли конфлікти в міграційних скриптах через те що бази стали більш унітарними.

Через якийсь час стало зрозуміло, що інфраструктура виробничого середовища (production) не має відрізнятись від середовища розробки. Тому дуже швидко цей підхід увімкнули у виробничому середовищу.

З того часу минуло 3 роки. Інфраструктура змінювалась, але підхід кочуючих даних лише обростав новими можливостями. Сьогодні важко уявити що щось може працювати по іншому.

Можливості та обмеження

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

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

Ще одна важлива можливість для виробничого середовища, це дуже швидке повернення навантаження на попередню версію сервісу.

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

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

Ми керуємось наступними обмеженнями.

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

Не робити current на попередню версію, лише restore. Це обмеження суб'єктивне, і його можна ігнорувати для деяких випадків. Але у разі проблем з новим сервісом, перемикання current на попередню версію не через процес «термінового повернення на попередню версію» може привести до того, що змінена схема або зламані дані будуть скопійовані в попередню версію. Що приведе до того, що обидві версії сервісу виявляться зламаними.

БД мають бути не великі. Для швидкого копіювання БД ми копіюємо журнал для реляційних БД, або майбутній master робимо реплікою поточного KV сховища. Це не єдині варіанти, але вони прості та достатньо швидкі для нас. Таким чином копія БД займає до хвилини часу. Також ми розглядали створення снапшотів ZFS. Цей підхід дозволяє зробити копію БД будь-якого розміру за декілька секунд, але вимагає додаткових витрат на експлуатацію самої ZFS. Зараз вивчаємо можливість тримати БД на Persistent Volumes.

Насправді це обмеження досить позитивно вплинуло на нашу архітектуру. Оскільки ми відразу почали замислюватись про масштабування за рахунок шардування сервісів. Це, в свою чергу, позитивно впливає на швидкість.

Розробники не мають доступу до баз версії в стані current. Таке обмеження пояснюється тим, що виникає бажання поправити проблему відразу в базі, а не додати відповідний скрипт міграції. Це приводить до ситуації розсинхронізації скриптів міграції з базами. Наприклад, якщо в БД версії з основної гілки вручну внесли зміни, то наступний скрипт міграції буде зроблений з урахуванням зміни в цій базі. На виробничому середовищі останній міграційний скрипт впаде, оскільки там відсутній крок зроблений руками. Ще гірша ситуація може виникнути в майбутньому, коли цей сервіс спробують розгорнути з нуля в новому середовищі, вже ніхто не буде пам'ятати про пропущений крок між скриптами міграції.

Цей підхід лише для операційної діяльності. Такі не великі БД підходять щоб тримати довідники та результати розрахунків. Наша операційна діяльність обмежена розкладом відправлення автобусів на пів року вперед, до розрахунку між всіма агентами на декілька місяців в минуле. Але безкінечний архів транзакцій варто зберігати в іншій базі, яка розгорнута стаціонарно. Цвяхи не потрібно забивати мікроскопом, про це варто не забувати.

Питання та відповіді

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

Наприклад резервне копіювання стає ще більш автоматичним ніж було. За нових обставин вам не потрібно пам'ятати додавати нову базу до системи резервного копіювання. Під час кроку перемикання навантаження з поточної версії на наступну версію ви автоматично реєструєте БД в системі автоматичного резервування. Інший приклад, для кожної нової версії ви можете генерувати новий пароль. І це теж без втручання людини. Автоматичне оновлення паролів з кожним оновленням сервісу також реалізується за допомогою версіонування, але на цей раз секретів в сучасних сховищах секретів. Але ці теми краще розкрити окремими статтями.

Безшовне перемикання навантаження працює за умови що ми не блокуємо на запис стару БД, та допускаємо втрату даних під час перемикання навантаження між версіями сервісу. Насправді ми так і не увімкнули для жодного сервісу блокування старої БД на запис, всі поточні мікросервіси спроектовані таким чином, що допускають короткочасну втрату даних без погіршення доступності. Ми тільки підійшли до відділення від нашого моноліту той функціональності, яка буде потребувати 100% обробки транзакцій.

Blue Green розгортання також можливе. Цей підхід не накладає обмеження, за умови що схема чи дані не мають змінитись і сумісні з новою версією. Ми можемо запустити окрему копію нової версії сервісу, яка приєднається до поточної версії БД, та подати на неї невеликий відсоток навантаження.

Чи існують проблеми з цим підходом? Так. Будь-який новий етап розвитку завжди породжує нову ниску проблем, які потребують вирішення. І це є невід'ємна частиною еволюції. По суті, всі нові проблеми з якими ми зіткнулись, пов'язані з дотриманням обмежень, які викладені вище. Наприклад проблема з дотриманням обмеження розміру БД пов'язана з тим що розробники не завжди можуть розрахувати розмір БД, або не повідомити про те, що БД буде зберігати декілька терабайт.

На жаль я не можу поділитись всіма внутрішніми технічними рішеннями, які реалізують цей підхід в нашій інфраструктурі. Більшість технічних рішень мало корисні за межами нашої внутрішньої інфраструктури. Але саме зараз ми на порозі міграції нашої інфраструктури в Kubernetes, ми хочемо зберегти можливості, які нам дає підхід кочуючих даних. Якщо ця ідея зацікавила вас, ми будемо раді поступово викласти всі пов'язані інструменти в OpenSource, а також ділитись тим, як відтворити цей підхід використовуючи відкриті інструменти.

Заключення

На мою думку напрямок вже визначено. Колись DevOps перемістив зону відповідальності за експлуатацію на команди розробки, передавши їм купу складних інструментів. В майбутньому зона відповідальності має залишитись на розробниках, але їм не потрібно знати як працює інфраструктура, як сконфігурувати docker, heml чи terraform. Єдине на чому має концентруватись увага розробника це його код.

Незабаром нас чекає нова хвиля нових інструментів та платформ. Виникають питання. Хто визначиться і готовий йти цим шляхом? Чи об'єднають вони свої зусилля?

Ми в Київській команді Pro Marketplace B2B BlaBlaCar бачимо за цим майбутнє. А ви готові йти разом з нами?

@levonet
Copy link
Author

levonet commented Oct 16, 2021

deploy staging
deploy current
deploy restore

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment