- PVSM.RU - https://www.pvsm.ru -

Centrifugo v2 — будущее сервера real-time сообщений и библиотека для Go

Возможно, некоторые из читателей уже слышали про Centrifugo [1] раньше. В данной статье речь пойдет о разработке второй версии сервера и новой real-time библиотеке для языка Go, лежащей в его основе.

Меня зовут Александр Емелин. Летом прошлого года я присоединился к команде Авито, где сейчас помогаю разрабатывать бэкенд мессенджера Авито. Новая работа, напрямую связанная с быстрой доставкой сообщений пользователям, и новые коллеги вдохновили меня продолжать работу над open-source проектом Centrifugo.

Centrifugo v2 — будущее сервера real-time сообщений и библиотека для Go - 1

В двух словах — это сервер, который берет на себя задачу держать постоянные соединения от пользователей вашего приложения. В качестве транспорта используется Websocket [2] или полифилл SockJS [3], умеющий, при невозможности установить Websocket-соединение, работать через Еventsource, XHR-streaming, long-polling и другие основанные на HTTP транспорты. Клиенты подписываются на каналы, в которые бекенд через API Центрифуги публикует новые сообщения по мере их возникновения – после чего сообщения доставляются подписанным на канал пользователям. Другими словами – это PUB/SUB сервер.

Centrifugo v2 — будущее сервера real-time сообщений и библиотека для Go - 2

На текущий момент сервер используется в достаточно большом количестве проектов. Среди них, например, некоторые проекты Mail.Ru (интранет, обучающие платформы Технопарк/Техносфера, центр Сертификации и др.), с помощью Centrifugo работает красивейший дашборд на ресепшн в московском офисе Badoo, а в сервисе spot.im 350 тысяч пользователей одновременно подключены к Центрифуге.

Несколько ссылок на предыдущие статьи, посвященные серверу и его применению, для тех, кто первый раз слышит про проект:

Работу над второй версией я начал в декабре прошлого года и продолжаю по сей день. Давайте посмотрим, что из этого получается. Я пишу эту статью не только чтобы как-то популяризировать проект, но и получить чуть больше конструктивного фидбека до релиза Centrifugo v2 – сейчас есть простор для маневра и обратно несовместимых изменений.

Real-time библиотека для Go

В Go-сообществе время от времени встает вопрос — а есть ли альтернативы socket.io на Go? Иногда я замечал, как разработчики в ответ на это советуют посмотреть в сторону Centrifugo. Однако Centrifugo это self-hosted сервер, а не библиотека — сравнение не справедливое. Также несколько раз меня спрашивали, можно ли переиспользовать код Centrifugo для того, чтобы писать real-time приложения на языке Go. И ответ был: теоретически можно, но на свой страх и риск — обратную совместимость API внутренних пакетов я гарантировать не мог. Понятно, что рисковать так никому причин особых нет, а форкать тоже вариант так себе. Плюс я бы не сказал, что API внутренних пакетов вообще было подготовлено к такому использованию.

Поэтому одна из амбициозных задач, которые я хотел решить в процессе работы над второй версией сервера — попытаться выделить ядро сервера в отдельную библиотеку на Go. Я верю, что это имеет смысл, принимая во внимание, сколько фич имеет Центрифуга для того, чтобы быть приспособленной к production. Есть много доступных из коробки особенностей, призванных помочь с построением масштабируемых real-time приложений, снимая с разработчика необходимость писать собственное решение. Об этих особенностях я писал ранее и еще обозначу некоторые из них ниже.

Попробую обосновать еще один плюс существования такой библиотеки. Большинство пользователей Centrifugo — это разработчики, которые пишут бекенд на языках/фреймворках со слабой поддержкой concurrency (например, Django/Flask/Laravel/...): работать с большим количеством постоянных соединений если и можно, то неочевидным или неэффективным способом. Соответственно, помочь с разработкой сервера, написанного на Go, могут далеко не все пользователи (банально из-за незнания языка). Поэтому даже совсем небольшое community Go-разработчиков вокруг библиотеки сможет помочь и в развитии использующего ее сервера Centrifugo.

