Web-Оповещения в нагруженных проектах

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

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

Предположим, что мы разрабатываем сервис брокеркой конторы, в которой обслуживются тысячи клиентов. Для того, чтоб узнать состояние курса акций на бирже или узнать кол-во свободных мест в отелях, нам необходимо обратиться к одному или нескольким внешнем сервисам. Так как, внешний сервис отвечает с задержкой, а у нас тысячи клиентов, то в случае, если мы будем делать запросы на прямую из 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 мин.

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

Комментарии (3)

  • 13 октября 2016 в 01:34

    –1

    > А можно обойтись без него?
    Да можно, например взять http://reactphp.org/
    • 13 октября 2016 в 09:06

      0

      Как reactphp ведет себя при 10 тыс соединений?
  • 13 октября 2016 в 08:36

    0

    А можно воспользоваться специализированным решением, например, https://github.com/postHawk/ и не нагружать nginx

© Habrahabr.ru