PHP
Erlang/OTP
30 May 2011

Моделируем полёт PHP на крыльях Erlang

В данной статье изложены размышления и фантазии на тему «как можно было бы скрестить Erlang и PHP, чтобы случилось вселенское счастье», а не описание готовой технологии или продукта. Впрочем, мы намерены это реализовать, скорее всего, в форме open-source проекта, если, конечно, уважаемая хабра-аудитория не отговорит :) Собственно, одна из главных задач этой статьи — понять, насколько идея интересна и потенциально полезна широкому PHP-сообществу. Кстати, некоторые из проблем, обсуждаемых в статье, справедливы и для других популярных скриптовых языков (тут я подразумеваю Ruby и Python), так что предлагаемое решение, возможно, будет актуально и для них.

Предыстория

В силу тех или иных объективных и субъективных обстоятельств в качестве языка для серверного программирования в Мегаплане используется PHP. Со временем нас начали стеснять и раздражать некоторые ограничения PHP, нам стало не хватать некоторых «продвинутых» фич, таких как: многопоточность, отложенный запуск, очереди сообщений и т.п. К тому же, мы ограничены в том, чтобы использовать под каждую проблему свои кастомные решения, поскольку Мегаплан является не только SaaS-приложением, но и поставляется в виде кроссплатформенной коробочной версии для трёх типов ОС: серверных Windows, Linux и FreeBSD. Добавление очередного кастомного компонента (даже если и нашлось кроссплатформенное решение) существенно усложняет установку и поддержку коробок. Даже отсутствие нормального стандартного crond в Windows напрягает.

В общем, назрела необходимость «прокачать» PHP. Есть, конечно, альтернативное радикальное решение — переехать на Java, но для нас это не вариант — слишком много не самого плохого кода уже написано за несколько лет. К тому же я не уверен, что на JVM можно элегантно решить проблему масштабного comet'а. Да и PHP списывать рано — он всё ещё является самым популярным языком для веб-разработки (по крайней мере, в России), программистов найти проще, чем под другие языки.

На почве созревшей необходимости и того, что мне в определённый момент попался на глаза и очень понравился Erlang, созрела идея решения проблем — встроить в наше стандартное окружение виртуальную машину Erlang'а и компенсировать недостатки PHP уникальными возможностями этого функционального языка.

Сначала расскажу о том, какие задачи при помощи «чистого» PHP делаются с трудом и каких фич нам не хватает, а затем опишу предполагаемое решение. Итак…

Проблемы

В данной статье я исхожу из того, что долгоиграющие процессы в PHP — это исключительно ненадёжный костыль, под который нужно подставить ещё один костыль, чтобы оно как-то жило. Не вселяет оптимизма даже то, что в PHP 5.3 вроде появился нормальный сборщик мусора. Не убеждает и phpDaemon, который многим нравится, но завязан на libevent, поэтому под Windows работать не будет. На текущий момент не существует нормального нативного и, главное, надёжного способа «демонизации» программ на PHP. И даже если пытаться переходить на тру FastCGI, всё равно над этим хозяйством нужен диспетчер, который будет регулярно процесс перезапускать. Короче, на мой взгляд, единственным продакшн-режимом работы PHP является классический FastCGI (или apache+prefork+mod_php под unix-like системами).

Почти все ограничения, о которых пойдёт речь, связаны с этим обстоятельством.

Фоновый запуск


Думаю, многие разработчики, которые писали на PHP чуть больше, чем «Hello World!», сталкивались с проблемой отложенного запуска, например, при элементарнейшей задаче отправки электропочты в ответ на какое-либо действие пользователя. В PHP есть 2 решения для такой задачи (оба некрасивые):
  • либо пытаться отправить письмо прямо сразу в интерактивном режиме в том же скрипте и рисковать нарваться на тормознутость и недоступность почтового сервиса,
  • либо записать где-нибудь задание на отправку и раз в минуту дёргать по cron'у скрипт, который эти задания будет разгребать.