В итоге получилась библиотека Centrifuge [7]. Это все еще WIP, но абсолютно все заявленные в описании на Github фичи реализованы и работают. Поскольку библиотека предоставляет достаточно богатое API, прежде чем гарантировать обратную совместимость, хотелось бы услышать о нескольких успешных примерах использования в реальных проектах на Go. Таких пока нет. Равно как и неуспешных:). Никаких нет.

Я понимаю, что, назвав библиотеку практически так же как сервер, буду вечно иметь дело с путаницей. Но я считаю это правильный выбор, так как клиенты (такие как centrifuge-js, centrifuge-go) работают и с библиотекой Centrifuge, и с сервером Centrifugo. Плюс название уже достаточно прочно закрепилось в умах пользователей, и не хочется эти ассоциации терять. И все же для чуть большей ясности уточню еще раз:

  • Centrifuge — библиотека для языка Go,
  • Centrifugo — готовое решение, отдельный сервис, который в версии 2 будет построен на библиотеке Centrifuge.

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

Centrifugo v2 — будущее сервера real-time сообщений и библиотека для Go - 3

Давайте посмотрим на простой пример: реализуем небольшой сервер на Go с использованием библиотеки Centrifuge. Сервер будет принимать сообщения от браузерных клиентов по Websocket, на клиенте будет текстовое поле, в которое можно вбить сообщение, нажать Enter — и сообщение отправится всем подписанным на канал пользователям. То есть максимально упрощенный вариант чата. Мне показалось, что удобнее всего будет разместить это в виде gist [8].

Запустить можно как обычно:

git clone https://gist.github.com/2f1a38ae2dcb21e2c5937328253c29bf.git
cd 2f1a38ae2dcb21e2c5937328253c29bf
go get -u github.com/centrifugal/centrifuge
go run main.go

И затем переходите по адресу http://localhost:8000 [9], откройте несколько вкладок браузера.

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

node.On().Connect(func(ctx context.Context, client *centrifuge.Client, e centrifuge.ConnectEvent) centrifuge.ConnectReply {

    client.On().Disconnect(func(e centrifuge.DisconnectEvent) centrifuge.DisconnectReply {
        log.Printf("client disconnected")
        return centrifuge.DisconnectReply{}
    })

    log.Printf("client connected via %s", client.Transport().Name())
    return centrifuge.ConnectReply{}
})

Подход на основе callback-функций мне показался наиболее удобным для взаимодействия с библиотекой. Плюс похожий, только слабо типизированный, подход применяется в реализации socket-io сервера на Go [10]. Если вдруг у вас есть мысли, как API можно было бы сделать более идиоматично — буду рад услышать.

Это очень простой пример, который не демонстрирует всех возможностей библиотеки. Кто-то может отметить, что для таких целей проще взять библиотеку для работы с Websocket. Например, Gorilla Websocket. Это на самом деле так. Правда, даже в таком случае вам придется скопировать приличный кусок кода сервера из примера в репозитории Gorilla Websocket. А что если:

  • вам нужно масштабировать приложение на несколько машин,
  • или вам нужен не один общий канал, а несколько – причем пользователи могут динамически подписываться и отписываться от них по мере навигации по вашему приложению,
  • или вам нужно работать тогда, когда Websocket-соединение установить не получилось (нет поддержки в браузере клиента, стоит браузерное расширение, какой-то прокси на пути между клиентом и сервером режет соединение),
  • или нужно восстанавливать сообщения, пропущенные клиентом во время коротких разрывов интернет-соединения не нагружая основную бд,
  • или нужен контроль авторизации пользователя в канале,
  • или нужно отключать постоянное подключение от пользователей, которых деактивировали в приложении,
  • или нужна информация о том, кто в данный момент присутствует в канале или события о том, что кто-то подписался/отписался от канала,
  • или нужны метрики и мониторинг?

Библиотека Centrifuge может вам с этим помочь — по сути она унаследовала все основные возможности, которые раньше были доступны в Centrifugo. Больше примеров, демонстрирующих заявленные выше пункты, можно найти на Github [11].

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

