Это первая часть статьи, в которой я расскажу о том, как мы построили процесс работы над большим проектом по миграции БД: про безопасные эксперименты, командное планирование и кросс-командное взаимодействие. В следующих статьях подробней расскажу про технические проблемы, которые мы решали: про масштабирование и отказоустойчивость PostgreSQL и нагрузочное тестирование.
Долгое время основной базой данных в RealtimeBoard был Redis. Мы хранили в нём всю основную информацию: данные о пользователях, аккаунтах, досках и т.д. Всё работало быстро, но мы столкнулись с рядом проблем.
Проблемы с Redis
- Зависимость от сетевой задержки. Сейчас в нашем облаке она составляет порядка 20 мск, но при её увеличении приложение начнёт работать очень медленно.
- Отсутствие индексов, которые нужны нам на уровне бизнес-логики. Их самостоятельная реализация может усложнить бизнес-логику и привести к неконсистентности данных.
- Сложность кода также усложняет обеспечение консистентности данных.
- Ресурсоёмкость запросов с выборками.
Эти проблемы вместе с ростом количества данных на серверах послужили причиной для миграции БД.
Постановка задачи
Решение о миграции принято. Следующий шаг — понять, какая из БД подойдёт для нашей модели данных.
Мы провели исследование, чтобы выбрать оптимальную БД для нас, и остановились на 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 млн объектов.
- Непрерывный выпуск новых фич другими командами, чьей работе мы не должны мешать.
Варианты решения задачи
Перед нами стоял выбор из двух вариантов миграции данных:
- Остановить разработку сервиса → переписать код на сервере → протестировать функциональность → запустить новую версию.
- Провести плавную миграцию: постепенно переводить части продукта на новую базу данных, поддерживая одновременно 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, по которому синхронизировались несколько раз в квартал, а с некоторыми командами еженедельно.
Выводы
Чему мы научились за время этого проекта:
- Не бояться браться за сложные проекты. После декомпозиции, оценки и выработки подходов к работе сложные проекты перестают казаться невыполнимыми.
- Не жалеть времени и сил на предварительные оценки, декомпозицию и планирование. Это помогает глубже разобраться в задаче до того, как вы начнёте работу над ней, и понять объём и трудоёмкость работ.
- Закладывать риски в тяжелые технические и организационные проекты. В процессе работ вы обязательно встретитесь с проблемой, которая не была учтена при планировании.
- Не делать миграцию, если в этом нет необходимости.
В следующих статьях я подробнее расскажу о технических проблемах, которые мы решали во время миграции.
Автор: stvlasov