PHP-Resque的使用(一) 安装
在第二部分我们使用php-resque作为队列系统,这一节讲如何安装php-resque。
PHP-Resque是依赖Redis的,所以需要先安装Redis及PHP的Redis扩展。以下是所有需要安装的组件:
- Redis
- PHP的Redis扩展(php-redis)
- php-resque
- PHP的PCNTL扩展
Redis
Redis是一个开源的KV数据库,数据是保存在电脑RAM中的,速度非常快,所以通常可以使用Redis来做缓存,或保存Session等。可以在Redis的官方网站下载最新稳定版本。Redis的安装方法本文不再赘述,安装完成后不要忘记启动。
PHP-Resque
php-resque是resque的PHP版本,很多特性都和原版相似或相同。
下载最新版本的zip压缩包,或克隆它的仓库:
git clone git://github.com/chrisboulton/php-resque.git
PHPredis以上下载的只是php-resque的库,只需要把文件夹放在任何你项目需要的位置即可。也可以使用Composer安装php-resque。
PHPredis扩展相当于是Redis的PHP API,但它不是PHP使用Redis的唯一接口,类似的库还有redisent、rediska、predis、redisentwrap等。但phpredis是其中最快也是最流行的。关于phpredis扩展的安装方法网上也有很多,也就不再赘述了。
PHP PCNTL扩展
PCNTL(进程控制扩展)依赖于Unix系列系统的进程管理,所以php-resque只能运行在UNIX架构的电脑上,如Linux。
一般可以通过编译安装PHP的时候启用PCNTL扩展,如果没有安装也可以:
- 下载对应版本的PHP源码
- 解压文件
tar -zxvf php-x.x.x.tar.gz
- 进入ext/pcntl目录
cd php-x.x.x/ext/pcntl/
- 配置、编译、安装
`sudo phpize && ./configure && make install`
- 添加
extension=pcntl.so
到php.ini - 重新启动Apache/Nginx
后台任务和PHP-Resque的使用(二) 使用Worker
注意,这篇教程仅适用于Linux和OS X的系统,Windows并不适用。
理解Worker的本质
技术上讲一个Worker就是一个不断运行的PHP进程,并且不断监视新的任务并运行。
一个简单的Worker的代码如下:
while (true) {
$jobs = pullData(); // 从队列中拉取任务
foreach ($jobs as $class => $args) { // 循环每个找到的任务
$job = new $class();
$job->perform($args); // 执行任务
}
sleep(300); // 等待5分钟后再次尝试拉取任务
}
以上这些代码的具体实现都可以交给php-resque。创建一个Worker,php-resque需要以下参数:
QUEUE: 需要执行的队列的名字
INTERVAL:在队列中循环的间隔时间,即完成一个任务后的等待时间,默认是5秒
APP_INCLUDE:需要自动载入PHP文件路径,Worker需要知道你的Job的位置并载入Job
COUNT:需要创建的Worker的数量。所有的Worker都具有相同的属性。默认是创建1个Worker
REDIS_BACKEND:Redis服务器的地址,使用 hostname:port 的格式,如127.0.0.1:6379,或localhost:6379。默认是localhost:6379
REDIS_BACKEND_DB:使用的Redis数据库的名称,默认是0
VERBOSE:啰嗦模式,设置“1”为启用,会输出基本的调试信息
VVERBOSE:设置“1”启用更啰嗦模式,会输出详细的调试信息
PREFIX:前缀。在Redis数据库中为队列的KEY添加前缀,以方便多个Worker运行在同一个Redis数据库中方便区分。默认为空
PIDFILE:手动指定PID文件的位置,适用于单Worker运行方式
以上参数中只有QUEUE是必须的。如果让Worker监视执行多个队列,可以用逗号隔开多个队列的名称,如:”queue1,queue2,queue3”,队列执行是有顺序的,如上queue2和queue3总是会在queue1后面被执行。
也可以设置QUEUE为*让Worker以字母顺序执行所有的队列。
Worker 必须以CLI方式启动。你不可以从浏览器启动Worker,因为:
你无法从浏览器执行后台任务
PCNTL扩展只能运行在CLI模式
启动Worker
可以从resque.php启动Worker,这个位置位于php-resque/bin目录下(也可能不带.php后缀)。
在终端中执行:
cd /path/to/php-resque/bin/
php resque.php
很显然Worker不会被启动,因为缺少必须的参数QUEUE,程序将会返回如下错误:
Set QUEUE env var containing the list of queues to work.
php-resque通过getenv获取参数,所以在启动Worker的时候应该传递环境变量过去。所以应该以下面的方式启动Worker:
QUEUE=notification php resque.php
如果启用VVERBOSE模式:
QUEUE=notification VVERBOSE=1 php resque.php
终端将会输出:
*** Starting worker KAMISAMA-MAC.local:84499:notification
** [23:48:18 2012-10-11] Registered signals
** [23:48:18 2012-10-11] Checking achievement
** [23:48:18 2012-10-11] Checking notification
** [23:48:18 2012-10-11] Sleeping for 5
** [23:48:23 2012-10-11] Checking achievement
** [23:48:23 2012-10-11] Checking notification
** [23:48:23 2012-10-11] Sleeping for 5
... etc ...
Worker会自动被命名为KAMISAMA-MAC.local:84499:notification,命名的规则是hostname:process-id:queue-names。
如果觉得这种启动方式太麻烦且难记,可以自己手动写一个bash脚本来帮助你启动Resque,如:
EXPORT QUEUE=notifacation
EXPORT VERBOSE=1
php resque.php
后台运行Worker
通过上面的方法成功启动了Worker,但只有在终端开启的状态下,关闭终端或按下Ctrl+C时Worker就会停止运行。我们可以在命令后面添加一个&来使其后台运行。
QUEUE=notification php resque.php &
这样就可以让resque在后台运行。但如果你开启了VERBOSE模式,所有的输出信息将会丢失。所以我们需要在resque后台运行时把输出的信息保存起来。
我们可以使用nohup来保持resque后台运行,即使是在用户登出后。
nohup QUEUE=notification php resque.php &
记录下Worker的输出
可以使用管道操作的方式重定向输出到文件:
nohup QUEUE=notification php resque.php >> /path/to/your/logfile.log 2>&1 &
这样一来所有的标准及错误输出都会被写入到logfile.log文件中。如果需要监视这个文件的内容:
tail -F /path/to/your/logfile.log
Worker的执行权限
无论何时你在终端中执行命令都是以当前登录用户的权限来执行。如果你登录的jerry的账户,php-resque将会运行于jerry的权限下。以root用户登录时也一样。
如果需要避开当前登录账户以其它用户的权限运行,如Apache通常运行在www-data用户下,让php-resque运行于www-data账户:
nohup sudo -u www-data QUEUE=notification php resque.php >> /path/to/your/logfile.log 2>&1 &
操作执行权限时需要注意:
通过Worker生成的文件无法被其它用户的php代码读取
Worker没有权限创建或编辑其它用户的文件
Let’s play
前面已经讲了如何启动、如何后台运行、以及记录运行日志,下面就用一些例子结束本节的内容。
创建一个执行default队列的Worker,并且每隔10秒检索一次任务:
INTERVAL=10 QUEUE=default php resque.php
创建5个执行default队列的Worker,每隔5秒检索一次任务:
QUEUE=default COUNT=5 php resque.php
INTERVAL参数没有被指定,因为默认值是5秒。
创建一个执行achievement和notification队列的Worker(需要注意队列名的顺序):
QUEUE=achievement,notification php resque.php
创建一个执行所有队列的Worker:
QUEUE=* php resque.php
如果你的Redis服务器在别的地址:
QUEUE=default REDIS_BACKENT=192.168.1.56:6380 php resque.php
使用自动载入php文件:
QUEUE=default APP_INCLUDE=/path/to/autoloader.php php resque.php
确认你的Worker成功运行了
通过管道操作无法知道Worker是否成功启动,当前通过查看log文件中有没有输出*** Starting worker .....的内容也可以知道是否启动。
也可以通过查看系统进程的方法确认Worker是否正在运行。
ps -ef|grep resque.php
将会输出名称中包含resque.php的进程,其中第二列是进程的PID。
使用这个方法可以很好的知道Worker是否正在运行,以及有没有意外终止。
暂停和停止Worker
要停止一个Worker,直接kill掉它的进程就行了。可以通过ps -ef|grep resque.php查看Worker进程的PID。当然通过这个命令你无法知道哪个PID代码的哪个Worker。
如果要结束一个PID是86681的进程:
kill 86681
这个命令将会立即结束掉PID为86681的进程及子进程。如果Worker正在执行一个任务也不会等待任务执行完成(未完成的部分将会丢失)。
有一个可以平滑的停止Worker的方法,可以通过给kill命令发送一个SIGSPEC信号来告诉kill应该怎么做,这需要PCNTL扩展的支持。
当然下面所讲述的所有命令都需要PCNTL扩展支持。
通过PCNTL扩展,Worker可以支持以下信号:
QUIT - 等待子进程结束后再结束
TERM / INT - 立即结束子进程并退出
USR1 - 立即结束子进程,但不退出
USR2 - 暂停Worker,不会再执行新任务
CONT - 继续运行Worker
当没有信号发出时默认是TERM / INT信号。
如果想在所有当前正在运行的任务都完成后再停止,使用QUIT信号:
kill -QUIT YOUR-WORKER-PID
结束所有子进程,但保留Worker:
kill -USR1 YOUR-WORKER-PID
暂停和继续执行Worker:
后台任务和PHP-Resque的使用(五) 创建任务
到目前为止已经让Worker运行了,我们需要创建并添加任务。这一节主要了解什么是任务(Job),以及如何使用任务。
简单的说,任务就是传递给Worker要执行的内容。我们需要把Job依次添加到Queue来执行。
要把任务添加到队列,程序必须要包含php-resque库以及Redis。
使用require_once '/path/to/php-resque/lib/Resque.php';
包含php-resque的库文件,它会自动连接到Redis服务器,如果你的Redis服务器不是默认的localhost:6379
,你需要使用Resque::setBackent('192.168.1.56:3680');
这样的格式来设置你的Redis服务器的地址,同样setBackent支持可选的第二个参数为使用的Redis数据库名,默认为0。
现在php-resque已经准备好了,使用以下代码添加一个任务到队列:
1 | Resque::enqueue('default', 'Mail', array('dest@mail.com', 'hi!', 'this is a test content')); |
- 第一个参数,’default’是指队列的名字,示例中将会把任务推送到名为default的队列中
- 第二个参数是Job的类名,表示要执行哪个Job
- 第三个参数是要发送给Job的参数也可以使用关联数组的形式
传递给Job的参数(上面第三个参数)可以是普通数组、关联数组的形式,也可以是一个字符串,但使用数组可以很方便的传递更多的信息给Job。所有的参数在推送到队列前都会经过json_encode
处理。
创建一个Job
如上面的例子中,第一个参数是队列的名字(还记得上一节里面启动php resque.php时传递的QUEUE环境变量吗?),第二个参数是Job的类名,即要执行的Job。Mail类就是一个Job类。
所有的Job类都应该包含一个perform()
方法,使用Resque::enqueue()
传递的第三个参数可以在perform()
方法中使用$this->args
来得到。一个典型的Job类如下所示:
1 2 3 4 5 | class Mail{ public function perform(){ var_dump($this->args); } } |
Job类也可以包含setUp()
和tearDown()
方法,可选的这两个方法分别会在perform()
方法之前和之后运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Mail{ public function setUp(){ # 这个方法会在perform()之前运行,可以用来做一些初始化工作 # 如连接数据库、处理参数等 } public function perform(){ # 执行Job } public function tearDown(){ # 会在perform()之后运行,可以用来做一些清理工作 } } |
包含Job类
在实例化Job类之前,必须让Worker找到并包含这个类。有很多种方法可以做到。
使用include_path
当PHP运行于Apache model方式的时候可以使用.htaccess设置包含:
1 | php_value include_path ".:/already/existing/path:/path/to/job-classes" |
或者通过php.ini
1 | include_path = ".:/php/includes:/path/to/job-classes" |
使用APP_INCLUDE包含
上一节说了使用APP_INCLUDE
指定Worker执行时要包含的PHP文件的路径,如:
1 | QUEUE=default APP_INCLUDE=/path/to/loader.php php resque.php |
loader.php的内容可以是下面的那样:
1 2 3 4 | include '/path/to/Mail.php'; include '/path/to/AnotherJobClass.php'; include '/path/to/somewhere/AnotherJobClass.php'; include '/JobClass.php'; |
当然也可以使用PHP的autoloader方法——sql_autoloader
。
在你的项目中使用后台任务
以下面的代码为例,把耗时较多的工作交给后台任务来做。
1 2 3 4 5 6 7 8 9 10 11 12 | class User{ # functions(){} // 其它函数 public function updateLocation($location) { $db->updateUserTable($this->userId, 'location', $location); $this->recomputeNewFriends(); # 此操作耗时较长 } public function recomputeNewFriends() { # 查找新的朋友 } } |
把以上代码改成:
1 2 3 4 5 6 7 8 9 10 11 | class User { # functions(){} // 其它函数 public function updateLocation($location) { $db->updateUserTable($this->userId, 'location', $location); # 把任务添加到队列 # 这里的队列名为 'queueName' # 任务名为 'FriendRecommendator' Resque::enqueue('queueName', 'FriendRecommendator', array('id' => $this->userId)); } } |
以下是任务FriendRecommendator类的实现代码:
1 2 3 4 5 6 7 | class FriendRecommendator { function perform() { # 这里没有User类,需要创建一个User类对象 $user = new User($this->args['id']); # 查找新朋友的操作 } } |
简单的说,你只需要把你的执行任务的代码放到Job类中并改名为perform()
即可,只要你愿意甚至可以将普通类改成Job类,但并不推荐这样做。
perform()
方法有个缺点,即一个Job类只能包含一个perform()
方法,也就是说一个Job类只能执行一种后台任务。例如你有一个发送通知信息的后台任务,但又有发送给用户和发送给管理员两个不同的需求,一般来说就得需要两个Job类才能实现。不过这里有个小小的Hack可以使一个Job能执行多个类型的任务。
首先就是给你的Job分类,把相似工作的Job放在同一个Job类中,因为完全不相关的Job即使放在同一个类中也没有任何意义。然后通过给Resque::enqueue()
方法传递一个表示不同Job的参数过去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # Job类中的写法 class Notification{ function sentToUser(){ # Code.. } function sentToAdmin{ # code.. } function perform(){ $action = $this->{array_shift($this->args)}; if(method_exists($this, $action)){ $this->$action(); } } } # 添加任务时的写法 Resque::enqueue('default', 'Notification', array('sendToAdmin', 'this is content')); |
也可以使用其它类继承Job类以获取相同的perform()
方法,但要注意必须同时包含这些类文件。
另外需要注意的是使用这种Hack的方法Resque::enqueue()
的第三个参数必须是一个数组,并且它的第一个元素是要执行的任务的方法名,并且这个元素会在执行时从$args
数组中移除。
必须在每次修改Job类后重新启动你的Worker。