В этом посте я опишу установку свежей версии Gearman из исходников, а так же создание инфраструктуры для Gearman и его связку и использование в PHP.
Установка Gearman
Качаем из исходников последнюю на текущий момент версию.
1 2 3 4 5 |
wget https://launchpad.net/gearmand/1.2/1.1.12/+download/gearmand-1.1.12.tar.gz tar xzvf gearmand-1.1.12.tar.gz cd gearmand-1.1.12 |
Предварительно перед сборкой надо установить зависимости. Тут аккуратно, могут быть другие версии. На Ubuntu у меня было как в листинге, на Debian версия была ниже.
1 2 3 |
aptitude install libboost-dev libboost-program-options1.54-dev gperf uuid-dev |
Дальше собираем, проблем не должно быть.
1 2 3 4 5 6 |
./configure make make install ldconfig |
Успещность установки можно проверить
1 2 3 |
gearmand -h |
Теперь нужно дебианизировать (убунтузировать) демон gearmand чтобы можно было обращаться с ним как с другими сервисами через команду service.
Для этого нужно создать файл запуска в /etc/init.d/gearmand и дать ему права chmod 777. Содержимое будет следующее.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
#! /bin/sh ### BEGIN INIT INFO # Provides: gearmand # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: gearmand # Description: This file should be used to construct scripts to be # placed in /etc/init.d. ### END INIT INFO # Author: Foo Bar <foobar@baz.org> # # Please remove the "Author" lines above and replace them # with your own name if you copy and modify this script. # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="gearmand" NAME=gearmand DAEMON=/usr/local/sbin/$NAME #DAEMON_ARGS="-d --log-file=/var/log/gearmand/gearmand.log" PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME LOGFILE=/var/log/gearmand/gearmand.log DAEMON_ARGS="-d -P $PIDFILE -l $LOGFILE" # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.2-14) to ensure that this file is present # and status_of_proc is working. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { log_daemon_msg "Starting Gearman Server" "gearmand" # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { log_daemon_msg "Stopping Gearman Server" "gearmand" # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other code # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; status) status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac : |
Теперь можно вызывать команды
1 2 3 4 5 6 7 8 |
service gearmand start service gearmand stop service gearmand restart #для проверки что он запущен ps aux | grep gearman |
Чтобы сделать запуск gearmand автоматическим при перезагрузке системы
1 2 3 |
update-rc.d gearmand defaults |
Что дальше? Дальше установка модуля gearman для php.
Gearman и PHP
Для начало необходимо установить расширение PHP
1 2 3 |
aptitude install php5-gearman |
Или если простой способ не работает, то можно руками
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
apt-get install php-pear php5-dev pecl install gearman-1.1.2 chmod 644 /usr/lib/php5/<long number like 20121212>/gearman.so nano /etc/php5/mods-available/gearman.ini extension=gearman.so cd /etc/php5/cli/conf.d ln -s ../../mods-available/gearman.ini 20-gearman.ini #or apache2 cd /etc/php5/fpm/conf.d ln -s ../../mods-available/gearman.ini 20-gearman.ini service php5-fpm restart |
На официальном сайте gearman есть несколько примеров с PHP. Это простые скрипты без каких либо фреймворков, обычно представляют из себя две части – клиент и воркер.
Рекомендую ознакомиться для начала с примерами, чтобы понимать принцип работы gearman и познакомиться с основными методами.
http://gearman.org/examples/
Я использую фреймворк Zend Framework 2 и поэтому использую модуль mwillbanks/MwGearman. На самом деле можно легко написать свою обертку в Service и самому инициировать его конфигом в фабрике, но зачем, если есть уже готовые решения. У этого модуля предусмотрен конфиг, в который можно внести список серверов gearman.
Ниже я приведу пример как я использую Gearman с Zend.
Для начала пример воркера, т.к. с ним все проще, чем с клиентом. После ознакомления с примерами по ссылке выше проблем с пониманием кода не должно быть.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
namespace Learning\Controller\Console; use GearmanClient; use GearmanJob; use GearmanTask; use GearmanWorker; use Zend\Debug\Debug; use Zend\Mvc\Controller\AbstractActionController; use Zend\Mvc\MvcEvent; class GearmanController extends AbstractActionController { //... public function workerAction() { $serviceMananger = $this->getServiceLocator(); /* @var $gearman \mwGearman\Client\Pecl */ $gearman = $serviceMananger->get('mwGearman\Worker\Pecl'); $gearman->connect(); //will die if no tasks in milliseconds $gearman->getGearmanWorker()->setTimeout(30000); /* @var $gearman \mwGearman\Worker\Pecl */ $gearman->getGearmanWorker()->addFunction('myJob', function (GearmanJob $job) { $workload = $job->workload(); $job->sendStatus(0, 1); //work sleep(1); $job->sendStatus(1, 1); echo __METHOD__ . " " . $workload . "\n"; }); try { $jobsDone = 0; while ($gearman->work() || $gearman->getGearmanWorker()->returnCode() == GEARMAN_TIMEOUT) { if ($gearman->getGearmanWorker()->returnCode() == GEARMAN_TIMEOUT) { # Normally one would want to do something useful here ... echo "Время вышло. Ожидание следующего задания...\n"; continue; } $jobsDone++; if (!$gearman->getError() && $jobsDone < 100) { $jobsDone++; } if ($jobsDone > 100) { throw new \Exception("test error log reached limit of jobs " . $jobsDone . "\n"); } //emulate sleep(1); } } catch (\Exception $e) { return $e->getMessage(); } } //... } |
Это обычный console контроллер который имеет экшн описывающий воркер и вызывается таким образом, так я настроил в zend console route.
1 2 3 |
php /var/www/site.com/public/index.php learning:gearmantest --action=worker |
После вызова воркер будет висеть и ждать задачи. В данном случае этот воркер не делает ничего полезного, работа симулируется через sleep(). Можно так же заметить, что каждые 100 удачно выполненных работ вызывают смерть воркера. Это рекомендуемая практика чтобы не вызвать утечек памяти.
А вот с клиентами поинтереснее, т.к. работы можно запускать в двух режимах – как job, которую нужно выполнить немедленно и нам важен результат выполнения, или как task который имеет меньший приоритет перед job и не так важен (логирование, емейл и т.д) . Наиболее полно различия описывают ответы в этом вопросе http://stackoverflow.com/questions/12689274/whats-the-difference-between-a-task-and-do
Пример клиента в режиме job используя Zend модуль
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public function clientPoolModAction() { try { $serviceMananger = $this->getServiceLocator(); /* @var $gearman \mwGearman\Client\Pecl */ $gearman = $serviceMananger->get('mwGearman\Client\Pecl'); $gearman->connect(); $i = 0; while ($i < 50) { $workload = 'some-string ' . $i; $unique = crc32($workload); $task = new \mwGearman\Task\Task(); $task->setBackground(true) ->setPriority('high') ->setFunction('myJob') ->setWorkload($workload) ->setUnique($unique); $handle = $gearman->addTask($task); echo $i . " "; $i++; } echo "\n"; //without module if (!$gearman->runTasks()) { echo "ERROR " . $gearman->getGearmanClient()->error() . "\n"; exit; } } catch (\Exception $e) { echo $e->getMessage(); } } |
То же самое, но с нативными функциями.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public function clientPoolAction() { try { $serviceMananger = $this->getServiceLocator(); /* @var $gearman \mwGearman\Client\Pecl */ $gearman = $serviceMananger->get('mwGearman\Client\Pecl'); $gearman->connect(); $i = 0; while ($i < 50) { $workload = 'some-string ' . $i; $unique = crc32($workload); //without module $gearman->getGearmanClient()->addTaskBackground('myJob', $workload, null, $unique); echo $i . " "; $i++; } echo "\n"; //without module if (!$gearman->getGearmanClient()->runTasks()) { echo "ERROR " . $gearman->getGearmanClient()->error() . "\n"; exit; } } catch (\Exception $e) { echo $e->getMessage(); } } |
Для вызова клиента аналогично воркеру вызывается команда
1 2 3 |
php /var/www/site.com/public/index.php learning:gearmantest --action=clientPool |
Лучше всего для тестирования открыть несколько терминалов и запустить в них воркеры и в одном терминале клиент, будет видно как параллелятся задачи.
Дальше идет клиент использующий job, я остановил свой выбор на этой реализации. В этой реализации отличие в том, что происходит ожидание на стороне клиента когда выполнятся все задачи, т.е. нужно точно быть уверенным в завершении, дальше можно получить результат, например из БД.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
public function clientPoolDoAction() { try { $uniqueMark = $this->getRequest()->getParam('uniq'); $serviceMananger = $this->getServiceLocator(); /* @var $gearman \mwGearman\Client\Pecl */ $gearman = $serviceMananger->get('mwGearman\Client\Pecl'); $gearman->connect(); $handles = []; $i = 0; while ($i < 100) { $workload = 'some-string ' . $i . "_" . $uniqueMark; $unique = crc32($workload); $handle = $gearman->getGearmanClient()->doBackground('myJob', $workload, $unique); $handles[] = $handle; echo $workload . " "; $i++; } $handlesCount = count($handles); echo "\n"; echo "Jobs added " . $handlesCount . " " . print_r($handles, 1); echo "\n"; /* * stat = [ * 0 - known * 1 - running * 2 - numerator * 3 - denominator * ] */ $timeout = 50; $i = 0; $done = 0; while (($i < $timeout) && ($done < $handlesCount)) { foreach ($handles as $key => $handle) { usleep(300); $stat = $gearman->getGearmanClient()->jobStatus($handle); // the job is known so it is not done if (!$stat[0]) { $done++; unset($handles[$key]); } echo "Running: " . ($stat[1] ? "true" : "false") . ", numerator: " . $stat[2] . ", denomintor: " . $stat[3] . "\n"; } $i++; sleep(1); }; if ($i >= $timeout) { //timeout is over if ($done == 0) { throw new \Exception('timeout'); } } //get result here!!! echo "$done all done!\n"; } catch (\Exception $e) { echo $e->getMessage(); } } |
В последнем примере клиента я добавил параметр unique для того, чтобы если я запускаю быстро 2 клиента из разных терминалов, у меня не повторялись уникальные идентификаторы задач Gearman. Если идентификатор повторяется, то Gearman считает, что знает эту работу и не добавит ее дважды, если работа с таким уником уже есть в очереди.
1 2 3 |
php /var/www/site.com/public/index.php learning:gearmantest --action=clientPoolDo --uniq=1 |
Дальше понадобится инструмент, который будет запускать пачку воркеров и следить за их жизненным циклом.
Установка Supervisor
Для начала не большая оговорка – почему Supervisor, а не Gearman Manager?
На самом деле мне не нравится идея в GM, что нужно класть классы воркеров в строго определенном месте и настраивать это в конфиге менеджера. При использовании с фреймворком я предпочитаю делать воркером просто экшн кнтроллера при помощи console системы фреймворка. Так же Supervisor кажется мне более универсальным инструментом, ведь он может быть использован не только для Gearman.
Установить можно из репозиториев, но более свежая версия обычно ставится так.
1 2 3 4 |
aptitude install python-setuptools easy_install supervisor |
Сразу же будет доступна возможность работать с supervisor как с service
1 2 3 |
service supervisor start |
Сам по себе без настроенных процессов демон бесполезен. Для выше приведенного примера воркера можно создать конфиг на запуск 20-ти процессов на фоне. Учитывая, что в реализации воркера задано ограничение на 100 работ – через каждые выполеннные 100 работ воркер будет умирать и запускаться заново supervisor-ом.
Конфиг файл /etc/supervisor/conf.d/gearmantest.conf с таким содержанием
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[program:gearmantest] command=php /var/www/site.com/public/index.php learning:gearmantest --action=worker --execute=1 process_name=%(program_name)s_%(process_num)02d numprocs=20 directory=/var/www/site.com/data/ autostart=true autorestart=true stdout_logfile=/var/www/site.com/data/logs/gearmantest.log stdout_logfile_maxbytes=10MB stderr_logfile=/var/www/site.com/data/logs/gearmantest.error.log stderr_logfile_maxbytes=10MB |
Теперь после рестарта supervisor будет висеть 20 воркеров и ожидать задач. Можно позапускать клиенты с терминала и убедиться, что задачи выполняются.
Далее хотелось бы видеть статус очереди – сколько воркеров висит на работе и сколько задач в очереди. Через системную утилиту supervisorctl можно смотреть список активных процессов, которые мониторит supervisor, но это не совсем то.
Установка Gearman Monitor
Веб инструмент с http авторизацией который позволяет мониторить статус очереди – https://github.com/yugene/Gearman-Monitor.git
Предварительно надо установить зависимость.
1 2 3 |
pear install Net_Gearman-0.2.3 |
Дальше установить сам проект
1 2 3 4 |
cd /opt git clone https://github.com/yugene/Gearman-Monitor.git gearman-monitor |
1 2 3 |
ln -s /opt/gearman-monitor /var/www/gearman-monitor |
Теперь настроить подключение к Gearman
1 2 3 4 5 6 7 |
cd /opt/gearman-monitor nano _config.php $cfgServers[$i]['address'] = '127.0.0.1:4730'; $cfgServers[$i]['name'] = 'Gearman'; |
Дальше нужно настроить хост, перезапустить веб сервер и должна открыться страница статуса. Будут строки с примерно такой информацией
1 2 3 4 |
Server Function Jobs in queue Jobs running Workers registered localhost myJob 0 0 20 |
Что еще можно сделать? Если бизнес требования подразумевают, что очередь в случае перезагрузки сервера не должна теряться, т.к. она хранится в памяти, можно организовать персистентность для очереди Gearman через MySQL или SQLite3. В данном примере будет через MySQL. Для этого в файле /etc/init.d/gearmand можно в разделе do_start() после –exec $DAEMON добавить следующие параметры подключения. Конечно лучше сначала протестировать их вызывая просто gearmand с этими параметрами.
1 2 3 4 5 6 7 8 9 10 11 12 |
gearmand \ --log-file=/var/log/gearmand.log \ --queue-type=MySQL \ --mysql-host=localhost \ --mysql-port=3306 \ --mysql-user=example \ --mysql-password=example01 \ --mysql-db=gearman \ --mysql-table=gearman_queue \ 2>> /var/log/gearmand.log |
Теперь очередь будет персистентной. Можно переместить MySQL на SSD и установить Percona для минимизации потери в скорости работы Gearman, по желанию.