Продолжаем рассказывать про чаты на вебсокетах, но уже со стороны бэкенда. Когда-то использовали сторонний сервис, но было важно решить ряд моментов, которые он не мог покрыть. Выбирать особо не пришлось, и мы принялись разрабатывать собственное решение.
Ниже подробности о том, что было до написания кастомных чатов и какие стояли требования к реализации, из каких компонентов они состоят, как вписываются в нашу инфраструктуру и что получилось в итоге. А в конце статьи — ссылки про особенности разработки наших чатов на вебсокетах для 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.
Архитектура бэкенда
Бэкенд состоит из двух сервисов — непосредственно сервис чатов, с которым клиент соединяется по вебсокет при переходе на вкладку «чаты» в приложении, и сервис хуков, у которого две роли:
-
Принимает вебхуки от чатов и делает дополнительную логику:
генерирует пуши;
обновляет поисковый индекс открытых чатов;
поддерживает коллекции, необходимые для модерации;
удаляет старые каверы с S3;
считает рейтинг открытых чатов для ранжирования в приложении.
-
Служит HTTP-proxy для внутренних RPC-вызовов по WAMP-протоколу. Этим пользуется php-монолит для:
получения счётчика непрочитанных сообщений и инвайтов для шторки меню в приложении;
получения информации о чатах и сообщениях в админке;
асинхронной загрузки медиа в чаты.
Вместо заключения
Это то, что касается бэкенда. Про клиентскую сторону, а именно особенности разработки чатов на вебсокетах для iOS и Android можно узнать из материалов коллег, опубликованных ранее.
Там подробнее про поддержку старых версий операционных систем, способы декодирования (которые можно применить и в других задачах) и особенности работы с WAMP.
Комментарии (17)
xakep666
02.11.2021 21:41+2Как решали проблему резкого скачка количества запросов за состоянием чата при массовых обрывах вебсокетов (деплой, перезагрузка балансировщика, etc.) и последующих переподключениях?
apapacy
03.11.2021 08:19+1Многие из этих вопросов WAMP решает на уровне протокола в частности пересоединение при разрыве сокета. Балансировка ему не ружен так как по сути сервер WAMP это брокер на котором регистрируются исполнители запросов возможно удаленные. Их может быть много и можно регистрировать дополнительные обработчики. То есть WAMP сервер это и есть балансировка. Пожалуй что там неудобно так это управление все этим хозяйством. Проверка доступных воркеров, мониторинг их работы и т.п. Если бы эти вопросы получили свое решение тотWAMP мог стать средой для выполнения микросервмсов но основанной не на идеологии оркестиации а на идеологии хореографии.
antoha-gs Автор
08.11.2021 13:36+1Инстансов запущено с запасом, деплоятся они по очереди. Т.е. берем одну машину, останавливаем старую версию приложения, поднимаем новую, и так для всех. При таком подходе одновременно переподключается только 1/N (N - кол-во инстансов) пользователей. Но в любом случае, с нашей нагрузкоймы легко перевариваем одновременный обрыв всех коннектов.
apapacy
02.11.2021 23:08+1WAMP прекрасен хотя и недостаточно популярен. Я бы хотел чтобы это протокол развивался так как он поддерживает масштабирование и потенц ально может стать средой для разворачивания микросервмсов. Но что касается при мнения в чатах в сравнении с mqtt он как мне кажется с льно проигрывает. На уровне логике то не может гарантировать доставку ровно один раз как это может делать mqtt., Также он менее производителем и сложнее коастеризуется если мы сравним с реализация и mqtt на erlang
mmmisha
03.11.2021 17:47+3Свой самописный чат - прекрасное решение!
Мне во множестве различных проектов приходилось делать чат. Первый раз, когда возникла такая задача, решили заюзать Spika . Сейчас, может быть, этот проект мертв, но тогда был одним из первых, в результатах поиска. Намучились - не то слово, и все равно лагало.
В следующий раз решили использовать Quickblox. Результат получился не лучше.
Много времени тратилось на то, чтобы добавить это в проект, потом тяжело было это поддерживать, и работало, мягко говоря, так себе (переписка с их сапортом велась постоянно).
А потом нам с бекендщиком пришла здравая мысль: а что если сделать свой, очень простой движок для чата на сокетах? Потрачено было не так много времени (меньше месяца, точно) и мы получили простой и надежный, как автомат Калашникова, чат. Добавляется в проект за пару часов, и работает без проблем. Это решение мы потом использовали в десятках следующих проектов и по сей день используем.
То же касается и звонков. Около года назад Twilio очень сильно подняли цены на свои сервисы, и мы благополучно перешли на голое и бесплатное WebRTC, и потом подумали, почему мы не сделали так сразу).vandy
03.11.2021 22:17+1Не думали выложить свое решение в открытый доступ?
mmmisha
05.11.2021 19:18Я бы с радостью, но я не фрилансер, как и мой коллега бекендщик и iOSник, с которыми мы это разрабатывали. Это решение - это собственность компании, на сколько я понимаю. Ну и во вторых, по работе обычно загрузка такая, что на свой опесорсный проект банально не остается времени.
Calc
03.11.2021 23:27+2берем спецификацию xmpp, переводим нужное в json, запускаем rabbitMQ с MQTT и гоняем по воркерам пакеты. Довольно безотказная система с кластеризацией и простейшим расширением. В добавок можно STOMP плагин запихнуть и web-mqtt.
antoha-gs Автор
08.11.2021 13:43Согласен, с RabbitMQ, пожалуй, самая популярная схема. Но у меня был ряд технических требований, по которым не желательно было использовать технологии вне стека. А RabbitMQ в нашем бэкенде отсутствует.
Suvitruf
А чего такого нету в xmpp, чтобы пришлось его расширять?
Про бекенд, всё же, хотелось бы поподробнее послушать. Какие нагрузки? Где узкое звено? У ejabberd были в своё время проблемы в синхронизации чатов между разными инстансами в кластере. У вас такой проблемы нету? Или один толстый инстанс запущен?
apapacy
Я сам очень долго не мог понять что делает WAMP и его архитектоуру. WAMP изначально распределения система. Запросы не выполняются на сервере WAMP он просто получает запрос от зарегистрированного слушателя и передаёт на обработку зарегистрированногму воркеру. То есть изначально все направлено на горизонтальное масштабирование. Конечно использовать для чата мне кажется не рациональным так как там вступают другие факторы. Это дополучение и доподтверждение запросов которые прошли во время разъединения и пересоединение слушателя или воркера. Как результат прекрасная работа в тестовый период и пропущенные сообщения и дубли сообщений в реальном приложении. Для таких случаев лучше использовать средства с гарантированной доставкой сообщений ровно один раз. Например mqtt но и другие брокеры подойдут. Например rabbitmq тот же.
movl
Как вариант еще можно посмотреть на engine.io, он тоже нацелен на надежность доставки и масштабирование. Возможно там несколько больше оверхеда по трафику, но зато очень простой в использовании.
movl
upd. Вспомнил, что доводилось использовать engine.io в большей степени для передачи непрерывного потока сообщений. И было важно постоянное подключение, нежели гарантия доставки каждого отдельного сообщения. Бэк получился достаточно простым: nginx распределял подключения с привязкой айпишников по нодам, а между нодами сообщения прокидывались тупо через редис. Все это уже и ранее использовалось в проекте, а мне тогда хотелось хотя бы примерно понять, что будет происходить с нагрузкой после добавления веб-сокетов. В итоге все оказалось в норме и я не стал ничего переделывать. Во многом наверное поэтому engine.io запомнился беспроблемным и приятным в использовании. Сейчас немного почитал про WAMP (спасибо за наводку), и подумал что в случае сложной маршрутизации и при непостоянстве подключения, engine.io похоже так себе решение.
apapacy
Да. Engine.io это собственно тот же socket.io тол коты качестве транспорта использует Легаси протоколы вместо вебсокеттв. Любые брокеры с гарантированной доставкой сообщений хорошая основа для таких приложений. Ещё из плюс что могу поддерживать миллионы подключенных устройств если они работают на erlang
antoha-gs Автор
Хотелось иметь RPC в протоколе.
На бэкенде крутится wamp-роутер. Бизнес-логика чата является wamp-клиентом по отношению к роутеру и крутится с ним в одном процессе. При таком подходе их легко разнести на разные сервера в будущем при разрастании бизнес логики и изменении профиля нагрузки. Сообщения и чаты хранятся в репликасете монги. Для операций типа "создать новый чат", "получить список сообщений с курсорной навигацией" используется RPC подход. Для пользовательских сообщений и различных событий типа "пользователь появился в сети", "пользователь покинул чат" используется PubSub. Механизм с realm используется для разделения на проекты.
Текущая инсталляция состоит из трех небольших инстансов (4 ядра / 8 ГБ). Все wamp-сообщения с кодом EVENT сервер повторяет всем членам кластера. Это позволяет общаться в одном чате пользователям, подключенным к разным серверам. При разрастании кластера до размеров, когда fan-out может стать проблемой, есть возможность шарить списки подписчиков и слать события адресно.
Сейчас нагрузка небольшая - до 1000 пользователей в онлайне в пике. С такой нагрузкой справляется и один инстанс. Три запущены, как говорится, на всякий случай.
При текущей нагрузке проблем пока не выявлено.