Как мы перестроили комментарии в ОК: от линейного хаоса к веточной гармонии

Комментарии в соцсетях — это как чипсы: начал читать, остановиться невозможно. Но в ОК до 2024 года они были плоскими — вместо структурированного диалога под постом пользователи видели бесконечную ленту сообщений, где ответы терялись в хронологическом порядке. Представьте: под постом про котиков кто-то спросил про корм, но ответ на вопрос появился в самом низу, через 500 комментариев с обсуждением хвостатых. Найти ответ на вопрос в такой системе, если он вообще существует, — тот ещё квест.
Меня зовут Александр Косницкий. Я разработчик в компании ОК. В этой статье я расскажу, как мы переходили с линейной структуры отображения комментариев к древовидной: с чего начали, с чем сталкивались и что получили в результате.
Немного контекста
Комментирование — важная функция нашей социальной сети, которая пользуется высоким спросом среди пользователей. Так, в ОК ежедневно пишут от 7 до 9 млн комментариев.

Но до 2024 года в ОК была линейная структура отображения комментариев, при которой все ответы идут друг за другом. Она имела некоторые изъяны:
ответы «терялись» в общем потоке;
было сложно вести параллельные дискуссии;
пользователи жаловались на неудобство навигации.
Это влияло на метрики активности (пользователи реже вовлекались в обсуждения) и на удобство администрирования (модерировать сообщения в таком потоке было сложно). Такая модель отображения комментариев нуждалась в модернизации или даже полной замене.
Исходные требования и варианты выбора
ОК — зрелый продукт с месячной аудиторией в 36 млн пользователей, поэтому любые ошибки во время релиза обновлений в проде просто недопустимы, иначе можно столкнуться с негативом и снижением лояльности пользователей.
Нам было важно перевести комментарии на новую структуру, сохранив обратную совместимость, чтобы:
осталась доступной исходная линейная структура;
старые клиенты (включая iOS/Android-приложения устаревших версий) могли работать как прежде;
новую фичу можно было включать для пользователей постепенно (канареечные релизы через PMS);
была возможность быстрого и мягкого отката, если метрики упадут.
После анализа вариантов, в качестве наиболее очевидной альтернативы линейной структуре отображения комментариев мы выбрали древовидную структуру: ветки позволяют группировать ответы, сохранять контекст и делать обсуждения структурированными.
Но подходов к реализации древовидной структуры комментариев тоже несколько.
Полно-древовидные структуры (например, Reddit, Хабр) — бесконечная вложенность. Главный недостаток подхода в том, что через 5 уровней комментарии уезжают вправо и читать их невозможно.
Плоско-древовидные (Например, YouTube, ВК) — ограничение вложенности. Ответ на ответ не создает новую ветку, а лишь добавляется в текущую линейно.

