Краткий обзор MQ (Messages queue) для применения в проектах на РНР. Часть 1

    Параллельно к основной работе, я в «фоне» обдумываю и прикидываю реализации архитектуры для игровых проектов (напомню, что основная область моих интересов и работ — создание онлайновых браузерных игр). Последнее время я все чаще и чаще возвращаюсь к мысли, что интересно было бы реализовать основной игровой сервер на основе очередей сообщений (MQ или Messages queue). То есть, движок такой игры будет представлять собой набор компонентов, которые будут общаться между собой посредством асинхронных сообщений, а каждый компонент может быть как генератором сообщений, так и подписчиком, то есть исполнять другие сообщения.

    Такой подход, насколько я понимаю, широко применяется в мире Java, там для этого есть стандарт Java Message Service (JMS) и применяются брокеры сообщений и на этом базируется архитектура Enterprise service bus (ESB), например, Apache ServiceMix. Но для нас это пока высокая сфера крупных проектов, а в специфике веба и веб-ориентированных приложений я бы хотел рассмотреть, можно ли что-то сделать подобное, но с меньшими затратами и обеспечить приложению отказоустойчивость, распределение нагрузки и асинхронную обработку. И конечно, очень желательно, чтобы это было реализовано на РНР как основном языке реализации всех компонентов сервера.

    И так, еще раз — MQ, это архитектура и ПО промежуточного уровня, которое занимается сбором, хранение и маршрутизацией (распределением) сообщений между компонентами. Я не претендую на полноту описания, и, вполне возможно, не учитываю множества нюансов, поэтому не рассматривайте мои определения как аксиомы, лучше всего почитать дополнительную литературу, если вы хотите поглубже разобраться в архитектуре MQ (например, вот эти статьи: [1], [2], [3]) и определение в Wikipedia — Message queue

    Пока давайте посмотрим очень кратко, обзорно, какие системы реализации систем сообщения есть, а, главное, какие из них можно рассматривать в качестве основы для специфика моего проекта. И хотя я думал рассмотреть только РНР реализации, оказалось, что нужно «копать глубже», поэтому мы затронем и системы на других языках, но обладающие возможность взаимодействовать с РНР-приложениями.

    Apache ActiveMQ — открытая реализация брокера сообщений (Message Broker) и Enterprise Integration Patterns (если сейчас и очень кратко — расширение для реализации дополнительной обработки согласно правилам). Этот проект, по моему мнению, из всех открытых, самый мощный и развивающийся, недавно вышла версия 5.1. Он реализовывает множество стандартов и обеспечивает все возможности, необходимые для решений уровня Enterprise, входит в стек Java-технологий от Apache. Что меня заинтересовало, так это возможность кросс-языкового обмена сообщениями, а значит — клиенты могут быть реализованы на любом языке. Для платформ Java, C, C++, C# это библиотека OpenWire, реализующая Wire протокол для нативного доступа к MQ, для остальных языков, включая, что нам и интересно, РНР, есть Stomp — реализация библиотек для разных скриптовых языков, которая превращает сообщения в формат JMS. Кстати, если необходимо обеспечить безопасную коммуникацию и передачу сообщений, можно использовать SSL.

    MQS (Minimalist Queue Services) — проект, если можно так сказать, с друго конца. Это небольшая система, написанная на Perl, организовывает систему очередей сообщений, используя XML-RPC протокол, сами сообщения могут хранится как в любой базе данных, так и в файлах. К сожалению, по всей видимости, проект заброшен, так как последняя новость на сайте датирована апрелем 2005 года, а текущая версия — 0.0.14.

    Spread — еще одна реализация очереди сообщений, на этот раз на С++, и версии есть для различных платформ, включая Win32, Linux, BSD и MacOS. Текущая версия — 4.0. Система распределенная и ориентирована на высокопроизводительные системы, где клиентов и, соответственно, сообщений, очень много. Заявлена поддержка, в последней 4.0 версии, технологии Virtual Synchrony. Что интересно — в поставку сразу включены бинарные версии для нескольких платформ, а также встроенные интерфейсы для некоторых языков — C/C++, Java, Perl, Python, Ruby. Странно, что четвёртого Р — РНР, среди них нет, но существует расширение в PECL, которое реализовывает весь интерфейс Spread API. Текущая версия 2.1 и достаточно новая, значит проект развивается. Существуют и реализации для других языков, в том числе и альтернативы встроенным интерфейсам, поэтому посмотрите на список здесь, там даже для MS Excel есть расширение. Среди интересных проектов — модуль mod_log_spread для Apache, позволяющий собирать логи доступа с нескольких веб-серверов.

    RabbitMQ — высокопроизводительная платформа, написанная на Erlang, основанная на Open Telecom Platform, а значит — очень надежная и масштабированная система, часто применяемая в телекоммуникационных приложениях и других подобных системах. Есть интерфейс только для Java и C++. Система поддерживает стандарт AMQP (Open Standard for Messaging Middleware). Система интересная, знать бы только Erlang, хотя что-то мне подсказывает, что проектируя весь серверный модуль на этой платформе, мы получили бы много «плюшек», в частности, на этой же платформе написан самый популярный Jabber-сервер ejabberd, который также можно применять в он-лайн игровых проектах.

    Beanstalkd — также интересный проект, созданный в рамках разработки одного из приложений для социальной сети Facebook, которым пользуется около 10 млн человек (приложением, не сетью). Это специализированный сервер для хранения и обработки очередей заданий, использующий хранение данных в памяти для обеспечения скорости, однако в ущерб отказоустойчивости. Этот проект очень похож на уже ранее описанный нами MemcacheQ, да и сами разработчики выражают благодарность создателям memcached за принципы архитектуры и протокол. Система предназначена для создания асинхронной очереди обработчиков пользовательских задач, которые не требуют немедленного ответа, например, отсылки писем, фоновые обработки и т.п. Существуют клиентские API для различных языков, в том числе для Erlang, OCaml, Perl, PHP, Python, Ruby. Для РНР библиотека расположена здесь и пока имеет версию 0.11, сама разработка началась всего пару месяцев назад (судя по регистрации проекта). Детальнее можно почитать отличный обзор: Beanstalk Messaging Queue Сервер написан на С, что обеспечивает высокую скорость работы, однако специфика проекта в плане хранения всех данных в оперативной памяти не подойдет для тех сфер, где остро необходима максимальная отказоустойчивость, пусть даже ценой дополнительного ПО и затрат на хранение данных в базе.

    Пока это все. А как же РНР, спросите вы? Описанные решения основаны на сторонних платформах, однако имеют библиотеки для обеспечения коммуникации с РНР-приложениями. Но существуют и нативные, РНР решения такого рода приложений, конечно, не полные аналоги, здесь уже скажется специфика среды РНР, однако достаточные для многих областей применения. О таких решения я расскажу вам в продолжении этого материала.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 32
    • +1
      Спасибо, как раз интересовался пдобным, и было дело уже начал писать свое. Но теперь приторможу, и поглубже ознакомлюсь с предоставленным материалом :)
      • 0
        спасибо за отзыв! подождите следующего материала, там будет больше про нативные РНР решения
      • 0
        Можно добавить еще starling github.com/starling/starling/tree/master
        • 0
          спасибо! Упомяну в следующей части наверное
        • 0
          Не забудьте о tuple space. Не совсем очереди, но тоже полезно.
          • 0
            Еще было интересно, если бы вы в сравнении отметили какие сервера умеют делать persitent queue
            • +1
              это умеет Apache ActiveMQ, Spread и RabbitMQ
              • 0
                Спасибо
                • 0
                  Во-первых, совсем непонятно, что имелось в виду под persistent queue.
                  Если это очередь, которая хранит сообщения до момента их получения потребителем, тогда spread этого не умеет. Во всяком случае, в его оригинальной инкарнации. Есть коммерческий продукт на основе spread, который это умеет. По-моему вот это они: www.spreadconcepts.com/secure_spread_info.html

                  В spread сделать это вообще проблематично в связи с его основной идеей гарантированности операций с очередями в многосерверной конфигурации. Это чистые in-memory queues.
                  • 0
                    да помоему они обое имеют поддержку этого. Вот же в описании то одинаково: «Enables message reliability in the presence of machine failures, process crashes and recoveries, and network partitions and merges. ».

                    Вместе с тем, часть MQ хранит сообщения в базе (или файлы), встроенной или внешней, поэтому это можно считать как раз persistent queue, остальные работают с памятью, однако могут также обеспечить устойчивость к сбоям отдельных узлов (видимо это как раз случай spread), или же заявляют сразу, что не рассчитаны на такие случаи.
                    • 0
                      Да, в spread именно за счет нескольких серверов обеспечивается reliability.
                      Если бы они сделали встроенный persistence, цены бы не было системе. Но для упрощения и еще по ряду причин, как я понял, они этого не хотели делать.
                    • 0
                      С другой стороны, гораздо проще, используя spread как network backend, сделать полноценные очереди, чем начинать лепить свои целиком через базу данных, как ниже предлагают, или чем строить сетевую систему с нуля, которая скорее всего будет работать хуже.
                    • 0
                      Под persistent queue подразумевалась очередь, которая сохраниться при перезапуске сервера очереди.

                      Т.е. beanstalk не обладает такой очередью, потому что хранить ее в памяти, а starling обладает.
                • +3
                  Memcacheq — штука хорошая, благо основана на memcachedb, который, в свою очередь, есть смесь memcached и bdb — с производительностью точно проблем не будет. Но есть один момент. Когда разгребаешь на скриптовом сервере очередь php-скриптами, ну, предположим, это обычный цикл while ($item = $queue->getNext()) $this->handle($item), внутри этого самого handle может иногда случиться что-то нехорошее. В корку php упадет из-за порушившейся вдруг шаред мемори акселератора или редкого бага в экстеншене, или просто в датацентре рубильник не тот заденут. А терять данные в очереди совсем не хочется. То есть, если это сбор статистики средней температуры по больнице, то и фиг с ним, а если это биллинг, то как-то не это самое. :)

                  Я использую очень простое решение. В mysql (или иной РСУБД) заводится табличка (или, если очереди большие, несколько табличек, обрабатываемых параллельно запущенными скриптами) вида

                  CREATE TABLE `ProcessQueue` (
                    `id` int(20) NOT NULL AUTO_INCREMENT,
                    `subscriber_id` int(10) NOT NULL DEFAULT '',
                    `error_count` smallint(6) NOT NULL DEFAULT '0',
                    `parameters` blob NOT NULL,
                    `status` enum('QUEUED','LOCKED','FAILED','DELETED') NOT NULL DEFAULT 'QUEUED',
                    `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
                    `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    PRIMARY KEY (`id`),
                    KEY `updated` (`updated`,`subscriber_id`,`status`)
                  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
                  


                  Subscriber_id — это идентификатор «подписчика», то есть тот, кому элемент очереди предназначается. Parameters — произвольные данные, нужные для обработки, например serialize()-ованные.

                  Обработка очереди выглядит примерно так:
                  $PQ = new ProcessQueue($subscriber_id);
                  
                  $element = false;
                  $error = false;
                  
                  while ($element = $PQ->getNextElement($element, $error)) {
                      // ... process $element ...
                      if (error occurred) {
                          $error = true;
                      } else {
                          $error = false;
                      }
                  }
                  


                  Сам класс выглядит (после небольших упрощений) вот так. Несмотря на кучу завязок на фреймворк, думаю, его логика должна быть понятной.

                  А что делать с «залипшими» элементами? Очень просто, по крону с разумной регулярностью разгребаем элементы, где status=«LOCKED» и updated достаточно стар, чтобы было ясно, что оно именно залипло — например, как-то так:

                  UPDATE ProcessQueue 
                  SET error_count = error_count + 1, STATUS = IF(error_count>5, 'FAILED', 'QUEUED')
                  WHERE updated < $время_минут_15_назад AND STATUS='LOCKED'
                  


                  А ставить и изучать монстроподобные системы ради такой простой вещи, как очереди, как то мне стремно, хотя надо взглянуть, может, найдутся хорошие идеи, чтобы позаимствовать :)
                  • 0
                    спасибо за подробный комментарий, воспользуюсь как буду практически пробовать свою архитектуру
                    • 0
                      спасибо большое. подобное формировалось в голове и теперь окончательно приняло форму после Ваших разъяснений :)

                    • 0
                      ApacheMQ — очень мощьное средство! использовал в одном из проектов. Очень понравилась их реализация AJAX доступа к Queue и подобным.
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • +2
                          Для хранения очередей в памяти PHP, конечно, подходит плохо, и демон писать смысла особого нет. Но для хранения очереди можно использовать любую СУБД, я выше приводил пример.

                          А для скриптов, обрабатывающих очереди, нет ничего страшного. Конечно, держать «вечный» цикл невыгодно — даже сам PHP, работая в fastcgi-режиме, имеет ограничение на количество обрабатываемых одним процессом запросов, чтобы не накапливать утечки памяти. Но вполне можно запускать несколько скриптов из крона параллельно, что-то вроде
                          $ crontab -e
                          * * * * * /path/to/script1.php
                          * * * * * /path/to/script2.php


                          Стратегии завершения можно использовать различные. Можно просто выгребать очередь, пока она не кончится, и на этом завершаться; можно поставить дополнительное условие по времени или количеству итераций. А чтобы не запустились одинаковые обработчики одновременно (если это, конечно, специально не задумано), прекрасно подходит стандартная юниксовая техника pid-файлов. Как-то так:

                          class Script extends Application {
                              // ...........
                              protected function checkPid() {
                                  $this->pid = posix_getpid();
                                  $pid = (int) @file_get_contents($this->pidfile);
                          
                                  if ($pid) {
                                      if (is_dir('/proc') && is_dir('/proc/curproc')) { // use procfs
                                          $have_pid = is_dir('/proc/'.$pid);
                                      } else { // if no procfs, use /bin/ps. warning: BSD-specific implementation
                                          $output = array();
                                          exec('/bin/ps -auxwwwp '.$pid, $output); 
                                          $have_pid = !empty($output[1]);
                                      }
                                      if ($have_pid) {
                                          $this->logScriptEvent(self::EVENT_WARNING, 'already running with pid '.$pid. ', exiting');
                                          exit(1);
                                      }
                                  }
                          
                                  try {
                                      file_put_contents($this->pidfile, $this->pid . "\n");
                                  } catch (PhpException $e) {
                                      $this->logScriptEvent(self::EVENT_WARNING, 'could not write pidfile: '.$e->getMessage());
                                  }
                                  return true;
                              }
                              // ...........
                              function run() {
                                  if (!$this->checkPid()) {
                                      $this->logScriptEvent(self::EVENT_NOTICE, 'the script ' . get_class($this) . ' is already running');
                                      exit(0);
                                  }
                                  // ....
                              }
                          }
                          


                          Если есть неободимость распараллелить операции уже после инициализации, тоже стандартный юниксовый подход — fork.
                          • +1
                            Ах, да. На винде, конечно, это все работать не будет (разве что под cygwin). Хотя я склонен считать это скорее преимуществом =)
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • +1
                                «работало неплохо…но на нагрузках ложилась…»

                                :)

                                Собственно, с работой с процессорами в php проблем то и нет. fork есть, exec есть, popen есть, что еще надо? :) дело-то не в этом, а в том, как обрабатывать параллельные соединения.

                                А тут, чтобы понимать, как эти все винтики крутятся, обязательно к прочтению RU.UNIX.PROG FAQ — Как писать сервера.
                                • 0
                                  С процессами, пардон.
                              • 0
                                Меня тут в привате пинают по делу:

                                «Для проверки существования процесса не нужно делать то, что вы нагородили. Достаточно сделать kill(pid, 0);»

                                И действительно.

                                srv01 ~$ php -r 'var_dump(posix_kill(posix_getpid(),0));'
                                bool(true)
                                srv01 ~$ php -r 'var_dump(posix_kill(33333,0));'
                                bool(false)
                                


                                Век живи, век учись :)
                            • 0
                              Ну из опенсорсных JBoss Messaging & Sun OpenMQ ещё — с каждого вендора по продукту, как это в Java-мире принято) Правда про интеграцию со сторонними языками не подскажу.
                              • 0
                                JBoss Messaging искренне не рекомендую использовать. Низкая производительность, галлюционоз и кривизна.

                                ActiveMQ и OpenMQ очень круты.
                              • 0
                                Еще небольшой список по теме:
                                — WebSphere MQ, кстати, имеет SOAP интерфейс и может быть использована в РНР.
                                — ObjectWeb JORAM joram.objectweb.org
                                — OpenJMS openjms.sourceforge.net
                                — MOM4J mom4j.sourceforge.net
                                — SwiftMQ www.swiftmq.com

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

                                Еще есть совет из опыта работы с MQ: посмотрите на процесс в целом и подумайте, а нужно ли вам использовать MQ или нет? Как раз учитывая тему высокой производительности, то MQ, особенно с сохранением сообщений, дает большую задержку, которую можно компенсировать только большим количеством параллельных процессов обработки.
                                • 0
                                  спасибо за ссылки, надо будет все пересмотреть. WebSphere yfмеренно не рассматривал, мне показался уж очень корпоративным и громоздким и тянет за собой много IBM-компонентов от инфраструктуры WebSphere
                              • 0
                                >RabbitMQ: Есть интерфейс только для Java и C++. С
                                а так же perl, ruby, python & PHP.
                                сам являюсь автором модуля PECL PHP pecl.php.net/package/amqp
                                www.rabbitmq.com/devtools.html
                                github.com/ruby-amqp/amqp/
                                pypi.python.org/pypi/amqp

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.