- PVSM.RU - https://www.pvsm.ru -
Возможно, некоторые из читателей уже слышали про Centrifugo [1] раньше. В данной статье речь пойдет о разработке второй версии сервера и новой real-time библиотеке для языка Go, лежащей в его основе.
Меня зовут Александр Емелин. Летом прошлого года я присоединился к команде Авито, где сейчас помогаю разрабатывать бэкенд мессенджера Авито. Новая работа, напрямую связанная с быстрой доставкой сообщений пользователям, и новые коллеги вдохновили меня продолжать работу над open-source проектом Centrifugo.
В двух словах — это сервер, который берет на себя задачу держать постоянные соединения от пользователей вашего приложения. В качестве транспорта используется Websocket [2] или полифилл SockJS [3], умеющий, при невозможности установить Websocket-соединение, работать через Еventsource, XHR-streaming, long-polling и другие основанные на HTTP транспорты. Клиенты подписываются на каналы, в которые бекенд через API Центрифуги публикует новые сообщения по мере их возникновения – после чего сообщения доставляются подписанным на канал пользователям. Другими словами – это PUB/SUB сервер.
На текущий момент сервер используется в достаточно большом количестве проектов. Среди них, например, некоторые проекты Mail.Ru (интранет, обучающие платформы Технопарк/Техносфера, центр Сертификации и др.), с помощью Centrifugo работает красивейший дашборд на ресепшн в московском офисе Badoo, а в сервисе spot.im 350 тысяч пользователей одновременно подключены к Центрифуге.
Несколько ссылок на предыдущие статьи, посвященные серверу и его применению, для тех, кто первый раз слышит про проект:
Работу над второй версией я начал в декабре прошлого года и продолжаю по сей день. Давайте посмотрим, что из этого получается. Я пишу эту статью не только чтобы как-то популяризировать проект, но и получить чуть больше конструктивного фидбека до релиза Centrifugo v2 – сейчас есть простор для маневра и обратно несовместимых изменений.
В 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. Плюс название уже достаточно прочно закрепилось в умах пользователей, и не хочется эти ассоциации терять. И все же для чуть большей ясности уточню еще раз:
Centrifugo из-за своего дизайна (отдельно стоящий сервис, не знающий о вашем бекенде ничего) предполагает, что поток сообщений по real-time транспорту будет идти от сервера клиенту. Что имеется в виду? Если, например, пользователь пишет сообщение в чат, то это сообщение нужно сначала отправить на бекенд приложения (например, AJAX-ом в браузере), на стороне бекенда его провалидировать, сохранить в базу данных при необходимости, а затем отправить в API Центрифуги. Библиотека это ограничение снимает, позволяя организовать двунаправленный обмен асинхронными сообщениями между сервером и клиентом, а также RPC-вызовы.
Давайте посмотрим на простой пример: реализуем небольшой сервер на 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. А что если:
Библиотека Centrifuge может вам с этим помочь — по сути она унаследовала все основные возможности, которые раньше были доступны в Centrifugo. Больше примеров, демонстрирующих заявленные выше пункты, можно найти на Github [11].
Сильное наследие Centrifugo может быть и минусом, так как библиотека переняла и всю механику сервера, которая достаточно самобытна и, возможно, кому-то может показаться неочевидной или перегруженной ненужными возможностями. Я старался организовать код таким образом, чтобы неиспользуемые фичи никак не сказывались на общей производительности.
В библиотеке есть некоторые оптимизации, которые позволяют более эффективно использовать ресурсы. Это объединение нескольких сообщений в один Websocket frame для экономии на системных вызовах Write или, например, использование Gogoprotobuf для сериализации 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. Для других сред, где размер ресурсов не играет столь критичной роли, клиенты могут о таком разделении не беспокоиться.
Одна из проблем в использовании такого 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 bidirectional streaming в качестве транспорта для общения между клиентом и сервером (помимо Websocket и основанных на HTTP фоллбеков SockJS). Что можно сказать? Он работал. Однако я не нашел ни одного сценария, где двунаправленный стриминг GRPC был бы лучше, чем Websocket. Я смотрел в основном на метрики сервера: на генерируемый трафик через сетевой интерфейс, на потребление CPU сервером при наличии большого кол-ва входящих соединений, на потребление памяти на соединение.
GRPC уступил Websocket по всем статьям:
Результаты оказались достаточно… ожидаемы. В общем, в 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 — чуть больше информации есть в 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
Нажмите здесь для печати.