Pull to refresh

phpDaemon — фреймворк асинхронных приложений

PHP *
Сегодня речь пойдет о phpDaemon — асинхронном модульном демоне-фреймворке, который берёт на себя обработку I/O (libevent) и другие низкоуровневые задачи, присущие демонам. С его помощью легко писать правильные сетевые приложения с блэкджеком и шлюхами.
Из коробки идут сервера FastCGI, HTTP, CGI, FlashPolicy, Telnet, WebSocket (!) — да-да тот самый волшебный пендаль новый протокол от Google. И клиенты mysql, memcached, mongodb… И многое другое, полный список под катом. Работать с сетью действительно просто. Программист средней руки может написать, к примеру, IRC-бота за считанные часы.
В качестве наглядного примера я реализовал вот этот чат на phpDaemon + WebSocket + MongoDB + jQuery. Он наглядно демонстрирует преимущества этой технологии: доставка сообщений мгновенна, накладные расходы при обмене данными минимальны, высока производительность, приложение масштабируется горизонтально. Исходники этого чата (в данный момент 17 кб). Прошу заметить, чат тестировался и работает в Chrome, FF, IE6+, Iron, Safari.

Где это лучше применить?


Область применения phpDaemon очень широка как в Веб-разработке, так и за её пределами. С его помощью хорошо делать real-time многопользовательские игры (например на Flash), сервисы с мгновенным взаимодействием, чаты, IM-гейты… Также хорошо использовать необходимые в хозяйстве полезности, такие как сервер flashpolicy.
Самое первое реальное применение проекта в production — мгновенная доставка личных сообщений: на страницах сайта был невидимый swf-файл (Flash) в несколько килобайт, он подключался к серверу и ждал команд. Как только пользователю приходило сообщения, Flash посылал команду странице (Javascript), который в свою очередь сразу же отображал всплывающую иконку и «пиликал». Мы с коллегами были поражены скоростью реакции, звук от соседнего компьютера был слышен еще до того как отправитель успевал оторвать палец от левой кнопки мыши.
А совсем недавно по заказу был реализован сервер предоставляющий API к Asterisk'у (для IP-телефонии), прекрасно держит нагрузку.

Архитектура и возможности


Приложения в phpDaemon содержат лишь логику обработки, а все низкоуровневые вызовы происходят автоматически. Проект написан исключительно на PHP, и вы, вероятно уже задались вопросом производительности. I/O происходит через libevent, библиотеку хорошо зарекомендовавшую себя, которая используется в memcached и в других известных проектах. При этом, скорость выполнения инструкций в PHP весьма велика, он превосходит в этом многие другие языки своей группы. В привычных всем синхронных скриптах львиная доля времени уходит на запуск среды и на блокировку при вводе-выводе, например при запросе к базе данных обычный скрипт не продолжит выполнение пока ответ не будет получен. В phpDaemon таких блокировок нет: отослали запрос, повесили если нужно callback-функцию на ответ, и пошли по своим делам.

phpDaemon — это 1 мастер-процесс и множество рабочих процессов, каждый из которых представляет собой среду выполнения приложений. Приложение является классом-наследником AppInstance, который описывает его реакцию на различные события. При запуске рабочих процессов, они создают экземпляры классов-приложений и выполняют метод init(). В методе init() приложение может забиндить сокет, куда-то подключиться, или открыть локальный дескриптор (при этом одно не исключает другого). Приложение также может взаимодействовать с другими приложениями в рабочем процессе, объявляя в них необходимые параметры: например, прописать роут в WebSocketServer.
После того как оно забиндило сокет, при поступлении подходящих соединений будет вызвано событие onAccepted(connId,addr). Пример этого метода из WebSocketServer:
public function onAccepted($connId,$addr)
{
 $this->sessions[$connId] = new WebSocketSession($connId,$this);
 $this->sessions[$connId]->clientAddr = $addr;
}


* This source code was highlighted with Source Code Highlighter.

Сервер отдающий политику crossdomain по 843 порту весит всего 1,67 кб — FlashPolicy.php. Между тем, его явное преимущество перед множеством аналогов очевидно: его нельзя положить открыв десяток-другой телнет-сессий. Многие популярные реализации flashpolicyd DoS-ятся открытием даже одной сессии, которая ничего не шлет, поскольку в них идёт синхронная обработка, и процесс ждет у моря погоды.

