Как мы полностью обновили VK Мессенджер: переписать нельзя рефакторить

89a0b16b3aea9dbedabfe1a1f243eb63.png

Случается, ты просыпаешься и осознаешь: так больше продолжаться не может и нужно что-то менять. Разные кодовые базы, избыточное легаси и нестабильность мешают пользователям получать удовольствие от общения в твоем приложении. И эта мысль подводит тебя к развилке: один путь ведет к сложному и долгому рефакторингу легаси за почти 10 лет, второй к не менее долгому, а, порой, более сложному процессу переписывания с 0. Но какой бы путь ты ни выбрал, в любом случае начинаешь испытывать азарт — предстоит большая Задача (именно с большой буквы). 

Привет Хабр, меня зовут Федор Неживой, я ведущий программист-разработчик в команде VK Мессенджера и сегодня расскажу вам, как мы перестраивали и обновляли один из крупнейших проектов в рунете. В статье будет боль, пот, реальный код и детали, как мы шаг за шагом пришли к масштабному обновлению, а потом внедряли то, что получилось. 

Что такое VK Мессенджер

VK Мессенджер по своей сути — сервис внутри еще большего сервиса. Он тесно связан со множеством других разделов ВКонтакте: Музыка, Фотографии, Видео, Лента — да практически со всеми. Для наглядности визуализируем. 

d2936fd2423fe5a5a2456856c040a500.png

При этом веб-мессенджер как платформа состоит из 4 продуктов:  

  • веб-версия vk.com;

  • fast-чаты (всплывающие чаты);

  • мобильная версия m.vk.com;

  • десктопный мессенджер ВКонтакте. 

Мало того, что все эти части имели раньше разные кодовые базы (даже мессенджер внутри vk.com), так и у них был разный приоритет. Самый высокий был у vk.com с его мобильными клиентами. Далее шел m.vk.com, для которого что-то могли уже не делать ради экономии времени и трудозатрат. На третьем месте по важности была десктопная версия. Этот проект моей команде достался уже готовым. Его когда-то написал на Electron один разработчик, который потом ушёл из компании. У команды не было знания кодобазы, а спросить было не у кого, поэтому что даже CI пришлось настраивать самим с 0 за пару месяцев. А fast-чаты мы поддерживали по остаточному принципу.

С чем мы подошли к развилке

Как уже сказал, изначально VK Мессенджер состоял из 4 независимых кодовых баз. Все они были написаны давно, и с тех пор в них внесли много изменений. Технологии развивались, что-то стали делать проще, что-то — иначе. Например, во многих языках начали активно использовать типизацию. Если же код легаси, то не всегда можно найти автора и расспросить о его устройстве. Поэтому каждый раз опасаешься вносить изменения, потому что неизвестно, как это аукнется.

В те времена после каждого релиза мы всей командой с замиранием сердца следили за графиками. И при любой проблеме приходилось долго искать причину: «Что и где отвалится, если я удалю этот кусок кода и заменю на другой?». С учетом размеров и устройства кодовых баз даже инструменты в IDE не особо помогали справиться с задачей. Часто приходилось пользоваться обычным поиском (всякими креативными способами), чтобы понять заденешь что-то или нет.

55339864a53ccdf0340305a2e8b56f92.pngd48ed9795e28a28672671959600f0eb7.png

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

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

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

Фича очень простая и не требует глубоких знаний в предметной области. Однако у меня ушло около месяца работы, потому что каждый раз я фактически начинал «с нуля», понимание того, как фича работает в vk.com, никак не помогало реализовать ее в m.vk.com и т.д. Чтобы погрузиться во всё, требовалось много времени. Я сам более-менее уверенно стал ориентироваться в коде только через полгода. 

Как мы выбирали, по какому пути пойдем

Нужно было что-то менять. Но мнения команды разделились: либо серьёзно рефакторить код, либо всё переписывать с нуля. Много обсуждали и спорили, что-то по-мелочи меняли, но долгое время ситуация в целом оставалась прежней. В тот момент проблема была в том, что оба пути были страшными и полными неопределенности. И нельзя было ничего быстро апробировать. Именно поэтому наши дискуссии оставались умозрительными.

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

