Несколько незамеченных подробностей о HipHop PHP

в 15:13, , рубрики: php, Веб-разработка, Программирование

Я думаю многие из вас слышали про HipHop PHP (далее hphp), разработанный программистами Facebook для оптимизации социальной сети с минимальными затратами по адаптации существующего кода. Но по какой-то причине, в интернете тема этого иструмента раскрыта, я считаю, не полностью. Большая часть тех, кто его использовал, или хотя бы крутил в руках, ограничивается лишь мануалами по установке, однако хотелось бы заглянуть немного глубже, что я и хочу сделать в этой статье.

Написание этой статьи стало результатом реального использования этого инструмента при разработке игрового приложения для социальной сети vk.com — Texas Hold'em покера на html+js/php.

Рациональность использования

Начать хотелось бы с самого важного вопроса, который, как я думаю, больше всего всего интересует читателей. Имеет ли смысл использовать hphp в реальных проектах? Однозначно ответить на этот вопрос довольно сложно.

Безусловно, hphp является скорее «костылем», правда куда более эффективным, чем системы кеширования Op-кода. Для многих может показаться странной сама идея того, что можно разрабатывать что-то на php, чтобы потом «оптимизировать» с помощью hphp, если можно было сразу написать на более производительных языках/стеках технологий. Но все же, помимо минусов, которые всем php-разработчикам хорошо известны, у php есть несколько плюсов, которые часто незаслуженно забываются, а именно доступность языка и определенная «простота» и прямолинейность. Это может стать плюсом проекта в коммерческом смысле, когда вопрос производительности можно будет частично решить с помощью hphp.

Все же вопрос адекватности использования hphp должен быть оценен для каждого случая отдельно. Если у вас есть проект, написанный на php, который содержит много кода, не слишком хорошо подвергается горизонтальному расширению, или в нем существует функционал, который «страдает» из-за отсутствия в php многопоточности, то возможно hphp идеально подойдет вам в качестве решения по «наименьшему пути сопротивления».

Вообще, я очень люблю эксперементировать с различными инструментами, меня ничто не останавливало, например, перед использованием Node.JS, который был (и остается) еще довольно «сырым» продуктом, так же меня ничего не остановило перед использованием hphp, тем более, что у меня на руках был прототип приложения, на котором можно было его полностью испробовать. Так что в моем случае это было вполне рационально.

Подготовка исходников

На самом деле использование hphp, в большинстве случаев, не накладывает каких-либо ограничений на php-код. Но, конечно же, есть некоторые различия, из-за которых работающий php-код может стать нерабочим под hphp. В основном, причины таких несоответствий можно найти на соответствующей странице документации HipHop PHP, однако, там описаны не все случаи, в которых hphp может работать иначе, чем обычный php.

Почти все такие «ошибки» связаны либо с динамическим созданием функций или исполнения кода (create_function или eval), либо с неправильным использованием локальной области видимости и $this. В качестве примера: у вас может быть реализован шаблонизатор, основанный на php интерпретаторе, без дополнительной обработки, но для удобства вы используете «локализацию» переменных, либо ссылаетесь в нем на $this:

index.tpl:

<!DOCTYPE html>
<html>
    <head><?php echo($title); ?></head>
    <?php $this -> getMETA(); ?>
</html>

Где-то в шаблонизаторе:

// ...
extract($this -> viewData);
include('templates/index.tpl');
// ...

Если в php такой код отработает нормально и вполне «ожидаемо», то в hphp выдаст ошибку, потому что в подключенном шаблоне переменные из extract не сработают, кроме того, $this будет равно NULL. Не знаю, баг ли это, или by design, но подобные места запросто могут встретиться в популярных движках, несмотря на то, что конкретно этот пример сильно утрирован.

Еще одним несоответствием является «странность» в работе функции json_encode, которая, по какой-то причине, транслирует значения false и null в строковые значения «false» и «null», что, естественно, требует отражения в клиенсткой части. Я думаю, что это тоже временный баг.

Безусловно, далеко не все бинарные библиотеки php портированы под hphp. Эта проблема решается либо ручным переписыванием и компиляцией библиотек, что довольно непросто, либо использованием «нативных» (в данном случае имеется в виду написанных на самом php) аналогов библиотек. Для сравнения, заметного падения производительности при использовании бинарной библиотеки биндинга к redis.io в сравнении с написанным на php биндингом — не было (после компиляции, конечно же). Однако, для многих библиотек таких аналогов не существует, что может стать еще одним препятствием перед использованием hphp.

