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

Ниже подробности о том, что было до написания кастомных чатов и какие стояли требования к реализации, из каких компонентов они состоят, как вписываются в нашу инфраструктуру и что получилось в итоге. А в конце статьи — ссылки про особенности разработки наших чатов на вебсокетах для iOS и Android.

Почему решили писать свои чаты

iFunny — приложение с лентой мемов и возможностью флуда в комментариях, но последней опции было мало по очевидным причинам. Например, людям хочется кидать мемы в личку и создавать тематические каналы. Чтобы не вынуждать их переходить в сторонние мессенджеры, мы решили сделать чаты внутри приложения. 

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

  • Скорость внедрения. Как заявляет производитель, с нуля до первого сообщения — считанные минуты, так как не нужно писать бэкенд, только UI. Правда, для внутренних фич нам всё равно пришлось написать много бэкенд-кода (в основном это связано с модерацией открытых чатов и синхронизацией пользователей, а также у нас есть рейтинг открытых чатов).

Минусы, куда же без них:

  • Высокая цена.

  • Слабый контроль за происходящим внутри (пользователи доставали токен из мобильного приложения и спамили через скрипты, делали ботов).

  • Невозможность внутри сделать фичи под потребности.

  • Нельзя провести А/B-тест.

  • Ограничение на 500 пользователей в чате.

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

Основным техническим требованием было сделать всё то же самое, что у Sendbird, только без минусов. А также по возможности не использовать технологии, которые ещё не интегрированы в стек.

Подготовка

Выбор транспорта

Были мысли и предложения использовать UDP. Он обеспечивает более высокую скорость работы по сравнению с TCP, потому что не имеет накладных расходов на установку и на разрыв соединения. Но у нас не миллионы пользователей онлайн одновременно, а опыта работы с UDP у меня и у клиентских разработчиков было не так много, поэтому такой манёвр мог стоить неопределённого количества времени.

Для транспорта прикладного уровня выбрал WebSocket. Он давно везде поддерживается, и можно в будущем сделать чат в веб-версии приложения без правок на стороне бэкенда. А так как для установления соединения WebSocket клиент и сервер используют протокол, похожий на HTTP, можем использовать тот же механизм авторизации, что и в HTTP API приложения — через заголовок Authorization.

Выбор протокола

На ум сразу пришёл XMPP. Это открытый, основанный на XML, свободный для использования протокол мгновенного обмена сообщениями. Основными преимуществами являются децентрализация, открытость стандарта и расширяемость. Из недостатков — невысокая эффективность передачи данных за счёт XML-формата. А наличие большого количества открытых клиентов может привести к оттоку пользователей из приложения.

Из открытых XMPP-серверов самым продвинутым показался ejabberd, написанный на Erlang. На такой подвиг я был не готов, кроме того, сам по себе протокол старый, и его пришлось бы расширять под свои нужды.

Также смотрел в сторону MQTT — простого сетевого протокола, построенного по модели pub-sub. Есть возможность пустить поверх WebSocket-соединения.

До последнего думал взять MQTT, но потом наткнулся на WAMP. Это открытый протокол, который поддерживает два шаблона обмена сообщениями: pub-sub и routed RPC. 

Выбор инструментов

В качестве языка был выбран Kotlin JVM, а базы данных для хранения сообщений — MongoDB. Просто потому, что это наш основной стек.

Реализация

Начал с реализации WAMP-протокола. Не найдя в открытом доступе достойных реализаций роутера на Java/Kotlin, написал свою. По сути, это движок и сердце сервера.

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

Для загрузки медиафайлов используется отдельный HTTP-эндпоинт, чтобы ничего не изобретать.

Перенос данных

В Sendbird есть механизм вебхуков. В настройках аккаунта приложения можно указать эндпоинт, и Sendbird будет слать туда информацию обо всех событиях, происходящих в приложении. Перед тем как приступить к реализации, мы сделали скрипт, который принимает такие события и складывает их в базу данных. А когда пришло время, проиграли эти события на базу данных наших чатов. Оставшиеся пробелы докачали через Platform API Sendbird по HTTP.

Отдельно можно отметить перенос медиа. Нужно было выкачать все картинки и видео, пережать их под наши стандарты и сохранить на свой S3. Процедура достаточно затратная по времени, поэтому после запуска чатов в продакшн ещё какое-то время медиа грузилось с CDN Sendbird, пока скрипты в фоне выкачивали его в наш сторадж.

Запуск

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

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