Для себя я выделял несколько аргументов в пользу переписывания с 0. Возможно, какой-то из них я рационализировал, когда писал этот текст):  

  • Рефакторинг никак не решал проблему гетерогенности кодовых баз. А «выдрать» одну и переиспользовать в других местах было невозможно.

  • К команде могут присоединиться новые инженеры. Погружать их одновременно в старые и новые части рефакторинга и нюансы их сочетания будет невероятно сложно и дорого. В нашем же случае мы просто подключали новичков только в новую кодобазу.

  • Мы (как и многие другие команды в мире) видели будущее в типизации и очень хотели использовать TypeScript. Кроме того у меня был крайне позитивный опыт использования типов для моделирования предметной области, что помогает избавляться от невозможных состояний в системе. Это распространенная практика в сообществах других ЯП, которыми я интересуюсь (Haskell, Elm, Rust). Для того, чтобы получить максимум от TypeScript, нам нужно было бы включить максимально жесткие ограничения настройки, но из-за существующего кода это было просто невозможно. Сначала я даже пытался как-то изменить ситуацию — ходил по всей монорепе vk.com и правил код разных команд. Но таким образом мне удалось «ужесточить» конфиг TypeScript лишь на одну настройку, а потом я просто сдался.

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

А еще в случае с переписыванием у нас все же был понятный путь того как получать выгоду от него сразу, а не через годы. Во-первых мы достаточно быстро могли заменить и оживить fast-чаты, которые отображаются на каждой странице и помогают вовлечь в мессенджер больше пользователей. Во-вторых, одной из важных целей новой кодовой базы с архитектурной точки зрения была «встраиваемость». Поэтому достаточно быстро появилась идея встраивать отдельные значимые кусочки в старый код, которые бы работали на новом состоянии и UI.

Но спорили мы не только о том, рефакторить или переписывать, но и о том, как организовывать код, как описывать модели, бизнес-логику и все остальное. Однажды споры надоели мне настолько, что я решил в свое свободное время попробовать написать новый прототип десктопного мессенджера. Подумал, что обосновывать плюсы своих идей будет куда проще на примерах реального работающего кода. Потратил на это примерно три недели один, а потом подключил еще одного старейшего члена команды (привет Тиму Чаптыкову!), который помог отполировать его еще пару недель. Прототип умел делать не более 10% того, что было реализовано в текущих клиентах, в то же время не во всех из них поддерживались некоторые фичи из прототипа. Кроме того там были примеры реализации ключевых частей мессенджера — работы со списками чатов и получение и обработки событий с сервера. Потом этот прототип мы показали сначала всей команде веб-мессенджера, а потом на очередном демо и всем остальным командам мессенджера. Фидбек оказался очень позитивным, к тому же прототип был хорошим пруфом нашему руководству, что мы сможем и начать, и закончить. А чтобы не брать паузу на несколько лет, договорились переписывать по частям. Это заняло немало времени — сказывалась разнородность кодовых баз и используемых в них технологий. 

Какие шаги мы предприняли 

Итак, мы определились с тем, по какому пути пойти, но впереди оставалось еще много проблем. Например огромное, количество точек интеграции — несколько десятков.

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

Все эти точки интеграции были отчасти легаси: ни документации, ни описаний типов. Приходилось заниматься настоящей археологией, разбираясь в работе кода. 

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

Хотя мы пошли по пути переписывания с нуля, но часть компонентов мы решили брать или существующие в мессенджере, или у коллег.Например, после интеграции fast-чатов само поле ввода текста мы позаимствовали из vk.com. Правда в итоге свой мы всё равно написали, потому что нужна была интеграция с виджетами и десктопной версией.  

В vk.com есть эффективный механизм определения мастер-вкладок на основе алгоритма нахождения консенсуса. Благодаря ему мы снижаем количество обращение фронтенда к серверам. Допустим, у вас открыто десять вкладок ВКонтакте, которые могут обращаться к сети за обновлениями от сервера, и чтобы не перегружать бэкенд, лишь одна из открытых вкладок стучится в сеть, получает данные на всех, а потом «раздаёт» остальным вкладкам.

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

Сложностей и ошибок добавляло и то, что VK Мессенджер — очень оживлённая и интерактивная часть ВКонтакте. Здесь всё время что-то обновляется, одни действия инициируют новые. Поэтому, чтобы повысить общую производительность, мы много времени потратили на оптимизацию. 

Чего мы добились 

