Путь одной фичи. Как отправлять миллион сообщений с рекомендациями каждый день и делать пользователей счастливее

Legostin
Kolesa Group
Published in
9 min readFeb 23, 2022

Backend-разработчик Kolesa Group Вячеслав Легостин расскажет про функционал уведомлений о сохраненных поисках в Kolesa.kz — сайте автомобильных объявлений №1 в Казахстане.

Затронет тему обратного поиска и архитектуры системы. О том с какими сложностями столкнулась команда во время разработки и после. Вячеслав Легостин расскажет о том, каких результатов достигли и как эта фича повлияла на пользователей продукта.

Вячеслав Легостин

В этой статье вы узнаете:

Какую проблему мы решали
Разработка
Релиз. Что дальше?
Результаты

Какую проблему мы решали

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

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

Что мы решили доработать?

1. Push-уведомление
Как только в системе опубликуется подходящее объявление, пользователю придет push-уведомление.

2. Новый чат-бот подписок на поиск
Перейдя по уведомлению, пользователь попадет в диалог с чат-ботом подписок на сохраненные поиски. Только релевантные объявления, которые он сможет сразу просматривать.

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

4. Переработка окна избранных поисков

Мы собираемся переработать окно избранных поисков. Из него можно будет включать и отключать подписки, изменять их частоту, удалять, переименовывать для удобства, а также переходить на полную выдачу по каждому из поисков. Так пользователь будет узнавать об объявлении, как только оно встанет опубликованным в нашей системе. А для бизнеса же это должно дать рост метрик контактов, NPS (Net Promoter Score) и DAU (Daily Active Users).

Разработка

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

• Elasticsearch — используем во всех наших проектах для реализации поиска PHP, на котором написано наше основное API.
• Go — применяем для создания наших микросервисов.
• MySQL — основная база для хранения информации о пользователях и об их сохраненных поисках. А также отдельная база для хранения личных сообщений.
• RabbitMQ — стандартный инструмент для организации очередей.
• Redis, Memcached — используем для кэширования данных.
• Graphite, Grafana, Zabbix, Graylog — наши основные инструменты для мониторинга системы.

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

В первую очередь мы реализуем обратный поиск. О том, что это такое поговорим немного позже. Далее перейдем к доработке самого мобильного API, в самом конце займемся непосредственно отправкой сообщений. Мобильные же разработчики подключится на этапе доработки API, когда уже будут готовы основные методы.

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

Давайте разберем на примере. У нас есть объявление о продаже автомобиля Nissan Pathfinder 2010 года выпуска по цене 8 млн тенге. Есть 4 сохраненных нашими пользователями поиска.

1) Первый пользователь ищет автомобиль марки Nissan, модель Pathfinder, год выпуска не старше 2008, а цена — ниже 9 млн тенге. Наше объявление ему полностью соответствует, отметим его как подходящее.

2) Второй пользователь также ищет Nissan, конкретная модель его не интересует. Год выпуска также не старше 2008, но вот цена должна быть меньше 8 млн тенге — этот параметр нашему объявлению не соответствует, поэтому отметим его как неподходящее.

3) Третьему пользователю несущественна ни марка автомобиля, ни модель, ему лишь важен год выпуска и цена больше 8 млн — наш Nissan Pathfinder ему подходит.

4) Но последний пользователь ищет Toyota, поэтому при проверке первого же параметра мы можем отбросить данный сохраненный поиск.

Теперь давайте поговорим о том, какой инструмент мы будем использовать для реализации обратного поиска в нашем проекте -

Elasticsearch Percolator. Почему? Потому что:

- Мы любим и умеем готовить Elasticsearch Percolator, используем его давно.
- Он отлично показал себя в реализации подписок на поиски в «Крыше» — продукте Kolesa Group, онлайн-сервису №1 в категории «Недвижимость» в Казахстане.
- Полностью подходит под наши требования, прописанные в техническом задании.

А как это работает?

В первую очередь делаем запрос для создания индекса с типом Percolator:

Далее необходимо заполнить его нашими поисковыми запросами:

После чего мы сможем подать на вход данного индекса определенный документ:

На выходе получаем поисковые запросы, подходящие под данный документ:

С технической частью разобрались, давайте интегрируем это в наш проект. Что нам нужно для этого сделать?

1. Копируем структуру индекса объявлений.
2. Удаляем лишние поля из структуры.
3. Определяем число шардов и реплик.
4. Создаем новый индекс для перколяции.
5. Добавляем элиас на новый индекс.

С обратным поиском разобрались, поэтому перейдем к доработке мобильного API и сформулируем основные принципы работы с ним.

