laravel队列-让守护进程处理耗时任务

待解决的问题

最近在做一个服务器集群管理的web项目,需要处理一些极其耗时的操作,比如磁盘格式化分区。对于这个需求,最开始的想法是,为了让节点上的rpc(远程过程调用) service端尽可能简单(简单到只需要popen执行一条指令即可,有时间我再专门写一篇博客讲讲这个项目的rpc是如何实现的),我们选择了让web端直接等待处理结果,那么问题来了,如何保证用户不必等待,又能保证任务准确的执行呢?

简单的rpc结构如下图

以往在处理一些稍微耗时的操作,可以通过优化代码结构,优化数据库操作次数,起一些线程来处理一些简单的比如发邮件,生成大的压缩文件,提取视频缩量图,服务器间互访等等操作,来避免用户在web页面的等待。

但现在这个操作显然不能用之前的这些方法做,因为现在的操作哪怕只执行一次,都是非常耗时的,更何况可能需要处理的可能是上百上千台服务器。这是在线程层面很难做,要知道在响应请求的web进程中起一个线程来做的话,在响应完断开tcp连接之后,这个进程很可能被kill掉,像Apache就是这样,当然可以通过配置改变apache的行为,但显然不太靠谱。

更好的做法是在web服务器上起一个守护进程去做这个事情,那么问题就在于如何创建守护进程了,好在laravel帮我们考虑了这个事情。

Laravel的队列

laravel的队列默认是以sync(同步)的方式来处理多个任务,这显然不是我们想要的。鉴于这个项目使用的是laravel4.1版本,我选择了beanstalkd来实现异步处理多个任务。

其中beanstalkd是一种比较专业的队列服务驱动器,是一个常驻后台服务,我们可以通过它提供的接口来把任务提交给它,由它创建的守护进程来执行队列。

配置队列执行环境

1.安装beanstalkd服务

我开发的电脑为CentOS5.4,版本比较低,所以装的过程中还是遇到些麻烦

首先执行下面的指令

1 wget ftp://fr2.rpmfind.net/linux/epel/5/ppc/epel-release-5-4.noarch.rpm
2 rpm -ivh epel-release-5-4.noarch.rpm
3 yum makecache
4 yum search beanstalkd

但最后发现找不到这个软件,于是将yum的源换成了163的

1 cd /etc/yum.repos.d
2 wget http://mirrors.163.com/.help/CentOS5-Base-163.repo
3 mv CentOS-Base.repo CentOS-Base.repo.bak

再次makecache && install 就OK了,安装完后启动beanstalkd服务

1 service beanstalkd start

另外可以搜索到beanstalkd的配置文件放在了sysconfig下

为laravel添加beanstalkd的驱动

beanstalkd的php驱动包为pda/pheanstalk

进入laravel的protected目录,composer.json在这个目录下

执行

1 composer require pda/pheanstalk 2.*

出现如下的错误

可以看出镜像地址响应 502,所以需要给composer找一个可用的镜像 http://www.phpcomposer.com/

修改~/.composer/config.json如下

然后回到protected目录,再执行前面安装驱动的命令安装,这回出现了不一样的错误

上面的php包中,第一个是为了phpstorm的对laravel更好的支持,后面一个symfony/yaml已经安装,并不需要升级,所以修改composer.json,直接将这两个项目删除掉就行了

删除完之后,再次执行安装命令安装

可以看到终于成功了,可以通过 composer show -i 查看安装了哪些包

测试

在TestController中添加一个action

 1 class TestController extends BaseController
 2 {
 3
 4     public function getQueue(){
 5         //
 6         Log::info("添加一个对列任务");
 7         Queue::push(‘SendEmail‘,array(‘message‘=>‘哈哈‘));
 8         Log::info(‘任务添加完毕‘);
 9         exit;
10     }
11
12 }

在app目录下新建tasks目录,并修改protected/composer.json和app/global.php,将这个目录加到类加载路径中

修改global.php

1 ClassLoader::addDirectories(array(
2
3    app_path().‘/commands‘,
4    app_path().‘/controllers‘,
5    app_path().‘/models‘,
6    app_path().‘/database/seeds‘,
7    app_path().‘/library‘,
8    app_path().‘/tasks‘,
9 ));

修改composer.json

1 "classmap": [
2   "app/commands",
3   "app/controllers",
4   "app/models",
5   "app/database/migrations",
6   "app/database/seeds",
7   "app/tasks",
8   "app/tests/TestCase.php"
9 ]

