Недавно я понял, что, хотя я лично понимаю, почему Centrifugo полезен, и считаю, что это круто, я не могу быстро объяснить его преимущества и причины, по которым кто-то может захотеть использовать его в производственном приложении. На самом деле - обслуживание постоянных соединений (Websocket, EventSource и т. Д.) С помощью Go или NodeJS в наши дни довольно просто и действительно глоток свежего воздуха, особенно после попытки сделать то же самое на Python, PHP, Ruby или подобных языках без встроенного параллелизма.

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

Centrifugo работает с любой серверной частью

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

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

В Mail.Ru, где я впервые интегрировал Centrifugo с приложением Django, мы разработали много интересных вещей, не выходя на скользкую дорожку - например, используя Gevent или Django Channels. Мы разработали чаты, лайв-счетчики, игры в реальном времени, ул. Панель управления ко Дню святого Валентина и многое другое. И все это без огромного рефакторинга или перехода с Django на асинхронный стек.

Совместная работа с Django означает возможность совместной работы с любыми существующими серверными технологиями. Вы можете продолжать использовать свой любимый фреймворк - будь то Django, Yii, Laravel, Rails - и по-прежнему добавлять события в реальном времени в свое приложение. Без взлома серверной части и изменения философии проекта. Процесс интеграции прост и понятен, как только вы поймете основные концепции сети и безопасности.

Тот факт, что Centrifugo работает как отдельный сервис и предоставляет API для связи с соединениями, означает, что он может быть универсальным элементом, который вы можете переносить из одного проекта в другой, даже если вы начинаете свой новый проект с новым языком.

Еще одна проблема, которая возникает здесь, - это аутентификация пользователя. Обычно вы аутентифицируете WebSocket или HTTP-соединение, используя какой-то механизм сеанса на вашем сервере. Centrifugo не знает о стратегии аутентификации вашего приложения, таким образом обеспечивая способ аутентификации соединения через JWT. JWT должен быть создан на сервере приложения и передан клиенту. Centrifugo также поддерживает перехватчик истечения срока действия JWT и автоматическое обновление.

Также существует постоянный запрос на вытягивание для проверки подлинности прокси для серверной части приложения через HTTP при каждой попытке подключения. В большинстве случаев это означает, что вам нужно запустить Centrifugo в том же домене, что и ваше основное приложение, чтобы cookie передавался в Centrifugo при подключении (по крайней мере, в браузере). Хотя проксирование аутентификации на серверную часть может соответствовать некоторым случаям использования, JWT по-прежнему имеет свои преимущества - особенно в рабочем процессе массового переподключения. С помощью соединений JWT можно повторно подключиться к Centrifugo без какого-либо участия вашего основного приложения, как только срок действия токена не истечет. В противном случае вам придется создать приложение, способное обслуживать огромное количество запросов, переподключаясь практически одновременно. Для многих развертываний в дикой природе это может стать настоящей катастрофой.

Масштабируемость

Еще один важный момент - масштабируемость. По мере роста вашего приложения все больше и больше пользователей будут устанавливать постоянные соединения с вашей конечной точкой в ​​реальном времени. Современный серверный компьютер может обрабатывать тысячи открытых подключений, но мощность одного процесса ограничена - в конечном итоге у вас закончится доступный ЦП или память. Так что в какой-то момент вам, возможно, придется масштабировать пользовательские соединения на нескольких машинах. Еще одна причина для масштабирования соединений на нескольких машинах - высокая доступность (когда один сервер выходит из строя).

На Github и платных онлайн-сервисах есть множество решений для обмена сообщениями в реальном времени. Но лишь немногие из них обеспечивают масштабируемость «из коробки» - большинство из них работают только в одном процессе. Я не хочу сказать, что Centrifugo - единственный сервер, который масштабируется. Есть еще много альтернатив, таких как Socket.IO, SocketCluster, Pushpin и множество других. Я считаю, что возможность масштабирования - одна из основных вещей, о которых вы должны думать при поиске решения в реальном времени или создании его с нуля. Вы не можете точно предсказать, насколько быстро ваше приложение исчерпает доступные ресурсы на одной машине - масштабируемость программного обеспечения не является преждевременной оптимизацией, и в большинстве случаев наличие масштабируемого решения из коробки просто даст вам больше возможностей для улучшения функциональности приложения.

