Кочуючі дані в інфраструктурі BlaBlaCar (вже ні)
Цю статтю мав опублікувати в лютому 2022 року. Але плани зламані, життя пішло шкереберть. З тих часів змінилось все, включаючи те що BBC відмовився від кастомних технологій в інфраструктурі, надавши перевагу спрощенню найму. А мій шлях завів від IT до розвитку власного виробництва електроніки.
Тим не менше, цей матеріал може бути корисним DevOps-ам, QA лідам, архітекторам інфраструктури і тим хто не байдужий до перформенсу розробки та якості продукту для користувачів. Тому годі вкриватись тексту пилом.
Стаття побудована наступним чином: спочатку технічний хардкор без прив’язки до технологій, потім пояснення чому так, та які переваги це дає.
Колись пощастило подивитись доповідь Beyond Serverless and DevOps наживо, де Aviran Mordo, VP of Engineering з Wix розповідав про інфраструктуру та її бачення. Про те, що розробнику не потрібно знати про складні налаштування розгортання. Мене надзвичайно вразила ідея, що достатньо додати в проєкт частину коду, яка буде щось писати в базу даних, і розробника не має хвилювати, звідки база даних візьметься. За це має відповідати платформа.
Такі ідеї були дуже близькі для нас, і ми вже рухалися в цьому напрямку. Тоді це була Київська команда Pro Marketplace B2B BlaBlaCar. Крок за кроком ми перейшли від ручного створення і обслуговування баз даних (БД) до динамічного менеджменту даними без участі людини.
Єдине, що тепер нам потрібно, це в описі конфігурації кожного сервісу декларативно вказати, що цьому сервісу потрібна БД, або KV сховище, або черга. На цьому завершується взаємодія людини з інфраструктурою. Все інше відбудеться за лаштунками. Потрібні бази будуть автоматично розгорнуті під час розгортання сервісу.
Але це верхівка айсберга, всередині закладений механізм створення нової бази для кожного розгортання. Саме цей механізм вирішує більшість викликів, пов’язаних з реалізацією такого підходу. Завдяки цьому механізму ми можемо мати необмежену кількість розгорнутих сервісів без жодних конфліктів. Це ключове.
На той момент ми мали до 800 різних БД розгорнутих автоматично. І ми на їхню підтримку не витрачали жодної хвилини. А розробники, менеджери, підтримка та QA могли безпечно гратися з власною версією даних.
Давайте розберемося, як цей механізм, який створює нову БД для кожної нової версії сервісу, влаштований.
Підхід з кочуючими даними
Основна ідея в тому, що сервіс декларує, що йому потрібна БД, і він її отримує під час запуску. Головна умова, що система може одночасно розгорнути будь-яку кількість версій одного сервісу в одному оточенні. При цьому існує лише одна версія на одне оточення, на яку подається основне навантаження.
В такому випадку нам потрібно для кожної версії сервісу створити окрему БД з копією даних з поточної версії, на яку подається основне навантаження. Лише таким чином ми отримаємо незалежні версії сервісів, над якими безпечно проводити будь-які дії.
Ці процеси є основними складовими в інфраструктурі розгортання сервісів:
- розгортання нової версії
- перемикання навантаження на нову версію
- термінове повернення на попередню версію, якщо виникли проблеми з новою версією
- видалення старої версії
Тому в першу чергу роздивимося механізм переміщення даних у межах цих етапів.
Також нам знадобляться дві змінні для збереження стану, які можна зберігати в інфраструктурному KV сховищі:
current— зберігає версію сервісу, на яку подається основне навантаження. Простіше, основний домен на балансері дивиться на контейнери саме цієї версії сервісу.restore— зберігає попередню версію сервісу, яка булаcurrentдо того.
Для спрощення далі описано механізм без прив’язки до жодної платформи чи системи розгортання.
Розгортаємо нову версію сервісу
Уявімо собі що ми маємо розгорнутий сервіс v1 який приєднується до БД, назвемо її так само v1. Сервіс v1 є поточним, тому має стан current, на нього подається навантаження.
Тепер нам потрібно розгорнути версію сервісу v2. Це відбувається наступними кроками:
- Зупиняємо БД
v2та видаляємо дані, в тому числі на репліках. Лише в випадку колиv2вже була розгорнута. - Створюємо в томі
v2копію базиv1яка зараз єcurrent. - Запускаємо master БД
v2над цією копією. - Запускаємо скрипти міграції схеми БД. Цей крок опціональний.
- Розгортаємо репліки БД кроками
1-2, але замість копії базиv1робимо копію masterv2. - Запускаємо сервіс
v2, який приєднується до БДv2.

