Centrifuge — брокер реал-тайм сообщений

Привет, Хабр!

В статье я опишу свой небольшой open-source проект — Centrifuge (далее Центрифуга). Это сервер на Python, задача которого — рассылка (broadcast) сообщений в реальном времени подключенным (в основном из браузера) клиентам.

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

Для начала, посмотрите, пожалуйста, скринкаст (не забудьте включить субтитры), если после просмотра интерес не пропадет, смело читайте дальше!




Идея сервера real-time сообщений совсем не новая, среди существующих подобных проектов могу привести в пример Pusher и Pubnub. Вот цитата с сайта Pusher:

Pusher is a hosted API for quickly, easily and securely adding scalable realtime functionality to web and mobile apps.


Pubnub на своей главной страницы говорит нам нечто похожее:

Thousands of mobile, web, and desktop apps rely on the PubNub Real-Time Network to deliver highly scalable real-time experiences to tens of millions of users worldwide.


Цели Центрифуги далеко не такие глобальные. Это не готовая распределенная по миру инфраструктура для создания реал-тайм приложений, это просто сервер, который вы устанавливаете себе на машину и используете в качестве брокера сообщений.

Не сомневаюсь, что подобных серверов существует немало. Мне и самому некоторое время назад довелось написать очень похожую штуку — cyclone-sse. Это демон на Twisted, который позволяет рассылать сообщения по каналам в реальном времени, используя технологию Server-Sent Events (SSE) (или откат (fallback) до Long-Polling для старичков вроде IE 7). Получился вполне приличный кусок кода, который мы успешно используем в бою.

Однако тот демон не решает некоторых важных проблем:

1) Отсутствие какой-либо авторизации. В нашем случае все проекты закрыты извне файрволлом компании и cyclone-sse мы используем только для публичных данных. Но чтобы добавить реал-тайм события в проект, который доступен всем пользователям интернета, нужен механизм авторизации.

2) Не использует Websockets. Протокол, который предоставляет еще и возможность двустороннего обмена данными (в то время как SSE — это однонаправленный протокол, сообщения возможны только от сервера клиенту). К тому же вебсокеты поддерживают кросс-доменное общение, что не всегда справедливо для Server-Sent Events.

Однажды коллега по работе пожаловался, что ему не хватает возможности мониторить обновления пакетов для JAVA. Я подумал, что это неплохая идея для небольшого проекта — отслеживать новые пакеты, возможно не только для Java, но и для других языков программирования, в реальном времени показывать обновления в веб-интерфейсе — и приступил.

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

Звучит сильно, но спустя некоторое время я понял, что на реализацию подобной задачи нужно нечто большее, чем есть у меня… И было принято решение все упростить — на тот момент уже был написан скелет рассылки сообщений подключенным клиентам, почему бы не развивать это направление? Я понял, что смогу написать код, похожий по назначению и области применения на cyclone-sse, но более приспособленный к массовому использованию.

Итак, вы python-программист и вам нужно написать такой вот сервер — что вы будете использовать? На выбор Twisted, Gevent и Tornado. И, пока Гвидо Ван Россум стандартизирует интерфейс event-loop программ в библиотеке Tulip, нам нужно выбирать.

Я выбрал Торнадо. Он работает на третьем питоне, он просто классный в конце концов. Во многом этот выбор и желание того, чтобы конечный код работал на Python 3.3, предопределил выбор остальных сопутствующих технологий — ZeroMQ (pyzmq), SockJS (sockjs-tornado), MongoDB (motor) и PostgreSQL (momoko). Все библиотеки — асинхронные, исключающие блокировки при взаимодействии с сокетами.

К ZeroMQ я пришел не сразу.

Когда есть несколько процессов приложения за балансировщиком, и клиенты теоретически могут подключиться к любому из них — необходимо каким-то образом поддерживать целостность внутреннего состояния такой системы и иметь возможность коммуникации между экземплярами приложения. Изначально для этих целей я использовал Pub/Sub механизм Redis. Но наткнулся на баг в реализации библиотеки Tornado-Redis и посмотрел в сторону других решений.

В итоге выбор пал на ZeroMQ — сокеты на стероидах, набор паттернов для организации самых разнообразных сетевых взаимодействий. Отсутствие отдельного брокера — это просто великолепно. Если вы еще не слышали об этой библиотеке, или слышали, но не вдавались в подробности — исправляйтесь сейчас же! Прочитайте их The Guide, оно того стоит. Это мой первый проект с использованием данной библиотеки, надеюсь, опытные участники сообщества посмотрят на код и укажут на возможные недочеты.