В библиотеке есть некоторые оптимизации, которые позволяют более эффективно использовать ресурсы. Это объединение нескольких сообщений в один Websocket frame для экономии на системных вызовах Write или, например, использование Gogoprotobuf для сериализации Protobuf сообщений и другие. Кстати о Protobuf.

Бинарный Protobuf протокол

Я очень хотел, чтобы Centrifugo могла работать с бинарными данными (и не только я [12]), поэтому в новой версии хотелось добавить бинарный протокол помимо имеющегося на основе JSON. Теперь весь протокол описан в виде Protobuf-схемы [13]. Это позволило сделать его более структурированным, переосмыслить некоторые неочевидные решения в протоколе первой версии.

Думаю, не нужно долго рассказывать какие есть преимущества у Protobuf над JSON — компактность, скорость сериализации, строгость схемы. Есть и недостаток в виде нечитаемости, однако теперь у пользователей есть возможность решить, что им важнее в той или иной ситуации.

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

Интереса ради я запустил на машине с Debian 9.4 и 32-мя Intel® Xeon® Platinum 8168 CPU @ 2.70GHz vCPU бенчмарк, который позволил сравнить пропускную способность клиент-серверного взаимодействия в случае использования JSON-протокола и Protobuf-протокола. Было 1000 подписчиков на 1 канал. В этот канал в 4 потока публиковались сообщения и доставлялись всем подписчикам. Размер каждого сообщения составлял 128 байт.

Результаты для JSON:

$ go run main.go -s ws://localhost:8000/connection/websocket -n 1000 -ns 1000 -np 4 channel
Starting benchmark [msgs=1000, msgsize=128, pubs=4, subs=1000]
Centrifuge Pub/Sub stats: 265,900 msgs/sec ~ 32.46 MB/sec
 Pub stats: 278 msgs/sec ~ 34.85 KB/sec
  [1] 73 msgs/sec ~ 9.22 KB/sec (250 msgs)
  [2] 71 msgs/sec ~ 9.00 KB/sec (250 msgs)
  [3] 71 msgs/sec ~ 8.90 KB/sec (250 msgs)
  [4] 69 msgs/sec ~ 8.71 KB/sec (250 msgs)
  min 69 | avg 71 | max 73 | stddev 1 msgs
 Sub stats: 265,635 msgs/sec ~ 32.43 MB/sec
  [1] 273 msgs/sec ~ 34.16 KB/sec (1000 msgs)
  ...
  [1000] 277 msgs/sec ~ 34.67 KB/sec (1000 msgs)
  min 265 | avg 275 | max 278 | stddev 2 msgs

Результаты для Protobuf случая:

$ go run main.go -s ws://localhost:8000/connection/websocket?format=protobuf -n 100000 -ns 1000 -np 4 channel
Starting benchmark [msgs=100000, msgsize=128, pubs=4, subs=1000]

Centrifuge Pub/Sub stats: 681,212 msgs/sec ~ 83.16 MB/sec
 Pub stats: 685 msgs/sec ~ 85.69 KB/sec
  [1] 172 msgs/sec ~ 21.57 KB/sec (25000 msgs)
  [2] 171 msgs/sec ~ 21.47 KB/sec (25000 msgs)
  [3] 171 msgs/sec ~ 21.42 KB/sec (25000 msgs)
  [4] 171 msgs/sec ~ 21.42 KB/sec (25000 msgs)
  min 171 | avg 171 | max 172 | stddev 0 msgs
 Sub stats: 680,531 msgs/sec ~ 83.07 MB/sec
  [1] 681 msgs/sec ~ 85.14 KB/sec (100000 msgs)
  ...
  [1000] 681 msgs/sec ~ 85.13 KB/sec (100000 msgs)
  min 680 | avg 680 | max 685 | stddev 1 msgs

Можно заметить что пропускная способность такой установки в 2 с лишним раза больше в случае Protobuf. Клиентский скрипт можно найти вот тут [14] — это адаптированный под реалии Centrifuge бенчмарк-скрипт Nats [15].

