Быстрый пул для php+websocket без прослойки nodejs на основе lua+nginx

nginx + lua

Кратко: nginx не умеет пулить websockets, а php работает per request. Нужна прослойка которая будет держать открытыми вебсокеты и при поступлении данных соединяться с php (через тот же fastcgi) и отправлять обратно ответ.

update: Здесь не идётся про решения на php, так как по сравнению даже с nodejs, они гораздо медленнее.

Тема, как оказалось, не нова, исходники тянуться аж из 2014, но, тем не менее, информации о трюке, про который здесь пойдёт речь, крайне мало. Можете погуглить "websockets php". Усугубляется тема ещё тем, что найденные примеры реализации (два, точнее) не работают, включая тот, что в документации :)

Вот где-то внутри чувствовал, знал, что есть. Мне настолько давно хотелось иметь этот Middleware внутри Nginx, чтобы не использовать разные довольно медленные php библиотеки (раз и два) и обойти стороной однопоточность nodejs. А вебсокетов хочется много (и как можно больше), и чтобы лишние затраты на прослойку были поменьше. Так вот, дабы не плодить кучу машин с nodejs (в будущем при высоких нагрузках так и поступают обычно), воспользуемся тем, что предоставляет Nginx с некоторыми пристройками в виде lua + resty. Nginx+lua можно установить из пакета nginx-extras или же собрать самому. От Resty нам понадобятся только websockets. Скачиваем и закидываем содержимое каталога lib куда-нибудь себе в пути (у меня это /home/username/lib/lua/lib, а по-хорошему надо бы в /usr/local/share/lua).

Стандартно nginx+websockets работает так:

  1. Клиент соединяется с nginx
  2. Nginx проксирует в upstream/открывает прокси поток с другим сервером (Middle Server на основе nodejs + sockets.io например), обслуживающим websockets.
  3. Middle Server сервер кидает socket соединение в какой-нибудь слушатель событий типа epoll и ждёт данных.
  4. При получении данных, Middle Server сервер, в свою очередь, открывает Fastcgi соединение с php, ожидает и забирает ответ. Отправляет его в socket. Возвращает socket снова в ожидание данных.
  5. И так по кругу, пока не прийдёт специальный фрейм закрытия websocket.

Всё просто, кроме накладных расходов на ресурсы и однопоточность этого решения.

В предлагаемой схеме MiddleServer превращается в middleware внутри nginx. К тому же нет никакого ожидания Fastcgi, всю работу делает тот же epoll, к которому nginx доверяет открытый сокет, а тем временем поток nginx'a может заняться другими делами. Схема позволяет одновременно работать с кучей вебсокетов раскиданными по потокам.

Здесь приведу только упрощённый код, который относится к задаче без остальных настроек хостинга. Я не старался сделать правильными все заголовки за ненадобностью оных.

lua_package_path "/home/username/lib/lua/lib/?.lua;;";

