company_banner

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

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



    Напомню, что помимо обычных возможностей, присущих многим другим 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 подписках можно почитать в разделе документации.


    Бенчмарк



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


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


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


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


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


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


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



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


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


    Заключение


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


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

    Авито
    У нас живут ваши объявления

    Комментарии 12

      0
      Давай уже делать Ётту!
        0
        Знакомьтесь, друзья — это Алексей, мой коллега — мечтает перенести игру Ётта (https://www.igroved.ru/games/iota/) в браузер и приделать к ней мультиплеер. Показалось необходимым пояснить:)
          +1

          А мы уже в 2016 году сделали*. Если интересно:



          *Конечно завершённой реализацией это не назовёшь, но играть можно.

          +4
          Мне кажется это знак, только ознакомился предыдущей статьёй и тут уже, статья о новых фишках, недавний релиз, причём в интересный день — 8 марта. Буду знакомиться дальше.
          Меседжер в авито, помню что действительно был не очень, мягко говоря, сейчас им очень непринуждённо пользоваться, супер.
          Спасибо за стоящий проект.
            0
            Александр, сколько инстансов центрифуги было запущено на тесте? А то не понятно по сколько коннектов и сообщений/сек на инстанс.
            Есть ли график для gc_duration? gc в последние годы хорош, но все же.
              +1
              Добрый день, более подробное описание по ссылке в документации – centrifugal.github.io/centrifugo/misc/benchmark, там это написано — было 20 подов сервера в Kubernetes, каждый примерно 2 ядра CPU потреблял при максимальной нагрузке. К сожалению, графика gc_duration не осталось – возможность посмотреть была, эти метрики экспортируются, но теперь уже нет возможности поднять эти циферки. Можно только сказать, что STW паузы не превышали latency доставки сообщений в указанном 99 персентиле:) Но вопрос хороший — жалею, что этой информации не осталось.
                0

                Т.е. это где-то по 10к fan-out сообщений на ядро?

                  +1
                  Если 500k разделить на 40 ядер, то да. Но CPU не только на fan-out тратился в этом случае, плюс большинство сообщений тут были индивидуальными, то есть проходили полный цикл всевозможных сериализаций и десериализаций на пути от продьюсера до консьюмера (чтение из ws, десериализация, понимание, что это публикация, сериализация для Редис, потом десериализация из PUB/SUB, далее сериализация консьюмера (1 раз для каждой годы, на которую долетело из PUB/SUB). Не знаю, ответил ли на вопрос?
                    0

                    Да, более чем :)


                    А то мне как-то привычнее измерять всё в попугаях на ядро, а не на систему в целом.

                      +2
                      Тогда, наверное, можно позапускать бенчмарки, которые в коде разбросаны — они плюс-минус отдельные части покрывают. А тут, получается, я взял определенный сценарий, поднял в реальных условиях — и посмотрел на поведение всей системы в целом причем в распределенном сценарии с брокером. Очень искусственно это все, но как я написал в статье – хоть какие-то цифры и их порядки. Fan-out-а можно добиться больше на порядок точно, вот когда-то делал не распределенный бенчмарк – и там видно, что это уже до 100k на ядро — но опять же это не чистый fan-out, там тоже было много другой работы (но не было Redis-а). Но в целом под капотом у Centrifugo Gorilla Websocket – ничего сверх того, на что способна эта библиотека вкупе со стандартной библиотекой Go нет. На разных этапах есть определенные оптимизации (Gogoprotobuf, где-то меньше write сисколлов на запись при большом рейте сообщений в соединение, pipelining + smart batching при работе с Redis, вот есть статья про некоторые оптимизации внутри).

                      Но в целом Centrifugo это скорее про удобство и кроссплатформенность, чем про низкоуровневые оптимизации производительности. Хотя благодаря Go и плюс-минус адекватному его использованию performance на достаточно высоком уровне.

                      Например, Centrifugo сейчас использует под капотом библиотеку Centrifuge (хотя скорее это фреймворком правильней назвать, так как API у нее очень большой плюс диктуется протокол и многие другие вещи) для Go — для этой библиотеки я делал POC, где для WebSocket используется библиотека gobwas/ws и epoll — вот пример в репозитории — github.com/centrifugal/centrifuge/tree/master/_examples/custom_ws_gobwas — там немного других цифр можно ожидать (в основном по потреблению памяти), но как бы ни хорош был выигрыш я по-прежнему продолжаю использовать Gorilla WebSocket в сервере, так как лучше понимаю как в случае чего чинить (да оно и не ломается). Есть еще новомодная либа для WebSocket, которая по возможностям сейчас уже равна Gorilla WebSocket (кроме PreparedMessage вроде все остальное автор уже добавил) — github.com/nhooyr/websocket — для нее тоже есть пример в репозитории библиотеки github.com/centrifugal/centrifuge/tree/master/_examples/custom_ws_nhooyr. Но чет тоже пока не горю желанием менять.
              0
              Давно присматривался к Вашей работе, как замене для github.com/tlaverdure/laravel-echo-server, тогда как раз не хватало нативных для Laravel функций авторизации каналов, поэтому написал свой вариан echo-server на Go.
                0
                В клиентских библиотеках нет ни одной для PHP. Но я знаю, что их существует несколько, в том числе заточенных под конкретные фрейворки. Они на столько плохие, что ни одну из них нельзя включить в этот список или просто ни у кого не дошли руки протестировать их?

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

                Самое читаемое