以后的耗时调度任务的代码就放在这个目录下面了

首先新建一个BaseTask.php

 1 /**
 2  * Created by PhpStorm.
 3  * User: Administrator
 4  * Date: 2015/8/19 0019
 5  * Time: 11:55
 6  */
 7 abstract class BaseTask
 8 {
 9     public abstract  function fire($job,$data);
10 }

然后新建一个SendMail.php

 1 /**
 2  * Created by PhpStorm.
 3  * User: Administrator
 4  * Date: 2015/8/19 0019
 5  * Time: 11:50
 6  */
 7 class SendEmail extends BaseTask
 8 {
 9
10     public function fire($job, $data)
11     {
12         // TODO: Implement fire() method.
13         Log::info("对列任务执行".json_encode($data)."Time : ".time());
14         sleep(30);
15         Log::info("对列任务执行完毕".time());
16         //  将任务从队列冲删除
17         $job->delete();
18         //  将任务返回到队列
19
20 //        $job->release();
21
22     }
23 }

最后就是去修改config目录下的配置文件queue.php文件了,修改为

1 ‘default‘   => ‘beanstalkd‘,
1 ‘beanstalkd‘ => array(
2    ‘driver‘ => ‘beanstalkd‘,
3    ‘host‘   => ‘localhost‘,
4    ‘queue‘  => ‘default‘,
5    ‘ttr‘   =>    60,
6 ),

关于ttr的解释这里有一个

http://stackoverflow.com/questions/25791633/change-beanstalkd-default-ttr

http://in355hz.iteye.com/blog/1395727

表示time to run ,这个参数可以覆盖默认参数让Beanstalkd 检测是否在这个时间内完成

至此配置和写代码完成,在shell中执行,要在protected目录下,artisan文件在这个目录下

1 php artisan queue:work
2 php artisan queue:listen

然后访问url,http://192.168.1.10/ssanlv/test/queue,可以发现请求马上就完成了,页面并没有等待

查看protected/app/storage/logs/laravel.log 可以看到下面的内容

508-478 刚好30秒

下面测试一个实际的问题,印象中apache服务器与客户端在请求完成断开连接后会kill掉负责处理的httpd进程,只有配置了keep-alive参数在会将进程保留到apache进程池中,所以,但用户请求一个耗时操作之后,关闭了浏览器,这个处理耗时任务的守护进程会不会也被kill掉呢?当然,其实有点多虑了,当响应完成之后tcp链接已经被断开掉了,如果进程会被kill掉,那么早就kill掉了,跟你浏览器关没关应该没多大关系,还是试试吧,实践才是硬道理

这里将SendMial中的sleep时间改长一点,改为 600秒

最后发现没有执行完,可以看到listen报出异常

很显然执行超时,看来是前面设置的ttr的问题

将ttr注释掉或者修改掉更高的值,发现还是不行,最后在仔细看看报错信息,发现

所以改变命令的执行方式

1 php artisan queue:listen --timeout=800

最后命令任务成功执行完毕

可以看到 1353-753 = 600 刚刚好

另外,看样子 这个任务对列应该是被保存起来了,当我没有启动 listen时,任务怎么都不会处理,但我一但启动listen,前面添加的任务就会立马执行

但最后还是有个问题这个是对列形式进行处理,要启动下一个对列任务,必须等上一个对列任务执行完毕,不过之前曾看到过,一个work对应一个任务队列,那么我完全可以起多个任务队列,有点多核CPU的调度哦。

更好的办法

最后,再跟一位大神讨论了一下,探讨出了另外一个更加优秀的办法,虽然会加重节点上rpc service代码的复杂度,不过也不是很麻烦。

这种方式就是回调,管理集群的web服务器可以不用等待,只需如下步骤,

  • 通过web服务器上的rpc client向要执行耗时操作的节点上的rpc service发送一条指令,
  • 节点上的rpc service收到指令后,不先执行指令,而是马上向web服务器,也就是rpc client返回一个任务ID。
  • web服务器将这个id作为一条任务记录保存起来。
  • 节点上的rpc service处理指令,至于处理指令,我们商量的也是在节点上在单独起一个进程 P 来处理,因为rpc service也不能让rpc client傻等着
  • 处理进程 P 处理完了之后,将执行结果和任务ID作为参数,回调web服务器的一个web接口
  • web服务器接到rpc service的回调之后,通过ID查找到任务,更新任务的执行状态,更新数据

很显然,这种方式更加可靠,也大大减轻了web 服务器的负担,要知道Linux 系统的线程数是有限制的,但这要耗时任务多了,如果然服务器去等,不管啥策略都很可能吧服务器整垮。