server {
    # магия, которая держит вебсокет открытым столько, сколько нам надо внутри nginx
    location ~ ^/ws/?(.*)$ {
         default_type 'plain/text';
         # всё что надо здесь для веб сокета - это включить луа, который будет его хендлить
         content_by_lua_file /home/username/www/wsexample.local/ws.lua;
   }

   # а это магия, которая отдаёт ответы от php
   # я шлю только POST запросы, чтобы нормально передать json payload
   location ~ ^/lua_fastcgi_connection(/?.*)$ {
       internal; # видно только подзапросам внутри nginx
        fastcgi_pass_request_body       on;
        fastcgi_pass_request_headers    off;

        # never never use it for lua handler
        #include snippets/fastcgi-php.conf;

        fastcgi_param  QUERY_STRING       $query_string;
        fastcgi_param  REQUEST_METHOD     "POST"; # $request_method;
        fastcgi_param  CONTENT_TYPE       "application/x-www-form-urlencoded"; #вместо $content_type;
        fastcgi_param  CONTENT_LENGTH     $content_length;

        fastcgi_param  DOCUMENT_URI       "$1"; # вместо $document_uri
        fastcgi_param  DOCUMENT_ROOT      $document_root;
        fastcgi_param  SERVER_PROTOCOL    $server_protocol;
        fastcgi_param  REQUEST_SCHEME     $scheme;
        fastcgi_param  HTTPS              $https if_not_empty;

        fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
        fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

        fastcgi_param  REMOTE_ADDR        $remote_addr;
        fastcgi_param  REMOTE_PORT        $remote_port;
        fastcgi_param  SERVER_ADDR        $server_addr;
        fastcgi_param  SERVER_PORT        $server_port;
        fastcgi_param  SERVER_NAME        $server_name;

        fastcgi_param SCRIPT_FILENAME "$document_root/mywebsockethandler.php";
        fastcgi_param SCRIPT_NAME "/mywebsockethandler.php";
        fastcgi_param REQUEST_URI "$1"; # здесь вообще может быть что угодно. А можно передать параметр из lua чтобы сделать какой-нибудь роутинг внутри php обработчика.
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        fastcgi_keep_conn               on;
       
}

И код ws.lua:

    local server = require "resty.websocket.server"

    local wb, err = server:new{
        -- timeout = 5000,  -- in milliseconds -- не надо нам таймаут
        max_payload_len = 65535,
    }

    if not wb then
        ngx.log(ngx.ERR, "failed to new websocket: ", err)
        return ngx.exit(444)
    end

    while true do
        local data, typ, err = wb:recv_frame()

        if wb.fatal then return
        elseif not data then
            ngx.log(ngx.DEBUG, "Sending Websocket ping")
            wb:send_ping()
        elseif typ == "close" then
            -- send a close frame back:
            local bytes, err = wb:send_close(1000, "enough, enough!")
            if not bytes then
                ngx.log(ngx.ERR, "failed to send the close frame: ", err)
                return
            end
            local code = err
            ngx.log(ngx.INFO, "closing with status code ", code, " and message ", data)
            break;
        elseif typ == "ping" then
            -- send a pong frame back:

            local bytes, err = wb:send_pong(data)
            if not bytes then
                ngx.log(ngx.ERR, "failed to send frame: ", err)
                return
            end
        elseif typ == "pong" then
            -- just discard the incoming pong frame

        elseif data then

            -- здесь в пути передаётся реальный uri, а json payload уходит в body
            local res = ngx.location.capture("/lua_fastcgi_connection"..ngx.var.request_uri,{method=ngx.HTTP_POST,body=data})

            if wb == nil then
                ngx.log(ngx.ERR, "WebSocket instaince is NIL");
                return ngx.exit(444)
            end

            wb:send_text(res.body)

        else
            ngx.log(ngx.INFO, "received a frame of type ", typ, " and payload ", data)
        end
end

Что ещё можно с этим сделать? Замерить скорость и сравнить с nodejs :) А можно внутри lua делать запросы в Redis, MySQL, Postgres… проверять куки и прочие токены авторизации, обрабатывать сессии, кешировать ответы в memcached и потом быстро-быстро отдавать другим клиентам с одинаковыми запросами внутри websocket.

Известные мне недоработки: максимальный размер пакета данных по вебсокету 65Кб. При желании можно дописать разбитие на фреймы. Протокол не сложный.

Тестовый html (ws.html):

HTML тут
<!DOCTYPE>
<html>
<head>
	<meta charset="utf-8" />
<script type="text/javascript">
"use strict";
	let socket;

function tryWebSocket() {
	socket = new WebSocket("ws://try6.local/ws/");
	socket.onopen = function() {
	  console.log("Соединение установлено.");
	};

	socket.onclose = function(event) {
	  if (event.wasClean) {
	    console.log('Соединение закрыто чисто');
	  } else {
	    console.log('Обрыв соединения'); // например, "убит" процесс сервера
	  }
	  console.log('Код: ' + event.code + ' причина: ' + event.reason);
	};

	socket.onmessage = function(event) {
	  console.log("Получены данные " + event.data);
	};

	socket.onerror = function(error) {
	  console.log("Ошибка " + error.message);
	};
}

function tryWSSend(event) {
	let msg = document.getElementById('msg');
	socket.send(msg.value);
	event.stopPropagation();
	event.preventDefault();
	return false;
}

function closeWebSocket(event) {
	socket.close();
}
</script>
</head>

<body onLoad="tryWebSocket(event);return false;">
<form onsubmit="tryWSSend(event); return false;">
<button onclick="tryWebSocket(event); return false;">Try WebSocket</button>
<fieldset>
Message: <input value="Test message 4444" type="text" size="10" id="msg"/><input type="submit"/>
</fieldset>
<fieldset>
	<button onclick="closeWebSocket(event); return false;">Close Websocket</button><br/>
</fieldset>
</form>
</body>

</html>


Тестовый php (mywebsockethandler.php):

PHP тут
<?php
header("Content-Type: application/json; charset=utf-8");
echo json_encode(["status"=>"ok","response"=>"php websocket json @ ".time(), "payload"=>[$_REQUEST,$_SERVER]]);
exit;