Архитектура бэкенда

Бэкенд состоит из двух сервисов — непосредственно сервис чатов, с которым клиент соединяется по вебсокет при переходе на вкладку «чаты» в приложении, и сервис хуков, у которого две роли:

  1. Принимает вебхуки от чатов и делает дополнительную логику:

    • генерирует пуши;

    • обновляет поисковый индекс открытых чатов;

    • поддерживает коллекции, необходимые для модерации;

    • удаляет старые каверы с S3;

    • считает рейтинг открытых чатов для ранжирования в приложении.

  2. Служит HTTP-proxy для внутренних RPC-вызовов по WAMP-протоколу. Этим пользуется php-монолит для:

    • получения счётчика непрочитанных сообщений и инвайтов для шторки меню в приложении;

    • получения информации о чатах и сообщениях в админке;

    • асинхронной загрузки медиа в чаты.

Вместо заключения

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

Там подробнее про поддержку старых версий операционных систем, способы декодирования (которые можно применить и в других задачах) и особенности работы с WAMP.

Комментарии (17)


  1. Suvitruf
    02.11.2021 19:57
    +1

    Из открытых XMPP-серверов самым продвинутым показался ejabberd, написанный на Erlang. На такой подвиг я был не готов, кроме того.
    Ещё есть MongooseIM. А ещё у самих Process One можно использовать ejabberd как saas.
    Сам по себе протокол старый, и его пришлось бы расширять под свои нужды.
    А чего такого нету в xmpp, чтобы пришлось его расширять?

    Про бекенд, всё же, хотелось бы поподробнее послушать. Какие нагрузки? Где узкое звено? У ejabberd были в своё время проблемы в синхронизации чатов между разными инстансами в кластере. У вас такой проблемы нету? Или один толстый инстанс запущен?


    1. apapacy
      03.11.2021 08:32
      +1

      Я сам очень долго не мог понять что делает WAMP и его архитектоуру. WAMP изначально распределения система. Запросы не выполняются на сервере WAMP он просто получает запрос от зарегистрированного слушателя и передаёт на обработку зарегистрированногму воркеру. То есть изначально все направлено на горизонтальное масштабирование. Конечно использовать для чата мне кажется не рациональным так как там вступают другие факторы. Это дополучение и доподтверждение запросов которые прошли во время разъединения и пересоединение слушателя или воркера. Как результат прекрасная работа в тестовый период и пропущенные сообщения и дубли сообщений в реальном приложении. Для таких случаев лучше использовать средства с гарантированной доставкой сообщений ровно один раз. Например mqtt но и другие брокеры подойдут. Например rabbitmq тот же.


      1. movl
        03.11.2021 16:31
        +1

        Как вариант еще можно посмотреть на engine.io, он тоже нацелен на надежность доставки и масштабирование. Возможно там несколько больше оверхеда по трафику, но зато очень простой в использовании.


      1. movl
        03.11.2021 19:42
        +1

        upd. Вспомнил, что доводилось использовать engine.io в большей степени для передачи непрерывного потока сообщений. И было важно постоянное подключение, нежели гарантия доставки каждого отдельного сообщения. Бэк получился достаточно простым: nginx распределял подключения с привязкой айпишников по нодам, а между нодами сообщения прокидывались тупо через редис. Все это уже и ранее использовалось в проекте, а мне тогда хотелось хотя бы примерно понять, что будет происходить с нагрузкой после добавления веб-сокетов. В итоге все оказалось в норме и я не стал ничего переделывать. Во многом наверное поэтому engine.io запомнился беспроблемным и приятным в использовании. Сейчас немного почитал про WAMP (спасибо за наводку), и подумал что в случае сложной маршрутизации и при непостоянстве подключения, engine.io похоже так себе решение.


        1. apapacy
          06.11.2021 11:56

          Да. Engine.io это собственно тот же socket.io тол коты качестве транспорта использует Легаси протоколы вместо вебсокеттв. Любые брокеры с гарантированной доставкой сообщений хорошая основа для таких приложений. Ещё из плюс что могу поддерживать миллионы подключенных устройств если они работают на erlang


    1. antoha-gs Автор
      08.11.2021 13:34
      +3

      А чего такого нету в xmpp, чтобы пришлось его расширять?

      Хотелось иметь RPC в протоколе.

      Про бекенд, всё же, хотелось бы поподробнее послушать.

      На бэкенде крутится wamp-роутер. Бизнес-логика чата является wamp-клиентом по отношению к роутеру и крутится с ним в одном процессе. При таком подходе их легко разнести на разные сервера в будущем при разрастании бизнес логики и изменении профиля нагрузки. Сообщения и чаты хранятся в репликасете монги. Для операций типа "создать новый чат", "получить список сообщений с курсорной навигацией" используется RPC подход. Для пользовательских сообщений и различных событий типа "пользователь появился в сети", "пользователь покинул чат" используется PubSub. Механизм с realm используется для разделения на проекты.

      У ejabberd были в своё время проблемы в синхронизации чатов между разными инстансами в кластере. У вас такой проблемы нету? Или один толстый инстанс запущен?

      Текущая инсталляция состоит из трех небольших инстансов (4 ядра / 8 ГБ). Все wamp-сообщения с кодом EVENT сервер повторяет всем членам кластера. Это позволяет общаться в одном чате пользователям, подключенным к разным серверам. При разрастании кластера до размеров, когда fan-out может стать проблемой, есть возможность шарить списки подписчиков и слать события адресно.

      Какие нагрузки?

      Сейчас нагрузка небольшая - до 1000 пользователей в онлайне в пике. С такой нагрузкой справляется и один инстанс. Три запущены, как говорится, на всякий случай.

      Где узкое звено?

      При текущей нагрузке проблем пока не выявлено.


  1. xakep666
    02.11.2021 21:41
    +2

    Как решали проблему резкого скачка количества запросов за состоянием чата при массовых обрывах вебсокетов (деплой, перезагрузка балансировщика, etc.) и последующих переподключениях?


    1. apapacy
      03.11.2021 08:19
      +1

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


    1. antoha-gs Автор
      08.11.2021 13:36
      +1

      Инстансов запущено с запасом, деплоятся они по очереди. Т.е. берем одну машину, останавливаем старую версию приложения, поднимаем новую, и так для всех. При таком подходе одновременно переподключается только 1/N (N - кол-во инстансов) пользователей. Но в любом случае, с нашей нагрузкоймы легко перевариваем одновременный обрыв всех коннектов.


  1. apapacy
    02.11.2021 23:08
    +1

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


  1. mmmisha
    03.11.2021 17:47
    +3

    Свой самописный чат - прекрасное решение!
    Мне во множестве различных проектов приходилось делать чат. Первый раз, когда возникла такая задача, решили заюзать Spika . Сейчас, может быть, этот проект мертв, но тогда был одним из первых, в результатах поиска. Намучились - не то слово, и все равно лагало.
    В следующий раз решили использовать Quickblox. Результат получился не лучше.
    Много времени тратилось на то, чтобы добавить это в проект, потом тяжело было это поддерживать, и работало, мягко говоря, так себе (переписка с их сапортом велась постоянно).
    А потом нам с бекендщиком пришла здравая мысль: а что если сделать свой, очень простой движок для чата на сокетах? Потрачено было не так много времени (меньше месяца, точно) и мы получили простой и надежный, как автомат Калашникова, чат. Добавляется в проект за пару часов, и работает без проблем. Это решение мы потом использовали в десятках следующих проектов и по сей день используем.
    То же касается и звонков. Около года назад Twilio очень сильно подняли цены на свои сервисы, и мы благополучно перешли на голое и бесплатное WebRTC, и потом подумали, почему мы не сделали так сразу).


    1. vandy
      03.11.2021 22:17
      +1

      Не думали выложить свое решение в открытый доступ?


      1. mmmisha
        05.11.2021 19:18

        Я бы с радостью, но я не фрилансер, как и мой коллега бекендщик и iOSник, с которыми мы это разрабатывали. Это решение - это собственность компании, на сколько я понимаю. Ну и во вторых, по работе обычно загрузка такая, что на свой опесорсный проект банально не остается времени.


  1. ZaMaZaN4iK
    03.11.2021 19:00
    +1

    Рассматривался ли Matrix в качестве протокола для чата?


    1. antoha-gs Автор
      08.11.2021 13:36

      Нет, не рассматривал. Спасибо за наводку, погуглю.


  1. Calc
    03.11.2021 23:27
    +2

    берем спецификацию xmpp, переводим нужное в json, запускаем rabbitMQ с MQTT и гоняем по воркерам пакеты. Довольно безотказная система с кластеризацией и простейшим расширением. В добавок можно STOMP плагин запихнуть и web-mqtt.


    1. antoha-gs Автор
      08.11.2021 13:43

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