Благодаря переезду в отдельный репозиторий VK Мессенджер теперь — своего рода пакет SDK, который мы устанавливаем и используем в разных частях ВКонтакте с небольшими доработками. Берём фрагмент кода с новыми фичами и с небольшими адаптациями добавляем во все 4 версии. Встраиваемость была одним из ключевых требований к новой кодобазе и нам удалось здорово с этим справиться благодаря тщательно спроектированным интерфейсам DI. А пятым проектом, который был сделан на основе нового SDK стал виджет мессенджера, который сейчас можно найти, например, в почте Mail.ru. Мы планируем заменить им старый виджет сообщений сообществ, который ранее могли себе встраивать сторонние сайты.

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

Одной из важных целей для нас было серьезно улучшить DX. Поскольку мы изменили подход к обработке ошибок и серьезно вложились в моделирование нашей предметной области типами TypeScript, понимать особенности продукта стало проще, а совершать ошибки по недосмотру куда тяжелее. Отличным доказательством этому служит онбординг новых людей в команду. Если в момент моего прихода в команду выкатывать какие-то серьезные доработки получалось только спустя несколько месяцев, то в последние годы ребята могут катить даже достаточно крупные задачи уже в свой первый месяц.

Раньше мессенджер умел рендериться дважды — в kPHP для server-side rendering и на клиенте для интерактивных частей. Где-то мы умели шарить «шаблоны» между kPHP и JavaScript, а где-то нам приходилось дублировать логику разметки. В новой кодовой базе мы с одной стороны не могли позволить себе server-side rendering (для этого надо было использовать Node.js, а инфраструктурно этого не хотелось), а с другой хотели ускорить холодный старт вкладки мессенджера. Поэтому мы сделали промежуточный вариант — «транслируем» наши запросы начальных данных в PHP и переиспользуем при загрузке страницы для создания кэша. Благодаря возможности стримить ответ с сервера, которую недавно внедрил KPHP, мы можем параллельно готовить данные и отдавать HTML и статику.

Ещё мы на основании схемы API бэкенда стали генерировать типизацию к этому API. Это помогает избегать ошибок вроде неправильной передачи аргумента. Сейчас типы для API используют все команды на вебе, но когда-то именно мессенджер был early-адоптером, приносил фидбек и идеи в реализацию. В этом нам помогает и очень развитая инфраструктура ВКонтакте: мы используем систему тестирования, подгружаем данные напрямую с бэкенда, чтобы у пользователей быстрее открывались страницы. 

Также из любопытных изменений — механизм парсинга текстов. Каждое сообщение разбивается на фрагменты (чанки). Раньше для поиска по ним использовали регулярные выражения. Работало специфически, обновлять было тяжело. А сейчас мы парсим массив смысловых чанков, из которых потом можем собирать единый текст так, как нам нужно.

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

История одного факапа

Чтобы никто не думал, что всё всегда было идеально, расскажу историю небольшого факапа. Собственного факапа. 

Вместе с запросом на отправку нового сообщения мы подкладываем уникальное число, назовем его ID сообщения. Это нужно, чтобы потом сопоставлять оптимистичное состояние с результатом работы сервера. Так вот, я решил, что неплохим способом формировать такое число будет текущая дата. При этом укладываться наш уникальный ID должен в int32, а значит текущую дату нужно ужать до этих размеров. Абсолютно логичным будет взять ее остаток от деления на желаемый диапазон и дело с концом. Не буду вдаваться в детали, но я почему-то решил перемудрить с математикой и брать по модулю определенного временного промежутка, который оказался больше половинки int32 (мы берем только отрицательный диапазон). 

В результате в какой-то момент отправка сообщений в fast-чатах просто перестала работать из-за переполнения int32. А это обновление уже было в «проде», хоть и на небольшую часть аудитории. К счастью, это было в момент минимальной нагрузки, и мы быстро исправили проблему.

О команде

Команда мессенджера ВКонтакте — лучшая, в которой я когда-либо работал. Как по профессионализму, так и по отношению к делу. Когда команда была маленькая, у нас даже не было никаких планирований, никто никому не назначал задачи: просто у всех была огромная инициативность. Закончив какую-нибудь задачу, каждый самостоятельно сразу брал себе новую: сделать фичу, исправить ошибки и т. д. И не просто старались закрыть тикет, а искренне думали — и думают! — о том, как сделать лучше для пользователя. Наверное, такие встречаются везде, но у нас — каждый. И когда тебя окружают такие товарищи, это дополнительно мотивирует, придаёт энергии. Для меня это невероятный опыт.

© Habrahabr.ru