В современном WEB Конструировании очень часто возникают задачи, когда необходимо оповестить пользователя о каком-нибудь событии: пришло новое сообщение, изменился курс на бирже или статус заказа, с конвертировался видео-контент или подскочила температура больной бабушки.
Есть несколько вариантов решения такого класса задач. Наиболее оптимальное и распространенное решение – это подписка на события. Как это реализуется в нагруженных проектах?
Предположим, что мы разрабатываем сервис брокерской конторы, в которой обслуживаются тысячи клиентов. Для того, чтоб узнать состояние курса акций на бирже или узнать кол-во свободных мест в отелях, нам необходимо обратиться к одному или нескольким внешнем сервисам. Так как, внешний сервис отвечает с задержкой, а у нас тысячи клиентов, то в случае, если мы будем делать запросы на прямую из WEB приложения и ждать ответа от сервиса, то в результате всё повиснет.
Поэтому, нам приходиться делать, так называемый, отложенный запрос. Наше WEB приложение сразу возвращает пользователю сформированную HTML страницу без результата, на которой показывается заставка, что запрос выполняется, а результат приходит чуть позже, по мере его исполнения. Как это происходит?
Перед началом формирования HTML страницы, наше WEB приложение кладёт в очередь данные. Демон, или вызываемая по крону задача, просматривает очередь и забирает из неё данные. Далее, на основании этих данных, она формирует запрос и отправляет его внешнему сервису (картинка 1).
Вроде бы, всё в этой схеме хорошо – работает без задержек. Но нам нужна обратная связь.Конечному пользователю необходима та информация, которую он запрашивал. И вот, эту информацию мы получили в нашем крон скрипте. Теперь её необходимо переправить пользователю.
Тут нам поможет паттерн Издатель-Подписчик. Многим, кто использует JavaScript известна эта схема:
Подписчик (Subscriber) подписывается на некоторый канал, а при свершении некоторого события, Издатель (Producer) в этот канал отправляет сообщение. В качестве такого механизма уведомлений можно использовать много разных решений: Redis, RabbitMQ, Tarantool, MsMQ, ZMQ, Kafka (брокера сообщений). Так как у нас ряд сервисов уже был завязан на Redis, мы решили не вводить новые сущности.
Как бы вы это использовали? Тут найдется несколько вариантов, но специалисты сразу в три горла заявят “Для связи WEB страницы и сервера надо использовать websockets”. Не буду спорить, да, на сегодня – это наиболее продвинутая технология моментального общения WEB-клиента и сервера. Рассмотрим серверную сторону.
Ни для кого не открою секрета, что уже, как несколько лет как nginx умеет проксировать websockets. Если у нас в качестве бэкенда используется php-fpm, то на каждый запущенный WEB-клиент, у нас должен быть запущен PHP процесс. Тут возникает проблема 10К, когда на 10К запросовбудет висеть 10К процессов. Банально не хватит памяти. Как один из вариантов, можно использовать node.js. Это, как раз его класс задач, где используются долгоиграющие не блокируемые соединения.
А можно обойтись без него? Ведь, не хотелось бы вводить новую сущность, тем более на неё возлагаем очень простую задачу. Чем сложнее архитектура, тем больше точек отказа и меньше вероятность безотказной работы. У нас уже был положительный опыт внедрения модуля nginx-lua (Более подробнее про nginx-lua можно почитать тут и тут). А может ли он выполнить эти функции? В общем, в итоге получилась вот такая картина (картинка 2):
Оказывается это не так сложно. Дополнительно к lua-nginx-module подключаем lua-resty-redis и lua-resty-websocket. Для этого, в отличие от lua-nginx-module ни чего собирать не надо, а лишь все исходные коды модулей, которые находятся в директории lib переписать в папку: /usr/share/nginx/lua/lib и подключить директивой в контексте http (конфигурационный файл nginx.conf):
Далее, в конфигурационном файле nginx.conf (или подключаемом конфиге для нашего виртуального хоста) определяем location /ws:
Сам файл websocket_server.lua не такой уж и сложный, выкладывать тут частями — смысла не вижу. Его полную версию можно найти на github.
Для проверки есть тестовый консольный клиент, который можно доработать и запустить в несколько тысяч экземпляров с разным тайм-аутом и проверить на практике. Данная версия клиента диалоговая, из консоли вводится наименование канала, и если в канал направляется публикация, то она моментально поступает на клиент. Клиент имеет тайм-аут 5 мин.
Надеюсь, данная фича кому-нибудь пригодится.
Есть несколько вариантов решения такого класса задач. Наиболее оптимальное и распространенное решение – это подписка на события. Как это реализуется в нагруженных проектах?
Предположим, что мы разрабатываем сервис брокерской конторы, в которой обслуживаются тысячи клиентов. Для того, чтоб узнать состояние курса акций на бирже или узнать кол-во свободных мест в отелях, нам необходимо обратиться к одному или нескольким внешнем сервисам. Так как, внешний сервис отвечает с задержкой, а у нас тысячи клиентов, то в случае, если мы будем делать запросы на прямую из WEB приложения и ждать ответа от сервиса, то в результате всё повиснет.
Поэтому, нам приходиться делать, так называемый, отложенный запрос. Наше WEB приложение сразу возвращает пользователю сформированную HTML страницу без результата, на которой показывается заставка, что запрос выполняется, а результат приходит чуть позже, по мере его исполнения. Как это происходит?
Перед началом формирования HTML страницы, наше WEB приложение кладёт в очередь данные. Демон, или вызываемая по крону задача, просматривает очередь и забирает из неё данные. Далее, на основании этих данных, она формирует запрос и отправляет его внешнему сервису (картинка 1).
Вроде бы, всё в этой схеме хорошо – работает без задержек. Но нам нужна обратная связь.Конечному пользователю необходима та информация, которую он запрашивал. И вот, эту информацию мы получили в нашем крон скрипте. Теперь её необходимо переправить пользователю.
Тут нам поможет паттерн Издатель-Подписчик. Многим, кто использует JavaScript известна эта схема:
Подписчик (Subscriber) подписывается на некоторый канал, а при свершении некоторого события, Издатель (Producer) в этот канал отправляет сообщение. В качестве такого механизма уведомлений можно использовать много разных решений: Redis, RabbitMQ, Tarantool, MsMQ, ZMQ, Kafka (брокера сообщений). Так как у нас ряд сервисов уже был завязан на Redis, мы решили не вводить новые сущности.
Как бы вы это использовали? Тут найдется несколько вариантов, но специалисты сразу в три горла заявят “Для связи WEB страницы и сервера надо использовать websockets”. Не буду спорить, да, на сегодня – это наиболее продвинутая технология моментального общения WEB-клиента и сервера. Рассмотрим серверную сторону.
Ни для кого не открою секрета, что уже, как несколько лет как nginx умеет проксировать websockets. Если у нас в качестве бэкенда используется php-fpm, то на каждый запущенный WEB-клиент, у нас должен быть запущен PHP процесс. Тут возникает проблема 10К, когда на 10К запросовбудет висеть 10К процессов. Банально не хватит памяти. Как один из вариантов, можно использовать node.js. Это, как раз его класс задач, где используются долгоиграющие не блокируемые соединения.
А можно обойтись без него? Ведь, не хотелось бы вводить новую сущность, тем более на неё возлагаем очень простую задачу. Чем сложнее архитектура, тем больше точек отказа и меньше вероятность безотказной работы. У нас уже был положительный опыт внедрения модуля nginx-lua (Более подробнее про nginx-lua можно почитать тут и тут). А может ли он выполнить эти функции? В общем, в итоге получилась вот такая картина (картинка 2):
Оказывается это не так сложно. Дополнительно к lua-nginx-module подключаем lua-resty-redis и lua-resty-websocket. Для этого, в отличие от lua-nginx-module ни чего собирать не надо, а лишь все исходные коды модулей, которые находятся в директории lib переписать в папку: /usr/share/nginx/lua/lib и подключить директивой в контексте http (конфигурационный файл nginx.conf):
http {
lua_package_path "/usr/share/nginx/lua/lib/?.lua;;";
...
}
Далее, в конфигурационном файле nginx.conf (или подключаемом конфиге для нашего виртуального хоста) определяем location /ws:
location /ws {
content_by_lua_file /path/to/file/websocket_server.lua;
}
Сам файл websocket_server.lua не такой уж и сложный, выкладывать тут частями — смысла не вижу. Его полную версию можно найти на github.
Для проверки есть тестовый консольный клиент, который можно доработать и запустить в несколько тысяч экземпляров с разным тайм-аутом и проверить на практике. Данная версия клиента диалоговая, из консоли вводится наименование канала, и если в канал направляется публикация, то она моментально поступает на клиент. Клиент имеет тайм-аут 5 мин.
Надеюсь, данная фича кому-нибудь пригодится.