Какими средствами можно добиться, чтобы сообщения по вебсокетам приходили всегда, вне зависимости от качества интернета и количества разрывов соединений в секунду.
1. Титульный слайд
Всем привет! Меня зовут Андрей и я тружусь на благо красной планеты.
2. Логотипы OpenSource-проектов Evil Martians
Красная планета знаменита на Земле многими вещами, в том числе и ощутимым вкладом в проекты, стоящие за вот этими картиночками, но сейчас не о том.
3. Дремучий лес
Итак, представим ситуацию: вы уехали на дачу, в лес дремучий, где сеть вроде бы и есть, но работает как попало и через раз. Ну или вы дома и у вас провайдер решил покапризничать. И вот в этот самый момент вам понадобилось поработать через ваше суперкрутое приложение, но…
4. Гифка-иллюстрация неприходящих сообщений
…иногда сообщения к вам почему-то не приходят. Приложение гордо рапортует: «я в сети», переподключается раз в пять минут всего лишь, но обновления большей частью времени не приходят, вы теряете нить разговора и вам приходится обновлять страницу или перезапускать приложение раз за разом, раз за разом и вы начинаете ненавидеть всё и всех вокруг. Знакомо? Давайте разберёмся, как же так получилось.
5. Sequence-диаграмма работы ActionCable через Redis PubSub
Для этого нужно понять, как работает PubSub через Redis, представителем которого является ActionCable, AnyCable, старый и заброшенный websocket-rails и многие другие. Клиент подключается по протоколу WebSocket к серверу приложения (в случае ActionCable он может жить в том же процессе, что и основное приложение), то, в свою очередь, подключается к Redis-серверу и оформляет подписку на определённый паттерн в нём. Когда что-то в приложении публикует новое сообщение, публикация уходит в Redis с определённым ключом и Redis «пинает» всех подключённых к нему клиентов (которые cable-сервера), а те уже рассылают сообщение подключенным к ним клиентам-браузерам. Всё работает по HTTP, который поверк TCP и значит сообщения дойдут. Или нет? Что же может пойти не так?
6. Sequence-диаграмма с сокетом Шрёдингера
Подвох кроется во фразе «подключенным к ним клиентам-браузерам». Дело в том, что подключение в TCP — всего лишь абстракция, под которой сервер и клиент пересылают пакеты с подтверждениями друг-другу. Подключение может жить очень долго без единого пересланного пакета и сервер и клиент могут расходиться во мнении, есть подключение или нет. Так, нередка ситуация, когда сервер разорвал соединение со своей стороны, потому что клиент долго не отвечал на пинги на уровне протокола websocket или на уровне приложения, но клиент ещё может со своей стороны думать, что он подключен, держать сокет открытым и ждать новых сообщений, которых ему даже не отправят. Подробнее об этом вы можете узнать из выступления моего коллеги Владимира Деменьтева:
● https://youtu.be/OXjNsLQPQUc
● https://www.slideshare.net/it-people/realtime-evil-martians
7. Список проблем
Это одна проблема и ActionCable решает её очень хорошо — у него есть пинги уровня протокола ActionCable и он очень быстро детектирует факт пропажи подключения и переподключается. Но вот в то время, пока мы не подключены (например мы заехали в тоннель и сможем подключиться только выехав из него), вот в это время мы сообщений не получим никак — это архитектурная особенность всех решений на базе Redis PubSub — Redis не хранит сообщений. Такие дела.
8. Методы обхода проблемы
Что можно сделать в этой грустной ситуации? Путей обхода множество: отслеживайте id последнего принятого сообщения и научите ваш бэкенд отдавать всё, свежее него после переподключения, ну и в крайнем случае не стесняйтесь попросить пользователя всё перезагрузить (Trello не стесняется). Подробнее об этих мерах тоже смотрите выступление Владимира. Да, это костыли, но скорее всего они будут работать довольно хорошо для вас. В принципе, если бы всем этого было достаточно, то можно было бы смело идти обедать.
Но что делать, если вот именно вам этого недостаточно?
9. Картинка со скорой помощью в лесу
Такая ситуация возникла у меня на предыдущем месте работы, где мы делали автоматизацию скорой помощи (да, на Ruby on Rails), причём это скорая не Москвы и не Питера, а одного из чуть более отдалённых регионов, где и рабочие места у операторов на станциях не отличаются стабильным и скоростным интернетом, да и бригады скорой колесят по лесам и деревням, где даже EDGE — счастье. Перезагружать мегабайты данных из-за каждого разрыва связи — не вариант. Пришлось включать мозг и думать:
10.
Как же найти способ как-то сохранить сообщения между переподключениями клиентов и отдать их ему все и сразу. Например, поставить их как-то в очередь. Опа, очередь! Что-то знакомое…
11. Очередь в булочную
Ну, у многих нормальных людей, скорее всего всплыла в памяти картинка, похожая на эту.
12. Схема MQ первая попавшаяся
Ну а у прочих — на эту. Действительно, очереди сообщений уже давно и успешно решили проблему доставки в межсерверном общении, где одни системы публикуют сообщения, а другие получают копии этих сообщений из очередей. Брокер же заботится о том, чтобы все заинтересованные клиенты получили по своей копии. Обычно несколько клиентов цепляются к одной очереди и разбирают сообщения из неё, примерно как это делают воркеры Sidekiq’а, например. Но, пожалуй, это не наш случай.
13. Схема MQ изменённая под нас
К счастью, конфигурацию MQ-систем можно менять. Поменяем её так, как нам надо: каждому клиенту по очереди и пусть никто не уйдёт обиженным. Продюсером станет наше приложение, бэкенд, оно подключится к брокеру через классический бэковый протокол. Потребителями будут сразу фронтовые приложения в браузерах, которые подключаться по вебсокетам напрямую к брокеру. Посередине между ними как раз и встанет брокер очередей сообщений. Кого же взять на эту важную и ответственную роль?
14. Протокол AMQP
15. Протокол STOMP
Спецификация протокола STOMP (протокол очень простой и читается она легко):
https://stomp.github.io/stomp-specification-1.2.html
16. Розовый кролик
Пусть это будет кролик, розовый, пушистый и с крылышками — ведь ему придётся летать.
17. Чувак чешет репу на логотип RabbitMQ
Речь естественно о RabbitMQ. Но почему он?
18. Почему RabbitMQ?
Причин, конечно, много. Главное: в нём из коробки уже есть всё, что нам необходимо для решения нашей задачи: он умеет в вебсокеты и умеет в гарантированную доставку, есть хорошие библиотеки для взаимодействия и его все знают. Ну и главное — это Open Source. Это важно. Никто же не хотел же предложить мне IBM WebSphere MQ вместе с Ораклом?
19. Откуда надёжность?
Казалось бы, ну ок, ну поставили RabbitMQ, чего тут сложного-то?
Но нет, для того, чтобы сообщения действительно всегда доходили, кролика нужно как следует приготовить, иначе получится невкусно. Итак, что же нам нужно?
1. Обменники и сами сообщения нужно пометить как персистентные, сиречь, надёжные, чтобы RabbitMQ не вздумал их потерять при рестарте самого себя.
2. Каждому клиенту нужно выделить по надёжной же очереди в личное и эксклюзивное пользование.
3. А так же каждого клиента нужно заставить явно подтверждать получение каждого сообщения (по умолчанию вам будут предлагать режим с подтверждением сразу всей пачки сообщений).
20. Мем с Линусом
Но это всё слова! Давайте уже посмотрим код! Я подготовил для вас немного примеров, чтобы вы могли попробовать это решение сами для себя.
21. Бэкенд!
Для бэкенда я сделал крошечный гем: https://github.com/Envek/durable-websockets/tree/example , который сам подключится к RabbitMQ, разрулит проблемы с многопоточностью и добавит пару нужных флагов в отправляемые сообщения. При этом, если вас не устроят дефолтные обменники RabbitMQ, то можно создать свои. А можете его вообще не использовать, а выдернуть нужное себе в приложение — там на самом деле немного. Обратите внимание, что ключ маршрутизации вам нужно придумать самостоятельно и подписываться на тот же обменник с подходящим паттерном из фронтенда. Обменники, кстати, гем попытается по умолчанию создать с нужными опциями (durable, persistent, вот это вот всё), но в реальности вы скорее всего захотите их настройками рулить силами DevOps, а подключаться именно с опцией passive: true
22. Фронтенд! Подключение.
Для фронта я ничего не сделал. Сорян. Будет немного более многословно. Просто берём клиентскую библиотеку https://github.com/JSteunou/webstomp-client , подключаемся по вебсокетам к кролику (на бою естественно спрячьте его за Nginx, на нём же и аутентификацию можно сделать), и подписываемся на тот же самый exchange, что и на бэкенде.
23. Фронтенд! Подписка.
Обратите внимание на последнюю строчку — не забывайте всегда подтверждать полученные сообщения — если вы этого делать не будете (или у вас вылетит исключение перед этой строчкой), то у вас возникнет затык и сообщения приходить перестанут. Тщательно тестируйте приём и обработку сообщений. В качестве костыля ещё можно увеличить параметр prefetch выше единицы, но это поможет ненадолго.
24. Фронтенд! Опции подписки.
Ну и чтобы магия случилась, именно с фронта при подписке на exchange нужно передать целый ворох опций. Самые критично важные из них первые три — это включение подтверждения каждого сообщения, а так же указание, что нам нужна durable-очередь, которую не нужно удалять. И у каждой подписки должен быть уникальный идентификатор, не меняющейся между подключениями. Для веб-приложений сгенерируйте UUID для каждой вкладки и запишите в sessionStrorage. Имя очереди задавать не обязательно, но удобно при отладке — включите туда идентификатор вкладки. Опцию x-expires лучше не задавать здесь, а задавать на уровне RabbitMQ через политики. Важно: если будете менять опции, то меняйте и имя очереди (иначе будут ошибки)
25. Посмотрим в действии?
https://github.com/Envek/durable-websockets/tree/example
Вот простой пример, который присылает нам строчки из Евгения Онегина по вебсокетам в браузер. В левом углу Action Cable, в правом — описанное мной решение на базе RabbitMQ. Смотрите, мы отключаем Wi-Fi и строчки бежать перестают. Обратите внимание, что ActionCable практически сразу заметил, что связь пропала. Включаем Wi-Fi обратно и… в правом углу все пропущенные строки появляются и дальше появляются одна за другой. Вот и всё.
26. Твит про две проблемы распределённых систем: https://twitter.com/mathiasverraes/status/632260618599403520
Разумеется, не обойдётся здесь и без ложки дёгтя: сообщения будут до нас гарантированно приходить. Каждое. Но в некоторых случаях некоторые сообщения могут приходить дважды (или трижды или больше раз). Данная система по этой классификации получается at least once delivery. Ваш фронтенд должен быть к этому готов.
Впрочем, здесь может помочь самый высокий уровень подтверждения в протоколе MQTT, когда сервер ещё присылает дополнительный ACK о получении ACK’а от клиента, возможно, что с его помощью можно сделать Exactly Once. Пока не пробовал.
27. Is it web-scale?
http://underthehood.meltwater.com/blog/2016/09/01/rabbitmq-performance/
RabbitMQ известен, как не самая высокопроизводительная система на свете и действительно, на сервере средней руки можно получить производительность в районе десятков тысяч передаваемых соообщений в секунду, да и оперативной памяти хочет многие и многие гигабайты (а лучше десятки гигабайт). Это не решение для Highload, скажем прямо — если вы делаете eBay, а я сейчас как раз делаю проект для eBay у марсиан и у нас и мысли нет брать RabbitMQ — но проекты среднего размера с такими строгими требованиями к вебсокетам могут его использовать смело. Так что ответ на вопрос из заголовка — нет, пока это не web scale (недотягивает до /dev/null по производительности немного).
28. Как снаряжать в бой
Настройки по максимуму осуществляйте через утилиту администрирования RabbitMQ через системы управления конфигурациями, как я уже говорил, менять их на клиенте муторно.
Каждая подписка — это очередь в недрах RabbitMQ, которая потребляет оперативную память и диск (и процессор при публикации сообщений). Старайтесь ограничиться минимальным количеством подписок на клиента и задавайте им минимально возможное время истечения, чтобы кролик подчищал их за вами.
Оставьте себе возможность связать сессию клиента и очереди в RabbitMQ, для простоты отладки.
Следите за количеством unacked сообщений — их должно быть ноль. Если это не так — это звоночек, что где-то есть баг на фронте и где-то что-то подвисло.
29. Что дальше?
В любой непонятной ситуации не стесняйтесь читать документацию ко всем задействованным компонентам и библиотекам — там всё написано, немного разрозненно, но там правда всё есть.
● http://rubybunny.info/articles/durability.html
● https://www.rabbitmq.com/stomp.html
● http://www.rabbitmq.com/web-stomp.html
● http://jmesnil.net/stomp-websocket/doc/
30. Вопросы?
Долго ли, коротко ли, но это всё, спасибо за внимание, давайте сюда ваши каверзные вопросы.
Напишите их мне на [email protected]