В прошлом году к Яндексу присоединился сервис TheQuestion. На тот момент уже был схожий сервис вопросов и ответов — Яндекс.Знатоки. У Знатоков была большая аудитория и много интересных вопросов, но не хватало экспертов, которые могли давать качественные ответы на эти вопросы. TheQuestion же, наоборот, имел сильное сообщество экспертов, но ему не хватало интересных вопросов. Логичным шагом было объединить два сервиса, чтобы взять лучшее у каждого из них. Но как это сделать, если у каждого сервиса своя технологическая база, контент и пользователи?
Сегодня я расскажу о том, как наша команда решила эту задачу с технологической точки зрения. Вы узнаете, какие варианты объединения мы рассматривали и какой в конце концов выбрали. Расскажу про «подменное API», миграцию баз данных, объединение профилей и тестирование бэкенда. А ещё — про ночь переезда без права на ошибку. Вы увидите, что скучать нам не пришлось.
Задача слияния двух сервисов в один не является новой, но от этого не становится проще. История знает много удачных (и не очень) примеров интеграции, однако «серебряной пули» и чёткой инструкции «делай так, и всё получится», к сожалению, нет. Всё очень сильно зависит от специфики объединяемых сервисов и желаемого результата.
В нашем случае цель была такой: чтобы весь контент, когда-либо написанный на каждом из сайтов, был доступен на объединённом сервисе, а его авторы могли бы управлять им.
Итак, как же объединить два сервиса вопросов и ответов, которые вроде бы так похожи, но так сильно отличаются по сути? Перевод контента и пользователей с одного сервиса на другой очень напоминает переезд из старой квартиры в новую.
Только в нашем случае пользователь может жить одновременно в двух квартирах (Знатоки и TheQuestion), и нужно бережно перевезти его в третью. Нужно переместить всю мебель, растения, кота и даже обои в новую квартиру (то есть вопросы, ответы, комментарии, лайки), а после этого предложить переехать и ему самому.
Как же это сделать? В голову сразу приходят несколько вариантов.
Вариант 1. Очень плохой
Давайте просто возьмём один из сервисов, перенесём весь контент на другой (хотя даже это уже не просто) и закроем исходный сервис.
Этот вариант совсем плохой с точки зрения пользователя первого сервиса. Мы просто снесли его старый дом и заставили переехать в новый. Любому человеку не понравится такое отношение, и вместо переезда он может просто уйти в закат. Для нас же главная ценность — это сообщество пользователей, поэтому никого обижать мы не планировали. И смело перешли к другим вариантам.
Вариант 2. Плохой
Давайте не будем менять ни один из сервисов, вместо этого запустим новый, объединённый, и будем периодически добавлять в него контент из двух других (допустим, раз в сутки).
В этом случае мы вроде бы не делаем пользователю хуже, но и лучше тоже не делаем. Его старая квартира остаётся без изменений, но нет никакого смысла переезжать в новую. Все соседи также живут в старом доме, только что купленный цветок перевезут в новую квартиру только через сутки. У такого объединённого сервиса нет никаких шансов стать новым домом.
Вариант 3. Хороший, но сложный
Давайте не будем закрывать ни один из сервисов, контент и профили будем мгновенно дублировать на объединённом сервисе, а люди сами со временем переедут.
Все соседи пользователя (и даже кот) живут одновременно и в старом, и в новом доме. Только что купленный цветок мгновенно появляется в новой квартире. То, что нужно! Этот вариант является самым комфортным с точки зрения пользователя. Поэтому мы и выбрали именно его.
Начинаем переезд
То, что мы в итоге сделали, можно описать в нескольких предложениях. Мы полностью повторили весь бэкенд API TheQuestion на базе бэкенда Знатоков, получив таким образом единый бэкенд, который умеет работать сразу с двумя (и даже с тремя) сайтами. Фронтенд TheQuestion при этом остался почти без изменений, а значит с точки зрения пользователей практически не изменился и сам сайт. Этот проект получил внутреннее название «подменное API». Но обо всём по порядку.
Что мы имели на входе: два полностью независимых сайта. Знатоки живут во внутренних облаках Яндекса. Бэкенд Знатоков написан на Python. TheQuestion живёт в облаках Microsoft Azure, бэкенд TheQuestion написан на Go. У сервисов совершенно разная схема хранения данных в базах. Кроме того, у TheQuestion есть два мобильных приложения (для Android и iOS), которые также необходимо было поддержать. В общем, врагу не пожелаешь объединять такой зоопарк.
Этап 0. Заезд в облако Яндекса
Этот этап, строго говоря, не является необходимым для «подменного API», но заметно упрощает дальнейшие шаги. На этом этапе мы полностью отказались от внешних хранилищ и мощностей. TheQuestion начал использовать DNS-серверы Яндекса. Рантайм-сервисы перевезли в Яндекс.Облако. Базу данных перевели в Yandex Managed Databases. Во время переезда мы также смогли найти и поправить несколько ошибок в TheQuestion, например незакрытые коннекты к Redis в коде 2015 года. В качестве бонуса мы также получили дополнительные мощности для TheQuestion.
Этап 1. Миграция данных
Независимо от того, какой вариант объединения сервисов мы бы выбрали, объединять данные пришлось бы в любом случае. Для единой базы данных решили взять PostgreSQL — эта СУБД уже использовалась как в Знатоках, так и в TheQuestion. Чтобы не переусложнять проект, не стали создавать третью базу для объединённого сервиса, а просто взяли базу Знатоков и расширили её так, чтобы она могла принять все данные TheQuestion. Это было первым серьёзным технологическим вызовом.
Каждую запись в каждой таблице из базы TheQuestion нужно было конвертировать и сложить в базу Знатоков. Затем — соотнести каждую колонку из одной и другой базы. Многие поля приходилось нетривиально конвертировать из одного формата в другой. Так, отдельной большой подзадачей была конвертация формата хранения текста (собственно формата хранения вопроса или ответа) из QML (TheQuestion) в Markdown (Знатоки).
Мы настроили регулярный (несколько раз в сутки) процесс переноса новых данных из одной базы в другую, но при этом сделали так, чтобы данные из TheQuestion нигде не отображались до завершения следующего этапа. Потому что «несколько раз в сутки» это далеко не обещанное «мгновенно», и данные могли находиться в неконсистентном состоянии с аналогичными данными на TheQuestion, что вводило бы пользователей в заблуждение. Так зачем мы начали с миграции данных, если бэкенд ещё не был готов?
Во-первых, таким образом мы стабилизировали процесс. Во-вторых, уменьшили количество новых данных, которые надо будет переносить в будущем, а это важно, так как весь импортируемый контент необходимо было прогнать ещё и через разметку на качество, нежелательный контент, спам, фрод.
Этап 2. «Подменный API»
Итак, мы решили первую из задач — научились забирать контент из базы TheQuestion и при желании даже отображать его. Теперь нужно было сделать так, чтобы этот контент попадал в объединённую базу мгновенно, а не несколько раз в сутки.
Для этого нужно было переписать весь бэкенд TheQuestion со всей необходимой логикой. Название проекта «Подменное API», строго говоря, не полностью отражает суть. Правильнее было бы назвать его «Подменным бэкендом». Дело в том, что, кроме непосредственной реализации всех необходимых для функционирования фронтенда TheQuestion «ручек», нужно было реализовать и другие возможности. Перед нами стояло несколько крупных задач.
Авторизация. В Яндексе есть централизованная система авторизации пользователей — Яндекс.Паспорт. И Знатоки, естественно, использовали Паспорт. Чтобы авторизоваться в нём, надо иметь аккаунт в Яндексе. В этом и заключалась проблема. Далеко не все пользователи TheQuestion авторизовывались на сайте через Яндекс (хотя такая возможность была). Многие пользователи вообще не имели логина в Яндексе и заходили через социальные сети (ВКонтакте, Facebook...). Естественно, мы должны были сохранить этот функционал при переезде. Поэтому мы реализовали «внепаспортную» авторизацию.
Поиск по сайту. На TheQuestion был реализован поиск по вопросам, ответам, пользователям и темам. Для поиска использовалось стороннее решение Sphinx. Очевидно, что если речь идёт о едином сервисе, то и поиск должен быть единым, то есть он не может работать сразу на двух системах. Таким образом, от Sphinx отказались в пользу внутреннего поискового движка с поддержкой необходимого функционала и индексацией всего контента TheQuestion.
Отгрузка страниц в Дзен и Турбо. На момент присоединения TheQuestion уже пользовался технологиями Яндекса. Поддерживались Турбо-страницы, интересный контент попадал в ленту Дзена. Всё это нужно было также поддержать в «подменном API».
Уведомления в сервисе и приложениях, почтовые рассылки. Всё, что связано с уведомлением пользователей: подписки, рассылки с интересным контентом, пуши про лайки и комментарии, многое другое. Всё это нужно было бережно перенести и ничего не забыть.
Система администрирования сайта. Под этим пунктом подразумевается всё, что связано с внутренним управлением сервисом: модерация, аналитика и так далее.
Единая система рейтинга пользователей. Эта задача была скорее не технической, а логической. Формально для «подменного API» разрабатывать единую систему рейтинга не нужно, но эта система всё равно нужна для будущего объединённого сервиса. На обоих сайтах пользователям начислялся рейтинг за количество и качество созданного контента. Подробности начисления рейтинга не разглашались, но чем чаще и лучше ты отвечаешь на вопросы, тем выше твой рейтинг. Принципы начисления рейтинга были одинаковы на обоих сервисах, но сама формула и факторы сильно различались. Необходимо было не просто правильно и честно сравнить между собой пользователей Знатоков и TheQuestion, но и научиться считать единый рейтинг для тех экспертов, которые писали сразу на двух сервисах.
А ещё переписать все API. Как ни крути, эта задача была самой важной и сложной. Многие процессы на сервисах были похожи, поэтому их мы взяли от Знатоков и не писали с нуля. Но также было немало нового, например прямые линии пользователей или черновики ответов. В итоге мы переписали более 100 «ручек» в «подменном API» и реализовали более 50 REST-ресурсов.
После того как мы реализовали весь описанный выше функционал, можно было приступать к переезду. Но прежде мы сделали один трюк.
Понятно, что перед переключением и выкаткой «подменного API» в продакшн его надо было очень хорошо протестировать. Протестировать нужно было, во-первых, функционально, то есть непосредственно проверить работоспособность всего сайта на новом API. Во-вторых, нагрузочно. Мы хотели быть уверены на 100%, что наша конструкция не «ляжет» под нагрузкой. Естественно, мы регулярно проводили «нагрузочные стрельбы», которые показывали, что у нас есть хороший запас производительности. Но в вопросах работоспособности сервиса всегда лучше перестраховаться. Любые, даже самые хорошие, синтетические нагрузочные тесты так или иначе отличаются от продакшн-нагрузки. Поэтому мы решили ещё до переключения API налить продакшн-нагрузку на наш стенд.
Для этого на фронтенде TheQuestion мы реализовали дублирование всех GET-запросов (имеются в виду запросы на получение данных, а не модификацию) сразу в два API: «старое API» TheQuestion, которое на тот момент являлось основным, и второстепенное «подменное API». При этом фронтенд не дожидался ответа второстепенного API и не обрабатывал ошибки, но так мы смогли протестировать бэкенд на реальных пользователях.
Этап 3. И тут мы вспомнили про приложения
Нет, конечно же, мы про них помнили всё это время, но столкнулись с одной проблемой. Те, кто работал с мобильными приложениями, знают, что хлопот с ними гораздо больше, чем с сайтом. Связано это в первую очередь с распространением новых версий.
Во-первых, необходимо работать с внешними сервисами App Store и Google Play и ждать, когда новые версии пройдут проверку (а иногда проверка может занять значительное время). Во-вторых, даже если ваше приложение уже прошло проверку и появилось в сторах, это ещё не значит, что пользователи сделают обновление.
В случае фронтенда сайта разработчики сами управляют тем, когда выйдет новая версия, и точно знают, что после этого все пользователи получат обновлённую версию сайта. В случае с приложениями такой гарантии нет. Чтобы такую гарантию получить, часто используют «принудительное обновление» приложения. Мало кто любит этот способ, и, конечно, всегда, если это возможно, нужно сохранять обратную совместимость приложения и бэкенда. Поэтому мы пошли по пути внесения изменений именно на стороне бэкенда с минимальными изменениями на фронтенде в приложениях. Но, как это часто бывает, план столкнулся с суровой реальностью.
Некоторые изменения было гораздо проще делать на стороне фронтенда, чем на бэкенде, поэтому в процессе разработки «подменного API» фронтенд незначительно, но поменялся. В частности, в старой базе TheQuestion использовались числовые 64-битные ID. В базе Знатоков и, соответственно, в объединённой базе и новом API для TheQuestion использовались строковые 128-битные ID. Вообще, для фронтенда, написанного на Node.js, это различие не является значительным. Но для приложений со строгой типизацией это оказалось фатальным. Мы потеряли обратную совместимость, и старые приложения не могли работать с «подменным API».
В какой-то момент даже появился проект под названием «подменное API для подменного API», сутью которого было написание небольшой прослойки между новым бэкендом и приложениями, которая бы конвертировала все данные в старый формат. Однако от этой идеи мы быстро отказались. Эта прослойка оказалась бы очень жёстким «костылём», который в будущем точно принёс бы нам много проблем. Например, нельзя так просто взять и перевести 128-битные ID в 64-битные. Пришлось бы переводить с потерей информации и, следовательно, возможными коллизиями по ID, либо поддерживать промежуточную таблицу с соответствием старых и новых ID (для всех элементов базы). И то, и другое — не лучшее архитектурное решение.
Кроме ID, был ряд других изменений, которые также было гораздо проще поддержать на стороне фронтенда и приложений. В итоге мы приняли решение реализовать изменения в приложениях и всё-таки воспользоваться форсированным обновлением. В короткие сроки мы разработали новые версии приложений, совместимые с «подменным API», благо изменений со стороны фронтенда было не так много и они были не очень серьёзные. Отправили в App Store и Google Play, успешно прошли модерацию и стали ждать.
Этап X. Поехали!
Итак, весь код написан. Стенд с «подменным API» протестирован и обстрелян. Новые версии приложения прошли проверку в сторах и готовы к публикации. Теперь всё это нужно было выкатить в продакшн.
Из-за того, что копирование новых данных из старой базы в новую происходит асинхронно и занимает какое-то время, переключить бэкенд (и базу под ним) на работающем сайте нельзя. Это может привести к потере или неконсистентности пользовательских данных. Поэтому мы выбрали дату, предупредили пользователей и подготовили табличку «Ведутся технические работы».
И вот наступил час, а точнее ночь X. План выкатки выглядел так:
- На сайте TheQuestion вешаем заглушку «Ведутся работы».
- Приложения переводим в режим Readonly. Пользователи могут читать контент из старой базы, но не могут создавать новый.
- Старую базу TheQuestion переводим в Readonly. Это на всякий случай: после первых двух действий пишущего трафика в базе быть не должно. Тем не менее, лучше перестраховаться, чтобы случайно не потерять контент пользователей.
- Последний раз копируем данные из старой базы в новую. В случае успеха никакие новые данные уже не появятся в старой базе, всё будет отображаться в новой.
- Ещё раз проводим оперативное тестирование работы всего функционала.
- Публикуем новые, уже одобренные версии приложений в сторах, совместимые с новым API.
- Заменяем API на сайте. Фактически — просто деплоим новую версию фронтенда, которая смотрит на API по новому адресу.
- Молимся, стучим по дереву, бьём в бубен.
- Снимаем плашку «Ведутся работы», то есть полностью открываем сайт на новом API.
- Включаем форсированное обновление в приложениях.
Что ж, выглядит не так страшно. В реальности всё прошло достаточно гладко. Из неожиданностей, с которыми мы столкнулись, было, пожалуй, только слишком большое время публикации приложения в App Store (приложение было проверено заранее, речь шла только о появлении в сторе). В итоге это заняло несколько часов, из-за чего вся операция немного затянулась.
Кроме того, в процессе переключения была одна ключевая особенность, которая многократно всё усложняла и увеличивала ответственность. Дело в том, что процесс переключения нельзя было повернуть вспять.
Хотя у нас был настроен и отлажен процесс копирования и конвертации данных из старой базы TheQuestion в новую, объединённую, — процесса обратного копирования (из новой базы в старую) не было. Это значит, что как только мы открываем сайт на «подменном API» и пускаем пользовательский трафик, все новые созданные вопросы, ответы, комменты и лайки уже не могут так просто попасть в старую базу TheQuestion. Если что-то пойдёт не так после открытия, например единая база не справится с нагрузкой, то быстро откатить всё назад будет нельзя.
На самом деле я, конечно, утрирую. Пользовательские данные мы бы в любом случае не потеряли. У нас был план Б и способ ручного обратного копирования данных из новой базы в старую. Но это всё равно заняло бы какое-то время, и совсем безболезненно для пользователей откат бы не произошёл.
К счастью, план А сработал, и ничего откатывать не пришлось.
Последний этап
Итак, бэкенд подменили, базу объединили, мобильные приложения не забыли. Для пользователей ничего не поменялось, потому что сайт Яндекс.Кью, который и должен был объединить данные с обеих площадок, на тот момент ещё не был запущен. И для его запуска нам нужно было решить ещё одну задачку.
В самом начале я писал, что мы должны были объединить не только вопросы и ответы с двух сервисов в одном новом, но и пользователей. Пользователи должны были получить возможность не только увидеть свой контент на Кью, но и управлять им на Кью. Технически объединить данные и передать права управления не сложно. Куда сложнее убедиться, что права переданы именно тому, кому они и должны быть переданы.
При переезде со Знатоков на Кью всё просто: в обоих случаях используется один и тот же аккаунт Яндекса. Но у TheQuestion свой аккаунт, авторизоваться которым на Кью нельзя. К счастью, мы подумали об этом заранее. Задолго до описанных выше действий мы дали возможность пользователям TheQuestion привязать свои профили Яндекса. И к моменту физического объединения сервисов более 90% активных пользователей это сделали. Это позволило нам безболезненно запустить миграцию контента и пользователей.
Итог
При переезде мы хотели сохранить каждого пользователя, поэтому сознательно пошли на наиболее трудоёмкий и рискованный вариант объединения платформ. Мы создали единую технологическую базу, научились мгновенно перевозить контент и профили. Вместо неожиданного закрытия и принудительного переезда сохранили работоспособность старых сервисов, запустили новый и объяснили его преимущества.
Мы запустили Яндекс.Кью в прошлом году. Сейчас более 80% активных авторов TheQuestion и Знатоков добровольно переехали в новый дом.