Ускоряем PHP (с ReactPHP)

6yka

Повелитель тьмы
Проверенный
Веселый флудер
Мастер реакций
Любитель реакций
Знаток письма высшего ранга
Знаток великого письма
Знаток письма
Знаток Lineage2
Старожил I степени
Победитель в номинации 2018
Победитель в номинации 2017
Медаль за активность на Форуме
За веру и верность форуму
Сообщения
1 730
Розыгрыши
0
Репутация
1 616
Реакции
1 219
Баллы
1 758
В этом посте я хотел бы поделиться не совсем обычным, для мира PHP, способе построения приложения, если угодно — архитектурой. Данный подход позволяет средствами PHP увеличить количество обрабатываемых запросов в разы. Так же я поделюсь своими наработками в этом направлении. Конечно данный подход не бесплатен, в плане требований к коду, но давайте всё по порядку.
статья на хабре, по этому не буду повторятся в описании этого подхода. Скажу только, что асинхронный подход позволяет нам создавать не только чат на websocket, но и запускать полноценный HTTP сервер, а это значит что нам не придётся инициализировать PHP на каждый запрос. Таким образом, не сложно догадаться, что такой подход сведёт на нет затрачиваемое время на старт различных фреймворков и в конечном счёте улучшиться время отклика.

Данное решение, как понятно из заголовка, построено на ReactPHP. Сам React это скорее инструмент для создания, а не готовое решение. Хотя в нём уже есть инструменты для обработки входящих Http соединений, а так же есть различные инструменты, например для работы с websockets или async redis, но нет реализованных привычных для современных фреймворков MVC патерна, роутинга и т.д. Для этих целей мы подключим ReactPHP к уже существующему Symfony2 приложению.

ReactPHP основывается на eventloop и для реализации этой архитектуры предлагает на выбор установить одну из ext-libevent, ext-libev, ext-event. В случае отказа, React работает через stream_select и возможности асинхронности сводятся к минимуму, т.к. по сути всё будет выполняться по очереди без возможности на прерывания процесса. Конечно, можно это опустить, т.к. по сути асинхронность, это и есть череда задач в пределах одного процесса. Но если функция будет использовать не блокирующие вызовы, то eventloop базирующийся на stream_select будет вынужден ждать выполнения этой функции, т.к. не возможности прервать функцию на время выполнения не блокирующего вызова, например к async-redis. Конечно это можно обойти разбиением функционала, но суть проблемы ясна.

Я сторонник нативных решений, и инсталляция pecl расширений туда не очень входит. К тому же установка pecl потребуется на всём парке серверов да и на хостингах будут проблемы. А ведь у PHP есть возможность реализации корутин средствами PHP 5.5. Благодаря замечательной статье от nikic (перевод на хабре), я решил впилить свою реализацию eventloop на основе описанного nikic планировщика задач. Да звучит не просто, и с непривычки действительно требует основательного изменения представления построения приложений на PHP. Но на мой взгляд за такими решениями будущее PHP.

Кстати Symfony был выбран не случайно. Реализация стека обработки входящих запросов Symfony, нам позволяет с лёгкостью работать не убивая PHP после каждого запроса. А пока я допиливал этот пост, предложения с подобной реализацией уже поступают на канале Symfony. И сами разработчики не скрывают, что подобное решение у них теплится в умах с начала запуска 2 версии.

Но давайте перейдём от слов к делу. Для начала нам потребуется ваш любимый Linux дистрибутив с установленными и настроенными nginx, php-cli 5.5.x, composer и вашим приложением на Symfony. Если у вас нет под рукой Symfony приложения, то можно взять голую инсталляцию с Symfony сайта, на которой и будет приведён пример. Если вам и composer не знаком, то вкратце можно ознакомится в моей статье к Satis.

Создаём новую папку, если проект уже есть то заходим в него:
mkdir fastapp && cd fastapp


Устанавливаем composer:
curl -sS | php


