True FastCGI для PHP - миграция и тесты
Начиная с PHP 5.3, язык стал готов к работе в режиме True FastCGI. Я решил попробовать эту возможность на практике... Ну и вот что из этого вышло.
В статье описана попытка использования технологии, сложности, которые пришлось преодолевать и некоторые бенчмарки, демонстрирующие возможный эффект перехода на True FastCGI.
PHP и Javascript часто используются вместе, а это мой единственный блог, поэтому, надеюсь, будет к месту.
FastCGI-модель, применяемая изначально для PHP, позволяет использовать только часть изначально заложенной в нее мощи.
Дело в том, что обычно FastCGI выглядит примерно так:
Framework::init();
while($request = FastCGI::accept()) {
// обработать запрос, выдать страницу
}
Framework::shutdown();
Запускается множество процессов, каждый из них один раз инициализуется, а затем в цикле обрабатывает поступающие запросы.
При этом фреймворк вместе со всеми классами, объектами, подключением к базе и другой машинерией загружается при изначальной иницилазиции и затем остается в памяти. При каждом новом запросе перезаполняются лишь структуры, непосредственно завязанные на данные запроса.
Эта модель - классическая для языков Perl, Ruby. Однако PHP изначально пошел другим путем. В этом языке интерпретатор полностью очищается между запросами и выполняет php файл "с нуля".
То есть, цикл вынесен из языка, и программист может лишь описать его внутреннюю часть, обработку запроса.
Действительно, почему? Первый ответ - потому что так проще, а PHP брал популярность простотой. Второй - потому что до 5.3 в PHP не было нормальной очистки памяти, включающей в себя очистку объектов от круговых ссылок.
Например, запустите следующий код в PHP 5.2. В нем создаются два объекта, ссылающиеся друг на друга, и один из них, доступный из скрипта, удаляется.
<?php
class Foo {
function __construct()
{
$this->bar = new Bar($this);
}
}
class Bar {
function __construct($foo = null)
{
$this->foo = $foo;
}
}
while (true) {
$foo = new Foo();
unset($foo);
// $foo убили, нужно освободить память от него и $bar
echo number_format(memory_get_usage()) . "\n";
}
PHP мгновенно (степень мгновенности зависит только от процессора ) съест всю доступную ему память.
А в PHP 5.3 наконец-то сделали нормальный сборщик мусора, и все будет ок.
Ну и, в-третьих, чтобы избежать перекомпиляции файлов при каждом запуске, созданы "опкод кешеры", которые частично нивелируют эффект повторной загрузки кода из одного и того же файла.
..Да просто потому, что есть возможность однократной инициализации FrameWork::init() . Кроме того, готовые структуры классов не исчезают из интерпретатора между запросами, так что нет необходимости их каждый раз перечитывать из опкод-кеша.
Для простых скриптов - там, где нет ни сложных классов, ни многоуровневой инициализации с конфигами, наверное, нет разницы. Но современные фреймворки содержат много файлов и делают массу вещей в процессе инициализации. И здесь "настоящий" FastCGI весьма полезен.
Задача о переходе на True FastCGI - не теоретическая, а под конкретный проект с конкретным целевым фреймворком: Symfony. Современный Zend Framework тоже подошел бы, но проект именно на Symfony 1.4.
Для тестовой миграции выбран базовый проект: sandbox из поставки Symfony.
Как организовать True FastCGI ? Обычно в языках для этого существуют модули и библиотеки. Для PHP такого почти нет, но, просмотрев несколько недоделок, я нашел довольно серьезный проект подобного рода -PHPDaemon.
Да, его делает один девелопер со странной фамилией (ником), это минус, но делает он его уже пару лет и коммитит стабильно. Nginx, знаете ли, тоже один девелопер делает - и ничего, справляется Главное качество.
А по качеству - оказалось очень даже ничего. True FastCGI - лишь одна небольшая возможность мощного демона, который представляет собой асинхронный фреймворк типа Twisted. И, что самое главное, True FastCGI на нем не ликает (т.е. не течет память).
По опыту перевода большого приложения на Perl на FastCGI (года 3-4 назад) - это редкость, т.к. при прокрутке большого количества запросов в одном скрипте с памятью нужно обращаться осторожно. Некоторые перловые модули замечательно текут.
Проявляется это как непрерывное увеличение процесса в памяти по ходу приема запроса, и лечится либо фиксом модулей, либо рестартом процесса по достижении определенного размера.. А лучше и тем и другим. В PHPDaemon с этим все ок.
Кроме того, разработчик расположен к диалогу, говорит по-русски и, вообще, весьма адекватен.
Фреймворк и PHPDaemon не заточены друг под друга. Вообще никак. Однако, скрещивание прошло почти без эксцессов.
PHPDaemon поддерживает протокол FastCGI, правильно ставит суперглобалы.. В общем, ведет себя как обычный сервер.
Для обработки входящих запросов вместо цикла используются каллбэки (PHP 5.3).
Есть два основных класса:
SymfonyApp extends AppInsance
- Объект создается один раз как синглтон и предоставляет ряд каллбэков, которые включаются на различных стадиях работы. В частности, метод
init() , используемый для изначальной инициализации фреймворка.
SymfonyRequest extends Request
- Объект запроса - второй по важности. Его методы
run() и onFinish() вызываются при обработке очередного пришедшего запроса. Основная работа происходит в run() .
В конце статьи находятся исходники, а главное - файл phpdaemon/applications/SymfonyApp.php , который содержит приложение под PHPDaemon и весь описанный в этой статье код.
Основных требований к скрещиванию несколько.
- Отсутствие утечек памяти. Чтобы один запуск скрипта обрабатывал 10000 запросов без роста в объеме.
- Минимум интегрирующих патчей как на фреймворк Symfony, так и на PHPDaemon. Лучше вообще без патчей. И то и другое - 3rd party код, патчи к нему поддерживать совсем не хочется.
- Оно должно работать, по возможности, безглючно
Подробности скрещивания - в отдельной статье, если к ним будет интерес. В этой секции разберем основные моменты и подводные камни, на которые пришлось наступить во время миграции.
Вызывая эту функцию, фреймворк хочет сделать что-то в конце запроса. А при использовании PHPDaemon она вызовется в конце FastCGI-цикла.
Чтобы вызывать каллбэк когда нужно - заменим эту функцию на свою. А, чтобы ничего не патчить, используем расширение runkit. Его багфиксаный вариант, поддерживающий 5.3 находитя в репозитарии Дмитрия Зеновича http://github.com/zenovich/runkit. Спасибо, Дима!
Для того чтобы phpDaemon мог посредством runkit переназначить register_shutdown_function, необходимо включить ini-опцию runkit.internal_override. Больше ничего не требуется.
То есть, вместо родной функции будет вызываться наша, которая запомнит обработчик, и вызовет при завершении запроса.
Эта проблема была неожиданной. Дело в том, что фреймворк использует вызов create_function для фильтрации массива и некоторых других операций. А этот метод при каждом вызове создает новую функцию.
В результате получалось, что функции создаются, создаются (но не уничтожаются) и PHP кушает все больше памяти.
Что делать? Можно заменить все create_function на замыкания, тогда будет все в порядке. Ведь в отличие от глобальных функций, которые создает create_function , замыкания являются объектами типа Closure и находятся в текущей области видимости. Поэтому при выходе из блока они будут корректно очищены.
Но такая замена требует патча фреймворка. Поэтому выбрано другое решение.
Опять, используя runkit , phpDaemon может переназначить create_function() на реализацию с кешированием. Таким образом, повторный запуск create_function с одинаковыми аргументами будет кеширован.
Это описывается следующим блоком кода.
Везде во фреймворке, create_function вызываются со статичным текстом в качестве аргументов и тела, наподобие такого:
// sfYamlInline.php
array_reduce($keys, create_function('$v,$w', 'return (integer) $v + $w;'), 0)
Функция с таким текстом тут же попадет в LRU-кеш и будет использована повторно.
С реализацией можно ознакомиться здесь.
Работа фреймворка в демон-редакции выглядит так. В начале - инициализация приложения (однократная):
class SymfonyApp extends AppInstance
{
...
public function init()
{
$this->configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);
$this->dbmanager = new sfDatabaseManager($this->configuration, array('auto_shutdown' => false));
}
...
Затем - обработка запроса:
class SymfonyRequest extends Request
{
...
public function run()
{
// создать контекст
// он заполняется данными запроса из суперглобалов
$instance = sfContext::createInstance($this->appInstance->configuration);
$instance->setDbmanager($this->appInstance->dbmanager);
// обработать запрос, выдать результат
$instance->dispatch();
}
...
И, чтобы память не текла, добавим дополнительную очистку в SymfonyRequest#onFinish , который демон вызывает после каждого запроса:
...
public function onFinish() {
// почистить синглтоны
sfTimerManager::clearTimers();
$this->appInstance->configuration->getEventDispatcher()->clear();
}
...
Чистка синглтонов необходима для того, чтобы информация от прошлого запроса не накладывалась на текущий. Как ни странно, хватило этих двух вызовов, во всяком случае, для стандартного демо-приложения симфони. Плагины могут потребовать дополнительных чисток.
Бенчмарки проводились siege в режиме "1 воркер, много запросов без задержек". Я также пробовал увеличивать количество конкурентных соединений, но никаких дополнительных данных это не дает.
Причина в том, что серверный паттерн что у PHP FastCGI, что у True FastCGI один и тот же: pre-fork. То есть, с большой конкурентностью они будут справляться одинаково (тесты это подтвердили), так что вполне достаточно 1 потока непрерывных запросов.
Строка siege:
siege -c 1 -b -t 1m http://symfony.ru/ >/dev/null
Конечно же, был включен PHP-акселератор для режима FastCGI, APC/XCache без проверки stat. В режиме PHPDaemon акселератор был выключен.
Перед каждым тестом был предварительный разогрев - несколько секунд siege вхолостую.
Итак, результаты для обычного Symfony:
# siege -c 1 -b -t 1m http://symfony.ru/ >/dev/null
** SIEGE 2.70
** Preparing 1 concurrent users for battle.
The server is now under siege...
Lifting the server siege... done.
Transactions: 4976 hits
Availability: 100.00 %
Elapsed time: 59.08 secs
Data transferred: 4.60 MB
Response time: 0.01 secs
Transaction rate: 84.22 trans/sec
Throughput: 0.08 MB/sec
Concurrency: 1.00
Successful transactions: 4976
Failed transactions: 0
Longest transaction: 0.03
Shortest transaction: 0.00
А вот - для True FastCGI:
root@debian6:~# siege -c 1 -b -t 1m http://phpdaemon.ru/ >/dev/null
** SIEGE 2.70
** Preparing 1 concurrent users for battle.
The server is now under siege...
Lifting the server siege... done.
Transactions: 9857 hits
Availability: 100.00 %
Elapsed time: 59.65 secs
Data transferred: 19.05 MB
Response time: 0.01 secs
Transaction rate: 165.25 trans/sec
Throughput: 0.32 MB/sec
Concurrency: 1.00
Successful transactions: 9857
Failed transactions: 0
Longest transaction: 0.26
Shortest transaction: 0.00
Как видите - разница в два раза.
Более того - я добавлял переинициализацию фреймворка в метод run() , и все равно True FastCGI был впереди... Хотя уже не в 2 раза, а всего лишь в 1.5 раза быстрее.
Дополнительные подводные камни
Пробовал включить акселератор в режиме демона. Однако демон запускается из командной строки, поэтому XCache/Eaccelerator не заработали (можно немного пропатчить их сырцы - запашут). А APC вообще расстроил - в режиме enable_cli он течет по памяти, да и вообще тормозит, скрипты работают медленнее.
Еще одна проблема -профилинг с XDebug. PHPDaemon форкается, поэтому профиль XDebug содержит лишь демона, а не воркера. Как ее преодолеть - пока непонятно, ждем ответа разработчика XDebug, Derick'а. Если проблему можно обойти, то продолжу миграцию.
True FastCGI позволило ускорить простое Symfony-приложение в 1.5-2 раза. Это весьма неплохо.
Есть легкие фреймворки, которые стараются свести время на инициализацию к минимуму, выигрыш на них будет меньше. Но цена этого - как правило, меньшее удобство. Извечное противостояние: удобство + универсальность VS быстродействие.
True FastCGI позволяет избежать этой проблемы. Здорово, что в PHP 5.3 наконец-то стало возможно то, что большинство языков умеет годами - неликающий FastCGI-цикл.
Да, а вот исходники базового symfony и phpdaemon (+symfony), включая конфиги для nginx: sources.zip.
P.S. UPDATE 10.09.2010: Разработчик PHPDaemon прочитал эту статью и добавил оверрайд create_function и register_shutdown_function в код демона. Необходим включенный runkit c опцией internal_override . Статья перенесена в его блог, надеюсь в дальнейшем он будет вносить правки при необходимости.
|
А нельзя ли уточнить, как в данном случае принимаются запросы?
В том плане что когда мы работаем с FastCGI, то мне всё ясно, нужно поставить вебсервер(например nginx), и он уже будет кидать php файлы на висящий в памяти php интерпритатор, вместе с параметрами запроса.
А вот как работает в данном случае я не совсем понимаю. Нужен ли какой-нибудь веб сервер? Или phpdaemon сам принимает и обрабатывает запросы на 80 порт?
Если не сложно поясните пожалуйста.
PHPD умеет сам обрабатывать запросы на 80-м порту. Но nginx перед ним все равно нужен, из тех же соображений, по которым он нужен перед апач.
Поэтому возожны два варианта - либо nginx проксирует в PHPD, либо используем FastCGI. В первом случае протокол HTTP, в другом FastCGI, что несколько лучше.
(Хотя в nginx+fastcgi есть недостаток: всегда буферизуется ответ, COMET не будет работать, так что комет - прокси или напрямую PHPD).
А PHPD принимает эти запросы, держит пре-форкнутых воркеров и вообще ведет себя целиком самостоятельно.
в phpDaemon самое сложное - то, что писать нужно иначе (асинхронно). Илья, как думаете, реально ли прикрутить доктрину к нему?
У PHPD есть несколько режимов работы. Я бы разделил их на два основных варианта.
Первое - это не более чем по одному запросу на воркера в один момент времени. В этом случае имеем True FastCGI с префоркающимися процессами. Этот режим я использовал в Symfony в статье. Доктрину к нему прикрутить проблемы нет, как и все остальное.
Второе - это много запросов одновременно. В этом случае PHPDaemon становится чем-то вроде Twisted / node.js. И здесь нужны асинхронные клиенты, драйвера и т.п. Некоторые драйвера, в частности MongoDB, PostgreSQL, присутствуют в поставке PHPD.
Так вот тут Доктрину тоже можно прикрутить. С ORM как таковой ничего не сделается, она будет работать. Но на уровне работы с драйвером базы ее придется пропатчить, чтобы она асинхронные запросы умела обрабатывать.
А как насчет symfony 2? Там всё достаточно сильно переделано, и напрямую Вашими исходниками воспользоваться не получится. Не могли бы Вы их адаптировать для symfony 2?
А не рановато? До релиза симфони 2 может еще PHP 5.4 появится
Не рано, Фабьен обещает релизнуть симфони 2 до конца года.
в чем заключается проблема сделать это самому?
В необходимости тратить время на понимание деталей чужого кода вместо того, чтобы с большей пользой тратить его на написание своего.
Халявщик.
Если Вам необходима данная функциональность, то можно взять код Ильи -- пересмотреть, и поправить (читай полностью переделать) под Symfiny2.
*Symfony2 -- конечно же
Спасибо за статью.
По аналогии скрестил phpdaemon и Yii Framework.
скорость выполнения выросла в 1.5-2раза
большая часть протестированного работает нормально, но и не без проблем конечно:
1) куки и сессия не работает.
2) некоторые запросы падают.
пока все, буду ковырять дальше.
по всей видимости со временем база рвет соединение и нужно делать реконнект?
Если вы используете персистентный коннект к базе, то я бы в любом случае рекомендовал вызывать ROLLBACK в начале запроса или что-то вроде того.
Т.е. иначе классическая проблема персистентных коннектов - один запрос рухнул (мало ли почему), транзакцию не закрыл, другой запустился и ненароком ее продолжил.
В случае с муськой можно просто переоткрывать коннект заново (в моем примере так и делается, на шатдауне запроса менеджер симфони закрывает коннект).
дык переоткрывать заново при каждом запросе не хочется. Один раз проинициализировали типа)
не переживайте по этому поводу, MySQL умный, он сам кэширует конекты и в течении сесии время на реконекты не тратится.
(проверял =)
А что с сессиями? У меня всё работает. Главное вызывать session_start() внутри запроса, а не снаружи. session_commit() происходит автоматически после завершения запроса.
кстати, у меня свой класс сессий, в нем autocommit в деструкторе. Получается я в request.onFinish должен руками делать unset объекта с сессиями?
Деструктор же у объектов вызывается, а не у классов. Если у вас какой-то «классовый деструктор» (например, синглтон), то — да, надо будет чистить.
А какое приложение ускорялось?
статейку уже копипастят tokarchuk.ru/2010/09/true-fastcgi-php/
Не смотрели App Server in PHP? Тоже соотечественник делает.
Попробовали повторить, но ускорения в 2 раза не получили.
Мы работаем с фреймворком Yii а он весь построен на lazyload файлов и инициализации компонентов только тогда, когда они оказываются востребоваными, т.е. инициализация фреймворка происходит за минимальное время. Опять же persistent соединение с базой данных.
В итоге прирост есть, но столь незначительный, что им можно пренебречь в свете всех проблем с внедрением такого решения.
Таким образом надо смотреть, что именно мы пытаемся ускорить. Видимо в simfony (а фремворк тяжеловатый) инициализация собственно и занимала половину времени работы скрипта.
Просьба подробнее о проекте, особенностях и процессе перевода. А то есть сведения об ускорении и для Yii.
Если пишите о результате - обязательно описывайте, что и как переводили - это сделает пост гораздо полезнее, да и вообще на него можно будет ответить, вдруг вы что-то делаете не так.
Ускоряли большое тяжелое приложение. Соответственно на фоне выполняемой при каждом запросе логики, экономия на инициализации кода и фреймворка выглядит незначительной.
Вот если "hello world" ускорять, то несомненно, и в 2 и 5 раз можно ускорить.
True FastCGI позволяет избежать оверхеда на загрузку классов и переинициализацию ряда структур.
Ну и если приложение более-менее заточено под fastcgi, то ускорение будет больше за счет того, что сам процесс работает как L0-кэш для структур данных, которые не нужно сериализовать.
Чем больше логики каждый запрос делает, тем меньше прирост от ускоренной инициализации - это так.. Но обычно как раз логика и оптимизируется самой первой. Кеши делаются, и т.п.
А True FastCGI в дополнение к этому убирает оверхед на загрузке классов и дает L0-кеш для основных объектов.
Уже вышел symfony 2, который ~200 000 строк, против 10 000 000 у первого.
А упоминание в комментах - про то, что он сырой)
Жду коммента;)
Отправить комментарий
Приветствуются комментарии:Для остальных вопросов и обсуждений есть форум.