Многие онлайн-сервисы тоже могут масштабироваться. Но посмотрите на цены - большинство из этих решений довольно дороги. В случае с pusher.com вы платите 500 долларов в месяц, но получаете максимум 10 тыс. Подключений и строго ограниченное количество ежемесячных сообщений, о которых вам следует заботиться. Это смешно. Конечно, Centrifugo размещается самостоятельно, и вы должны потратить мощности своего сервера, чтобы запустить его. Но я полагаю, что во многих случаях стоимость несопоставима.

Centrifugo хорошо масштабируется с Redis PUB / SUB, поддерживает согласованное сегментирование Redis на стороне приложения и интегрируется с Redis Sentinel для обеспечения высокой доступности. Мы обслужили до 500 тыс. Соединений с Centrifugo, имеющей 10 узлов узлов Centrifugo для соединений в Kubernetes и только один экземпляр Redis, который потреблял только 60% ядра одного процессора!

Также существует постоянный запрос на вытягивание, который добавляет возможность масштабирования PUB / SUB с сервером Nats в качестве брокера.

Хорошо структурированный протокол Protobuf

Centrifugo - проект не молодой. Разработка началась около 7 лет назад, а в конце 2018 года сервер подвергся масштабному рефакторингу с выпуском версии v2. С Centrifugo v2 у нас теперь есть протокол клиент-сервер, определенный как схема Protobuf.

И хотя Centrifugo по-прежнему может передавать данные JSON по сети, теперь он также поддерживает двоичные соединения Websocket с форматом сообщений Protobuf. Это означает, что данные, передаваемые между клиентом и сервером, можно компактно и очень эффективно сериализовать.

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

Полифилл Websocket для браузеров

Хотя сегодня Websocket в основном работает, бывают случаи, когда пользователи не могут установить соединение WebSocket (даже с TLS). Это может произойти из-за отсутствия поддержки браузера, корпоративных прокси или расширений браузера. В некоторых приложениях это приемлемо, но что, если у вас есть требование, чтобы каждый пользователь мог подключаться к вашему приложению отовсюду? В этом случае вам действительно нужен запасной вариант.

Centrifugo предоставляет альтернативный вариант по сравнению с SockJS - очень зрелой и популярной библиотекой полифилла, которая имеет несколько протоколов на основе HTTP, таких как Eventsource, XHR-streaming, XHR-polling и т. Д. С необработанными WebSocket и SockJS почти каждый пользователь сможет подключаться и получать реальные сообщения. время обновления.

У нас была история, когда соединение Websocket с нашим сервером было заблокировано популярным расширением браузера для блокировки рекламы. И никто из наших пользователей от этого не пострадал - они фактически перешли на XHR-стриминг. Другой рассказ о корпоративной интеллектуальной игре в реальном времени, которую я разработал, где каждый игрок (сотрудник компании) пришел со своим устройством - смартфоном, планшетом, ноутбуком - и игра просто работала везде. Это было большим подспорьем для организаторов игры - не нужно было тратить время на решение проблем с подключением.

Полезные примитивы для создания приложений в реальном времени

У приложений реального времени есть своя специфика. Чтобы абстрагироваться от коммуникации с отдельными подключениями, Centrifugo предоставляет механизм каналов. Каждое соединение может подписаться на один или несколько каналов, и связь между серверной частью и клиентом может осуществляться посредством публикации данных в канале. Так что на самом деле это просто механика PUB / SUB. На стороне сервера Centrifugo предоставляет HTTP и GRPC API для публикации данных в каналах.

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

Внутри канала разработчик имеет некоторые полезные функции, такие как кеш истории, информация о присутствии (информация об активных подписчиках канала), а также уведомления о присоединении и выходе, когда клиент подписывается на канал (или отписывается от него). Это очень важно, например, для создания игрового лобби.

