Хабрахабр

Сравниваем PHP FPM, PHP PPM, Nginx Unit, React PHP и RoadRunner

2.
Целью являлось сравнение характеристик сервисов при разных нагрузках и нахождение оптимального варианта.
Для удобства все собрано в docker-контейнеры и поднимается с помощью docker-compose.
Под катом много таблиц и графиков.
Тестирование производилось с помощью Yandex Tank.
В качестве приложения использовались Symfony 4 и PHP 7.

Исходный код лежит тут.
Все примеры команд, описанные в статье, должны выполняться из директории проекта.

Приложение

2. Приложение работает на Symfony 4 и PHP 7.

Отвечает только на один роут и возвращает:

  • случайное число;
  • окружение;
  • pid процесса;
  • имя сервиса, с помощью которого работает;
  • переменные php.ini.

Пример ответа:

curl 'http://127.0.0.1:8000/' | python -m json.tool

}

В каждом контейнере настроен PHP:

Логи пишутся в 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

Написан на C. Менеджер PHP процессов.

Плюсы:

  • не нужно следить за памятью;
  • не нужно ничего менять в приложении.

Минусы:

  • на каждый запрос 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

Написан на Golang. Web-сервер и менеджер PHP-процессов.

Плюсы:

  • можно написать воркер, который будет инициализировать переменные только один раз и дальше работать с ними.

Минусы:

  • необходимо написать код для воркера;
  • необходимо следить за памятью.

При таком подходе не нужно следить за памятью. Если использовать для воркера флаг --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 МБ
HDD: 50 GB SSD
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)

В зависимости от скорости ответа сервиса время теста может быть больше, чем задано в конфигурации тестов. Yandex Tank заранее определяет, сколько раз ему нужно выстрелить в цель, и не останавливается, пока не кончатся патроны. Чем медленнее отвечает сервис, тем длиннее будет его график. Из-за этого графики разных сервисов могут иметь разную длину.

Из-за этого цифры могут быть неточными. Для каждого сервиса и конфигурации Yandex Tank проводился всего один тест. Важно было оценить характеристики сервисов относительно друг друга.

100 rps

Конфигурация phantom Yandex Tank

phantom: load_profile: load_type: rps schedule: line(1, 100, 60s) const(100, 540s)

Ссылки с детальным отчетом

Перцентили времени ответа

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.

2 Средняя нагрузка процессора в секунду График 1.

3 Среднее потребление памяти в секунду График 1.

500 rps

Конфигурация phantom Yandex Tank

phantom: load_profile: load_type: rps schedule: line(1, 500, 60s) const(500, 540s)

Ссылки с детальным отчетом

Перцентили времени ответа

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

Графики

1 Среднее время ответа в секунду График 2.

2 Средняя нагрузка процессора в секунду График 2.

3 Среднее потребление памяти в секунду График 2.

1000 rps

Конфигурация phantom Yandex Tank

phantom: load_profile: load_type: rps schedule: line(1, 1000, 60s) const(1000, 60s)

Ссылки с детальным отчетом

Перцентили времени ответа

95%(ms)

90%(ms)

80%(ms)

50%(ms)

HTTP OK(%)

HTTP OK(count)

php-fpm

11050

11050

9040

195

80.67

72627

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-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

Графики

1 Среднее время ответа в секунду График 3.

2 Среднее время ответа в секунду (без php-fpm, php-ppm, road-runner-reboot) График 3.

3 Средняя нагрузка процессора в секунду График 3.

4 Среднее потребление памяти в секунду График 3.

10000 rps

Конфигурация phantom Yandex Tank

phantom: load_profile: load_type: rps schedule: line(1, 10000, 30s) const(10000, 30s)

Ссылки с детальным отчетом

Перцентили времени ответа

95%(ms)

90%(ms)

80%(ms)

50%(ms)

HTTP OK(%)

HTTP OK(count)

php-fpm

11050

11050

11050

1880

70.466

317107

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-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

1 Среднее время ответа в секунду График 4.

2 Среднее время ответа в секунду (без php-fpm, php-ppm) График 4.

3 Средняя нагрузка процессора в секунду График 4.

4 Среднее потребление памяти в секунду График 4.

Итоги

При просмотре графиков стоит учитывать, что не все сервисы ответили на 100% запросов. Здесь собраны графики, отображающие изменение характеристик сервисов в зависимости от нагрузки.

1 95% перцентиль времени ответа График 5.

2 95% перцентиль времени ответа (без php-fpm) График 5.

3 Максимальная нагрузка процессора График 5.

4 Максимальное потребление памяти График 5.

Он показывает хорошие результаты в скорости ответа и имеет поддержку компании. Оптимальным решением (без изменения кода), на мой взгляд, является менеджер процессов Nginx Unit.

В любом случае подход к разработке и инструменты нужно выбирать индивидуально в зависимости от ваших нагрузок, ресурсов сервера и возможностей разработчиков.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть