Тестирование производилось с помощью Yandex Tank.
В качестве приложения использовались Symfony 4 и PHP 7.2.
Целью являлось сравнение характеристик сервисов при разных нагрузках и нахождение оптимального варианта.
Для удобства все собрано в docker-контейнеры и поднимается с помощью docker-compose.
Под катом много таблиц и графиков.
Исходный код лежит тут.
Все примеры команд, описанные в статье, должны выполняться из директории проекта.
Приложение
Приложение работает на Symfony 4 и PHP 7.2.
Отвечает только на один роут и возвращает:
- случайное число;
- окружение;
- pid процесса;
- имя сервиса, с помощью которого работает;
- переменные php.ini.
Пример ответа:
curl 'http://127.0.0.1:8000/' | python -m json.tool
{
"env": "prod",
"type": "php-fpm",
"pid": 8,
"random_num": 37264,
"php": {
"version": "7.2.12",
"date.timezone": "Europe/Paris",
"display_errors": "",
"error_log": "/proc/self/fd/2",
"error_reporting": "32767",
"log_errors": "1",
"memory_limit": "256M",
"opcache.enable": "1",
"opcache.max_accelerated_files": "20000",
"opcache.memory_consumption": "256",
"opcache.validate_timestamps": "0",
"realpath_cache_size": "4096K",
"realpath_cache_ttl": "600",
"short_open_tag": ""
}
}
В каждом контейнере настроен PHP:
- включен OPcache;
- настроен bootstrap кеш с помощью composer;
- настройки php.ini соответствуют лучшим практикам Symfony.
Логи пишутся в stderr:
/config/packages/prod/monolog.yaml
monolog:
handlers:
main:
type: stream
path: "php://stderr"
level: error
console:
type: console
Кеш пишется в /dev/shm:
/src/Kernel.php
...
class Kernel extends BaseKernel
{
public function getCacheDir()
{
if ($this->environment === 'prod') {
return '/dev/shm/symfony-app/cache/' . $this->environment;
} else {
return $this->getProjectDir() . '/var/cache/' . $this->environment;
}
}
}
...
В каждом docker-compose запускаются три основных контейнера:
- Nginx — реверсивный прокси-сервер;
- App — подготовленный код приложения со всеми зависимостями;
- PHP FPM\Nginx Unit\Road Runner\React PHP — сервер приложения.
Обработка запросов ограничивается двумя инстансами приложения (по числу ядер процессора).
Сервисы
PHP FPM
Менеджер PHP процессов. Написан на C.
Плюсы:
- не нужно следить за памятью;
- не нужно ничего менять в приложении.
Минусы:
- на каждый запрос PHP должен инициализировать переменные.
Команда для запуска приложения с docker-compose:
cd docker/php-fpm && docker-compose up -d
PHP PPM
Менеджер PHP процессов. Написан на PHP.
Плюсы:
- инициализирует переменные один раз и затем использует их;
- не нужно ничего менять в приложении (есть готовые модули для Symfony/Laravel, Zend, CakePHP).
Минусы:
- нужно следить за памятью.
Команда для запуска приложения с docker-compose:
cd docker/php-ppm && docker-compose up -d
Nginx Unit
Сервер приложений от команды Nginx. Написан на С.
Плюсы:
- можно менять конфигурацию по HTTP API;
- можно запускать одновременно несколько инстансов одного приложения с разными конфигурациями и версиями языков;
- не нужно следить за памятью;
- не нужно ничего менять в приложении.
Минусы:
- на каждый запрос PHP должен инициализировать переменные.
Чтобы передать переменные окружения из файла конфигурации nginx-unit, необходимо поправить php.ini:
; Nginx Unit
variables_order=E
Команда для запуска приложения с docker-compose:
cd docker/nginx-unit && docker-compose up -d
React PHP
Библиотека для событийного программирования. Написана на PHP.
Плюсы:
- c помощью библиотеки можно написать сервер, который будет инициализировать переменные только один раз и дальше работать с ними.
Минусы:
- необходимо написать код для сервера;
- необходимо следить за памятью.
Если использовать для воркера флаг --reboot-kernel-after-request, то Symfony Kernel будет инициализироваться заново на каждый запрос. При таком подходе не нужно следить за памятью.
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Component\Debug\Debug;
use Symfony\Component\HttpFoundation\Request;
require __DIR__ . '/../config/bootstrap.php';
$env = $_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'dev';
$debug = (bool)($_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? ('prod' !== $env));
if ($debug) {
umask(0000);
Debug::enable();
}
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) {
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
}
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) {
Request::setTrustedHosts(explode(',', $trustedHosts));
}
$loop = React\EventLoop\Factory::create();
$kernel = new Kernel($env, $debug);
$kernel->boot();
$rebootKernelAfterRequest = in_array('--reboot-kernel-after-request', $argv);
/** @var \Psr\Log\LoggerInterface $logger */
$logger = $kernel->getContainer()->get('logger');
$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) use ($kernel, $logger, $rebootKernelAfterRequest) {
$method = $request->getMethod();
$headers = $request->getHeaders();
$content = $request->getBody();
$post = [];
if (in_array(strtoupper($method), ['POST', 'PUT', 'DELETE', 'PATCH']) &&
isset($headers['Content-Type']) && (0 === strpos($headers['Content-Type'], 'application/x-www-form-urlencoded'))
) {
parse_str($content, $post);
}
$sfRequest = new Symfony\Component\HttpFoundation\Request(
$request->getQueryParams(),
$post,
[],
$request->getCookieParams(),
$request->getUploadedFiles(),
[],
$content
);
$sfRequest->setMethod($method);
$sfRequest->headers->replace($headers);
$sfRequest->server->set('REQUEST_URI', $request->getUri());
if (isset($headers['Host'])) {
$sfRequest->server->set('SERVER_NAME', current($headers['Host']));
}
try {
$sfResponse = $kernel->handle($sfRequest);
} catch (\Exception $e) {
$logger->error('Internal server error', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
$sfResponse = new \Symfony\Component\HttpFoundation\Response('Internal server error', 500);
} catch (\Throwable $e) {
$logger->error('Internal server error', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
$sfResponse = new \Symfony\Component\HttpFoundation\Response('Internal server error', 500);
}
$kernel->terminate($sfRequest, $sfResponse);
if ($rebootKernelAfterRequest) {
$kernel->reboot(null);
}
return new React\Http\Response(
$sfResponse->getStatusCode(),
$sfResponse->headers->all(),
$sfResponse->getContent()
);
});
$server->on('error', function (\Exception $e) use ($logger) {
$logger->error('Internal server error', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
});
$socket = new React\Socket\Server('tcp://0.0.0.0:9000', $loop);
$server->listen($socket);
$logger->info('Server running', ['addr' => 'tcp://0.0.0.0:9000']);
$loop->run();
Команда для запуска приложения с docker-compose:
cd docker/react-php && docker-compose up -d --scale php=2
Road Runner
Web-сервер и менеджер PHP-процессов. Написан на Golang.
Плюсы:
- можно написать воркер, который будет инициализировать переменные только один раз и дальше работать с ними.
Минусы:
- необходимо написать код для воркера;
- необходимо следить за памятью.
Если использовать для воркера флаг --reboot-kernel-after-request, то Symfony Kernel будет инициализироваться заново на каждый запрос. При таком подходе не нужно следить за памятью.
#!/usr/bin/env php
<?php
use App\Kernel;
use Spiral\Goridge\SocketRelay;
use Spiral\RoadRunner\PSR7Client;
use Spiral\RoadRunner\Worker;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Component\Debug\Debug;
use Symfony\Component\HttpFoundation\Request;
require __DIR__ . '/../config/bootstrap.php';
$env = $_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'dev';
$debug = (bool)($_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? ('prod' !== $env));
if ($debug) {
umask(0000);
Debug::enable();
}
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) {
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
}
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) {
Request::setTrustedHosts(explode(',', $trustedHosts));
}
$kernel = new Kernel($env, $debug);
$kernel->boot();
$rebootKernelAfterRequest = in_array('--reboot-kernel-after-request', $argv);
$relay = new SocketRelay('/tmp/road-runner.sock', null, SocketRelay::SOCK_UNIX);
$psr7 = new PSR7Client(new Worker($relay));
$httpFoundationFactory = new HttpFoundationFactory();
$diactorosFactory = new DiactorosFactory();
while ($req = $psr7->acceptRequest()) {
try {
$request = $httpFoundationFactory->createRequest($req);
$response = $kernel->handle($request);
$psr7->respond($diactorosFactory->createResponse($response));
$kernel->terminate($request, $response);
if($rebootKernelAfterRequest) {
$kernel->reboot(null);
}
} catch (\Throwable $e) {
$psr7->getWorker()->error((string)$e);
}
}
Команда для запуска приложения с docker-compose:
cd docker/road-runner && docker-compose up -d
Тестирование
Тестирование производилось с помощью Yandex Tank.
Приложение и Yandex Tank были на разных виртуальных серверах.
Характеристики виртуального сервера с приложением:
Virtualization: KVM
CPU: 2 cores
RAM: 4096 МБ
SSD: 50 GB
Connection: 100MBit
OS: CentOS 7 (64x)
Тестируемые сервисы:
- php-fpm
- php-ppm
- nginx-unit
- road-runner
- road-runner-reboot (c флагом --reboot-kernel-after-request)
- react-php
- react-php-reboot (c флагом --reboot-kernel-after-request)
Для тестов 1000/1000 rps добавлен сервис php-fpm-80
Для него использовалась конфигурация php-fpm:
pm = dynamic
pm.max_children = 80
Yandex Tank заранее определяет, сколько раз ему нужно выстрелить в цель, и не останавливается, пока не кончатся патроны. В зависимости от скорости ответа сервиса время теста может быть больше, чем задано в конфигурации тестов. Из-за этого графики разных сервисов могут иметь разную длину. Чем медленнее отвечает сервис, тем длиннее будет его график.
Для каждого сервиса и конфигурации Yandex Tank проводился всего один тест. Из-за этого цифры могут быть неточными. Важно было оценить характеристики сервисов относительно друг друга.
100 rps
Конфигурация phantom Yandex Tank
phantom:
load_profile:
load_type: rps
schedule: line(1, 100, 60s) const(100, 540s)
Ссылки с детальным отчетом
- php-fpm https://overload.yandex.net/150666
- php-ppm https://overload.yandex.net/150670
- nginx-unit https://overload.yandex.net/150675
- road-runner https://overload.yandex.net/150681
- road-runner-reboot https://overload.yandex.net/151961
- react-php https://overload.yandex.net/150697
- react-php-reboot https://overload.yandex.net/152063
Перцентили времени ответа
95%(ms) | 90%(ms) | 80%(ms) | 50%(ms) | HTTP OK(%) | HTTP OK(count) | |
---|---|---|---|---|---|---|
php-fpm | 9.9 | 6.3 | 4.35 | 3.59 | 100 | 57030 |
php-ppm | 9.4 | 6 | 3.88 | 3.16 | 100 | 57030 |
nginx-unit | 11 | 6.6 | 4.43 | 3.69 | 100 | 57030 |
road-runner | 8.1 | 5.1 | 3.53 | 2.92 | 100 | 57030 |
road-runner-reboot | 12 | 8.6 | 5.3 | 3.85 | 100 | 57030 |
react-php | 8.5 | 4.91 | 3.29 | 2.74 | 100 | 57030 |
react-php-reboot | 13 | 8.5 | 5.5 | 3.95 | 100 | 57030 |
Мониторинг
cpu median(%) | cpu max(%) | memory median(MB) | memory max(MB) | |
---|---|---|---|---|
php-fpm | 9.15 | 12.58 | 880.32 | 907.97 |
php-ppm | 7.08 | 13.68 | 901.72 | 913.80 |
nginx-unit | 9.56 | 12.54 | 923.02 | 943.90 |
road-runner | 5.57 | 8.61 | 992.71 | 1,001.46 |
road-runner-reboot | 9.18 | 12.67 | 848.43 | 870.26 |
react-php | 4.53 | 6.58 | 1,004.68 | 1,009.91 |
react-php-reboot | 9.61 | 12.67 | 885.92 | 892.52 |
Графики
График 1.1 Среднее время ответа в секунду
График 1.2 Средняя нагрузка процессора в секунду
График 1.3 Среднее потребление памяти в секунду
500 rps
Конфигурация phantom Yandex Tank
phantom:
load_profile:
load_type: rps
schedule: line(1, 500, 60s) const(500, 540s)
Ссылки с детальным отчетом
- php-fpm https://overload.yandex.net/150705
- php-ppm https://overload.yandex.net/150710
- nginx-unit https://overload.yandex.net/150711
- road-runner https://overload.yandex.net/150715
- road-runner-reboot https://overload.yandex.net/152011
- react-php https://overload.yandex.net/150717
- react-php-reboot https://overload.yandex.net/152064
Перцентили времени ответа
95%(ms) | 90%(ms) | 80%(ms) | 50%(ms) | HTTP OK(%) | HTTP OK(count) | |
---|---|---|---|---|---|---|
php-fpm | 13 | 8.4 | 5.3 | 3.69 | 100 | 285030 |
php-ppm | 15 | 9 | 4.72 | 3.24 | 100 | 285030 |
nginx-unit | 12 | 8 | 5.5 | 3.93 | 100 | 285030 |
road-runner | 9.6 | 6 | 3.71 | 2.83 | 100 | 285030 |
road-runner-reboot | 14 | 11 | 7.1 | 4.45 | 100 | 285030 |
react-php | 9.3 | 5.8 | 3.57 | 2.68 | 100 | 285030 |
react-php-reboot | 15 | 12 | 7.2 | 4.21 | 100 | 285030 |
Мониторинг
cpu median(%) | cpu max(%) | memory median(MB) | memory max(MB) | |
---|---|---|---|---|
php-fpm | 41.68 | 48.33 | 1,006.06 | 1,015.09 |
php-ppm | 33.90 | 48.90 | 1,046.32 | 1,055.00 |
nginx-unit | 42.13 | 47.92 | 1,006.67 | 1,015.73 |
road-runner | 24.08 | 28.06 | 1,035.86 | 1,044.58 |
road-runner-reboot | 46.23 | 52.04 | 939.63 | 948.08 |
react-php | 19.57 | 23.42 | 1,049.83 | 1,060.26 |
react-php-reboot | 41.30 | 47.89 | 957.01 | 958.56 |
Графики
График 2.1 Среднее время ответа в секунду
График 2.2 Средняя нагрузка процессора в секунду
График 2.3 Среднее потребление памяти в секунду
1000 rps
Конфигурация phantom Yandex Tank
phantom:
load_profile:
load_type: rps
schedule: line(1, 1000, 60s) const(1000, 60s)
Ссылки с детальным отчетом
- php-fpm https://overload.yandex.net/150841
- php-fpm-80 https://overload.yandex.net/153612
- php-ppm https://overload.yandex.net/150842
- nginx-unit https://overload.yandex.net/150843
- road-runner https://overload.yandex.net/150844
- road-runner-reboot https://overload.yandex.net/152068
- react-php https://overload.yandex.net/150846
- react-php-reboot https://overload.yandex.net/152065
Перцентили времени ответа
95%(ms) | 90%(ms) | 80%(ms) | 50%(ms) | HTTP OK(%) | HTTP OK(count) | |
---|---|---|---|---|---|---|
php-fpm | 11050 | 11050 | 9040 | 195 | 80.67 | 72627 |
php-fpm-80 | 3150 | 1375 | 1165 | 152 | 99.85 | 89895 |
php-ppm | 2785 | 2740 | 2685 | 2545 | 100 | 90030 |
nginx-unit | 98 | 80 | 60 | 21 | 100 | 90030 |
road-runner | 27 | 15 | 7.1 | 3.21 | 100 | 90030 |
road-runner-reboot | 1110 | 1100 | 1085 | 1060 | 100 | 90030 |
react-php | 23 | 13 | 5.6 | 2.86 | 100 | 90030 |
react-php-reboot | 28 | 24 | 19 | 11 | 100 | 90030 |
Мониторинг
cpu median(%) | cpu max(%) | memory median(MB) | memory max(MB) | |
---|---|---|---|---|
php-fpm | 12.66 | 78.25 | 990.16 | 1,006.56 |
php-fpm-80 | 83.78 | 91.28 | 746.01 | 937.24 |
php-ppm | 66.16 | 91.20 | 1,088.74 | 1,102.92 |
nginx-unit | 78.11 | 88.77 | 1,010.15 | 1,062.01 |
road-runner | 42.93 | 54.23 | 1,010.89 | 1,068.48 |
road-runner-reboot | 77.64 | 85.66 | 976.44 | 1,044.05 |
react-php | 36.39 | 46.31 | 1,018.03 | 1,088.23 |
react-php-reboot | 72.11 | 81.81 | 911.28 | 961.62 |
Графики
График 3.1 Среднее время ответа в секунду
График 3.2 Среднее время ответа в секунду (без php-fpm, php-ppm, road-runner-reboot)
График 3.3 Средняя нагрузка процессора в секунду
График 3.4 Среднее потребление памяти в секунду
10000 rps
Конфигурация phantom Yandex Tank
phantom:
load_profile:
load_type: rps
schedule: line(1, 10000, 30s) const(10000, 30s)
Ссылки с детальным отчетом
- php-fpm https://overload.yandex.net/150849
- php-fpm-80 https://overload.yandex.net/153615
- php-ppm https://overload.yandex.net/150874
- nginx-unit https://overload.yandex.net/150876
- road-runner https://overload.yandex.net/150881
- road-runner-reboot https://overload.yandex.net/152069
- react-php https://overload.yandex.net/150885
- react-php-reboot https://overload.yandex.net/152066
Перцентили времени ответа
95%(ms) | 90%(ms) | 80%(ms) | 50%(ms) | HTTP OK(%) | HTTP OK(count) | |
---|---|---|---|---|---|---|
php-fpm | 11050 | 11050 | 11050 | 1880 | 70.466 | 317107 |
php-fpm-80 | 3260 | 3140 | 1360 | 1145 | 99.619 | 448301 |
php-ppm | 2755 | 2730 | 2695 | 2605 | 100 | 450015 |
nginx-unit | 1020 | 1010 | 1000 | 980 | 100 | 450015 |
road-runner | 640 | 630 | 615 | 580 | 100 | 450015 |
road-runner-reboot | 1130 | 1120 | 1110 | 1085 | 100 | 450015 |
react-php | 1890 | 1090 | 1045 | 58 | 99.996 | 449996 |
react-php-reboot | 3480 | 3070 | 1255 | 91 | 99.72 | 448753 |
Мониторинг
cpu median(%) | cpu max(%) | memory median(MB) | memory max(MB) | |
---|---|---|---|---|
php-fpm | 5.57 | 79.35 | 984.47 | 998.78 |
php-fpm-80 | 85.05 | 92.19 | 936.64 | 943.93 |
php-ppm | 66.86 | 82.41 | 1,089.31 | 1,097.41 |
nginx-unit | 86.14 | 93.94 | 1,067.71 | 1,069.52 |
road-runner | 73.41 | 82.72 | 1,129.48 | 1,134.00 |
road-runner-reboot | 80.32 | 86.29 | 982.69 | 984.80 |
react-php | 73.76 | 82.18 | 1,101.71 | 1,105.06 |
react-php-reboot | 85.77 | 91.92 | 975.85 | 978.42 |
График 4.1 Среднее время ответа в секунду
График 4.2 Среднее время ответа в секунду (без php-fpm, php-ppm)
График 4.3 Средняя нагрузка процессора в секунду
График 4.4 Среднее потребление памяти в секунду
Итоги
Здесь собраны графики, отображающие изменение характеристик сервисов в зависимости от нагрузки. При просмотре графиков стоит учитывать, что не все сервисы ответили на 100% запросов.
График 5.1 95% перцентиль времени ответа
График 5.2 95% перцентиль времени ответа (без php-fpm)
График 5.3 Максимальная нагрузка процессора
График 5.4 Максимальное потребление памяти
Оптимальным решением (без изменения кода), на мой взгляд, является менеджер процессов Nginx Unit. Он показывает хорошие результаты в скорости ответа и имеет поддержку компании.
В любом случае подход к разработке и инструменты нужно выбирать индивидуально в зависимости от ваших нагрузок, ресурсов сервера и возможностей разработчиков.
UPD
Для тестов 1000/1000 rps добавлен сервис php-fpm-80
Для него использовалась конфигурация php-fpm:
pm = dynamic
pm.max_children = 80
Комментарии (40)
mmasiukevich
14.01.2019 23:08+1небольшой оффтоп:
Тот же reactphp используется дай бог на треть возможностей. Мало просто всё запихнуть в event loop, надо ещё гарантировать, что этот самый loop ничего не блокирует.
Именно поэтому он абсолютно не годится для всяких фреймворков. Воткнуть можно, но вреда будет больше, чем пользы (ну только в hello world'ах всё красиво)greabock
14.01.2019 23:32Смотря как этот react готовить. Тот же php-pm отлично работает.
mmasiukevich
14.01.2019 23:40+1Отлично — понятие довольно расплывчатое.
У того же php-pm под капотом будут ровно те же проблемы с блокирующим io. Ну т.е. мы не получаем эффекта от кооперативной многозадачности. Только минус к бутстрапингу.
Но, да, в случае с php-pm не будет одного из ключевых минусов reactphp с заблокированным лупом. Главным образом за счёт воркеров
Arbane
15.01.2019 00:07Выводов можно было сделать больше. Вроде: прощай php-fpm.
mikevmk
15.01.2019 02:01Ну прощаться, конечно, рановато еще. Но начало заката — да, всё отчетливее
mmasiukevich
15.01.2019 02:09PHP так или иначе будет развиваться в сторону демонов. Вопрос лишь в том, какой ценой.
Если взять в рассчёт не простое echo, а +\- вменяемое приложение, то там есть ещё запас (php-pm и road runner не выгребают всё). На сколько разумно его использовать, глядя на «конкурентов» — хз, ведь это потребует очень большого разворота в головах php программистов.
Наверное, проще всё же взять какой-нибудь kotlin и не страдать.
Gemorroj
15.01.2019 11:58до выхода php 8 с предполагаемой искоробочной асинхронностью основной менеджер процессов таки php-fpm. остальное — частные случаи для небольших приложений, где таки реально уследить за памятью и блокировками.
OnYourLips
15.01.2019 22:00+1Вы перед тем, как выводы делать, посмотрели бы лучше на конфиг php-fpm.
Он кривой наглухо, результат с таким конфигом не удивителен.
SerafimArts
15.01.2019 03:37На моей практике Swoole работает в 3-4 раза быстрее React PHP (увы, только их и сравнивал), кушая примерно на 1/4 меньше оперативки. Очень жаль, что его нет в бенчмарках, но подборка действительно крутая, спасибо.
shandy
15.01.2019 09:37Да Swoole крутая штука, с корутинами и асинхронным io, было бы интересно впихнуть сравнение в этот бенчмарк.
Вот только мой бенчмарк FPM vs Swoole (zend-expressive hello world) показал скорость выше всего на 20% (1000RPS). Возможно это было связано с мощным i7 процом (4/8).SerafimArts
15.01.2019 15:43Вот только мой бенчмарк FPM vs Swoole (zend-expressive hello world) показал скорость выше всего на 20% (1000RPS). Возможно это было связано с мощным i7 процом (4/8).
Блин, я очепятался, простите. Конечно же я хотел написать «не треть/на четверть», а по привычке написал «в 3-4 раза». Посыпаю голову пеплом.
iproger
15.01.2019 04:24В статье странно указан диск
HDD: 50 GB SSD
В общем, плохие новости про php последнее время. Но в случае fpm я не вижу проблемы. Да 80% сайтов до сих пор работают на медленном апаче. Единицы сайтов хорошо оптимизированы.
Для них это все не грозит.
e45456
15.01.2019 09:26+1За счет чего unit такой быстрый? Чем он принципиально отличается от php-fpm?
mrsuh
15.01.2019 09:28-1Для каждого языка, в том числе и PHP, собирается модуль для работы с nginx unit. Наверняка в таком модуле есть оптимизации и кеширование.
github.com/nginx/unit/blob/db631917190c44b3b55a15e4e5e88aa92e6b5334/src/nxt_php_sapi.c
VBart
15.01.2019 17:18У него другая архитектура. Я рассказывал об этом в докладе на БИФ2018: yadi.sk/i/yczMbKccJ7Ujkg (где-то с 16 минуты).
igormwd
15.01.2019 11:03Скажите, а в настройках PHP-FPM (www.conf) была проведена оптимизация этих параметров:
pm.max_children
pm.start_servers
pm.min_spare_servers
pm.max_spare_servers
А то чувствуется, что они остались по-умолчанию установленными…mrsuh
15.01.2019 11:05+1Все конфиги лежат в репо, так что вы можете сами их посмотреть для каждого сервиса.
Конкретно для php-fpm конфигурация тут:
github.com/mrsuh/php-load-test/blob/master/docker/php-fpm/php-fpm/php-fpm.confquantum
15.01.2019 11:13max_children=2…
mrsuh Автор
15.01.2019 11:20все верно, как написано в статье:
Обработка запросов ограничивается двумя инстансами приложения (по числу ядер процессора).
quantum
15.01.2019 11:27Вот только max_children никогда не ставят равным количеству ядер. Кроме самой обработки запроса есть еще io операции, во время которых процессор простаивает. В это время могли обрабатываться другие воркеры. Первая ссылка из гугла по настройке hcbogdan.com/php/2016/09/16/php-fpm-dynamic
mrsuh Автор
15.01.2019 11:29Хотелось сделать условия для всех более менее одинаковые, поэтому такие настройки. В других сервисах тоже по 2 воркера.
quantum
15.01.2019 11:31Условия — утилизировать железо, fpm не утилизировал. Разные подходы — разные настройки
pmurzakov
15.01.2019 14:15Условия — утилизировать железо, fpm не утилизировал. Разные подходы — разные настройки
Поддерживаю. Бенчмарк, имеющий условие, что какая-то настройка (у всех значащая разное) равна двум, не имеет смысла.
mrsuh может быть добавите правильно настроенный PHP-FPM к результатам?
mrsuh Автор
15.01.2019 15:45Добавил сервис php-fpm-80 c
pm.max_children = 80
fisher
15.01.2019 16:41ну а графики все построены со старыми настройками?
mrsuh Автор
15.01.2019 16:47Заново построены все графики, где в легенде присутствует сервис php-fpm-80
Графики: 3.1, 3.3, 3.4, 4.1, 4.2, 4.3, 4.4fisher
15.01.2019 17:01Да, где-то есть 80 где-то нет — понял, спасибо.
Идея теста и исполнение интересные, но вообще, похоже, вы померили менеджеры процессов со слишком простым кодом приложения, и скорее всего бОльшую часть процессорного времени в вашем тесте занимает работа менеджера, а не кода приложения. В то время как практическую ценность имел бы тест: как менеджер процессов ускоряет приложение, которое само по себе в режиме php-fpm ожирает хотя бы 0.05 процессорных секунд (не за счет моделей обработки соединений, а за счёт экономиях на инициализациях, например). Но здесь наверное основная проблема в том, что «правильный» тест нельзя сделать одним и тем же кодом.quantum
15.01.2019 17:45>вы померили менеджеры процессов со слишком простым кодом приложения
Так и есть. Я как раз на днях тоже заценил, что простое приложение с парой запросов к бд и рендерингом шаблона не выдает те тысячи rps, которые декларируют авторы фреймворков
igormwd
16.01.2019 09:20Мало будет один параметр поправить, нужно все зависимые тоже править:
pm.max_children — максимальное количество дочерних процессов
pm.start_servers — количество процессов при старте
pm.min_spare_servers — минимальное количество процессов, ожидающих соединения (запросов для обработки)
pm.max_spare_servers — максимальное количество процессов, ожидающих соединения (запросов для обработки)
В этой статье есть пример как это сделать: hcbogdan.com/php/2016/09/16/php-fpm-dynamic
igormwd
15.01.2019 13:26Это совсем не правильно! В вашем случае должно быть 80!
pm.max_children — максимальное количество дочерних процессов
pm.start_servers — количество процессов при старте
pm.min_spare_servers — минимальное количество процессов, ожидающих соединения (запросов для обработки)
pm.max_spare_servers — максимальное количество процессов, ожидающих соединения (запросов для обработки)
Это же видно как на 1000 rps начал захлебываться — 72627
Lachezis
15.01.2019 11:15Спасибо, очень грамотный бенчмарк.
Будем изучать аномалию с rr-reboot выше 1000rps, в теории поведение должно быть такое-же как и reactphp-reboot.
pupsegadm
15.01.2019 19:11Коллеги, не слова об ondemand, а только про dynamic. Почему?
FPM пока держит рынок, но закат близко.sirmax123
16.01.2019 12:00мой опыт говорит что при пиковых нагрузках никаких динамических пре-форков, хоть динамик хоть он-деманд
нафоркать столько воркеров сколько тянет машина и с ними жить — иначе при резком скачке нагрузки все помрет
Berkof
16.01.2019 07:39-2Чтобы тестировать производительность на единичных запусках на единичных виртуальных серваках надо быть очень большим оптимистом.
Nickmob
16.01.2019 09:46Если интересно, недавно запилил собственный бенчмарк с PHP-FPM, Apache mod_php и Nginx Unit. Спойлер: результаты отличаются от этой статьи.
Видео бенчмарка
kricha
del.