Из-за того, что в самом начале я не смог разобраться с логикой того, как именно подключает исходники hphp «внутри», мне пришлось исключить из кода все include/require и использовать такой скрипт для компиляции проекта:

#! /bin/sh

export HPHP_HOME=/opt/hiphop-php
export HPHP_LIB=/opt/hiphop-php/bin
export CMAKE_PREFIX_PATH=/opt

find . -name "*.php" > files.list
rm -rf tmp/

# Build the project:
$HPHP_HOME/src/hphp/hphp -k 1 --input-list=files.list --log=3 
                         --include-path="." -o tmp --program="run"

# Copy files:
cp tmp/run bin/run
rm -rf tmp/

# Cleanup:
rm files.list

Другими словами, я составил список файлов и скомпилировал их все, по input-list. Впоследствии, так оказалось удобнее держать проект в целостности, но из-за каких-то несоответствий подключение файлов через include/require с относительным путем иногда сбоило и hphp выдавал ошибку, что файл не найден. Скорее всего, это была моя ошибка и мне оказалось удобнее обойти её, чем решить. Тем более, что количество подключенных при компиляции файлов никак не влияет на конечную производительность, в случае hphp.

Запуск у меня производится через такой скрипт:

bin/run -m daemon -c "./server.cfg" # server.cfg - соответствующий конфигурационный файл

Для запуска скомпилированного приложения доступны несколько «типов» работы, в основном интересен параметр "-m", основными значениями которого являются «server» и «daemon». Из названий должно быть понятно, что первый тип запускает приложение с присоединением к текущей сессии терминала, а второй — создает демон. Но тут кроется еще одна небольшая проблема, дело в том, что по какой-то причине cwd для первого и второго вариантов внутри php выбирается разный. Для server это директория, указанная в конфигурации, когда для daemon у меня выбирается корневая директория "/". Я так и не смог найти объяснения этому багу, но на него приходится обращать внимание.

«Структурные» особенности hphp

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

Почти полностью конфигурация hphp описана на соответствующей странице документации, но гораздо интереснее знать чем именно могут быть полезны эти настройки, чем просто прочитать о них. Поэтому в этом разделе я подробнее опишу то, что в hphp есть интересного помимо трансляции в C++ и компиляции.

В основном речь пойдет про структуру документов hphp, которая в документации изображена следующим образом:
Несколько незамеченных подробностей о HipHop PHP

В конфигурации типы документов задаются через параметры, соответсвующие их названиям, за исключением того, что некоторых типов документов в одном проекте может быть несколько, например ThreadDocuments и ThreadLoopDocuments, в таком случае в конфигурации они задаются следующим образом:

ThreadDocuments {
 * = somedoc.php
 * = another.php
}

Где вместо астерикса может быть любое название, которое ни на что не влияет, а указывается только для удобства разработчика.

Но начать, пожалуй, стоит с того, что в hphp приходится более активно использовать функции apc_store/apc_fetch/apc_delete, которые присутствуют и в обычном php. Но тут они приобретают новый смысл, поскольку сохраненные таким образом данные доступны во всех типах документов hphp.

Необходимо отметить, что работа функций apc_store/apc_fetch устроена таким образом, что если вы сохраните в неё объект, например таким образом:

$object = new StdClass();
apc_store('object', $object);

То в самом объекте не будет производиться какой-либо сериализации при сохранении, магические методы, конечно же, не будут вызваны, что может стать неожиданностью. Другой неожиданностью, но вполне логичной, является то, что объекты, при сохранении в apc, копируются, а также копируются при загрузке. Другими словами, это не ссылка на область памяти, которая хранит объект, а просто кеш, из которого можно загрузить и в который можно сохранить данные. Поэтому любые модификации сохраненного объекта должны быть пересохранены по окончанию работы над ним, иначе они будут незаметны для других скриптов, которые его загрузят.

StartupDocument

Этот тип документа указывает на php-файл, который будет запущен при запуске сервера. Он используется для инициализации, но его область видимости при этом не копируется в запускаемые страницы, либо в Service Threads. Так что для передачи любых данных инициализации из StartupDocument в другие документы, необходимо их сохранять через apc_* функции.

Отмечу, что загрузка сервера (на уровне приложения) происходит следующим образом, сначала запускается StartupDocument, затем запускаются все документы ServiceThreads (ThreadDocuments, ThreadLoopDocuments) и по получении сигнала о готовности ThreadDocuments, создаются WorkerThreads. При этом, если сигнал (вызов hphp_service_thread_started()) не будет получен из всех соответствующих документов, сервер вообще не запустится. Подробнее об этом будет написано далее.