Знакомо? Вы, наверное, возразите, что есть и другие способы, например, такое извращение: отфоркать новый процесс с помощью pcntl. Но тут я отвечу, что это как минимум не кроссплатформенно, и вообще выглядит как хак. А хочется иметь красивое и надёжное решение.

Comet


Все новомодные вкусные вещи типа long-polling, websockets и т.п., конечно, можно сделать и на чистом PHP, но вот только ценой целого долговисящего php-процесса на каждое соединение. На сколько-нибудь серьёзном количестве клиентов ресурсов не напасёшься на такую роскошь.
Я слышу, как в дальних рядах кто-то заикнулся про многопоточный (worker) mod_php с apache на пару. Ребята, не используйте thread-safe php в продакшне, это неправильно. Тем более, что на действительно серьёзных нагрузках даже это не спасёт.
А нам очень хочется иметь общее пространство событий между клиентом и сервером. Ну вот представьте себе такую сказку: сервер инициирует событие:

<?php
    Event::publish( 'common.users.onlinecountchanged', array('count' => 10) );
?>

А в браузере можно на него подписаться и среагировать практически сразу и не дёргая запросами сервер каждую секунду:

Event.subscribe( 'common.users.onlinecountchanged', function(data) {
    $('#onlinecount').html(data.count);
} );

Для того, чтобы организовать адекватный web-jabber-чатик, тоже нужна эта технология.

Мощный кроссплатформенный cron


Cron-заданиями пользуются почти все PHP-приложения, но в классическом cron'е есть два недостатка:
  • Нет универсального кроссплатформенного решения.
  • Неудобно динамически управлять заданиями из PHP-приложения и, соответственно, создавать одноразовые задания на какой-то момент в будущем.
В Мегаплане для реализации механизма напоминаний необходимо запускать некоторые действия единоразово в определённый момент в будущем. Для этого используется внутренняя система планирования заданий, однако для того, чтобы вовремя инициировать запуск задания, приходится применять различные костыли вроде «запускать каждую минуту по cron'у и проверять — не нужно ли чего-то сделать в данный момент», а когда на одном сервере работают тысячи аккаунтов и у каждого свой список заданий, приходится придумывать специальные дополнительные костыли, чтобы не дёргать понапрасну каждую минуту тысячи PHP-скриптов.

Было бы здорово иметь надёжный кросплатформенный планировщик заданий,
  • которым можно было бы динамически управлять из PHP-приложения,
  • который без проблем поддерживал бы тысячи заданий,
  • который дёргал бы PHP только тогда, когда действительно нужно запустить задачу,
  • который заменил бы cron, чтобы не думать об операционной системе, на которой работает наше PHP-приложение.

Очереди сообщений (message queue)


Очереди сообщений — мощный инструмент для целого класса задач. В частности, фоновые задания или событийную модель, о которых говорилось выше, логично реализовать через очереди сообщений. Думаю, никто бы не отказался иметь готовую инфраструктуру для организации асинхронных очередей «из коробки» без лишней головной боли.

Подробнее о пользе очередей сообщений можно почитать, например, в этой подборке презентаций на тему.

Решение

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

Почему Erlang?
  • Erlang создан для того, чтобы работать без перерывов долгое время. На нём работают крутые телефонные коммутаторы. Даже обновление кода можно делать без остановки работы.
  • Он кроссплатформенен и открыт с нормальной не GPL-подобной лицензией, позволяющей распространять его с коммерческими приложениями.
  • Концепция легковесных процессов позволяет создавать параллельно многие тысячи висящих соединений без особого ущерба для производительности — это позволяет эффективно решать проблему Comet'а.
  • На Erlang'е написан один из самых известных и мощных серверов очередей сообщений с поддержкой AMQP — RabbitMQ.
  • И вообще, этот язык хорошо подходит для задач диспетчеризации, надзора и всякой асинхронной кухни.
