Centrifuge - я більше не буду оновлювати сторінку перед відправкою коментаря

Пройшов деякий час з тих пір, як я писав про Центрифугу попередній разів. Відбулося безліч змін за цей період. Багато чого з того, що було описано в попередніх статтях (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.

Варто також відзначити, що до Центрифузі також можна підключатися, використовуючи «чисті» вебсокеты — без огортання взаємодії в 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, як запускається, яка авторизація… Хлопці, задавали ті питання, якщо ви читаєте цю статтю, дайте Центрифузі шанс, вона чудово підходить для подібних завдань.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.