После сравнения вариантов, для ОК мы выбрали плоско-древовидную модель. Такое решение обусловлено несколькими факторами:
плоско-древовидная структура более привычна пользователям ОК, поэтому переход к новой структуре будет более плавным и безболезненным;
такую структуру легче масштабировать в будущем: сначала внедряем базовое дерево, потом можно добавить уровни;
для внедрения достаточно минимальных изменений в UI.
Стек «под капотом» ОК
ОК — зрелый legacy-проект, у которого «под капотом» не менее зрелый стек.
Язык — Java 17. 80% кодовой базы написано на Java ещё с 2006 года.
БД — модифицированная Cassandra. Добавили механизм двухфазного коммита для операций и off-heap кеш, чтобы уменьшить GC-паузы за счёт хранения горячих данных вне кучи. Cassandra обеспечивает крайне высокую скорость записи (благодаря своей LSM-tree структуре) и чтения (благодаря кешу). При этом все комментарии дублируются в три ЦОДа, что обеспечивает высокую доступность данных.
Архитектура — микросервисы и оркестратор. Централизованное управление запросами. Благодаря такой конфигурации, даже если падает условный сервис лайков, комментарии продолжают работать.
Фронт — веб и мобильные клиенты с поддержкой старых версий.
Конфигурация — PMS. Представляет из себя распределённое хранилище в формате «ключ-значение». С его помощью можно включать или выключать целые функциональные блоки внутри микросервисов прямо во время их работы.
Любые изменения нам надо было интегрировать с учётом имеющего стека технологий — здесь нельзя переписать всё с нуля. Нужно решение, которое:
совместимо с 2,3 млрд существующих дискуссий;
работает на Cassandra — базе данных, которая не поддерживает JOIN-запросы;
не требует обновления приложений для iPhone 5 и Android 4.4.
Варианты реализации
Мы рассматривали несколько вариантов перехода к древовидной структуре комментариев.
Миграция схемы базы данных. Изначально самой простой идеей показалась модификация схемы нашей базы данных таким образом, чтобы она могла полноценно поддерживать новый способ отображения дискуссий напрямую. Cassandra действительно могла представить данные в новом виде без особых проблем. Однако в таком случае нам необходимо было бы переносить петабайты данных из старого колоночного семейства в новое, либо отказаться от отображения старых дискуссий в веточном виде, сохранив их в старом семействе, в то время как новые дискуссии создавались и читались бы из нового. А если вспомнить, что мы хотели ещё и сохранить возможность отображения новых дискуссий на старых клиентах в линейном формате, то всё становится вообще плачевным. Поэтому от этой идеи мы отказались
Древовидный индекс. Решение оказалось на поверхности: оставить основную БД нетронутой, а поверх неё натянуть «дерево» через отдельный индекс. Это как добавить оглавление к книге, не переписывая страницы.
В таком случае:
новые комментарии пишутся в два места: основную таблицу (для линейного отображения) и индекс (для веток);
старые данные индексируются фоновым воркером без остановки сервиса;
новые клиенты сами решают, какой формат использовать — дерево или список, (например, в зависимости от настроек PMS).
Более того, так как старую БД мы не трогаем, у нас сохраняется поддержка линейного отображения, а значит нет проблем с поэтапным включением новой структуры отображения комментариев и старыми клиентами.
Реализация
Следующим шагом мы приступили к программной реализации. Комментарии в ОК хранятся в колоночном семействе Cassandra. Поэтому нам нужно было создать аналогичное семейство для древовидного индекса.
Поскольку мы решили, что структура данных не будет меняться, а любые изменения мы будем накатывать поверх основной БД, все «манипуляции» так или иначе затрагивали Cassandra.
Чтобы максимально нативно и безопасно перейти от линейной структуры отображения комментариев к древовидной (с возможностью сохранения двух форматов), мы реализовали несколько дополнительных модулей и сервисов.
ok-comments-storage
Первый из них — новый модуль ok-comments-storage. Это толстый клиент для Cassandra, который:
перехватывает запросы к БД;
ищет данные в индексе;
для старых клиентов отдаёт линейные комментарии как раньше;
для новых клиентов идёт в TreeIndex, понимает, какие именно комментарии нужно взять из линейного представления и как их расположить, после чего возвращает веточное представление.
TreeIndex
В отличие от линейной структуры комментариев, когда мы сортируем комментарии в дискуссии по дате их написания, в древовидной структуре сначала отображается базовый комментарий, а потом все ответы на него.
Чтобы реализовать такое представление данных в линейном ряде Cassandra, где никаких веток нет, мы создали новое колоночное семейство, данные в рядах которого будут сортироваться особым образом: в «основном» семействе, которое позволяет получать нам комментарии линейно, партиционным ключом является ID дискуссии, а кластерными ключами — ID комментариев. В ID комментариев зашита временная метка, поэтому они лежат внутри ряда упорядоченно по времени написания.

