#php #nodejs #многопоточность #веб_сервер #процесс
Я, допустим, написал одно веб-приложение на php, которое располагается на моем сервере.
То есть имеется один код. Но клиентов ведь много.
Допустим, сразу 10 человек подключаются к моему серверу. И как это один единственный
код обрабатывает запросы всех человек сразу? Он ведь это делает одновременно.
Я слышал, это связано с потоками и процессами, какими-то экземплярами. В общем. объясните
это, пожалуйста.
А ещё объясните, чем архитектура php приложения отличается от архитектуры node.js
приложения. Слышал, разница в каких-то потоках, то есть в их количестве.
Ответы
Ответ 1
Есть HTTP. Он работает по схеме "клиент послал запрос @ сервер ответил".
В случае с PHP обычно есть вебсервер. Многопоточностью занимается обычно именно он.
Он принимает запрос и передаёт его интерпретатору PHP.
Если вебсервер использует CGI, то получив запрос, он запускает новый процесс интерпретатора
PHP, передаёт ему параметры запроса, передаёт выведенные интерпретатором данные обратно
клиенту и дожидается, пока процесс закончит работу. Параллельная обработка может происходить
за счёт того, что вебсервер контролирует несколько процессов интерпретатора PHP.
Запуск каждый раз нового процесса потребляет оперативную память, разумеется. И время.
Припоминаю случаи, когда слишком много параллельных клиентов приводили к слишком большому
потреблению ОЗУ и слабопредсказуемым последствиям.
Если вебсервер использует FastCGI, то он заранее, при запуске, поднимает набор процессов
интерпретатора PHP в особом режиме. В наборе каждый интерпретатор может быть занят
или свободен.
Вебсервер, получив запрос, берёт из своего набора свободный процесс интерпретатора
(делая его занятым), сообщает ему (с помощью сокетов) параметры запроса, и выдаваемый
ответ передаёт пользователю, после чего освобождает интерпретатор.
По сравнению с CGI, процессы больше не завершаются/запускаются на каждый запрос,
что уже неплохо. И процессов конечное количество. Если каждый из них ограничить по
памяти, они будут потреблять предсказуемое количество ресурсов.
Но осталась другая проблема, актуальная и для CGI: код приложения обычно перед непосредственно
обработкой запроса выполняет некоторую подготовительную работу: загружает библиотеки,
настройки, создаёт системные объекты. Это довольно внушительная часть кода, которая
для разных запросов приводит к одним и тем же результатам. Но эти результаты не сохраняются,
а выбрасываются в никуда после каждого запроса. Не круто.
На этой ноте выходим из мира PHP, пропуская экзотические способы заставить PHP обслуживать
запросы, и переходим к более интересным темам. Про что вы там спрашивали... NodeJS! Окей.
JavaScript исторически достаточно сильно привязан к циклу событий. Сам по себе он
не многопоточен, программы на JS принято разделять на небольшие порции, между которыми
делать всё равно нечего и можно дать поработать кому-то ещё. Поэтому очень часто код
на NodeJS "общается с внешним миром" асинхронно: он не просто говорит "сделай Х", а
"сделай Х, и когда получишь результат, сообщи в Y, а я пойду отдыхать".
Действий, которые полезно выполнять асинхронно в мире веб-сайтов довольно много:
ожидание поступления данных от клиента, ожидание результатов от базы данных, ожидание
записи в файл, и прочая, прочая.
Вебсервер в Node.js собственный, встроенный. И поскольку он управляется непосредственно
из интерпретатора, с контролем над тем, что выполнить до и после, можно заранее один
раз подготовить объекты и выполнять запросы, не конструируя их по новой каждый раз.
Внутри Node.js крутится "цикл событий" вокруг "очереди событий" (упрощённо). Цикл
событий: {берёт очередное событие из очереди, выполняет связанное с ним действие} и
так по кругу, причём действия могут добавлять новые записи в очередь событий.
Когда запросы поступают по очереди, очередь событий не раздувается более чем на 1
запись и порядок выполнения кода в целом похож на обычный:
приходит запрос (в очередь!)
цикл событий обнаружиает пришедший запрос (из очереди!)
вызвал обработчик f для запроса
обработчик f попросил выполнить запрос в БД (в очередь!)
а результат передать в обработчик g
запрос в БД выполняется, ожидание, очередь событий пуста
но есть обработчики, поэтому процесс ещё не закончен
в цикл событий пришло уведомление о завершении запроса (из очереди!)
вызвал обработчик g для результатов запроса
обработчик g на основе результатов запроса составил страницу и отправил пользователю
Теперь вопрос: как Node.js выполняет несколько запросов параллельно?
Ответ: так только кажется. Просто из-за асинхронности код запроса разбит на отдельные
небольшие кусочки, между которыми допускается ожидание. Если запросов мало, то это
реально ожидание, а если много, то во время ожидания можно делать что-то полезное.
Выше описан типичный запрос простенького веб-приложения, где асинхронных действий
всего два: получение запроса от вебсервера и запрос к БД. На практике их обычно больше.
В то время, как БД выполняет запрос для первого клиента, может прийти запрос от второго.
Если событие о приходе второго запроса придёт раньше результатов из БД для первого,
то Node.js:
сначала обработает второй запрос (2.II)
отправит второй запрос в БД (3.II)
получит данные из БД к первому запросу (5.I), сформирует и запишет ответ первому
(если запросы в БД более-менее одинаковые и новых запросов нет)
получит данные из БД ко второму запросу (5.II), сформирует и запишет ответ второму
И выходит, что в каждый момент времени выполняется только одно действие, но при этом
один процесс в каждый момент времени может обслуживать несколько клиентов. И допускаются
долгие запросы отдельных клиентов. К примеру, не по HTTP а, скажем, Websockets.
В CGI и FastCGI, теоретически обслуживание Websocket-соединения занимает целый процесс
и не даёт ему заняться ничем другим. N процессов, создаём N соединений и ба-бах --
на запросы отвечать больше некому. Вебсервер из Node.js и другие событийные вебсерверы
этим не страдают.
Конечно, этого может быть недостаточно. С многоядерными процессорами даже процессы
Node.js может иметь смысл поднимать не поодиночке, а сразу наборами. Но какая-то единая
точка должна будет раскидывать по ним запросы от клиентов. Балансировщик нагрузки.
nginx, HAProxy, Varnish или что-то ещё.
Встроенный в язык вебсервер может быть и менее умным: к примеру, есть язык Ruby,
в нём принято использовать интерфейс Rack (в котором обработчик запроса -- одна здоровенная
функция из кучи частей). Этот интерфейс реализуется вебсервером Unicorn. Получив запрос,
он вызывает обработчик-функцию и отдаёт возвращённое ею значение.
Разумеется, каждый процесс Unicorn может обслуживать только один запрос. И если он
торчит "наружу" (клиенты общаются непосредственно с ним), может возникнуть проблема,
если клиент медленный: Unicorn будет терпеливо ждать, пока клиент примет ответ, сколько
бы это времени ни заняло, пока клиент кажется живым.
N процессов, N медленных клиентов и вуаля -- запросы обслуживать больше некому.
Поэтому Unicorn всегда размещают "наружу" за балансировщиком нагрузки, потому что
у балансировщика есть собственный буфер в который Unicorn может быстро выкинуть ответ
и приступить к следующему запросу, пока балансировщик своим эффективным вводом/выводом
(асинхронном событийным) вдалбливает ответы даже в медленных клиентов, освободив от
неблагодарной работы Unicorn.
...так, сколько килобайт я уже тут понаписал... Почти 7! Ну... думаю, вы поняли,
что это довольно обширная тема.