Давайте окремо розглянемо декілька кроків.
Перший крок потрібен, тому що ми можемо запустити розгортання тієї самої версії більше ніж один раз. Наприклад, щоб освіжити дані до чи після тестування.
Четвертий крок потрібен лише для реляційних БД, але навіть з ними ми можемо оптимізувати процес, вимкнувши міграцію, якщо між версіями сервісу відсутні зміни в скриптах міграції БД.
П’ятий та шостий кроки ми можемо запустити паралельно, оскільки сервіс може не чекати запуску реплік БД.
Тепер ми маємо розгорнутий сервіс з власною БД. Такий сервіс можна розгорнути як для Pull Request в середовищі розробки, так і у виробничому середовищі після створення нового тега.
Для середовища розробки на кожну зміну коду ми щоразу видаляємо сервіс разом з базами даних і розгортаємо версію сервісу заново.
Таким чином, ми можемо безпечно взаємодіяти з цією версією сервісу, автоматично запускати інтеграційні тести чи займатися відладкою коду або міграційних скриптів.
В якості БД може бути як реляційна база, так і KV сховище. Якщо сервіс використовує обидва типи БД, то кроки розгортання БД виконуються паралельно і незалежно. Відповідно, сервіс має дочекатися розгортання master БД обох типів.
В окремих випадках сервіс може чекати запуску найближчої репліки, коли архітектурно сервіс робить зміни в master БД, а читає з реплік.
Перемикання навантаження на нову версію
Для виробничого середовища ми маємо вміти перемкнути навантаження на нову версію. Але в нашому випадку цей процес складніший за попереднє розгортання, оскільки після попереднього розгортання могли відбутися зміни даних як в новій версії так і в current БД.
Перемикання в стан current відбувається з повторенням частини попередніх кроків:
- Зупиняємо БД
v2та видаляємо дані, в тому числі на репліках. - Блокуємо на запис схему БД
v1(current). Цей крок опціональний. - Повторюємо кроки
2-5 з попереднього етапу: - Запускаємо master БД
v2над цією копією. - Запускаємо скрипти міграції схеми БД.
- Розгортаємо репліки БД кроками
1-2, але замість копії базиv1робимо копію masterv2. - Запускаємо сервіс
v2, який приєднується до БДv2. - Перемикаємо навантаження з сервісу
v1на сервісv2. Зберігаємо інформацію про те що сервісv2єcurrent, а сервісv1помічаємо якrestore. - Реєструємо БД в планувальнику резервних копій. Цей крок опціональний.

Другий крок можливий для реляційних БД. Але навіть для них він може бути опціональним, якщо архітектура сервісу дозволяє часткову втрату даних. Пропуск цього кроку дозволяє робити перемикання навантаження безшовним, саме тому бажано закладати можливість часткової втрати даних сервісом, якщо це можливо, ще під час архітектурного проєктування сервісу.
Щодо останнього кроку, то створення резервних копій має сенс лише для версій які мають стан current. Оскільки БД розгортаються динамічно, а велику кількість сервісів чи оточень складно контролювати, ми маємо автоматично додати інформацію про новий сервіс до системи створення резервних копій.
Тепер навантаження подається на сервіс v2, сервіс v1 залишається в очікуванні.
Термінове повернення на попередню версію
Час від часу виникає ситуація, коли проблема проявляється лише під повним навантаженням на сервіс. В такому випадку потрібно мати можливість швидко повернути навантаження на попередню версію:
- Знімаємо блокування зі схеми БД
v1(restore). Цей крок опціональний. - Перемикаємо навантаження з сервісу
v2на сервісv1. Зберігаємо інформацію про те що сервісv1єcurrent, аrestoreобнуляємо.