Каждый процесс Центрифуги создает PUB сокет, который биндится на определенный адрес/порт. Процесс также имеет SUB сокет, который соединяется с PUB сокетом текущего процесса и PUB сокетами остальных инстансов (если таковые запущены). Минусом такой схемы является необходимость вручную указывать все адреса PUB сокетов при запуске процесса. Поэтому есть возможность запустить XPUB/XSUB прокси в отдельном процессе и запускать все процессы Центрифуги с использованием этого прокси. То есть организовать все взаимодействие вот по такой схеме:

image

Последняя часть головоломки — клиентская. Tornado из коробки работает с вебсокетами, но я решил пойти чуть дальше и позволить клиенту использовать еще и SockJS. Так что всё будет работать и в браузерах без поддержки вебсокетов. Хотелось бы отдельно поблагодарить Serge S. Koval (mrjoes) за sockjs-tornado. Поддержка socket.io не планируется.

Итак, что в итоге? А что-то вроде этого:

image

Как видно на диаграммке, в качестве базы данных используется MongoDB или PostgreSQL. Что нам хранить? Проекты и их настройки, категории внутри этих проектов. Подробнее об этом расскажу чуть ниже.

Мне кажется, я до сих пор так и не смог внятно объяснить, что же такое я тут всем впариваю. Итак, вот примерно так, по пунктикам:

1) Вы захотели добавить на свой сайт нечто реал-таймовое — комментарии, графики, обновляющиеся счетчики, уведомления…

2) Однако ваш сайт не на асинхронном бэкенде, или на самом что ни на есть асинхронном, но вам не хочется писать с нуля логику менеджмента каналов, подписок и т.д.

Центрифуга вполне может подойти вам в таком случае.

3) pip install centrifuge. Или чуть более подробно в документации (документация пока скомканная, местами непонятная, но в будущем надеюсь изменить ее к лучшему).

4) Интегрировать все же придется… Подчеркну некоторые важные моменты.

Для начала, нужно Центрифугу запустить. Да, там много всяких параметров для запуска, но я верю, что у вас получится, а если нет — пишите мне, я помогу.

После запуска нужно зайти в административный веб-интерфейс, создать новый проект. У проекта есть несколько настроек, описывать в этой статье их не буду — текста и так получается слишком много.

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

Самая, пожалуй, важная опция для категории — bidirectional. Если отметить ее галочкой, то подключившиеся клиенты смогут сами посылать сообщения в канал, без задействования вашего приложения. Иначе, только однонаправленное сообщение от сервера к клиенту — когда ваше приложение отправляет с помощью POST запроса событие в Центрифугу. POST запрос содержит данные о проекте, категории, канале и непосредственно пересылаемое сообщение. Сообщение рассылается всем подписанным на канал клиентам.

Итак, Центрифуга (или несколько Центрифуг) крутит свой event-loop, проекты и категории созданы, дело за клиентской частью. Как я уже говорил, для общения можно использовать нативные вебсокеты или библиотеку SockJS. На данный момент не существует javascript-библиотек, которые упрощают взаимодействие с Центрифугой, возможно, они появятся в будущем. Пока, чтобы взаимодействовать, нужно отправлять JSON сообщения, соответствующие JSON-схемам. На данный момент существуют только 4 метода для таких команд:

  • auth — первое сообщение после установки соединения — авторизация.
  • subscribe — после успешной авторизации можно подписываться на каналы в различных категориях, на которые был получен доступ во время авторизации.
  • unsubscribe — отписаться от канала
  • broadcast — отправить сообщение в канал, работает только для каналов, принадлежащих двунаправленной (bidirectional) категории


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

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

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

В репозитории на Github есть пример приложения, использующего Центрифугу. А еще в документации есть пример конфигурации Nginx для деплоя. Лицензия — BSD. Если по каким-либо причинам вы хотите, но не можете использовать Centrifuge из-за лицензии — напишите мне, я пересмотрю.