时间: 2024-12-14 14:56:11

laravel队列-让守护进程处理耗时任务的相关文章

th5.1 队列使用守护进程

起因 公司项目使用到了redis队列, 使用队列就会遇到队列被干掉的情况, 所以需要使用守护进程 过程 当项目上线之后, 需要在子项目根目录启动队列 php think queue:listen 但是这种写法, 一旦窗口关闭, 队列就会取消, 所以我就想了一个取消救国的方法, 让它在后台运行 nohup php think queue:listen 2 > &1 & 使用nohup 可以是该命令在后台运行,并把所有输出都被重定向到一个名为nohup.out的文件中 但是这样做还是避免

PHP高级编程之守护进程,实现优雅重启

PHP高级编程之守护进程 http://netkiller.github.io/journal/php.daemon.html Mr. Neo Chen (陈景峰), netkiller, BG7NYT 中国广东省深圳市龙华新区民治街道溪山美地 518131 +86 13113668890 +86 755 29812080 <[email protected]> 版权 ? 2014 http://netkiller.github.io 版权声明 转载请与作者联系,转载时请务必标明文章原始出处和

linux 脚本--守护进程

#/bin/bash #队列的守护进程 Date = `date +"%F-%H:%M:%S"` XMML = "/var/www/html/xiangmu" project= "src.plan" log = "RIZHI" num = `ps aux|grep ${project}|grep -V 'grep'|wc -l` if [$num -eq 1] then echo "${Date}队列进程ok&quo

守护进程,互斥锁,IPC,队列,生产者与消费者模型

小知识点:在子进程中不能使用input输入! 一.守护进程 守护进程表示一个进程b 守护另一个进程a 当被守护的进程结束后,那么守护进程b也跟着结束了 应用场景:之所以开子进程,是为了帮助主进程完成某个任务,然而,如果主进程认为自己的事情一旦做完了就没有必要使用子进程了,就可以将子进程设置为守护进程 例如:在运行qq的过程,开启一个进程,用于下载文件,然而文件还没有下载完毕,qq就退出了,下载任务也应该跟随qq的退出而结束. from multiprocessing import Process

13 join 线程锁之Lock\Rlock\信号量 将线程变为守护进程 Event事件  queue队列 生产者消费者模型 Queue队列 开发一个线程池

本节内容 操作系统发展史介绍 进程.与线程区别 python GIL全局解释器锁 线程 语法 join 线程锁之Lock\Rlock\信号量 将线程变为守护进程 Event事件 queue队列 生产者消费者模型 Queue队列 开发一个线程池 进程 语法 进程间通讯 进程池 操作系统发展史 手工操作(无操作系统) 1946年第一台计算机诞生--20世纪50年代中期,还未出现操作系统,计算机工作采用手工操作方式. 手工操作程序员将对应于程序和数据的已穿孔的纸带(或卡片)装入输入机,然后启动输入机把

守护进程,模拟抢票例子,互斥锁,信号量,队列总结

 守护进程 主进程创建守护进程 其一:守护进程会在主进程代码执行结束后就终止 其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children 注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止 # 守护进程 from multiprocessing import Process import os,time,random def task(): print('%s

python并发编程基础之守护进程、队列、锁

并发编程2 1.守护进程 什么是守护进程? 表示进程A守护进程B,当被守护进程B结束后,进程A也就结束. from multiprocessing import Process import time ? def task(): print('妃子的一生') time.sleep(15) print('妃子死了') ? if __name__ == '__main__': fz = Process(target=task) fz.daemon = True #将子进程作为主进程的守护进程.必须在

python并发编程(守护进程,进程锁,进程队列)

进程的其他方法 P = Process(target=f,) P.Pid 查看进程号  查看进程的名字p.name P.is_alive()  返回一个true或者False P.terminate()  给操作系统发送一个结束进程的信号 验证进程之间是空间隔离的 from multiprocessing import Process num = 100 def f1(): global num num = 3 print(num) # 结果 3 if __name__ == '__main__

进程对象的其他方法、守护进程、使用多进程实现 socket tcp协议 server端的并发(抢票程序)、队列、进程之间的通信(IPC)

# 进程对象的其他方法 from multiprocessing import Process import time class MyProcess(Process): def __init__(self, a, b): # 为了给子进程传递参数 super().__init__() self.a = a self.b = b def run(self): print("子进程开始执行") time.sleep(2) print("子进程结束", self.a,