В новом же колоночном семействе (индексе), партиционным ключом все еще являются ID дискуссии, благодаря этому мы гарантируем, что основное колоночной семейство дискуссии N и индекс этой конкретной дискуссии будут лежать физически на одной ноде. Это для нас очень важно, поскольку в таком случае нам не придётся ходить куда-либо по сети, чтобы достать все нужные данные. При этом кластерным ключом внутри ряда выступает особая структура, которая представляет из себя либо просто ID комментария (если он базовый), либо (для ответа) ID комментария, совмещённый с базовым комментарием, на который был ответ.
Такая структура ключа позволяет нам сортировать их так, чтобы под комментарием-родителем находились все его ответы. Причём сами родители и их ответы между собой сортируются всё так же по дате написания.
BackgroundCFWorker
Чтобы сделать индекс для миллиардов уже существующих комментариев, помимо прочего мы также создали отдельную библиотеку с абстрактным представлением фонового рабочего, который умеет асинхронно проходиться по колоночным семействам Cassandra (BackgroundCFWorker).
Также создали конкретную реализацию — TreeIndexBuilder, которая:
проходит по каждой дискуссии (ряду), лежащему на ноде;
для каждого комментария определяет корневого родителя (чтобы вложенность не превышала 1);
пишет данные в индекс.
Чтобы ускорить процесс, экземпляры рабочего были запущены параллельно на всех нодах (их больше 100).
Здесь важно, что комментарии анализируются в обратном порядке относительно того, как они лежат в линейном ряду — от старых к новым. Это позволяет нам просто разрешать ситуации цикличных ответов (ответ на ответ на ответ на ответ…). Поскольку первые комментарии в дискуссии по определению не могут быть ответами, как только мы встречаем первый комментарий-ответ, мы сразу понимаем, что ID комментария, на который он «отвечает» — коренной.
Однако теперь давайте представим ситуацию, что мы спустились по дискуссии ещё ниже, где на этот комментарий, который был ответом на коренной комментарий, написали свой собственный ответ. Так как структура у нас плоско-древовидная, мы должны этот комментарий «ответ на ответ» добавить в поддискуссию коренного комментария, а не создавать еще одну вложенную поддискуссию. Так как мы шли сверху вниз, мы к этому моменту уже знаем, что комментарий, на который отвечает этот комментарий, точно является ответом (мы разрешили эту ситуацию ранее), поэтому мы легко можем определить настоящий коренной комментарий и добавить «ответ на ответ» в уже созданную нужную линейную поддискуссию ответов. Если бы мы шли от новых комментариев к старым, такой информации у нас не было бы и ломать голову над этим вопросом нам пришлось бы гораздо сильнее.
Запуск древовидной системы
После завершения индексации мы окончательно добавили изменения в мастер и обновили до новой версии все модули, затронутые в рамках миграции на новую структуру.
Отдельно все серверы — узлы Cassandra, используемые модулем комментариев и работающие через толстый клиент one-list-storage, — последовательно переключили на использование модуля ok-comments-storage вместо one-list-storage.
Все изменения добавляли в пользовательское окружение в полностью отключённом виде, процесс обновления проходил бесшовно — без отключения старой функциональности ОК.
Как только успешно обновили, провели регрессионное тестирование и несколько дней обрабатывали обратную связь от пользователей — жалоб не поступало.
Следом мы подключили автоматическое индексирование всех новых дискуссий, то есть любая созданная дискуссия начинала автоматически заносить новые комментарии не только в БД, но и в индекс. Также на различных узлах Cassandra стали постепенно запускать TreeIndexBuilder.
Индексация старых дискуссий на узлах заняла примерно месяц.

Для наглядности объёма проделанной работы можно посмотреть график, который показывает, какой длины рабочие находили дочерние дискуссии за каждый день своей работы. Видно, что чаще всего находили комментарии, на который был дан лишь один комментарий-ответ: за один день их нашли больше 27 миллионов. Однако и комментариев, на которые ответили 4–12 раз, тоже много: таких находили около 4 миллионов за день. Есть и уникальные случаи: нашли комментарий, на который было дано более 100 тыс. ответов (побольше большинства обычных дискуссий будет). Это на практике показывает, что веточные дискуссии имеют хороший потенциал и люди пользовались функциональностью ответов, даже когда они были линейными.
После окончания индексации старых дискуссий индекс был полностью готов к работе и поддерживался в актуальном состоянии, то есть мы могли запускать функциональность на пользователей. При помощи PMS веточное отображение комментариев включили на 1/16 пользователей, а далее последовательно и на всю аудиторию ОК.
Полученные результаты
После сбора данных и аналитики мы убедились, что переход к древовидной структуре отображения комментариев дал хороший буст по метрикам. Так:
количество написанных комментариев-ответов выросло на 11%;
количество пользователей, оставляющих комментарии, увеличилось на 8%;
комментариев к постам стало больше на 12%;
количество комментариев к фото выросло на 8%;
на кнопку «Ответить» стали жать на 8% чаще.
Более того, судя по сообщениям от службы поддержки, большинство пользователей отнеслось к изменениям положительно.
Вместо выводов
Любые глобальные изменения в столь зрелом и высоконагруженном продукте как ОК — сложная задача. Причём как на уровне технической реализации, так и на уровне «знакомства» постоянных пользователей соцсети с нововведениями.
Но наш случай показал, что при должной подготовке и тщательной проработке всех аспектов (в том числе возможных «подводных камней») реализовать и выкатить такое обновление в прод можно довольно бесшовно и безболезненно — как минимум, без влияния на старые клиенты или потери петабайт данных.
Главное — с внедрением древовидной структуры отображения комментариев соцсеть стала еще удобнее для пользователей, а их активность выросла. То есть любые затраченные на смену структуры ресурсы, оправдали себя с лихвой.