Страницы

Поиск по вопросам

четверг, 5 декабря 2019 г.

Работа серверного приложения

#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! Ну... думаю, вы поняли, что это довольно обширная тема.

Комментариев нет:

Отправить комментарий