Centrifugo — новости не в реальном времени

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

9hqzj02j29em-wygpharoz-kzcg.png

Напомню, что помимо обычных возможностей, присущих многим другим open-source решениям для real-time нотификаций, Центрифуга предоставляет некоторые приятные бонусы:


  • Возможность интеграции с любым бэкендом.
  • Поддержка Protobuf протокола, помимо JSON.
  • SockJS для случаев, когда WebSocket-транспорта недостаточно.
  • Масштабируемость на миллионы соединений с помощью шардированного Редиса.
  • Кроссплатформенность — работает в том числе на Windows.
  • Восстановление пропущенных сообщений при кратковременных разрывах соединения.
  • Presence-информация об активных пользователях в каналах.
  • Готова к production — используется в проектах известных компаний, например, Mail.Ru, Badoo, Spot.im, ManyChat.

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

В данный момент я работаю в Авито, и основное моё достижение за прошедший период в том, что подход, используемый внутри Centrifugo, был успешно внедрен на бэкенде мессенджера Авито. Это дало ощутимый прирост производительности по сравнению с предыдущей используемой схемой на основе федераций RabbitMQ. Время от времени у нас бывает до миллиона одновременных Websocket-соединений. Если кому интересно послушать про это подробнее — посмотрите мой доклад c Highload++ об архитектуре мессенджера Авито.


Данная статья в первую очередь рассчитана на пользователей Centrifugo, знакомых с сервером не понаслышке. Если вы первый раз читаете про проект, то лучше начать знакомство с документации или со странички на Гитхабе.

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


Проксирование вызовов до бэкенда

Пользователи, которые были знакомы с Центрифугой ранее, наверняка помнят, что основной парадигмой сервера всегда был однонаправленный поток данных со стороны сервера клиенту. Если произошло событие, то сначала оно должно быть доставлено до вашего бэкенда любым доступным способом (AJAX в вебе, например), затем после успешной валидации и сохранения в БД при необходимости опубликовано в канал Центрифуги через API. То есть Центрифуга вступает в дело на этапе движения real-time нотификации от бэкенда клиенту.

В основном этот механизм продиктован архитектурой Centrifugo как отдельно стоящего независимого сервера — это не библиотека а-ля Socket.IO, SocketCluster, Faye или Primus, где можно накрутить бизнес-логику при обработке входящих сообщений от клиентов по WebSocket, например. В случае с Centrifugo бизнес-логика отдается на откуп вашему бэкенду, и просто предоставляется возможность мгновенно уведомить заинтересованных пользователей.

Примерно то же самое относилось и к аутентификации. В случае перечисленных выше библиотек для real-time коммуникации вы можете выполнять аутентификацию пользователя непосредственно в процессе, обрабатывающем соединения — с помощью middleware механизмов, ну или как вам заблагорассудится. А вот Centrifugo, чтобы отвязаться от деталей конкретного бэкенда, всегда для аутентификации использовала HMAC token. Начиная с версии 2.0 — это всем знакомый JWT, где в качестве алгоритмов сейчас доступны HMAC (HS256, HS384, HS512) и RSA (RS256, RS384, RS512).

При этом протокол Centrifugo сам по себе использует bidirectional транспорты — WebSocket и SockJS, который эмулирует WebSocket. Клиент обменивается фреймами с сервером в двустороннем режиме — это и первый фрейм, содержащий JWT, и подписки на каналы.

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

Это подтолкнуло меня к расширению Centrifugo важной функциональностью — возможностью проксировать аутентификацию по HTTP на любой сервис бэкенда при подключении клиента (по WebSocket, или другому протоколу из доступных в SockJS). При этом копируются определённые заголовки оригинального запроса (например, Cookie, Origin, некоторые X-заголовки) так, что бэкенд приложения имеет возможность аутентифицировать соединение, используя стандартный для него механизм сессий, например, на основе сессионных кук. Для разработчиков это означает и то, что не нужно придумывать способ, как доставить JWT до клиента.

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

Собственно, можно в будущем эти два подхода объединить и получить лучшее от обоих — если при первом коннекте от пользователя возвращать на фронтенд JWT с определенным временем жизни. Этого сейчас в Centrifugo нет, но в скором времени может появиться.

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

Для клиента это выглядит следующим образом:

