В современном WEB Конструировании очень часто возникают задачи, когда необходимо оповестить пользователя о каком-нибудь событии: пришло новое сообщение, изменился курс на бирже или статус заказа, с конвертировался видео-контент или подскочила температура больной бабушки.

Есть несколько вариантов решения такого класса задач. Наиболее оптимальное и распространенное решение – это подписка на события. Как это реализуется в нагруженных проектах?

Предположим, что мы разрабатываем сервис брокерской конторы, в которой обслуживаются тысячи клиентов. Для того, чтоб узнать состояние курса акций на бирже или узнать кол-во свободных мест в отелях, нам необходимо обратиться к одному или нескольким внешнем сервисам. Так как, внешний сервис отвечает с задержкой, а у нас тысячи клиентов, то в случае, если мы будем делать запросы на прямую из WEB приложения и ждать ответа от сервиса, то в результате всё повиснет.

Поэтому, нам приходиться делать, так называемый, отложенный запрос. Наше WEB приложение сразу возвращает пользователю сформированную HTML страницу без результата, на которой показывается заставка, что запрос выполняется, а результат приходит чуть позже, по мере его исполнения. Как это происходит?

Перед началом формирования HTML страницы, наше WEB приложение кладёт в очередь данные. Демон, или вызываемая по крону задача, просматривает очередь и забирает из неё данные. Далее, на основании этих данных, она формирует запрос и отправляет его внешнему сервису (картинка 1).

image

Вроде бы, всё в этой схеме хорошо – работает без задержек. Но нам нужна обратная связь.Конечному пользователю необходима та информация, которую он запрашивал. И вот, эту информацию мы получили в нашем крон скрипте. Теперь её необходимо переправить пользователю.

Тут нам поможет паттерн Издатель-Подписчик. Многим, кто использует 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):

image

Оказывается это не так сложно. Дополнительно к 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 мин.

Надеюсь, данная фича кому-нибудь пригодится.