Symfony — очень популярный php фреймворк, плюсы которого заключаются в прекрасном разделении кода на бандлы, DI, профилировщике, поддержке сторонних модулей. Однако же он очень медленный.
Был у меня проект на самопальном PHP фреймворке, самодельном twig-подобном шаблонизаторе без кеширования, потом он был переписан на symfony. Результаты такого преобразования оказались очень печальными: 1000 req/s превратились всего лишь в 250 запросов в секунду (production mode). Было решено выявить самые тормозные моменты этого фреймворка и попробовать ускорить его.
Итак начнем с Hello world на чистом PHP, Hello world в symfony контроллере, а также для сравнения — статика nginx, nodejs, tomcat.
<?php
echo "Hello world";
2) Symfony вариант — контроллер
<?php
namespace AppBundleController;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SymfonyBundleFrameworkBundleControllerController;
use SymfonyComponentHttpFoundationRequest;
class DefaultController extends Controller
{
/**
* @Route("/", name="homepage")
*/
public function indexAction(Request $request)
{
echo "Hello world";
exit;
}
}
3) Nodejs
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello Worldn');
});
server.listen(port, hostname, () => {
console.log(`Hello world`);
});
# Core I3 8Gb SSD PHP7.0-fpm, nodejs6.10, tomcat8
# ab -c100 -n1000 http://localhost/hello.php, Requests per second:
1) Чистый PHP: 7216.15 [#/sec] (mean)
2) Nginx hello.txt: 14053.83 [#/sec] (mean)
3) Symfony (prod, чистый проект): 522.82 [#/sec] (mean)
4) nodejs: 3125.31 [#/sec] (mean)
5) tomcat (jsp): 15023.1 [#/sec] (mean)
Как видим, падение производительности даже в простейшем Hello world — катастрофическое (Для nodejs не выяснил в чем проблема, почему так мало, т.к. на другом сервере он выдает результаты сравнимые с nginx).
Для выяснения узких мест попробуем создать свой Kernel класс и постепенно будем добавлять к нему функциональные блоки. Для начала изменим загрузчик:
<?php
use SymfonyComponentHttpFoundationRequest;
$loader = require '.../project1/app/autoload.php';
include_once '.../project1/app/bootstrap.php.cache';
// Вместо AppKernel создаем наш переопределенный класс MyKernel
include_once '.../project1/app/MyKernel.php';
$kernel = new MyKernel('prod', false);
$kernel->loadClassCache();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response); // это пока отключим
Как видим, ничего сложного тут нет, сначала инициализируется стандартный автозапуск composer, затем создается класс *Kernel, у которого вызывается несколько методов.
Далее создадим самый простейший класс, подходящий под эту схему:
<?php
use SymfonyComponentHttpKernelKernel;
use SymfonyComponentConfigLoaderLoaderInterface;
use SymfonyComponentHttpFoundationRequest;
class MyKernel
{
private $dev = '';
public function registerBundles()
{
$bundles = array(
new SymfonyBundleFrameworkBundleFrameworkBundle(),
new SymfonyBundleSecurityBundleSecurityBundle(),
new SymfonyBundleTwigBundleTwigBundle(),
new SymfonyBundleMonologBundleMonologBundle(),
new SymfonyBundleSwiftmailerBundleSwiftmailerBundle(),
new DoctrineBundleDoctrineBundleDoctrineBundle(),
new SensioBundleFrameworkExtraBundleSensioFrameworkExtraBundle(),
new AppBundleAppBundle(),
);
return $bundles;
}
public function __construct($dev, $is)
{
$this->dev = $dev;
}
public function getEnvironment()
{
return $this->dev;
}
public function loadClassCache($name = 'classes', $extension = '.php')
{
}
public function handle(Request $req, $type = Kernel::MASTER_REQUEST, $catch = true)
{
echo "Hello world";
exit;
}
public function send()
{
}
public function terminate()
{
}
}
В результате получаем 3894.87 [#/sec] (mean). Как видим, инициализация автозагрузки и создание нескольких классов уменьшает отзывчивость уже в 2 раза по сравнению с Hello world на чистом PHP.
На очереди загрузка app контейнера (чтобы можно было вызывать функции из services):
<?php
public function handle(Request $req, $type = Kernel::MASTER_REQUEST, $catch = true)
{
// 1) Загрузка кешированных классов (необязательно)
// Уменьшает rps с 3.9к до 3.3к
require_once ".../project1/cache/prod/classes.php";
// 2) Загрузка бандлов.
// Этот блок кода почти не уменьшает rps, если загружен classes.php
$bundles = $this->registerBundles();
foreach ($bundles as $bundle) {
$name = $bundle->getName();
$this->bundls[$name] = $name;
$parentName = $bundle->getParent();
}
// 3) Инициализация контейнера (из app-prod кеша)
// Этот блок тоже почти не влияет на rps
$class = "appProdProjectContainer";
require_once ".../project1/cache/prod/appProdProjectContainer.php";
$this->container = new $class();
$this->container->set('kernel', $this);
$request_stack = $this->container->get('request_stack');
$request_stack->push($req);
echo "Hello world";
exit;
}
?>
Результат уже в принципе впечатляющий — 3316.42 [#/sec] (mean)! Вы уже можете загружать сервисы из своих бандлов через $this->container->get и использовать этот Kernel например для API и REST запросов (авторизация тут конечно не работает, загрузки контроллеров пока нет).
Результат дальнейших экспериментов: удалось загрузить код контроллеров с помощью своей реализации роутинга. Возможно, если покопаться, удастся задействовать роутинг самой symfony. Twig шаблоны теоретически работают, но есть проблема с лоадером — в путях поиска не прописаны бандлы, кроме того не работают некоторые стандартные переменные, например {{ app.request }}. На данный момент эксперименты приостановлены, в проекте создан отдельный APIKernel для соответствующих api запросов, который вполне себе успешно работает.
Автор: new player