Еще одна проблема, которая должна быть решена в некоторых сценариях, - это как восстановить сообщения, пропущенные клиентом во время повторного подключения (временная потеря интернет-соединения, перезапуск серверной части в реальном времени). Это может быть особенно полезно в случае перезапуска, чтобы предотвратить перегрузку вашей основной базы данных тысячами клиентских запросов, которые хотят восстановить состояние. Centrifugo решает эту проблему, предоставляя функцию восстановления. Каждый канал может дополнительно хранить поток публикаций, и клиент может восстанавливать пропущенные сообщения при повторном подключении, предоставляя порядковый номер последней просмотренной публикации в канале. Таким образом, эффективное восстановление своего состояния точно так же, как и соединение, не было потеряно вообще. Centrifugo также решает проблему, заключающуюся в том, что вам необходимо передать начальный порядковый номер клиенту при первой подписке на канал (это сложно даже в случае EventSource, где last-event-id механизм встроен в протокол, но начальный event-id должен быть каким-то образом передан клиенту).

Лучше всего то, что Centrifugo использует высокоэффективный механизм брокера PUB / SUB (то есть Redis на данный момент) и объединяет его с хранением потока публикаций в хранилище истории (также Redis на данный момент). В момент восстановления сообщения Centrifugo синхронизирует PUB / SUB и извлечение пропущенных публикаций из хранилища, и в результате клиент получает публикации в правильном порядке. Алгоритм этого описан в одном из моих предыдущих постов. Таким образом, вместо гарантированной доставки вы получите хотя бы раз гарантию с включенной функцией восстановления истории.

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

Правильное постоянное управление подключением

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

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

Представление

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

Согласно проведенному мной бенчмарку Centrifugo может транслировать до 700 тысяч сообщений в секунду по протоколу Protobuf и до 270 тысяч сообщений по протоколу JSON. Цифры довольно искусственны, как и в любом тесте, и сильно зависят от многих факторов, но я думаю, что порядок довольно хороший. Я считаю, что с точки зрения производительности он должен подходить для многих приложений.

Centrifugo имеет высокооптимизированную связь с Redis - при ограниченном количестве подключений, используя конвейерную обработку и интеллектуальные методы пакетной обработки, чтобы уменьшить объем сетевого RTT.

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

Вы можете найти больше об оптимизации в одном из моих недавних сообщений по теме - https://medium.com/@fzambia/building-real-time-messaging-server-in-go-5661c0a45248

Готов к развертыванию

Для упрощения процесса развертывания существуют пакеты Docker image и RPM и DEB для популярных дистрибутивов Linux. Фактически, из-за того, что Centrifugo, написанный на Go, разработчик имеет возможность просто загрузить готовый двоичный файл для целевой операционной системы и запустить его так, как он хочет. Не требуется установка дополнительной среды выполнения - только один двоичный файл, который статически связывает все необходимое.

Centrifugo поставляется с метриками в формате Prometheus и, при желании, может экспортировать метрики в Graphite. Также метрики можно извлекать через API или просто наблюдать в веб-интерфейсе администратора (интерфейс администратора также встроен в двоичный файл сервера). Это означает, что разработчик может отслеживать состояние работающих узлов Centrifugo.

Клиентские библиотеки для популярных прикладных сред

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

Создан на основе библиотеки для языка Go

И еще один интересный момент: Centrifugo построен на базе библиотеки Centrifuge для языка Go. Это означает, что разработчики Go могут дополнительно настроить сервер обмена сообщениями в реальном времени и предоставить настраиваемую аутентификацию, правила авторизации каналов, настраиваемые брокеры PUB / SUB (вместо Redis), настраиваемые хранилища сведений о присутствии и истории. И хотя сервер Centrifugo в основном предназначен для потоковой передачи сообщений в одном направлении - от сервера к клиенту, - библиотека позволяет обмениваться сообщениями полностью двунаправленным способом с полным контролем над каждым клиентским подключением.

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

Заключение

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

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