Как работает традиционный PHP (FPM)

FPM Model

Если бы PHP-серверное приложение было рестораном, то оно вероятно считалось бы элитным заведением, где один столик обслуживает один официант.

Каждый новый запрос к серверу обрабатывается отдельной PHP-VM, процессом или потоком, после чего состояние уничтожается. Эквивалентно тому, что официант обслуживает один столик, а потом увольняется или ему стирают память.

У такой модели есть преимущество: если в PHP происходит ошибка, утечка памяти, забытое соединение с базой данных, то она не влияет на другие запросы. Каждый запрос изолирован. А значит разработка проще, отладка проще, есть высокая толерантность к ошибкам.

Последние несколько лет PHP-сообщество пытается внедрить stateful модель, когда одна PHP-VM может обслуживать несколько запросов, сохраняя состояние между ними. Например, проект Laravel Octane, который использует Swoole или RoadRunner достигает лучшей производительности за счет сохранения состояния между запросами. Но это далеко не предел возможностей.

Увольнять официанта после каждого заказа слишком дорогое удовольствие. Из-за того, что блюда готовятся на кухне медленно, официант большую часть времени ожидает. То же самое происходит с PHP-FPM: PHP-VM простаивает. Происходит больше переключений контекста, больше накладных расходов на создание и уничтожение процессов или потоков, больше потребление ресурсов.

// Традиционный PHP-FPM
$user = file_get_contents('https://api/user/123');     // стоим и ждем 300ms
$orders = $db->query('SELECT * FROM orders');          // стоим и ждем 150ms
$balance = file_get_contents('https://api/balance');   // стоим и ждем 200ms

// Потратили: 650ms чистого ожидания
// Процессор простаивает. Память простаивает. Все ждут.

Конкурентность

Concurrency Model

Так как кухня не может мгновенно готовить блюда, и у официанта есть время ожидания между приготовлением, появляется возможность обработать заказы нескольких клиентов.

Такая схема способна работать достаточно гибко: Столик 1 сделал заказ из трёх блюд Столик 2 сделал заказ из двух блюд Официант приносит первое блюдо столику 1, а потом первое блюдо столику 2. А может быть, он успел принести два блюда первому столику и одно второму. Или наоборот!

Это и есть конкурентность (concurrency): разеделение одного ресурса (CPU) между разными логическими потоками выполнения, которые называются корутинами (coroutines).

use function Async\spawn;
use function Async\await;

// Запускаем все три запроса "конкурентно"
$userTask = spawn(file_get_contents(...), 'https://api/user/123');
$ordersTask = spawn($db->query(...), 'SELECT * FROM orders');
$balanceTask = spawn(file_get_contents(...), 'https://api/balance');

// Пока один запрос ждет ответа, делаем другие!
$user = await($userTask);
$orders = await($ordersTask);
$balance = await($balanceTask);

// Потратили: 300ms (время самого медленного запроса)

Конкурентность ≠ Параллелизм

Важно понимать разницу.

Конкурентность (concurrency) — как в True Async, JavaScript, Python:

Параллелизм (parallelism) — это многопоточность (Go):

Что дальше?

Теперь вы понимаете суть. Можно копать глубже: