http://habrahabr.ru/company/mailru/blog/237257/
Прошло некоторое время с тех пор, как я писал про
Центрифугу в
предыдущий раз. Произошло множество изменений за этот период. Многое из того, что было описано в ранних статьях (
1,
2) кануло в лету, но суть и идея проекта остались прежними — это сервер рассылки real-time сообщений пользователям, подключенным из веб-браузера. Когда на вашем сайте возникает событие, о котором вам нужно моментально сообщить некоторым вашим пользователям, вы постите это событие в Центрифугу, а она, в свою очередь, отправляет его всем заинтересованным пользователям, подписанным на нужный канал. В самом простом виде это показано на схеме:
Проект написан на Python с использованием асинхронного веб-сервера Tornado. Использовать можно даже если бекенд вашего сайта написан не на Python. Хотелось бы рассказать о том, что Центрифуга представляет собой на данный момент.
Попробовать проект в действии совсем несложно, если вы знакомы с установкой Python-пакетов. Внутри virtualenv:
$ pip install centrifuge
Запуск:
$ centrifuge
После этого по адресу
http://localhost:8000 будет доступен административный интерфейс процесса Центрифуги, который вы только что запустили.
Специально для статьи я запустил инстанс Центрифуги на Heroku —
habrifuge.herokuapp.com. Пароль — habrahabr. Я надеюсь на вашу честность и благоразумие — демо никак не защищено от попыток все поломать и не дать остальным оценить проект. Запущено на бесплатном дино со всеми вытекающими. Heroku, конечно, не лучшее место для хостинга такого рода приложений, но для целей демонстрации сойдет.
Думаю, я не буду далек от истины, если скажу, что аналогов Centrifuge, как минимум в open-source мире Python, нет. Попробую пояснить, почему я так считаю. Существует масса способов добавить real-time события на сайт. Из того, что приходит на ум:
- отдельно стоящий асинхронный сервер;
- облачный сервис (pusher.com, pubnub.com);
- gevent (gunicorn, uwsgi);
- модули/расширения Nginx;
- BOSH, XMPP.
В JavaScript есть Meteor, Derby — совсем другой подход. Еще есть замечательный Faye — сервер, легко интегрирующийся с вашим JavaScript или Ruby бекендом. Но это решение для NodeJS и Ruby. Центрифуга реализует первый из перечисленных выше подход. Преимущество отдельно стоящего асинхронного сервера (и облачного сервиса) в том, что вам не нужно менять код и философию существующего бекенда, что неизбежно произойдет, как только вы, например, решите использовать Gevent, пропатчив стандартные библиотеки Python. Подход с отдельным сервером позволяет легко и безболезненно интегрировать real-time сообщения в уже существующую архитектуру бекенда.
Недостаток — на выходе при подобной архитектуре получается немного «урезанный» real-time. Ваше веб-приложение должно выдерживать HTTP запросы от клиентов, генерирующие события: новые события попадают первоначально на ваш бекенд, проходят валидацию, сохраняются в базу данных, если необходимо, и только потом отправляются в Centrifuge (в pusher.com, в pubnub.com и др.). Однако это ограничение в большинстве случаев никак не сказывается на задачах веба, от этого могут пострадать динамичные real-time игры, где один клиент генерирует очень большое количество событий. Для таких случаев, пожалуй, нужна более тесная интеграция real-time приложения и бекенда, возможно, что-то вроде gevent-socketio. Если события на сайте генерирует не клиент, а сам бекенд, то в этом случае озвученный выше недостаток роли не играет.
Говоря, что аналогов Центрифуги в open-source мире Python нет, я не имею в виду то, что нет другой реализации отдельно стоящего сервера рассылки сообщений через веб-сокеты и полифиллы к ним. Я просто не нашел ни одного подобного проекта, в полной мере из коробки решающего большинство проблем реального использования.
Набрав в поисковике "
python real-time github" вы получите массу ссылок на примеры подобных серверов. Но! Большинство из этих результатов лишь демонстрируют подход к решению проблемы, не вдаваясь вглубь. Вам не хватает одного процесса и нужно как-то масштабировать приложение — хорошо, если в документации проекта будет написано, что для этих целей нужно использовать PUB/SUB брокер — Redis, ZeroMQ, RabbitMQ — это правда, но придется реализовывать это самостоятельно. Зачастую все такие примеры ограничиваются переменной класса типа set, в которую добавляются новые объекты соединений и рассылка нового сообщения всем клиентам из этого сета подключений.
Основная цель Центрифуги — из коробки предоставлять решение проблем реального использования. Давайте посмотрим на некоторые моменты, с которыми придется иметь дело подробнее.
Полифиллы
Одних вебсокетов недостаточно. Если не верите, посмотрите выступление с говорящим названием «Websuckets» от одного из разработчиков Socket.io. Вот
слайды. А вот видео:
Конечно, есть проекты (опять же, динамичные real-time игры), для которых использование именно веб-сокетов — критично. Центрифуга использует SockJS для эмуляции веб-сокетов в старых браузерах. Это означает поддержку браузеров вплоть до IE7 с помощью использования таких транспортов как xhr-streaming, iframe-eventsource, iframe-htmlfile, xhr-polling, jsonp-polling. Для этого используется замечательная реализация SockJS сервера —
sockjs-tornado.
Cтоит также отметить, что к Центрифуге также можно подключаться, используя «чистые» вебсокеты — без оборачивания взаимодействия в SockJS-протокол.
В репозитории есть JavaScript-клиент с простым и понятным API.
Масштабирование
Вы можете запустить несколько процессов — они будут общаться между собой, используя Redis PUB/SUB. Хотелось бы отметить, что Centrifuge совсем не претендует на инсталляцию внутри огромных сайтов с миллионами посетителей. Пожалуй, для таких проектов нужно найти другое решение — те же облачные сервисы или свои разработки. Но для подавляющего большинства проектов нескольких инстансов сервера за балансировщиком, связанных PUB/SUB механизмом Redis, будет более чем достаточно. У нас, например, один инстанс (Redis не нужен в таком случае) выдерживает 1000 одновременных подключений без проблем, среднее время отправки сообщений при этом менее 50мс.
Вот, кстати, график из Graphite за недельный период работы Центрифуги, используемой интранетом Mail.Ru Group. Синяя линия — количество активных подключений, зеленая — среднее время рассылки сообщений в миллисекундах. Посередине – выходные. :)
Аутентификация и авторизация
Подключаясь к Центрифуге, вы используете симметричное шифрование на основе секретного ключа проекта для генерации токена (HMAC). Этот токен валидируется при подключении. Также при подключении передаются ID пользователя и, по желанию, дополнительная информация о нем. Поэтому Центрифуга знает достаточно о ваших пользователях, чтобы обрабатывать подключение к приватным каналам. Этот механизм по своей сути очень похож на
JWT (JSON Web Token).
Хотелось бы отметить одно из недавних нововведений. Как я рассказывал в предыдущих статьях, если клиент подписывается на приватный канал, то Центрифуга отправит POST запрос вашему приложению, спрашивая, можно ли пользователю с таким-то ID подключиться к определенному каналу. Сейчас появилась возможность создать приватный канал, при подписке на который ваше веб-приложение вообще не будет задействовано. Просто назовите канал как вам удобно и в конце после специального символа # напишите ID пользователя, которому позволено подписываться на этот канал. Только пользователю с ID 42 будет позволено подписаться на этот канал:
news#42
А еще можно делать вот так:
dialog#42,56
Это приватный канал для 2-х пользователей с ID 42 и 56.
В последних версиях был также добавлен механизм истечения срока действия подключения — он по умолчанию выключен, так как для большинства проектов не нужен. Механизм стоит рассматривать как экспериментальный.
Пожалуй, в процессе развития проекта было два наиболее сложных решения: как синхронизировать состояние между несколькими процессами (в итоге был выбран самый простой путь — использование Redis) и проблема с клиентами, подключившимися к Центрифуге до того, как их деактивировали (забанили, удалили) в веб-приложении.
Сложность тут в том, что Центрифуга вообще не хранит ничего кроме настроек проектов и неймспейсов проектов в постоянном хранилище. Поэтому нужно было придумать способ надежно отключать невалидных клиентов, не имея при этом возможности сохранять идентификаторы или токены этих клиентов, учитывая возможные даунтаймы Центрифуги и веб-приложения. Этот способ в конечном итоге удалось найти. Однако применить его в реальном проекте пока не довелось, отсюда экспериментальный статус. Попробую описать, как решение работает в теории.
Как я уже описывал раньше, для того чтобы из браузера подключиться к Центрифуге, нужно передать, помимо адреса подключения, некоторые обязательные параметры — ID текущего пользователя и ID проекта. Также в параметрах подключения должен присутствовать HMAC токен, сгенерированный на основе секретного ключа проекта на бекенде веб-приложения. Этот токен подтверждает корректность переданных клиентом параметров.
Беда в том, что раньше, единожды получив подобный токен, клиент мог без проблем пользоваться им и в будущем: подписываться на публичные каналы, читать из них сообщения. Благо не писать (так как сообщения изначально проходят через ваш бекенд)! Это для многих публичных сайтов вполне нормальная ситуация. Тем не менее, я был уверен, что дополнительный механизм защиты данных нужен.
Поэтому среди обязательных параметров при подключении появился параметр
timestamp
. Это Unix секунды (
str(int(time.time()))
). Этот
timestamp
также участвует в генерации токена. То есть подключение теперь выглядит вот так:
var centrifuge = new Centrifuge({
url: 'http://localhost:8000/connection',
token: 'TOKEN',
project: 'PROJECT_ID',
user: 'USER_ID',
timestamp: '1395086390'
});
В настройках проекта появилась опция, отвечающая на вопрос: сколько секунд новое соединение должно считаться корректным? Центрифуга периодически ищет соединения, у которых истек срок действия и добавляет их в специальный список (на самом деле set) для проверки. Раз в определенный интервал времени Центрифуга отправляет POST запрос вашему приложению со списком ID пользователей, нуждающихся в проверке. Приложение в ответ отправляет список ID пользователей эту проверку не прошедших — эти клиенты будут сразу же насильно отключены от Центрифуги, при этом автоматического реконнекта на стороне клиента не будет.
Но не все так просто. Существует вероятность, что «злоумышленник», подправив, например, JavaScript на клиенте, моментально переподключится после того, как его насильно выгнали. И если
timestamp
в его параметрах подключения всё еще валиден — соединение будет принято. Но на следующем же цикле проверки, после того как его соединение просрочится, его ID будет по тому же механизму отправлен веб-приложению, оно скажет, что юзер невалиден, и после этого он будет отключен навсегда (так как срок действия
timestamp
уже истек). То есть существует небольшая щель во времени, в течение которой у клиента есть возможность продолжать читать из публичных каналов. Но её величина конфигурируется — думаю, совсем не страшно, если после фактической деактивации пользователь еще некоторое время сможет читать сообщения из каналов.
Возможно, с помощью схемы понять этот механизм будет гораздо проще:
Деплой
В репозитории есть примеры реальных конфигурационных файлов, которые используются у нас для деплоя Центрифуги. Мы запускаем ее на CentOS 6 за Nginx под супервизором (Supervisord). Есть spec-файл — если у вас CentOS, то вы можете собрать rpm на его основе.
Мониторинг
В последней версии Центрифуги появилась возможность экспортировать различные метрики в Graphite по UDP. Метрики агрегируются в заданном интервале времени, à la StatsD. Выше по тексту была как раз картинка с графиком из Graphite.
В предыдущей статье про Центрифугу я рассказывал, что в ней используется ZeroMQ. И комментарии были единодушны — ZeroMQ не нужен, используй Redis, производительности которого хватит с головой. Сначала я немного подумал и добавил Redis в качестве опционального PUB/SUB бекенда. А потом был вот этот бенчмарк:
Я был удивлен, правда. Почему ZeroMQ оказался настолько хуже для моих задач, чем Redis? Я не знаю ответа на этот вопрос. Поискав в интернете, нашел статью, где автор также сетует на то, что ZeroMQ для быстрого real-time веба подходит плохо. К сожалению, сейчас уже потерял ссылку на эту статью. В итоге — ZeroMQ в Центрифуге уже не используется, осталось только 2 так называемых engine-а — Memory и Redis (первый подойдет, если вы запускаете один инстанс Центрифуги, и вам ни к чему Redis и его PUB/SUB).
Как вы могли видеть на гифке выше, веб-интерфейс никуда не исчез, он по-прежнему используется для того, чтобы создавать проекты, менять настройки, следить за сообщениями в некоторых каналах. Через него также можно отправлять Центрифуге команды, например, опубликовать сообщение. В общем-то, это было и раньше, я просто решил повторить, если вы вдруг не в курсе.
Из других изменений:
- MIT лицензия вместо BSD;
- рефакторинг неймспейсов: теперь это не отдельная сущность и поле в протоколе, а просто префикс в имени канала, отделенный двоеточием (
public:news
);
- улучшения в JavaScript-клиенте;
- работа с JSON теперь может быть значительно ускорена, если дополнительно установить модуль ujson;
- организация Centrifugal на GitHub — с репозиториями, имеющими отношение к проекту, помимо Python-клиента для Центрифуги — там сейчас в том числе есть пример как развернуть проект на Heroku и первая версия библиотеки adjacent — небольшая обертка для интеграции с Django (упрощает жизнь с генерацией параметров подключения, есть методы для удобной отправки сообщений в Центрифугу);
- поправлена/расширена документация в соответствии с изменениями/добавлениями;
- множество других изменений отражены в changelog.
Как уже
упоминалось ранее в блоге Mail.Ru Group на Хабре, Центрифуга используется в нашем корпоративном интранете. Real-rime сообщения добавили удобства использования, красок и динамики нашему внутреннему порталу. Пользователям не нужно обновлять страничку перед отправкой комментария (не нужно обновлять, не нужно обновлять...) — разве это не прекрасно?
Заключение
Как и любое другое решение, использовать Центрифугу нужно с умом. Это не серебряная пуля, нужно понимать, что по большому счету это лишь брокер сообщений, единственная задача которого держать соединения с клиентами и отправлять им сообщения.
Не стоит ждать гарантированной доставки сообщения клиенту. Если, например, пользователь открыл страничку, потом погрузил свой ноутбук в сон, то когда он «разбудит» свою машинку, соединение с Центрифугой вновь установится. Но все события, которые произошли, пока ноутбук находился в спящем режиме, будут потеряны. И пользователю нужно либо обновлять страничку, либо вам самостоятельно дописывать логику догрузки потерянных событий с вашего бекенда. Также нужно помнить, что практически все объекты (подключения, каналы, история сообщений) хранятся в оперативной памяти, поэтому за ее потреблением важно следить. Нужно не забывать о лимитах операционной системы на открытые файловые дескрипторы и увеличить их при необходимости. Необходимо думать о том, какой канал создать в той или иной ситуации — приватный или публичный, с историей или без, какова должна быть длина этой истории и т.д.
Как я упоминал выше, способов добавить real-time на ваш сайт — масса, выбирать нужно с умом, возможно в вашем случае более выигрышным окажется не вариант с отдельным асинхронным сервером.
P.S. В конце весны я посетил конференцию python-разработчиков в Санкт-Петербурге Piter Py. В одном из докладов зашла речь о мгновенных уведомлениях пользователей о том, что их задача, которая выполнялась асинхронно в Celery-воркере, готова. Докладчик сказал, что для этих целей они используют как раз Tornado и вебсокеты. Далее последовало несколько вопросов о том, как это работает в связке с Django, как запускается, какая авторизация… Парни, задававшие те вопросы, если вы читаете эту статью, дайте Центрифуге шанс, она замечательно подходит для подобных задач.