centrifuge.rpc(rpcRequest).then(function(data){
   console.log("RPC reply", data);
}, function(err) {
   console.log("RPC error", err);
});

А под капотом запрос долетает до Centrifugo, от неё прокируется на указанный endpoint по HTTP, ответ проксируется до клиента.

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


Server-side подписки

Второе важное нововведение — это server-side подписки на каналы. Что это такое?

Вообще, Centrifugo — это по большому счёту PUB/SUB сервер. И как в любом PUB/SUB сервере именно клиенты сервера определяют список каналов, на которые они хотят быть подписаны в конкретный момент.

Это означает, что клиенты должны делать вызов метода Subscribe на каждый канал.

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

Собственно, теперь список server-side каналов для подписки можно указать внутри JWT или вернуть Центрифуге в ответ на проксирование аутентификации. Соединение автоматически будет подписано на эти каналы и начнет получать сообщения из них.

Это в дополнение ко всему и хорошее упрощение для реализации клиентских библиотек — возможность не реализовывать объект Subscription с достаточно нетривиальным циклом жизни в клиентском коде дорогого стоит. Всё что должен уметь клиент в случае server-side подписок — соединиться с сервером и отправить один connect фрейм, а далее просто обрабатывать поток асинхронных событий. Как я не раз упоминал ранее и ещё вернусь к этому вопросу в статье — поддержка клиентов является наиболее сложной частью проекта, поэтому любая победа на этом фронте дорогого стоит.

В коде это выглядит следующим образом:

const centrifuge = new Centrifuge(address);

centrifuge.on('publish', function(ctx) {
    console.log('Publication from server-side channel', ctx.channel, ctx.data);
});

centrifuge.connect();

Заметьте отсутствие создания объекта подписки с помощью метода Subscribe, что было обязательным ранее. При этом появилась возможность на уровне конфигурации Centrifugo включить автоматическую подписку аутентифицированных пользователей на персональный канал. Это частая необходимость, и сейчас подписка на такой канал не требует никаких дополнительных вызовов и действий — соединение пользователя готово получать персональные события сразу после подключения.

Однако для более сложных кейсов, включающих в себя динамическую работу с подписками вариант с client-side подписками по-прежнему вне конкуренции. Просто теперь есть выбор.

Подробнее о server-side подписках можно почитать в разделе документации.


Бенчмарк

4o4n4nl6halyewy_fqv76mopz0e.gif

Мне наконец-то удалось сделать первый плюс-минус адекватный бенчмарк сервера на реальном железе. Количество железа было лимитировано, поэтому пришлось остановиться на определённом количестве соединений и размере нагрузки. В итоге бенчмарк ограничился миллионом WebSocket-соединений и ~500k доставленных (fan-out) сообщений в секунду при latency доставки сообщений 250 мс в 99 персентиле.

Эксперимент запускался внутри Kubernetes-кластера, были развернуты поды сервера и поды-клиенты, создающие нагрузку. Я попробовал поискать готовый инструмент для нагрузочного тестирования WebSocket, но ничего подходящего (и при этом бесплатного) для создания распределенной нагрузки не нашел. В итоге нагрузка создавалась собственными клиентами Centrifugo на Go. Код клиента доступен в виде gist.

С более подробным описанием и результатами бенчмарка вы можете ознакомиться в документации Centrifugo.

Любой бенчмарк — это искусственный эксперимент, зачастую не отражающий реальную ситуацию. Но этих цифр ждали многие пользователи Centrifugo. Cейчас они появились, и есть от чего отталкиваться.


Ситуация с клиентскими библиотеками

Тут всё не настолько плачевно, как было ранее. У Centrifugo всегда была стабильная и проверенная временем клиентская библиотека для JavaScript. Однако с библиотеками для других языков всегда были проблемы, на которые я не раз жаловался ранее.

Сейчас у нас есть следующие библиотеки:

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

Однако по-прежнему клиентские библиотеки — это основная сложность поддержки. Нужны опытные люди, способные помочь. К сожалению, в мире open-source должно сложиться слишком много обстоятельств, чтобы такие нашлись.


Заключение

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

Я по-прежнему считаю, что по совокупности возможностей аналогов просто нет — и Centrifugo может стать универсальным и одновременно достаточно легковесным компонентом в арсенале любого разработчика.

© Habrahabr.ru