Для обработки HTTP-запросов (в том числе по FastCGI) существует отдельная сущность — классы-наследники Request. Эта сущность имеет входные параметры (get,post,cookie,server...) и состояния — run/sleep/dead. Запросы висят в очереди, и диспетчер вызывает их по порядку. Если запросу не нужно прерываться, он может сделать всю работу и вернуть код завершения, чтобы диспетчер удалил его из очереди. Если запрос производит операции, требующие ожидания (например делает запрос к MongoDB), то ему необходимо сделать $this->sleep(30) чтобы заснуть максимум на 30 на секунд. А в callback-функции к запросу MongoDB достаточно указать $request->wakeup(), чтобы немедленно прогнать сон. Тогда диспетчер обратится к нему незамедлительно. Запрос сможет продолжить своё выполнение, имея ответ от MongoDB. Если же ответ по каким-то причинам не получен, запрос может вывести «We're sorry, try again shortly later». Для полного завершения запроса в методе run() вызывается либо return 1, либо $this->terminate().
После того как заголовки запроса приняты приложения-сервера (коробочные — FastCGI, HTTP) обращаются к appResolver'у, который определяет к кому приложению поступает запрос и вызывает у приложения метод beginRequest. А он, в свою очередь, создает и возвращает экземпляр класса-наследника Request. Затем объект запроса кладется в очередь (queue). Приложение может по желанию добавлять в очередь и собственные «запросы» посредством метода pushRequest, это необходимо чтобы вызывать определенный код через задаваемые промежутки времени (например, это делается в MongoNode для опроса курсора).
Запросами в полной мере поддерживаются POST-данные (и multipart), Upload'ы, и прочее (страничка-пример), более того — больше нет необходимости вручную обрабатывать UCS-2 кодировку в запросах (%uFFFF) — это происходит автоматически.
Поддерживаются X-Sendfile (запись ответа на запроса веб-сервера в файл) и Request-Body-File (чтение body-часть запроса из файла). Можно начать выполнение запроса до того как тело полностью принято, и можно программировать обработку upload'а (например, кидать сразу в memcached, а не во временный файл на диске).

Предвижу просьбы дать конкретные цифры показывающие выигрыш в производительности, но некорректно сравнивать синхронные и асинхронные фреймворки. Понятно что последние быстрее. Однако, разница будет напрямую зависеть от приложения, а именно — от количества и времени блокировок в синхронном варианте. Добавлю лишь субъективный бенчмарк странички-примера… Requests per second: 4784.80 [#/sec] (mean).
В данный момент используется последняя стабильная версия libevent (1.4.10-stable), скоро будет выпущена libevent 2.0, это даст возможность рабочим процессам принимать соединения через epoll_wait и принесет дополнительный выигрыш в производительности перед текущим вариантом. Как только так сразу.

Управляемость, администрирование, конфигурирование


Встроенный управляющий скрипт:
# phpd
usage: phpd (start|(hard)stop|update|reload|(hard)restart|fullstatus|status|configtest|help)…

# phpd fullstatus
[STATUS] phpDaemon 0.2 is running (/var/run/phpdaemon.pid).
State of workers:
Total: 1
Idle: 1
Busy: 0
Shutdown: 1
Pre-init: 0
Wait-init: 0
Init: 0

Рабочими процессами поддерживается команды suid, sgid, chroot. Они задаются на уровне конфигурации. Встроенный динамический MPM (Multi-Process Manager) определяет загруженность рабочих процессов и запускает новые в рамках настроек. Используя API, алгоритм MPM можно переопределить в конфигурационном файле. При разработке проекта делается упор как на расширяемость и заменяемость компонентов, так и на простоту использования.

Искаропки


На данный момент в дистрибутив включены следующие модули:
  • FastCGI — позволяет подключаться к приложениям по протоколу FastCGI.
  • HTTP — позволяет подключаться к приложениям по HTTP.
  • CGI — позволяет пускать обычные CGI-приложения по FastCGI, HTTP и любым другим транспортам.
  • Flashpolicy — раздает политику crossdomain для Flash по 843 порту.
  • WebSocketServer — обслуживает WebSocket-сеансы.
  • MongoClient — драйвер MongoDB.
  • MySQLClient — драйвер MySQL (через него также можно подключаться к SphinxQL).
  • MySQLProxy — проксирующий MySQL-сервер.
  • MongoNode — реализация slave-ноды MongoDB, позволяет вешать события на изменения объектов в базе.
  • MemcacheClient — драйвер Memcache.
  • LockServer — распределенный сервис блокировок.
  • LockClient — клиент для собственного сервиса блокировок.
  • TelnetHoneypot — простейший сервер telnet.
  • RTEPServer/RTEPClient — реализация Real-Time Events Protocol, позволяющая клиентам подписываться на события и анонсировать их.
  • BitTorrentTracker — трекер BitTorrent с использованием MongoDB.
  • DebugConsole — сервер интерактивной отладочной консоли a-la telnet, который позволяет выполнять код в запущенном процессе.
Также в поставку включены инструменты для асинхронной работы с процессами и дескрипторами (asyncProcess, asyncStream).
Помимо этого в комплекте идут приложения-примеры.

Будем рады вашим модулям в дистрибутиве!

Баранов в стоило, холодильник в дом


Лицензия — LGPL. Проект относительно свежий, в чем-то сыроватый, но стабильный, и используется в production. Разработка ведется активно. В случае отсутствия форс-мажора, фиксы выходят от нескольких минут до двух суток после репорта.

Web: http://github.com/kakserpom/phpdaemon
Группа: http://groups.google.com/group/phpdaemon
IRC: irc.freenode.org #phpdaemon
E-Mail мне лично — kak.serpom.po.yaitsam@gmail.com

Спасибо за внимание!

P.S. Если вам подобная архитектура импонирует — возможна поддержка, даже в принципе написание модулей на заказ.
Tags:
Hubs:
Total votes 114: ↑99 and ↓15 +84
Views 43K
Comments Comments 103