Кстати, в заключительной части статьи про то, как подружить популярные PHP-фреймворки и CMS с YAWS (YAWS — это веб-сервер, написанный на Erlang'е) у автора точно такие же мысли про симбиоз PHP и Erlang'а, это греет мне душу — я не один такой, нас, как минимум, двое! :)

Схема


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

Некоторые комментарии:
  • Nginx я нарисовал только для того, чтобы отбиться сразу от фраз типа «Nginx — наше всё, как же нам без него жить?!». Да, конечно же, это наше всё, если нужно, можно его поставить до Erlang'а и обрабатывать им то, что нужно. Но и без него всё будет работать.
  • Mochiweb я нарисовал как один из вариантов. Какой именно из веб-серверов, написанных на Erlang'е, использовать — это ещё вопрос дополнительного анализа.
  • Я нарисовал только менеджер FastCGI, однако вполне можно написать и менеджер долговисящих PHP-процессов, которые, например, «слушают» свои очереди по AMQP и спокойно их обрабатывают (например, те же письма шлют), а задача менеджера — регулярно их перезапускать и следить, чтобы их было нужное количество.
Далее немного о компонентах и о том, как на всём этом можно решать обозначенные задачи.

Сервер очередей сообщений — RabbitMQ


Сервер очередей — один из основных компонентов, ради которого всё и задумывалось. Это «транспортное средство» для различных очередей заданий и для организации единого пространста событий между компонентами, которые могут быть написаны на разных языках и работать на удалённых серверах.

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

Универсальная шина событий (pub-sub)


При помощи RabbitMQ довольно просто построить гетерогенную pub-sub систему. Давайте представим, что в приложении на PHP возникают события, на которые могут подписываться и реагировать какие-либо внешние сервисы. Тогда на стороне PHP при возникновении события мы написали бы примерно такой код (здесь используется библиотека PECL AMQP):

<?php
// соединяемся с RabbitMQ
    $cnn = new AMQPConnection();
    $cnn->connect();
// Объявляем обменник, который поддерживает подписку по маске
    $ex = new AMQPExchange( $cnn );
    $ex->declare( 'event-exchange', AMQP_EX_TYPE_TOPIC );
// публикуем событие
    $ex->publish( 'message', 'some.object.event' );
?>

Дальше представим, что мы при помощи какого-либо интерфейса зарегистрировали в Erlang'е, что при возникновении события some.object.event нужно вызвать некий внешний REST: http://example.com/example-action. Тогда процесс в Erlang, который слушает и реагирует на событие, имел бы примерно такой код (пишу с сохранением логики на псевдо-PHP, чтобы было понятнее):

// объявляем обменник и очередь
    amqp_declare_exchange( 'event-exchange', AMQP_EX_TYPE_TOPIC );
    amqp_declare_queue( 'external-app-queue' );
// подписываем очередь на все сообщения с обменника по маске
    amqp_bind( 'external-app-queue', 'event-exchange', 'some.object.*' );
// подписываем текущий процесс на очередь
    amqp_subscribe( 'external-app-queue' );
// слушаем, когда из очереди пойдут сообщения о событии
    while ( 1 )
    {
        $msg = receive_message();
        file_get_contents( "http://example.com/example-action?msg=$msg" );
    }

Таким образом, когда PHP-приложение публикует сообщение в обменник event-exchange с ключом some.object.event, обменник видит, что очередь external-app-queue подключена к нему с маской, которой соответствует ключ some.object.event, и отправляет сообщение в эту очередь. Далее очередь, зная, что на неё подписан Erlang-процесс, отправляет сообщение в него, а процесс уже выполняет логику по вызову callback'а для этого события (в данном случае — обыкновенный REST-запрос по HTTP).
Если вы осилили прочтение предыдущего абзаца, возьмите с полки пирожок :)

Менеджер FastCGI


Зачем может понадобиться писать FastCGI-менеджер на Erlang'е? Всё из-за той же кроссплатформенности. Я был бы счастлив, если бы php-fpm поддерживал Windows, но, к сожалению, это не так и, видимо, никогда не будет. В конечном итоге хочется получить полноценный стек для запуска веб-приложений на PHP, наподобие LAMP.