Чтобы воспользоваться FastCGI для Lua, установите ещё одно Resty расширение.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 30
  • 0
    Есть nginx-push-stream модуль, который может работать через вебсокет
    • +1
      А он делает тоже самое, что и приведённый выше код на луа?
    • 0
      А можно внутри lua делать запросы в Redis, MySQL, Postgres…

      Тогда может и не нужен тут PHP?

      А так, никакие прослойки уже давненько не нужны. Есть Ratchet, Aerys и т.д.
      • 0
        Ratchet и Aerys на чём написаны? Пост про то, чтобы избавится от оверхеда, а не про то, что есть другие решения на php.
        • 0
          Простите, я реально не понимаю в чем оверхед решения в схеме nginx + aerys.
          Получаем чистый, не блокирующий код на PHP без Lua прослойки.
          • 0
            PHP не многопоточный. Каждый поток php — отдельный процесс со всеми вытекающими оверхедами. Плюс сам php в отличие от lua намного медленнее. PHP пусть занимается тем, для чего он нужен — обслуживание бизнес логики, а демоны должны работать как можно быстрее. Я так себе представляю.
            • 0
              Что простите? PHP 7+ медленнее чем Lua? Это в чем же он более медленный?
              • +2
                Я думаю, об этом даже спорить не нужно. А по потребляемым ресурсам так тем более.
                • 0
                  Ясно) На самом деле проверяется очень легко, берешь и сам пишешь тесты на сравниваемых языках и проверяешь скорость, и тогда ты увидишь правду, что быстрее, а что нет, и что сколько ресурсов поедает тоже. Я когда-то тоже доверял тестам из интернета, а потом начал сам перепроверять и результаты меня удивили. С тех пор любая проверка скорости, ресурсов и т.п, только самому, а не с помощью непонятных статей.

                  Можешь взять и элементарно сравнить:
                  1) Скорость наполнения массива
                  2) Скорость склеивания строк
                  3) Скорость чтения/записи файлов
                  4) Обход массива через foreach и через for
                  5) Серелизацию данных в JSON / из JSON'a

                  Это основная работа бэкендов, не считая обращения в базы данных.

                  Серьезно, не поленись, эти тесты на PHP и на Lua ты напишешь максимум минут за 20-30, а результаты тебя сильно удивят. Проверь именно скорость PHP 7.1 или 7.2 vs Lua
                  • 0
                    Вот даже результат из вашей статьи, где Lua проигрывает PHP разгромно.
                    image
                    • +1

                      LuaJit на 7 месте с 8 секундами

                      • +1
                        LuaJIT который используется в nginx, не чистый Lua
                  • 0
                    Под многопоточностью вы имеете в виду thread'ы? Если да, то в nginx'е воркеры это тоже не thread'ы, а процессы. Thread'ы внутри nginx'а, насколько я помню, используются только для доступа к файловой системе.
            • 0
              Так и не увидел преимущества решения над тем же nodejs. Разве что, нода может давать небольшой оверхед, кмк. Можете прояснить?
              • 0
                Многопоточность и минус оверхед, который даёт куча кода на ноде.
                • 0
                  Ну оверхед это еще явление неподтвержденное, это лишь мои догадки. Ни тестов, ни замеров в статье приведено не было, опять же не видно преимущества луа над нодой. Алсо, нода умеет в многопоточность, пускай и немного костыльными путями.
                • 0
                  Наверное удобство работы с привычным языком.
                  Или, когда сайт уже написан на PHP, цеплять к нему ноду и заного писать все механизмы, а потом поддерживать их… брр… А так все сразу работает :)
                  • 0
                    Так тут прослойка из луа получается. Да и нет смысла переписывать все на ноду, в крайнем случае, можно просто проксить на нее сокеты, собственно, как и написано у автора в общем решении вопроса. Просто мне кажется, что это какое-то шило на мыло, во всяком случае, из статьи я не вижу никаких плюсов.
                • 0
                  Если основная проблема, что php работает per request, почему бы не запустить его в режиме демона и не обрабатывать запросы по событиям?
                  • 0
                    Скорость и потребляемые ресурсы.
                    • 0
                      Что с ними не так?
                  • 0
                    Не очень понятно как в этой схеме решается ожидание событий от бекэнда (php в данном случае)
                    • 0
                      С помощью доступных способов хранения очередей/брокер сообщений. Собственно, это в любом случае решалось бы так, используя даже демон написанный на php.
                    • 0
                      Нужна прослойка которая будет держать открытыми вебсокеты и при поступлении данных соединяться с php (через тот же fastcgi) и отправлять обратно ответ.


                      В чём преимущество отправки сообщения в вебсокет, которое будет позже направлено в php? Не проще ли сразу через ajax отправлять запросы на сервер, а вебсокеты использовать для доставки сообщений с сервера в клиент а не на оборот.

                      Я делаю свой проект комет сервера на C++ который именно даёт апи для отправки сообщений от сервера клиенту. И не могу придумать ситуации где имеет смысл делать на оборот отправку от клиента на сервер не простыми ajax запросами а через вебсокеты добавляя ещё одну лишнею прослойку между отправителем и адресатом.
                      • 0
                        WebSocket имеет смысл при уведомлениях клиента об изменениях данных на сервере мимо схемы запрос-ответ. И если клиент уже держит открытое готовое соединение, то имеет смысл это соединение и использовать вместо ajax — экономим накладные расходы на установку http соединения
                      • 0
                        Хорошо написанный PHP код, чаще всего, работает медленно из-за разворачивания окружения по каждому запросу. (Имеется ввиду загрузка классов в память, сборка DIC и соединение с базами данных). Если вы хотите ускорить этот процесс — выходом может быть только демонизация приложения на PHP. Вопрос медленности AJAX запросов (установка соединения, отправка запроса, ожидание ответа, получение данных и закрытые соединения) решается созданием постоянного соединения WebSocket. Подход автора решает связь от клиента к серверу. Но WebSocket дает неоспоримое преимущество со связью от сервера к клиенту. Тут осмелюсь предложить решение написанное на pyton и использующий протокол WAMP — crossbar.io; С набором всевозможных библиотек (SDK) на разных языках (и PHP в часности github.com/voryx/Thruway)
                        • 0
                          чтобы не использовать разные довольно медленные php библиотеки (раз и два)
                          У вас тут обе ссылки на одну и ту же статью. К слову сказать, статья моя и в ней я не говорил, что библиотеки «довольно медленные». У меня есть и другая статья, в которой я сравнивал php, node.js и go и говорил, что node.js работает медленнее. Сам по себе «код на js в вакууме» работает в среднем быстрее php, но сетевая подсистема реализована хуже и работает сильно медленнее. Из-за чего итоговый код на js работает в полтора раза медленнее и к сожалению под высокими нагрузками не удалось избавиться от падений ни мне ни другим участникам конкурса. А при средних нагрузках лучшее решение на node.js было опять же в полтора раза медленнее чем моё решение на swoole.
                          В моей статье есть примеры кода на swoole, workerman + libevent, node.js — так что вы можете самостоятельно запустить тесты и убедиться что php может работать быстрее чем node.js, особенно для задач связанных с сетью.
                          • 0
                            Я не сильно понял смысл замера времени голого хендлинга сокетов на php без связки с ответами nginx, который обычно стоит за любым бэкэндом. У меня разогретый nginx+lua+php7 выдаёт 50-80ms в текущем примере.

                            Тест: 30 потоков по 50 повторов.
                            php 5 потоков
                            nginx 100 потоков
                            результатом просто вывод
                            json_encode([$_REQUEST,$_SERVER]);

                            • 0
                              Да, обычно nginx используется для бекенда, но он не обязателен ни для node.js, ни для swoole, ни для go. И не удивительно, что в топ100 не вошло ни одного решения на нём. Для хайлоад задач он может быть избыточен.
                              «30 потоков по 50 повторов» — это не хайлоад, в хайлоадкапе нагрузка была 10к повторов в секунду при 2к соединениях при этом ответ был порядка 0.1 милисекунды, что в 500-1000 раз быстрее вашего примера.
                              Дело в том что в вашем примере nginx+lua просто держит соединения, а php используется для обработки сообщений от клиентов, и php-скрипт запускается каждый раз заново, а это очень большие накладные расходы.
                              При написании обработчика на swoole, php-скрипт будет запущен только один раз, поэтому такой подход работает гораздо быстрее, чем на каждое сообщение запускать php-скрипт заново, даже если перед ним поставить nginx. В вашем случае для увеличения производительности лучше либо отказаться от php и делать всю обработку на lua, либо использовать библиотеки swoole/workerman.
                              Песочница работает ещё две недели, вы можете написать своё решение и сравнить результаты с другими.
                              • 0
                                Извините, это решение не для чемпионата по хайлоаду, под который пишется специализированное решение.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.