Стоит также отметить, что производительность сериализации JSON на сервере можно «прокачать» используя тот же самый подход, что и в gogoprotobuf — пул буферов и генерацию кода — в данный момент JSON сериализуется пакетом из стандартной библиотеки Go, построенном на reflect. Например, в Centrifugo первой версии JSON сериализуется вручную с использованием библиотеки [16], предоставляющей пул буферов [17]. Что-то подобное можно будет в будущем сделать и в рамках второй версии.

Стоит подчеркнуть, что protobuf можно использовать и при общении с сервером из браузера. Javascript клиент использует для этого библиотеку protobuf.js. Так как библиотека protobufjs достаточно тяжелая, а количество пользователей бинарного формата будет невелико, с помощью webpack и его tree shaking алгоритма мы генерируем две версии клиента — одна только с поддержкой JSON протокола, а другая с поддержкой и JSON, и protobuf. Для других сред, где размер ресурсов не играет столь критичной роли, клиенты могут о таком разделении не беспокоиться.

JSON Web Token (JWT)

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

Для этого в Центрифуге первой версии при подключении использовалась SHA-256 HMAC подпись, основанная на секретном ключе, известном только бекенду и Центрифуге. Это гарантировало то, что передаваемый клиентом User ID действительно принадлежит ему.

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

Когда Центрифуга появилась, стандарт JWT [18] еще не был столь популярен. Сейчас, несколько лет спустя, библиотеки для генерации JWT есть для большинства популярных языков [19]. Основная идея JWT — именно та, что нужна Центрифуге: подтверждение подлинности передаваемых данных. Во второй версии HMAC подпись, генерируемая вручную, уступила место использованию JWT. Это позволило убрать необходимость поддержки функций-хелперов для правильной генерации токена в библиотеках для разных языков.

Например, на Python токен для подключения к Centrifugo можно сгенерировать следующим образом:

import jwt
import time

token = jwt.encode({"user": "42", "exp": int(time.time()) + 10*60}, "secret").decode()

print(token)

Важно отметить, что в случае использования библиотеки Centrifuge аутентифицировать пользователя можно нативным для языка Go способом — внутри middleware. Примеры есть в репозитории.

GRPC

В процессе разработки я попробовал GRPC bidirectional streaming в качестве транспорта для общения между клиентом и сервером (помимо Websocket и основанных на HTTP фоллбеков SockJS). Что можно сказать? Он работал. Однако я не нашел ни одного сценария, где двунаправленный стриминг GRPC был бы лучше, чем Websocket. Я смотрел в основном на метрики сервера: на генерируемый трафик через сетевой интерфейс, на потребление CPU сервером при наличии большого кол-ва входящих соединений, на потребление памяти на соединение.

GRPC уступил Websocket по всем статьям:

  • GRPC генерирует на 20% больше трафика в аналогичных сценариях,
  • GRPC потребляет в 2-3 раза больше CPU (в зависимости от конфигурации подключений – все подписаны на разные каналы или все подписаны на один канал),
  • GRPC потребляет в 4 раза больше оперативной памяти на соединение. Например, на 10k подключений Websocket-сервер отъел 500Mb памяти, а GRPC — 2Gb.

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

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

Сложности с клиентами

Изменениями, сделанными во второй версии, я убрал обязательность поддержки библиотек для серверного API — интегрироваться на серверной стороне стало проще, однако, клиентский протокол в проекте свой, изменился и имеет достаточное количество особенностей. Это делает достаточно сложной реализацию клиентов. Для второй версии у нас сейчас есть клиент для Javascript [20], который работает в браузерах, должен работать с NodeJS и React-Native. Есть клиент на Go [21] и построенные на его основе и на основе проекта gomobile [22] биндинги под iOS и Android [23].

Для полного счастья не хватает нативных библиотек под iOS и Android. Для первой версии Centrifugo их законтрибьютили ребята из open-source сообщества. Хочется верить, примерно так случится и теперь.