Это, пожалуй, самое сложное из того, что предстоит сделать.

Веб-сервер и Comet


Веб-сервер на Erlang'е нужен по одной простой причине — можно легко позволить себе держать по одному процессу на долгоживущее (long-polling) соединение и не думать о ресурсах — накладные расходы на создание и работу легковесных процессов Erlang'а очень небольшие. Если вы вдруг не знаете, что такое comet и как его можно организовать между браузером и веб-сервером, отправляю вас в соответствующую обзорную статью.

Comet в предлагаемой схеме работает при помощи универсальной шины событий, описанной чуть выше, только в роли Erlang-процесса-обработчика событий выступает процесс веб-сервера, обслуживающий long-polling соединение с клиентом, и вместо http-запроса он просто отправляет все полученные сообщения по push-каналу в браузер. Полный алгоритм примерно такой:
  1. Javascript-код в браузере открывает долгое соединение, одновременно подписываясь на определённые события.
  2. Erlang-процесс, обслуживающий это соединение создаёт на RabbitMQ свою очередь, подписывает её на нужные события на обменнике и сам подписывается на получение всех сообщений из очереди. Здесь нужно ещё не забыть про механизм авторизации, чтобы клиент смог подписаться только на те события, к которым у него есть доступ.
  3. PHP-приложение публикует событие на обменнике RabbitMQ, оно раскидывается по всем очередям comet-клиентов, которые подписаны на это событие.
  4. Сообщение попадает в обслуживающий процесс каждого клиента, который подписан на событие. Сообщение передаётся в браузер по push-каналу.
  5. В браузере срабатывает callback push-канала и в зависимости от события происходит соответствующая реакция.
  6. Push-канал тут же открывается заново (если это long-polling).

Менеджер заданий


Если вы уже поняли основную идею, то, наверное, вам уже очевидно, что организовать выполнение фоновых заданий в предложенной схеме проще простого: достаточно создать отдельный обменник для заданий, одну или несколько очередей для самих фоновых задач и соответствующие обработчики этих очередей. Обработчики можно придумать двух типов:
  • Элементарный вызов по HTTP-REST API. В этом случае параметры запроса нужно присылать в самом сообщении.
  • Долгоживущий PHP-процесс (запускаемый и надзираемый из Erlang'а), который «слушает» свою очередь заданий по AMQP и выполняет одну специфическую задачу.

Cron


Отложенный на момент времени в будущем запуск также несложно организовать в виртуальной машине Erlang. В ней есть встроенные средства для эффективного хранения данных как в памяти, так и на диске. Достаточно сохранить время, в которое нужно совершить действие, и соответствующее сообщение с информацией о задании для менеджера заданий. Написать на Erlang'е процесс, который будет, имея эту информацию, вовремя отправлять задание в очередь менеджера заданий (см. выше), не составляет большого труда. А чтобы запланировать задание, можно либо отправить сообщение в нужный обменник AMQP, либо сделать REST API и управлять заданиями через него.

Что касается периодических заданий, как в настоящем cron'е, можно, конечно, и их реализовать (а может, это уже кто-то сделал в просторах Интернета), но это не столь критичная задача, поскольку её вполне можно и на PHP сделать (собственно, в Мегаплане такой механизм есть), главное, чтобы был надёжный инициатор, который вовремя вызовет PHP-скрипт.

Вместо заключения

Дочитавшим до этого места большой респект и спасибо, надеюсь было не очень скучно :)
Итак, что мы получим, если описанный проект вдруг реализуется:
  • Кроссплатформенную и при этом надёжную среду для запуска веб-приложений на PHP.
  • Кучу дополнительных продвинутых возможностей для PHP с разнообразной асинхронной магией.
  • Более быстрые и отзывчивые веб-приложения с real-time оповещениями.
  • Популяризацию замечательного Erlang'а.
В общем, один сплошной профит :)

Хотите поучаствовать? Welcome!

+77
8.1k 154
Comments 166