Как мы мигрировали базу данных из Redis и Riak KV в PostgreSQL. Часть 1: процесс +19


Это первая часть статьи, в которой я расскажу о том, как мы построили процесс работы над большим проектом по миграции БД: про безопасные эксперименты, командное планирование и кросс-командное взаимодействие. В следующих статьях подробней расскажу про технические проблемы, которые мы решали: про масштабирование и отказоустойчивость PostgreSQL и нагрузочное тестирование.



Долгое время основной базой данных в RealtimeBoard был Redis. Мы хранили в нём всю основную информацию: данные о пользователях, аккаунтах, досках и т.д. Всё работало быстро, но мы столкнулись с рядом проблем.

Проблемы с Redis

  1. Зависимость от сетевой задержки. Сейчас в нашем облаке она составляет порядка 20 мск, но при её увеличении приложение начнёт работать очень медленно.
  2. Отсутствие индексов, которые нужны нам на уровне бизнес-логики. Их самостоятельная реализация может усложнить бизнес-логику и привести к неконсистентности данных.
  3. Сложность кода также усложняет обеспечение консистентности данных.
  4. Ресурсоёмкость запросов с выборками.

Эти проблемы вместе с ростом количества данных на серверах послужили причиной для миграции БД.

Постановка задачи


Решение о миграции принято. Следующий шаг — понять, какая из БД подойдёт для нашей модели данных.

Мы провели исследование, чтобы выбрать оптимальную БД для нас, и остановились на PostgreSQL. Наша модель данных хорошо ложится на реляционную БД: у PostgreSQL есть встроенные инструменты для обеспечения консистентности данных, есть тип JSONB и возможность индексации определенных полей в JSONB. Это нам подходит.

Упрощённо архитектура нашего приложения выглядела так: есть Application Servers, которые через слой работы с данными обращаются в Redis и RiakKV.

Наш Application Server — это монолитное Java-приложение. Бизнес-логика написана на фреймворке, который адаптирован под NoSQL. В приложении реализована своя транзакционная система, которая позволяет обеспечивать работу множества пользователей на любой из наших досок.

RiakKV мы использовали для хранения данных архивных досок, которые не открывались в течение 7 дней.

Добавляем в эту схему PostgreSQL. Делаем так, чтобы Application servers работали с новой базой данных. Копируем данные из Redis и RiakKV в PostgreSQL. Задача решена!

Ничего сложного, но есть нюансы:

  • У нас 2,2 млн зарегистрированных пользователей. Ежедневно в RealtimeBoard работают 50 тысяч пользователей, пиковая нагрузка — до 14 тысяч одновременно. Пользователи не должны столкнуться с ошибками из-за наших работ, они вообще не должны заметить момент переезда на новую базу.
  • 1 Тб данных в БД или 410 млн объектов.
  • Непрерывный выпуск новых фич другими командами, чьей работе мы не должны мешать.

Варианты решения задачи


Перед нами стоял выбор из двух вариантов миграции данных:

  1. Остановить разработку сервиса > переписать код на сервере > протестировать функциональность > запустить новую версию.
  2. Провести плавную миграцию: постепенно переводить части продукта на новую базу данных, поддерживая одновременно PostgreSQL и Redis и не прерывая разработки новых фичей.

Остановка развития сервиса — это потеря времени, которое мы могли бы использовать для роста, а это значит — потеря пользователей и долей рынка. Для нас это критично, поэтому выбрали вариант с плавной миграцией. Несмотря на то, что по сложности этот процесс можно сравнить с заменой колёс на автомобиле во время движения.


При оценке работ мы разбили наш продукт на основные блоки: пользователи, аккаунты, доски и так далее. Отдельно вынесли работы по созданию инфраструктуры PostgreSQL. И заложили в оценку риски на случай, если что-то пойдёт не так (так оно и вышло).

Спринты и цели


Следующий шаг — построить работу команды из пяти человек так, чтобы все двигались с нужной скоростью к общей цели.

У нас есть две точки: начало работы над задачей и конечная цель. Идеально, когда мы движемся к цели прямым путем. Но часто случается, что мы хотим идти прямым путём, а получается так:



Например, из-за сложностей и проблем, которые не могли предусмотреть заранее.

Возможна ситуация, при которой мы вообще не придём к цели. Например, если уйдём в глубокий рефакторинг или переписывание всего приложения.



Мы разбили задачу на недельные спринты, чтобы минимизировать описанные выше сложности. Если вдруг команда уходит в сторону, она может быстро вернуться обратно с минимальными потерями для проекта, так как короткие итерации не позволяют уйти слишком далеко «не туда».

У каждой итерации есть своя цель, которая двигает команду к конечному большому результату.



Если во время спринта появляется новая задача, мы оцениваем, приближает ли нас к цели её выполнение. Да — берём в следующий спринт или меняем приоритеты в текущем, если нет — не берёмся за неё. Если появляются ошибки — ставим им высокий приоритет и быстро исправляем.