1. Совместимость со старыми клиентами
В системе есть пользователи, которые годами не обновляют свои приложения. У них не должно ничего сломаться.

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

3. Доставлять на прод всё по готовности
Это упростит нам тестирование и сопровождение.

4. Следить за временем ответа endpoints
Для этого будем использовать наши дашборды в Grafana.

5. Покрывать код функциональными и Unit-тестами
Причем делать это нужно в процессе разработки, а не в самом конце.

4 500 000

Именно столько сохраненных поисков было в базе на момент старта разработки. И нам ни в коем случае нельзя было их потерять.

Пишем поиски в Elasticsearch

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

С введением подписок на сохраненные поиски в нашей системе появилась новая сущность — так называемые рассылки. В них хранятся ID объявлений, подходящих под данный поиск и опубликованных в заданный промежуток времени. Мы пришли к такой структуре:

- Рассылки хранятся в MySQL
- Таблица должна быть минимального размера
- Нужно хранить превью объявлений для сообщений

Рассмотрим требования к обработке объявлений и рассылок.

1. Все действия должны происходить в фоновом режиме.
2. Работа уведомлений по сохраненным поискам
не должна мешать другим модулям.
3. Все метрики должны отслеживаться и собираться
в единый дашборд.
4. Заложить возможность масштабирования.

Обработка объявлений

После модерации объявления данные об этом событии попадают в очередь в RabbitMQ. Оттуда их разбирает ряд обработчиков. Они, в свою очередь, делают следующее.

1) Получают из хранилища полную информацию о недавно опубликованном объявлении по его ID.
2) На основе этих данных формируют запрос в наш индекс перколятора.
3) На выходе из перколятора получают список из ID подходящих под это объявление поисков.

В среднем, такой список состоит из 1 100 сохраненных поисков для одного объявления.

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

1) Обрабатываем только первые 3 объявления в рассылке.
2) Правильно проставляем индексы для полей, что участвуют в выборке для обновления.
3) Запросы на обновления группируем по несколько сотен и выполняем одним запросом.
4) Не забываем добавить обработку дедлоков, которых никак не избежать. Делаем самую простую повторную попытку выполнить запрос.

Отправка сообщений

Как устроены наши личные сообщения

- Группа микросервисов на Go.
- Единая платформа для всех приложений.
- Отправка сообщений через http-запросы (на момент старта разработки).
- Рендеринг сообщений, основанный на компонентах.
- База mySQL (на момент старта разработки).
- Поддержка сервисных ботов.

Теперь покажем, как нам достать данные из рассылок и отправить их пользователям.

И вот мы почти завершили работу, и у нас остались последние приготовления.

- Добавляем хуки для отправки сообщений о новых подписках, об отключении подписки
- Обработчик для отправки сообщений о продлении поиска
- Задаем минимальные версии приложений для отображения сообщений
- Создаем дашборд для отслеживания работоспособности системы.

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

Активность пользователей:

- Число создаваемых поисков
- Число удаляемых поисков
- Число продлеваемых поисков
- Число ошибок.

Метрики производительности:

- Число обработанных объявлений
- Скорость обработки объявления
- Число отправленных сообщений
- Состояние связанных очередей.

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

- Алерты не должны дублировать друг друга.
- Все ключевые параметры должны отслеживаться.
- Кратковременные аномалии должны игнорироваться.
- Доставка самым удобным способом (телеграм-чат с подключенным ботом).

Релиз. Что дальше?

И вот релиз совершился. Что было дальше? Спустя всего несколько дней после релиза время обработки объявлений устремилось ввысь, и не собиралось снижаться.

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

В итоге, когда мы его добавили, ситуация изменилась кардинально.

Мораль сей басни такова — всегда перепроверяйте запросы на использование индексов.

Мы сделали еще множество вещей:

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

В итоге мы получили такие американские горки на графике производительности обработки объявлений.

В результате мы пришли к решению, которое работает стабильно, и не требует внешнего вмешательства разработчиков/администраторов.

Параллельно со всем этим проводилась большая работа сервисом личных сообщений:

- Полностью перешли в проекте на базу Scylla.
- Добавили время жизни сообщений в ЛС.
- Подняли сервис в нескольких дата-центрах.
- Автовыбор дата-центра из клиентов.
- Полностью перешли на отправку сообщений через очереди для ботов.

Результаты

Отправляем в день более 1 млн сообщений с новыми объявлениями

Причем это число растет от недели к неделе на протяжении уже 6 месяцев.

+6% контактов за первый месяц

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

Полезные рекомендации:
1. Алихан Калиев, «Scylla в личных сообщениях kolesa.kz».
2. Мирас Лес, «О сложном поиске простыми словами».

--

--