Таким чином, ми відкотилися на попередню робочу версію за мінімальну кількість кроків та часу. Підхід версіонування даних дозволяє не хвилюватися за повернення схеми даних до попередньої версії у разі її зміни.
Видалення сервісу
Коли ми впевнилися, що нова версія працює, а стара вже не потрібна, ми видаляємо стару версію разом зі старою базою даних. Це можна робити як вручну, так і за допомогою певної логіки для автоматизації, щоб не перевантажувати інфраструктуру старими копіями сервісів.
Видалення також відбувається певними кроками:
- Робимо резервне копіювання даних версії яка видаляється
- Видаляємо сервіс
- Видаляємо БД разом з даними. Обнуляємо
restore, якщо він містив видалену версію.
Обов’язковою умовою видалення має бути те, що версія не є current.
Щоб убезпечити себе від втрати даних, ми створюємо backup старої БД безпосередньо перед її видаленням.
Буває таке, що потрібно зовсім видалити сервіс з оточення. Для цього потрібно очистити змінну current, далі ті самі кроки.
Як ми до цього прийшли
У нас все відбувалося еволюційно. Ми вирішували задачі спрощення та прискорення розробки, прибираючи з процесів розгортання зайві ланки.
Перше питання про БД постало, коли ми автоматизували розгортання сервісу по зміні (commit) в Pull Request. До якої бази має приєднуватися сервіс? Якщо вона одна, як розробникам ділити її між собою? Якщо кожен розробник має власний сервер з БД, то як Pull Request дізнається про цю базу? А якщо і дізнається, як уникнути конфліктів?
Так, більшість цих питань вирішується різними процесами, але більшість із них залежить від людського фактора або має інші проблеми. Ми відкинули ідею створення спільних баз зі снепшотів, оскільки вони будуть завжди застарівати, через що можуть виникати конфлікти в конвеєрі розгортання сервісів в середовищі розробника. Також хотілося позбутися людського фактора трохи більше ніж повністю.
Тоді здалося очевидним, що для кожного Pull Request ми можемо копіювати БД з версії яка розгорнута з основної гілки. Далі накладати скрипти міграції на базу, після чого розгортати контейнери з побудованим сервісом.
Ми так і зробили. Всі задоволені. Кожен розробник має стільки БД, скільки має задач. І це не коштує нічого з боку підтримки інфраструктури. Тепер ніщо не заважає експериментувати з власною копією БД. Крім цього, зникли конфлікти в міграційних скриптах через те, що бази стали більш унітарними.
Через короткий час стало зрозуміло, що інфраструктура виробничого середовища (production) не повинна відрізнятися від середовища розробки. Тому дуже швидко цей підхід увімкнули у виробничому середовищі. Задоволених побільшало. Підтримка випромінювала щастя від того, що у розробників стало драматично менше можливостей зламати production, а будь-які бізнес-проблеми усувалися за секунди безпечним відкочуванням на попередню версію сервісу.
З того часу минуло 3 роки. Інфраструктура змінювалась, але підхід кочуючих даних лише обростав новими можливостями. Стало важко уявити, що щось може працювати інакше.
Можливості та обмеження
В першу чергу цей підхід не обмежує кількість розробників та оточень, де ви розгортаєте сервіс. Достатньо один раз декларативно вказати, що цьому сервісу потрібна БД, і крапка. Всі інші дії далі на себе бере автоматизація. Далі, де б сервіс не був розгорнутий, він завжди буде розгорнутий з власною базою даних. І якщо сервіс розгорнутий в тому самому оточенні, то він відразу буде розгорнутий з актуальною копією даних. Для оточення розробки це дозволяє розробникам ніколи не перетнутися через використання спільних ресурсів.
Для виробничого середовища це дозволяє безпечно протестувати нову версію на актуальних даних до того, як на цю версію буде подане навантаження. В нашому випадку ми маємо окремі оточення для різних країн та континентів, тому тестування відбувається з урахуванням локальних особливостей даних.
Ще одна важлива можливість для виробничого середовища — це дуже швидке повернення навантаження на попередню версію сервісу.
Це,в свою чергу, зменшує кількість обмежень під час розробки. Розробники більше не думають про те, що робити, коли через зміну схеми чи даних зламається база чи сервіс у виробничому середовищі. І не тільки через те, що це можна відкотити, а ще через те, що процес міграції даних буде декілька разів автоматично протестований конвеєром, починаючи від етапу розробки до етапу розгортання у виробничому середовищі.
Всі ці нові можливості прискорюють процес розробки, або іншими словами, роблять розробку дешевшою. Тому що будь-яка помилка дасть про себе знати на самому ранньому етапі, відразу після commit. Але це не срібна куля і підхід кочуючих даних має обмеження, які диктуються безпекою, чи фізичними або економічними можливостями.
Ми керуємося наступними обмеженнями.
До однієї БД може приєднуватися лише один сервіс (одна його версія). Якщо ми спробуємо приєднатися до БД третім сервісом, то звідки він дізнається, коли ця БД перестане бути поточною, чи взагалі буде видалена? Звісно, це можна вирішити через проксі, яке буде автоматично перемикатися разом з перемиканням навантаження. Але ми так не робимо щоб не спровокувати іншу проблему, — звідки третій сервіс дізнається про зміну схеми?
Не робити current на попередню версію, лише restore. Це обмеження суб’єктивне, і його можна ігнорувати у деяких випадках. Але у разі проблем з новим сервісом, перемикання current на попередню версію не через процес «термінового повернення на попередню версію» може призвести до того, що змінена схема або зламані дані будуть скопійовані в попередню версію. Що призведе до того, що обидві версії сервісу виявляться зламаними.
БД мають бути невеликі. Для швидкого копіювання БД ми копіюємо журнал для реляційних БД, або майбутній master робимо реплікою поточного KV сховища. Це не єдині варіанти, але вони прості та достатньо швидкі для нас. Таким чином, копія середньої БД займає
Насправді це обмеження досить позитивно вплинуло на нашу архітектуру. Оскільки ми відразу почали замислюватись про масштабування за рахунок шардування сервісів. Це, в свою чергу, позитивно впливає на швидкість.
Розробники не мають доступу до баз версії в стані current. Таке обмеження пояснюється тим, що виникає бажання виправити проблему відразу в базі, а не додавати відповідний скрипт міграції. Це призводить до ситуації розсинхронізації скриптів міграції з базами. Наприклад, якщо в БД версії з основної гілки вручну внесли зміни, то наступний скрипт міграції буде зроблений з урахуванням змін в цій базі. На виробничому середовищі останній міграційний скрипт впаде, оскільки там відсутній крок, зроблений руками. Ще гірша ситуація може виникнути в майбутньому, коли цей сервіс спробують розгорнути з нуля в новому середовищі — вже ніхто не буде пам’ятати про пропущений крок між скриптами міграції.
Цей підхід лише для операційної діяльності. Такі невеликі БД підходять для зберігання довідників та результатів розрахунків. Наша операційна діяльність обмежена розкладом відправлення автобусів на пів року вперед та розрахунком між всіма агентами на декілька місяців у минуле. Але безкінечний архів транзакцій варто зберігати в іншій базі, яка розгорнута стаціонарно. Цвяхи не потрібно забивати мікроскопом, про це варто не забувати.
Питання та відповіді
Здається, що така схема розгортання дуже сильно обмежує можливості поточних підходів розгортання та обслуговування сервісів. Насправді ні, а в деяких випадках навпаки, робить її ще менш залежною від людського фактора.
Наприклад, автоматичне резервне копіювання тепер з коробки. За нових обставин вам не потрібно пам’ятати додавати нову базу до системи резервного копіювання. Під час кроку перемикання навантаження з поточної версії на наступну версію ви автоматично реєструєте БД в системі автоматичного резервування.
Інший приклад: для кожної нової версії ви можете генерувати новий пароль. І це теж без втручання людини. Автоматичне оновлення паролів з кожним оновленням сервісу також реалізується за допомогою версіонування, але цього разу секретів у сучасних сховищах секретів. Але ці теми краще розкрити окремими статтями. Щоб спростити відладку розробникам, посилання з паролем на приєднання до БД додаємо в опис PR. В межах однієї версії або PR пароль не змінюється.
Безшовне перемикання навантаження працює за умови що ми не блокуємо на запис стару БД, та допускаємо втрату даних під час перемикання навантаження між версіями сервісу. Насправді ми так і не увімкнули для жодного сервісу блокування старої БД на запис. Всі поточні мікросервіси спроєктовані таким чином, що допускають короткочасну втрату даних без погіршення доступності. Ми тільки підійшли до відділення від нашого моноліту тієї функціональності, яка буде потребувати 100% обробки транзакцій.
Blue Green розгортання також можливе. Цей підхід не накладає обмеження, за умови, що схема чи дані не мають змінитися і сумісні з новою версією. Ми можемо запустити окрему копію нової версії сервісу, яка приєднається до поточної версії БД, та подати на неї невеликий відсоток навантаження.
Чи існують проблеми з цим підходом? Так. Будь-який новий етап розвитку завжди породжує нову низку проблем, які потребують вирішення. І це є невід’ємною частиною еволюції. По суті, всі нові проблеми, з якими ми зіткнулися, пов’язані з дотриманням обмежень, які наведені вище. Наприклад проблема з дотриманням обмеження розміру БД пов’язана з тим що розробники не завжди можуть розрахувати розмір БД, або не повідомити інфраструктуру про те, що БД буде зберігати декілька терабайт.
На жаль, не можу поділитися всіма внутрішніми технічними рішеннями, які реалізують цей підхід в нашій інфраструктурі. Більшість технічних рішень мало корисні за межами нашої внутрішньої інфраструктури.
Заключення
Колись DevOps перемістив зону відповідальності за експлуатацію на команди розробки, передавши їм купу складних інструментів. В майбутньому зона відповідальності має залишитись за розробниками, але їм не потрібно знати, як працює інфраструктура, як сконфігурувати k8s, Helm чи Terraform. Єдине, на чому має концентруватися увага розробника, — це його код.
Маю надію, що наші ідеї підштовхнуть до створення ще кращої інфраструктури, ніж це колись вдалося зробити нам. Головне — зробити крок в цьому напрямку та не зупинятися.
4 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівДякую за статтю. Виникло декілька запитань:
— яким чином налаштовуються доступи в git? Якщо пароль до бд зберігається у описі до PR, то будь-хто з read only доступом до git може отримати повний доступ до бд.
— і для копій з production оточення паролі зберігаються у описі PR?
— розробники мають можливість отримувати повні копії бд з production, і підключатися до них напряму?
— який відсоток бд з 800 є зомбі, залишені з тих чи інших причин, і як ви їх виявляєте?
1. Git організації приватні. До репозіторіїв не мають доступ не розробники. Це хелпер виключно для розробників, надати прямий доступ до копії БД, як піднята під конкретний ПР. В дев середовищі відсутні реальні дані, тому ніякі політики безпеки це не порушує, навіть якщо розробник з сусідньої команди має доступ до цього ПР і БД.
2. Єдине джерело правди для паролей це який небуть сторедж секретів і краще з функцією версіювання секретів. Перед тим як розгорнеться нова версія БД там автоматично створюється нова версія пароля. Інфраструктура бере звідтв пароль щоб сказати з яким запуститись даному інстансу БД. Відповідно до цих секретів є доволі обмежений доступ з моніторингом і всіма справами.
3. Так, такий механізм є, зробити копію даних між середовищами, це найпростіше реалізується через механізм бекапів. Але знов, до продових бекапів доволі обмежений доступ, тому з доступами існують окремі процедури, що сильно за рамками цієї статті.
4. Відсоток зомбі 0, хіба буває що який сервіс був виведений з експлуатації в проді, і його забули підчистити, бо повне видалення сервіса відбувається лише через ручну очисту змінної current. В дев середовищі зомбі відсутні бо є механізм який слідкує за ПР, і якщо ПР висить якийсь ПР без змін, то інфраструктура автоматично видаляє розгорнутий сервіс разом з базами і артифактами. Розробник або QA завжди має можливість відновити роботу над ПР.
сама велика проблема «гарячого перемикання» в тому щоб «доливати» ті данні які сиплятся прям зараз
перемикання «на холодну» ніколи не було проблемою
а в високонавантаженних системах ще е проблема каналу коли просте перемикання може залмати сусідні ноди бо пікове навантаження на шину між двома ланками труби «тормозить» все інше на цьому каналі
з точки зору розробників — а ви не тестуете на великі навантаження, коли потрібно як раз склонувати базу на пару терабайт щоб у тестовому середовищі поганяти міліарди операций ?
Ми з такими проблемами не стикались через наші особливості інфраструктури.
Таке бувало, але як правило робилось вручну і під це тимчасово створювалось окреме середовище щоб не вплинути навантаженням на щось інше.