Недавно я попытал счастья, отправив заявку на MOSS грант от Mozilla [24], собираясь вложить деньги в разработку клиентов, но получил отказ. Причина — недостаточно активное сообщество на Github. К сожалению, это правда, но, как видите, какие-то шаги я предпринимаю, чтобы ситуацию улучшить.

Centrifugo v2 — будущее сервера real-time сообщений и библиотека для Go - 4

Заключение

Я не озвучил все фичи, которые появятся в Centrifugo v2 — чуть больше информации есть в issue на Github [25]. Релиз сервера пока не состоялся, но он в скором времени случится. Есть еще незаконченные моменты, в том числе нужно дописать документацию. Прототип документации можно посмотреть по ссылке [26]. Если вы пользователь Centrifugo, то сейчас правильное время, чтобы повлиять на вторую версию сервера. Время, когда не так страшно что-то сломать, чтобы впоследствии сделать лучше. Для заинтересовавшихся: разработка сосредоточена в ветке c2 [27].

Мне сложно судить, насколько будет востребована библиотека Centrifuge, лежащая в основе Centrifugo v2. На данный момент я доволен, что смог довести ее до текущего состояния. Самый важный показатель для меня сейчас это ответ на вопрос «а стал бы я сам использовать эту библиотеку в личном проекте?». Мой ответ — да. На работе? Да. Поэтому я верю, что и другие разработчики оценят.

P.S. Хотелось бы поблагодарить ребят, которые помогали делом и советами — Дмитрия Королькова, Артемия Рябинкова, Олега Кузьмина. Без вас было бы туго.

Автор: FZambia

Источник [28]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/open-source/285757

Ссылки в тексте:

[1] Centrifugo: http://bit.ly/2N9TjIu

[2] Websocket: https://ru.wikipedia.org/wiki/WebSocket

[3] SockJS: https://github.com/sockjs/sockjs-client

[4] Dive into Centrifugo: https://habr.com/company/mailru/blog/280346/

[5] Centrifugo – 3.5 миллиона оборотов в минуту: https://habr.com/post/326236/

[6] А также статья на английском, рассказывающая про историю проекта и мотивацию его появления: https://medium.com/@fzambia/four-years-in-centrifuge-ce7a94e8b1a8

[7] Centrifuge: http://bit.ly/2zyuGDu

[8] gist: https://gist.github.com/FZambia/2f1a38ae2dcb21e2c5937328253c29bf

[9] http://localhost:8000: http://localhost:8000

[10] socket-io сервера на Go: https://github.com/googollee/go-socket.io/tree/v1.4

[11] Github: http://bit.ly/2KRhSxl

[12] и не только я: https://github.com/centrifugal/centrifugo/issues/109

[13] Protobuf-схемы: https://github.com/centrifugal/centrifuge/blob/master/misc/proto/client.proto

[14] вот тут: https://github.com/centrifugal/centrifuge-go/tree/c2/examples/benchmark

[15] адаптированный под реалии Centrifuge бенчмарк-скрипт Nats: https://nats.io/documentation/tutorials/nats-benchmarking/

[16] библиотеки: http://github.com/valyala/bytebufferpool

[17] пул буферов: https://github.com/centrifugal/centrifugo/blob/master/libcentrifugo/proto/response.go#L47

[18] стандарт JWT: https://tools.ietf.org/html/rfc7519

[19] большинства популярных языков: https://jwt.io/

[20] клиент для Javascript: https://github.com/centrifugal/centrifuge-js/tree/c2

[21] клиент на Go: https://github.com/centrifugal/centrifuge-go/tree/c2

[22] проекта gomobile: https://github.com/golang/mobile

[23] биндинги под iOS и Android: https://github.com/centrifugal/centrifuge-mobile/tree/c2

[24] MOSS грант от Mozilla: https://wiki.mozilla.org/MOSS

[25] issue на Github: https://github.com/centrifugal/centrifugo/issues/221

[26] по ссылке: https://centrifugal.github.io/centrifugo/

[27] в ветке c2: https://github.com/centrifugal/centrifugo/tree/c2

[28] Источник: https://habr.com/post/416915/?utm_source=habrahabr&utm_medium=rss&utm_campaign=416915