Случается, ты просыпаешься и осознаешь: так больше продолжаться не может и нужно что‑то менять. Разные кодовые базы, избыточное легаси и нестабильность мешают пользователям получать удовольствие от общения в твоем приложении. И эта мысль подводит тебя к развилке: один путь ведет к сложному и долгому рефакторингу легаси за почти 10 лет, второй к не менее долгому, а, порой, более сложному процессу переписывания с 0. Но какой бы путь ты ни выбрал, в любом случае начинаешь испытывать азарт — предстоит большая Задача (именно с большой буквы).
Привет Хабр, меня зовут Федор Неживой, я ведущий программист‑разработчик в команде VK Мессенджера и сегодня расскажу вам, как мы перестраивали и обновляли один из крупнейших проектов в рунете. В статье будет боль, пот, реальный код и детали, как мы шаг за шагом пришли к масштабному обновлению, а потом внедряли то, что получилось.
Что такое VK Мессенджер
VK Мессенджер по своей сути — сервис внутри еще большего сервиса. Он тесно связан со множеством других разделов ВКонтакте: Музыка, Фотографии, Видео, Лента — да практически со всеми. Для наглядности визуализируем.
При этом веб-мессенджер как платформа состоит из 4 продуктов:
веб-версия vk.com;
fast-чаты (всплывающие чаты);
мобильная версия m.vk.com;
десктопный мессенджер ВКонтакте.
Мало того, что все эти части имели раньше разные кодовые базы (даже мессенджер внутри vk.com), так и у них был разный приоритет. Самый высокий был у vk.com с его мобильными клиентами. Далее шел m.vk.com, для которого что‑то могли уже не делать ради экономии времени и трудозатрат. На третьем месте по важности была десктопная версия. Этот проект моей команде достался уже готовым. Его когда‑то написал на Electron один разработчик, который потом ушёл из компании. У команды не было знания кодобазы, а спросить было не у кого, поэтому что даже CI пришлось настраивать самим с 0 за пару месяцев. А fast‑чаты мы поддерживали по остаточному принципу.
С чем мы подошли к развилке
Как уже сказал, изначально VK Мессенджер состоял из 4 независимых кодовых баз. Все они были написаны давно, и с тех пор в них внесли много изменений. Технологии развивались, что‑то стали делать проще, что‑то — иначе. Например, во многих языках начали активно использовать типизацию. Если же код легаси, то не всегда можно найти автора и расспросить о его устройстве. Поэтому каждый раз опасаешься вносить изменения, потому что неизвестно, как это аукнется.
В те времена после каждого релиза мы всей командой с замиранием сердца следили за графиками. И при любой проблеме приходилось долго искать причину: «Что и где отвалится, если я удалю этот кусок кода и заменю на другой?». С учетом размеров и устройства кодовых баз даже инструменты в IDE не особо помогали справиться с задачей. Часто приходилось пользоваться обычным поиском (всякими креативными способами), чтобы понять заденешь что‑то или нет.
Примеры старого кода скринами, потому что такого больше в кодобазах нет
Кроме того, всё взаимодействие вэба с бэкэндом было построено на актах. Это такой устаревший формат взаимодействия клиент‑сервер, который использовался только на вэбе. Их нам приходилось писать и поддерживать самостоятельно, потому что команда бэкенда работала с 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. А это обновление уже было в «проде», хоть и на небольшую часть аудитории. К счастью, это было в момент минимальной нагрузки, и мы быстро исправили проблему.
О команде
Команда мессенджера ВКонтакте — лучшая, в которой я когда‑либо работал. Как по профессионализму, так и по отношению к делу. Когда команда была маленькая, у нас даже не было никаких планирований, никто никому не назначал задачи: просто у всех была огромная инициативность. Закончив какую‑нибудь задачу, каждый самостоятельно сразу брал себе новую: сделать фичу, исправить ошибки и т. д. И не просто старались закрыть тикет, а искренне думали — и думают! — о том, как сделать лучше для пользователя. Наверное, такие встречаются везде, но у нас — каждый. И когда тебя окружают такие товарищи, это дополнительно мотивирует, придаёт энергии. Для меня это невероятный опыт.