Выявляем узкие места symfony

в 17:21, , рубрики: symfony, метки: ,

Symfony — очень популярный php фреймворк, плюсы которого заключаются в прекрасном разделении кода на бандлы, DI, профилировщике, поддержке сторонних модулей. Однако же он очень медленный.
Был у меня проект на самопальном PHP фреймворке, самодельном twig-подобном шаблонизаторе без кеширования, потом он был переписан на symfony. Результаты такого преобразования оказались очень печальными: 1000 req/s превратились всего лишь в 250 запросов в секунду (production mode). Было решено выявить самые тормозные моменты этого фреймворка и попробовать ускорить его.
Итак начнем с Hello world на чистом PHP, Hello world в symfony контроллере, а также для сравнения — статика nginx, nodejs, tomcat.

Исходный код Hello world на различных языках

1) Чистый PHP
<?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, у которого вызывается несколько методов.
Далее создадим самый простейший класс, подходящий под эту схему:

Самая простая и тупая реализация MyKernel.php

<?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):

MyKernel.php, позволяющий работать с DI

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js