• 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的解释这里有一个
    表示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 系统的线程数是有限制的,但这要耗时任务多了,如果然服务器去等,不管啥策略都很可能吧服务器整垮。

  • 相关阅读:
    ASP.NET操作DataTable各种方法总结(给Datatable添加行列、DataTable选择排序等)
    SharePoint服务器端对象模型 之 使用CAML进展数据查询
    django-rest_framwork 序列化
    django模型中的抽象类
    微博与本地用户绑定
    微博三方登陆--回调接口
    微博三方登陆开发平台新建APP
    生成微博授权的url(二维码登陆页面)
    博客第三方登陆原理
    redis的pipeline使用
  • 原文地址:https://www.cnblogs.com/lvyahui/p/4743526.html
Copyright © 2020-2023  润新知