WarmupDocument

Этот тип документа используется внутри WorkerThread для инициализации среды выполнения. Как раз использование WarmupDocument может помочь заметно сократить расходы на инициализацию, как конфигураций, так и подключений к базе данных, или кеширования постоянных, или редко обновляемых данных.

Поскольку все WorkerThread работают однопоточно, то проблем совместного доступа к данным, сохраненным из WarmupDocument, не будет. Однако и изменять эти данные, фактически, выполняемые скрипты не могут, поскольку состояние откатывается после каждого выполнения. Но все равно этот тип документа может сохранить процессорное время, которое тратится на инициализацию данных/объектов, которые не изменяют свое состояние после запросов пользователя (например, шаблонизаторы).

ThreadDocuments и ThreadLoopDocuments

Эти документы выполняются в отдельном, от обработки пользовательских запросов, потоке. Они полностью независимы и могут обмениваться данными с остальными потоками только посредством apc_*. Такие документы очень удобны для выполнения каких-то операций в «бекграунде», особенно «ThreadLoopDocuments», которые отличаются от «обычных» тем, что при окончании выполнения запускаются заново. Если в конце документа, который выбран, как ThreadLoopDocuments, вызвать функцию sleep, то можно реализовать таймер, который будет выполнять задачи с определенным интервалом. В моем приложении, например, такой вид документа используется, чтобы собирать изменения статистики и отправлять их разово, а не при каждом изменении, что тоже влияет на конечную производительность.

ThreadDocuments после своей инициализации должны вызывать функцию hphp_service_thread_started(), которая сигнализирует о том, что этот ServiceThread готов к использованию. Только когда от всех ThreadDocuments будет получен этот сигнал, WorkerThreads начнут обрабатывать запросы пользователей.

Xbox Server

И наверно самая интересная особенность hphp это Xbox сервер. Но, как вы понимаете, никакой связи с игровой консолью XBOX360 тут нет. Название расшифровывается, как cross-box сервер, что означает «сервер для организации взаимодействия между серверами», но это не полностью раскрывает его суть.

Этот сервер может быть использован не только на отдельном физическом сервере, для выполнения своей первоочередной задачи — коммуникации между другими серверами, но и определен локально, для реализации эмуляции многопоточности в php.

В основном, операции с этим сервером происходят с помощью двух функций:

xbox_send_message($msg, $ret, $timeout_ms, $host);
xbox_post_message($msg, $host);

Единственным различием между этими функциями является то, что «send» блокирует выполнение текущего потока и ожидает ответа или срабатывания таймаута ($timeout_ms), а «post» работает в неблокирующем режиме, но не получает ответ в текущий поток.

В самом Xbox Server (который является простым сервером, имеющим обычные настройки, но указанным в размеле конфигурации Xbox) должна быть реализована функция xbox_process_message($data), которая принимает сообщение и обрабатывает его.

Но самое интересное заключается в том, что если вызывать функции xbox_send_message/xbox_post_message без указания $host, а сам Xbox объявить в том же конфигурационном файле, что и основной сервер, то документ, запущенный в Xbox сервере будет иметь доступ ко всем данным, которые сохранены через apc_*.

Это позволяет (и является самым адекватным способом для hphp) эмулировать многопоточность выполнения, отправляя задачи, которые не должны блокировать основной поток выполнения, на Xbox сервер, обновляя данные о результатах выполнения посредством редактирования данных через apc_*.

В заключение

Использование hphp, особенно вкупе с расширенным функционалом специальных документов hphp может не только повысить производительность, но при условии адаптации проекта под их использование, может решить некоторые концептуальные вопросы php, например, отсутствие многопоточности.

Конечно же, даже с использованием всех этих функций, это не является полноценной заменой сервера, написанного на более подходящих для таких задач языках, однако, в определенной области задач, hphp может оказаться очень полезен для того, чтобы минимизировать расходы на разработку и поддержку, без большого урона производительности.

К сожалению, статья получилась не слишком полезной в практическом плане, но она обращает внимание на расширенный функционал HipHop PHP, который, по какой-то причине, всегда обходится без упоминаний в других статьях. Если эта тема будет интересна сообществу, я могу раскрыть не покрытые в этой статье части hphp в следующих статьях.

Автор: jlbyrey

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


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