Бывает, что разработчики внутри спринта должны выполнять задачи в строго определённой последовательности. Или, например, разработчик передаёт готовую задачу QA-инженеру для срочного тестирования. На этапе планирования мы стараемся выстраивать подобные зависимости между задачами для каждого участника команды. Это позволяет всей команде видеть, кто, что и когда будет делать, не забывая про зависимость от других.

В команде есть ежедневные и еженедельные синхроны. Ежедневно по утрам мы обсуждаем, кто, что и в каком приоритете будет делать сегодня. После каждого спринта синхронизируемся друг с другом, чтобы быть уверенными, что все движутся в правильном направлении. Обязательно составляем план на крупные или сложные релизы. Назначаем дежурных разработчиков, которые, если нужно, присутствуют во время релиза и мониторят, что всё в порядке.

Планирование и синхронизация внутри команды позволяют вовлекать всех участников во все этапы работы над проектом. Планы и оценки не приходят к нам сверху, мы сами их составляем. Это увеличивает ответственность и заинтересованность команды в выполнении задач.

Так выглядит один из наших спринтов. Ведём всё на доске RealtimeBoard:



Режимы и безопасные эксперименты


Во время миграции мы должны были гарантировать стабильную работу сервиса в боевых условиях. Для этого нужно быть уверенными в том, что всё протестировано и нигде нет ошибок. Чтобы добиться этой цели, мы решили сделать нашу плавную миграцию ещё более плавной.

Идея заключалась в том, чтобы постепенно переключать блоки продукта на новую базу данных. Для этого мы придумали последовательность режимов.

В первом режиме “Redis Read/ Write” работает только старая база данных — Redis.



Во втором режиме “PostgreSQL Passive Write” мы можем убедиться, что запись в новую базу происходит корректно и базы консистентны.



Третий режим “PostgreSQL Read/Write, Redis Passive Write” позволяет убедиться в корректности чтения данных из PostgreSQL и посмотреть, как ведёт себя новая БД в боевых условиях. Основной базой при этом остаётся Redis, что давало нам возможность находить специфичные случаи работы с досками, которые могли приводить к ошибкам.



В последнем режиме “PostgreSQL Read/ Write” работает только новая база данных.



Работы по миграции могли затронуть основные функции продукта, поэтому мы должны были быть на 100% уверены, что ничего не сломаем и новая база данных работает как минимум не медленнее, чем старая. Поэтому мы начали проводить безопасные эксперименты с переключением режимов.

Переключать режимы начали на нашем корпоративном аккаунте, который используем ежедневно в работе. После того, как мы убедились, что в нём ошибок нет, начали переключать режимы на небольшой выборке внешних пользователей.

Timeline запуска экспериментов с режимами получился такой:

  • Январь-февраль: Redis read/write
  • Март-апрель: PostgreSQL passive write
  • Май-июнь: PostgreSQL read/write, основная база — Redis
  • Июль-август: PostgreSQL read/write
  • Сентябрь-декабрь: полная миграция.

При возникновении ошибок у нас была возможность быстро их исправлять, потому что мы сами могли делать релизы на серверы, где работали пользователи, участвующие в эксперименте. Мы никак не зависели от основного релиза, поэтому исправляли ошибки быстро и в любое время.

Кросс-командное взаимодействие


Во время миграции мы часто пересекались с командами, которые выпускали новые фичи. У нас единая code base, и в рамках своих работ команды могли изменять в новой БД существующие структуры или создавать новые. При этом могли происходить пересечения команд по разработке и выводу новых фич. Например, одна из продуктовых команд пообещала команде маркетинга выпустить новую фичу к конкретной дате; команда маркетинга запланировала рекламную кампанию на этот срок; команда продаж ждёт фичу и кампанию, чтобы начать общаться с новыми клиентами. Получается, все зависят друг от друга, и затягивание сроков одной командой срывает планы другой.

Чтобы избежать таких ситуаций, мы вместе с другими командами составили единый продуктовый roadmap, по которому синхронизировались несколько раз в квартал, а с некоторыми командами еженедельно.

Выводы


Чему мы научились за время этого проекта:

  1. Не бояться браться за сложные проекты. После декомпозиции, оценки и выработки подходов к работе сложные проекты перестают казаться невыполнимыми.
  2. Не жалеть времени и сил на предварительные оценки, декомпозицию и планирование. Это помогает глубже разобраться в задаче до того, как вы начнёте работу над ней, и понять объём и трудоёмкость работ.
  3. Закладывать риски в тяжелые технические и организационные проекты. В процессе работ вы обязательно встретитесь с проблемой, которая не была учтена при планировании.
  4. Не делать миграцию, если в этом нет необходимости.

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




К сожалению, не доступен сервер mySQL