Ставим Symfony2.4.4:
php composer.phar create-project symfony/framework-standard-edition symfdir/ 2.4.4 && mv symfdir/* ./ && rm -fr symfdir


Получаем

Добавляем такие строчки в ваш composer.json:
{
"repositories": [
{ "type": "vcs", "url": " " },
{ "type": "vcs", "url": " " }
],
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"imunhatep/php-pm": "@dev"
}
}


Чтоб выглядело примерно так

Запускаем обновление пакетов:
php composer.phar update


Получаем

Подготавливаем Symfony cache:
php app/console cache:warmup --env=dev


И запускаем веб сервер, пока средствами только PHP и в одном экземпляре, тестовый так сказать. Порт можно подобрать по вкусу:
php bin/ppm start --workers 1 --port 8080


Проверяем, что всё работает открыв в любимом браузере localhost:8080. Должна открыться страничка приветствия от Symfony, правда картинки не покажутся и css неподгрузится. Таким образом мы получили PHP веб сервер, обрабатывающий и входящие запросы и не умирающий. Но у нас только 1 процесс, нет обработки статики и нет балансера. Как многие догадались, для этого нам и понадобится nginx.
222.png
Настраиваем nginx для проксирования динамических запросов на наш PHP сервер, попутно выполняя роль балансера, а статику отдавать без участия PHP:
upstream backend {
server 127.0.0.1:5501;
server 127.0.0.1:5502;
server 127.0.0.1:5503;
server 127.0.0.1:5504;
server 127.0.0.1:5505;
server 127.0.0.1:5506;
server 127.0.0.1:5507;
server 127.0.0.1:5508;
}

server {
root /path/to/symfony/web/;
server_name fastapp.com;

location / {
# try to serve file directly, fallback to rewrite
try_files $uri @rewriteapp;
}

location @rewriteapp {
if (!-f $request_filename) {
proxy_pass
break;
}
}
}


При этом server_name (fastapp.com) нужно прописать в /etc/hosts:
127.0.0.1 fastapp.com


Теперь чтоб нам не мучиться с ручным запуском n-количества процессов нашего PHP приложения (представленная nginx конф. настроена на n=8), заходим в папку нашего проекта и выполняем:
cp vendor/imunhatep/rephp/ppm.json ./


Подправляем ./ppm.json файлик:
{
"bootstrap": "\\PHPPM\\Bootstraps\\Symfony",
"bridge": "HttpKernel",
"appenv": "dev",
"workers": 8,
"port": 5501
}


Иногда после изменений требуется обновить кэш, возможно это только в моём случае, т.к. при написании статьи производил изменения в коде:
app/console cache:warmup --env=dev


Заново запускаем наш PHP Process Manager:
php bin/ppm start


Получаем в ответ:
8 slaves (5501, 5502, 5503, 5504, 5505, 5506, 5507, 5508) up and ready.


Сначала проверяем в браузере линк localhost:5501, если всё открылось то пробуем открыть fastapp.com. Должно всё открываться, с картинками и css.

Теперь можно жечь при помощи тулзы siege или ab, на выбор:
siege -qb -t 30S -c 128


Приведу несколько результатов тестирования своего (не helloworld) Symfony приложения, на девелоперской машине с AMD 8core, 8RAM и Fedora20.
Php 5.5.10, через nginx + php-fpm:

siege -qb -t 30S -c 128

Lifting the server siege... done.

Transactions: 983 hits
Availability: 100.00 %
Elapsed time: 29.03 secs
Data transferred: 4.57 MB
Response time: 0.91 secs
Transaction rate: 34.26 trans/sec
Throughput: 0.16 MB/sec
Concurrency: 124.23
Successful transactions: 983
Failed transactions: 0
Longest transaction: 1.81
Shortest transaction: 0.42


Php 5.5.10 с включенным OPcache, через nginx + php-fpm:

siege -qb -t 30S -c 128
Lifting the server siege... done.

Transactions: 5298 hits
Availability: 100.00 %
Elapsed time: 29.54 secs
Data transferred: 24.15 MB
Response time: 0.70 secs
Transaction rate: 179.35 trans/sec
Throughput: 0.82 MB/sec
Concurrency: 126.43
Successful transactions: 5298
Failed transactions: 0
Longest transaction: 1.68
Shortest transaction: 0.07


Php 5.5.10 с включенным OPcache, через nginx + ReactPHP + Coroutine eventloop:

siege -qb -t 30S -c 128
Lifting the server siege... done.

Transactions: 30553 hits
Availability: 100.00 %
Elapsed time: 29.85 secs
Data transferred: 157.63 MB
Response time: 0.12 secs
Transaction rate: 1023.55 trans/sec
Throughput: 5.28 MB/sec
Concurrency: 127.43
Successful transactions: 30553
Failed transactions: 0
Longest transaction: 0.76
Shortest transaction: 0.00


Увеличиваем количество параллельных запросов до 256.
Php 5.5.10 с включенным OPcache, через nginx + php-fpm

siege -qb -t 30S -c 256

siege aborted due to excessive socket failure;

Transactions: 134 hits
Availability: 10.48 %
Elapsed time: 1.58 secs
Data transferred: 0.78 MB
Response time: 1.21 secs
Transaction rate: 84.81 trans/sec
Throughput: 0.49 MB/sec
Concurrency: 102.93
Successful transactions: 134
Failed transactions: 1145
Longest transaction: 1.56
Shortest transaction: 0.00


К сожалению php-fpm свалился и отказался работать с лимитом в 32 процесса против 256 параллельных запросов.
Пробуем Php5.5.10 + ReactPHP + Coroutine eventloop

siege -qb -t 30S -c 256

Lifting the server siege... done.

Transactions: 29154 hits
Availability: 100.00 %
Elapsed time: 29.16 secs
Data transferred: 150.40 MB
Response time: 0.25 secs
Transaction rate: 999.79 trans/sec
Throughput: 5.16 MB/sec
Concurrency: 252.70
Successful transactions: 29154
Failed transactions: 0
Longest transaction: 3.66
Shortest transaction: 0.00


Заключение.

Идея запускать Symfony приложения через ReactPHP не моя, позаимствовал у Marc из его статьи, за что ему большое спасибо. Кстати он делал свои замеры и даже сравнивал с HHVM. Ниже приведён график из его статьи:
1.jpg
Мой вклад заключается в создании eventloop на основе работы nikic и допиливании менеджера процессов до, в целом, работоспособности, а также нюансов запуска ReactPHP с новым eventloop. Возможно с pecl event lib, будет это всё работать быстрее, не проверял. К сожалению мои текущие проекты не соответствуют требуемому качеству кода, вот наработка и пылится на полках «лаборатории», т.к. такой подход требует кода без ошибок. То есть PHP, по сути, не имеет права падать, а всеядность и динамика PHP ни как этому не способствует. Это можно исправить, дописав PHP PM, чтоб перезапускал упавшие процессы, а так же можно дописать отслеживание изменений в коде и также перезапускать процессы. Но пока не востребовано. Так же на этой базе можно запускать и websocket сервер. Что было в планах, но так там и осталось.

Оставлял такой сервер на все выходные под нагрузкой, утечек памяти не было. Есть одна проблема которую пока нет ни времени ни необходимости искать: по каким то причинам, после нагрузки, остаются не закрытыми 1-2 соединения. На малых нагрузка выявить причину не удаётся, а для больших нужно потратить время чтоб придумать как выявить причину. Пока что, добавил таймер, которые каждые 10 секунд проверяет текущие соединения на валидность (ресурс, не ресурс) и убивает мёртвые.

Еще стоит отметить, что приложение, в идеале, должно учитывать новые возможности асинхронности и прерывания (yield), а не выполнятся монолитно. Так же хорошо бы использовать не блокирующий функционал.
Автор статьи Bayer
 
Последнее редактирование модератором:

Назад
Сверху Снизу