В современном 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 мин.

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

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


  1. Fedcomp
    13.10.2016 01:34
    -3

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


    1. bat
      13.10.2016 09:06

      Как reactphp ведет себя при 10 тыс соединений?


  1. Slavenin999
    13.10.2016 08:36
    -1

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


    1. akalend
      13.10.2016 11:38
      +1

      А можно вообще заточить все под свои нужды, используя https://github.com/warmcat/libwebsockets будет быстрее и могу реализовать даже без Редиса, но надо ли оно, если трудозатраты на разработку lua скрипта ушло 1-2 дня.

      А теперь, подумай, как будешь не нагружая nginx проксировать на postHawk? Использовать другие порты, как -то не кошерно, и поднимать еще один сервер и пулить на поддомен, тоже как -то не вариант.


      1. Slavenin999
        13.10.2016 13:56

        Очень просто сервер запускается на указанном порту указанного домена и слушает его. Не вижу проблемы открыть один порт, на котором будет висеть демон.


        1. akalend
          13.10.2016 14:31

          Конечно, проблемы нет открыть еще один порт, но как бы правильно, что все что связано с HTTP было на 80 порту.


          1. Slavenin999
            13.10.2016 15:13

            Как говорит википедия

            WebSocket — протокол полнодуплексной связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени.

            Я согласен, что handshake идёт по http, но на этом он (http) и заканчивается. Поэтому не вижу ничего криминального слушать другой порт. А вообще дело ваше.


  1. foxmits
    13.10.2016 13:23
    +1

    Спасибо, как раз скоро придётся решать такую задачу. У меня вопрос есть. Сколько клиентов обслуживает и на какой машине это работает?


    1. akalend
      13.10.2016 13:43
      -1

      Честно говоря, у меня ответы от внешнего сервиса приходят не так быстро, по этому одновременно находится где-то 100-200 клиентов, специально не считал.

      машина: 2 процессора Xeon E5-2620v3 2.0 ГГц 12 ядер: nginx + php + redis + MySQL

      Я подумаю, как лучше провести нагрузочное тестирование, и сообщу результаты. Тут свои проблемы: подакшен нагружать нельзя, а на площадке разработчика — получатся другие данные. Очевидно, цель узнать, будет ли держать более 10К? Для этого сильный процессор не нужен… Попробую запустить туеву кучу клиентов…


  1. b0rmann
    14.10.2016 20:16
    -1

    mosquitto прекрасно поддерживает mqtt через вебсокеты. что ещё надо?


    1. akalend
      14.10.2016 20:22

      Ответ немного не в тему,
      в статье прямым текстом сказано, не хотелось бы вводить новую сущность, а редис уже активно используется, да и потом, mosquitto опять через nginx проксировать надо.

      Если бы можно было заменить редис, то я бы поменял его на Tarantool, и поженил бы Tarantool с web-сокетами.
      Tarantool и кеширование данных, и сервер очередей, и прямое общение с nginx и поиск по гео-индексу и все в одном флаконе