Жду ваших замечаний и предложений по улучшению.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 22

    +1
    Я пишу похожую штуку — эмуляцию бекенда pusher.com на эрланге github.com/arrowcircle/erlypusher
    У Вас есть функционал presense-каналов?
    Стресстест делали?
      0
      в статье описал почти весь функционал, который на данный момент имеется, за исключением, пожалуй, мелких фич вроде использования exponential back-off при авторизации клиентов во время реконнекта. Поэтому, нет, таких каналов пока что нет. Доступ к определенному каналу сейчас ограничивает пользовательское приложение во время авторизации. Никакой дополнительной информации о канале пока что получить нельзя.

      Стресс-тест еще не делал.
        0
        Добавьте модулям префиксы пока не поздно…
          0
          можно подробнее? я новичок в эрланге и не знаю как там и что?
            0
            Ну, такие имена как channel helper middleware uuid общеупотребительные. В Erlang пространство имён двухуровневое. Так что, если вы вдруг захотите подключить к своему приложению стороннюю библиотеку вроде github.com/avtobiff/erlang-uuid то поймаете конфликт имён.
            Ну и ваше приложение никто не захочет встраивать в свою систему из за этого.
            Идея в том, чтобы вы своим модулям добавляли префикс типа
            epusher_channel
            epusher_helper
            epusher_middleware
            и т.п. — чтобы уменьшить вероятность коллизии имён.
            Можете посмотреть как сделано в том же Cowboy: у всех модулей префикс cowboy_.
        +2
        Код можно упростить, SockJS умеет подключать напрямую через websocket, в обход механизма детектирования транспорта. Для этого надо url задавать в виде ws://host:port/centrifuge/websoket.
          0
          спасибо за замечание, но не могли бы вы более подробно пояснить, что имеете в виду? В каком месте упростить и зачем обходить детектирование транспорта — поддержки вебсокетов в браузере ведь может и не быть?
            +1
            Я имею в виду, убрать
            tornado.web.url(
                            r'/connection/websocket',
                            WebsocketConnection,
                            name="connection_websocket"
                        ),
            

            и класс WebsocketConnection
            К SockJS серверу можно подключаться, как через клиентскую библиотеку так и напрямую через raw WebSocket.
              0
              о! спасибо большое, не знал, обязательно поправлю
          0
          Существует более быстрая чем Server-Sent Events реализация передачи сообщений — Websockets

          Что, простите?)
            0
            спасибо, поправил
              0
              Я не понял в чем WebSockets «более быстрая», чем SSE.
              Два текстовых протокола, с минимальными различиями на уровне кодирования
                0
                Server-Sent Events — это http протокол со всеми его оверхедами, WebSockets — это надстройка поверх TCP, изначально разработанная для обмена сообщениями между браузером и веб-сервером в режиме реального времени (wikipedia). Посмотрите вот на этот замечательный пост на SO — там многое объясняется.
                  0
                  Oh, so wrong…
                  Этот пост — о сравнении HTTP-запросов и Web Socket.
                  И его главный постулат — websocket быстрее, потому что не надо каждый раз слать хедеры.
                  Но, внезапно, SSE это тоже долгоживущий TCP-сокет. Который передает заголовки лишь при подключении(как и WebSocket). То есть, это сравнение совершенно не о том
                    0
                    Задержки на установку соединения сказываются на производительности, разве нет? Ну и также все общение при использовании SSE инкапсулируется в HTTP — а это промежуточные прокси, файрволлы, которые могут буфферизовать траффик, что приводит к увеличению времени ответа.
                      +2
                      >Задержки на установку соединения сказываются на производительности, разве нет?
                      Соединение устанавливается один раз при подключении. Как и у WebSockets, они ведь наверное тоже требуют первоночальное подключение?)

                      >Ну и также все общение при использовании SSE инкапсулируется в HTTP — а это промежуточные прокси, файрволлы, которые могут буфферизовать траффик, что приводит к увеличению времени ответа.
                      Так позвольте, а что, WebSocket это не касается?

                      Вобщем давайте я сформулирую свою мысль. Единственное значимое отличие WebSocket от SSE — наличие двухсторонней связи(SSE — односторонняя).
                        0
                        Пожалуй, вы правы. Поисследовал этот вопрос повнимательней — и да, скорее всего увеличения производительности нет, во многих тестах, которые я смог найти в интернете SSE не уступает вебсокетам по производительности. Думаю, это утверждение у меня на подсознательном уровне сформировалось и не хотелось так просто с ним расставаться. Спасибо.
            0
            Упс, не туда написал, но успел отредактировать:)
              0
              А как вы относитесь к неутешительным результатам тестов вебсокетов на питоне?
              github.com/ericmoritz/wsdemo/blob/results-v1/results.md
                0
                Отношусь спокойно, так как выбрал для разработки язык, которым лучше владею. Конечно, Erlang в подобных серверах просто всеми красками расцветает, но сколько я за него не брался — никак не мог преодолеть рубеж между книгой и написанием чего-либо.

                По вашей ссылке приведены плачевные результаты для Gevent, а я вижу, что у них в репозитории есть еще и Python c Tornado, не знаете, есть ли результаты для этой связки?
                    0
                    По этой ссылке совершенно другая картина. Добавились бенчмарки с количеством запущенных процессов по количеству ядер, и конечные результаты поменялись. По-моему, все не так уж неутешительно. А если взять Pypy в расчет — то и вовсе замечательно. Опять же, логично, что Erlang и Go показали лучшие результаты — оба языка спроектированы с уклоном на concurrency. В любом случае, какие бы ни были результаты, питон для Centrifuge был выбран не из соображений производительности.

              Only users with full accounts can post comments. Log in, please.