PHP异步任务worker

1.概述

异步任务框架主要包含两个角色:

  • WorkerServer
    主要负责管理(启动,重启,监控等)worker工作进程。
  • Worker
    负责从指定消息队列获取任务消息并执行任务。

 

为了提高worker任务处理效率,目前按任务处理时间长短,区分不同的任务队列,目前可用的任务名(不同任务名代表不同的队列)如下:

  • defaultJob
    默认任务队列,主要处理一些小任务
  • largeJob
    主要处理一些比较耗时的任务

2.开发流程

2.1编写异步任务处理接口

编写异步任务处理接口的开发方式,跟普通的接口开发没有明显区别。

例子:

下面是对顾客进行群发短信的接口定义,实现部分这里忽略掉。

<?php

//短信群发模块

Interface IBroadcastSMS {

/**

* @param int $adminId - 商户id

* @param int $content - 短信内容

* @param array $params - 群发客户筛选条件, 具体参数说明本例忽略...

* @return bool 成功返回true, 失败返回false

*/

public function send($adminId, $content, array $params = []);

}

 

 

短信群发模块的类型定义为:

SrvType::CRM_MARKETING_BROADCAST_SMS

2.2配置worker

主要涉及两个配置文件的配置,分别是:

Ps: 配置文件均在config目录

 

  • constants/workerTypes.php
    worker类型定义文件
  • worker.php
    worker任务相关配置

worker类型定义:

<?php

/**

 * worker type 定义

 */

class WorkerTypes

{

    /**

     * 群发短信

 * 常量定义格式:

* [类型名] = '类型值必须唯一,且按名字空间按点分割,最后接上接口名';

     */

const BROADCAST_SMS = 'crm.marketing.broadcastsms.send';

}

 

Worker任务配置:

worker.php

蓝色部分代码为任务配置,每一个异步任务都需要添加一个类似的配置。

 

<?php

/**

 * worker配置

 */

return [

    "pid" => PROJECT_PATH .'/runtime/workerServer.pid',

    //php命令路径

    "php" => "",

    //进程运行角色

    "user"   => 'www',

    //当队列为空时候,获取消息时等待多少秒,范围限制:0-30秒

    "pollingWaitSeconds" => 30,

    "workers" => [

     WorkerTypes::BROADCAST_SMS  => [

         //任务名, 任务名相同则共用同一个消息队列,关于任务名,文档另行说明

     "jobName" => "defaultJob",

            //worker获取消息后,多长时间内其他worker不能消费同一条消息,单位秒,最长12小时内

            "visibilityTimeout" => 300,

            //true则预先消费消息,worker获取消息后立即删除消息, false则任务执行返回为真才会删除消息

            "preConsume" => false,

            //当前任务并发执行的worker数量

     "threadNum" => 10,

            //每个worker生存时间, 超时则重启

     "lifeTime" => 3600,

            //每个worker最大任务处理数,超过则重启

     "maxHandleNum" =>  10000,

            //任务处理器, 格式[service类型, '业务接口']

     "handler"    => [SrvType::CRM_MARKETING_BROADCAST_SMS, 'send'],

            //任务描述信息

            "desc"  => '群发短信',

     ],

    ]

];

 

2.3发送异步任务消息

发送异步任务消息需要MessageServer 类,这个类负责所有消息队列操作。

 

对于发送worker消息,只需要MessageServer的一个接口

 

/**

     * 把消息发送给指定的worker执行

     * @param string $workerType - worker type worker类型

     * @param array $params - 任务参数, 任务参数数组内容,按顺序对应service接口参数

     * @param int $delaySeconds - 延迟时间,单位秒 0-604800秒(7天)范围内某个整数值

     * @return bool 成功返回true, 失败返回false

     * @throws \CDbException

     */

public function dispatch($workerType, array $params = [], $delaySeconds = null);

 

 

接上面的例子:

//派发worker异步任务消息

MessageServer::getInstance()->dispatch(

            WorkerTypes::BROADCAST_SMS,

                [

                    1,    //对应adminId

                    "群发短信内容",  //对应content短信内容参数

                    [       ///对应params参数群发客户筛选条件

                        "is_member" => true

                    ]

                ]);

 

下面是worker会调用的service类型为SrvType::CRM_MARKETING_BROADCAST_SMS的send接口,留意service接口参数和dispatch派发任务参数的关系。

/**

* @param int $adminId - 商户id

* @param int $content - 短信内容

* @param array $params - 群发客户筛选条件, 具体参数说明本例忽略...

* @return bool 成功返回true, 失败返回false

*/

public function send($adminId, $content, array $params = [])

 

 

Ps: dispatch任务参数数组按顺序对应service接口的参数,他们是一一对应关系.

3.部署

Ps: 为便于调试,开发阶段不需要部署worker server,  派发worker消息,会直接调用对应的service接口,没有经过消息队列。

 

3.1环境安装

Worker server依赖php扩展库:

  • Swoole v1.9.17
    PHP的异步、并行、高性能网络通信引擎.
  • Pcntl
    php源码内置的的进程管理库,因为需要处理阻塞式信号处理,不能使用swoole的进程管理。

 

安装Swoole:

 

#git clone https://git.oschina.net/swoole/swoole.git

//切换到指定版本

#git checkout v1.9.17

 

#phpize

#./configure

#make && make install

 

修改php.ini文件

添加extension=swoole.so

 

安装pcntl

因为pcntl包含在php源码目录,因此需要php源码

 

#cd php源码目录/ext/pcntl

#phpize

#./configure

#make && make install

修改php.ini文件

添加extension=pcntl.so

 

3.2启动/关闭

切换至脚本目录

#cd /www/scripts

 

设置执行权限

#chmod u+x workerServer.sh

 

启动worker server

#./workerServer.sh start

 

关闭

#./workerServer.sh stop

 

 declare:

A tick is an event that occurs for every N low-level tickable statements executed by the parser within the declare block. The value for N is specified using ticks=N within the declare block's directive section.

这是PHP中对 declare 中的 ticks 的定义

中文翻译 Tick(时钟周期)是一个在 declare 代码段中解释器每执行 N 条可计时的低级语句就会发生的事件。N 的值是在 declare 中的 directive 部分用ticks=N 来指定的. 

我个人理解的大意就是说, tick 这个周期上会绑定一个事件, 这个事件, 当Zend引擎执行到所设置的 tick (就是N) 行低级语句时, 就执行  register_tick_function()  定义的事件 

关于什么是低级语句, http://my.oschina.net/Jacker/blog/32936 这篇文章说的很详细, 有时间的话, 可以研究一下的, 涉及到了PHP的底层 zend 引擎.

 

 

 

<?php
namespace \base\worker;
use \base\Config;
use \base\MessageServer;
use \base\WorkerApplication;
use Swoole\Process;
use Swoole\Timer;
/**
 * Worker server, 主要用于管理和维护worker进程
 */
class WorkerServer
{
    /**
     * 当前实例
     * @var WorkerServer
     */
    private static $_instance = null;

    /**
     * 整个worker服务的配置
     */
    private $_conf;

    /**
     * @var WorkerApplication
     */
    private $_app;


    /**
     * 正在运行的workers
     * 格式:
     *    'Worker type' => [pid1 => true, pid2 => true, pid3 => true]
     * @var array
     */
    private $_runningWorkers = [];

    /**
     * pid to worker type
     * 格式:
     *  pid => worker type
     * @var array
     */
    private $_pidMapToWorkerType = [];

    /**
     * 监控worker的Timer ID
     */
    private $_monitorTimerId;

    /**
     * 用于控制dev,test环境,每个队列只启动1个进程
     * @var array
     */
    private $_queueWorkers = [];

    private function __construct()
    {
        $this->_log("start worker server...");
        $this->_conf = Config::getInstance()->get('', 'worker');
        $this->_app = \YII::app();

        //masker进程注册相关信号处理
        Process::signal(SIGCHLD, [$this, 'doSignal']);
        Process::signal(SIGTERM, [$this, 'doSignal']);

        //根据 -d 参数确认是否后台运行
        $options = getopt('d');
        if (isset($options['d'])) {
            Process::daemon();
            file_put_contents($this->_conf['pid'], posix_getpid());
        }

        //初始化worker队列
        $this->_initWorkerMessageQueue();
    }

    /**
     * 获取定时任务服务
     * @return WorkerServer
     */
    public static function getInstance()
    {
        if (self::$_instance == null) {
            self::$_instance = new WorkerServer();
        }
        return self::$_instance;
    }

    /**
     * 启动worker server
     */
    public function run()
    {
        $this->startWorker();

        //监控worker进程
        Timer::after(5*60*1000, function () {
            $this->_monitorTimerId = Timer::tick(1000, function () {
                $this->startWorker();
            });
        });
    }

    /**
     * 启动worker, 允许重复执行
     */
    public function startWorker()
    {
        $workersConf = $this->_conf['workers'];
        if (empty($workersConf)) {
            return;
        }

        foreach ($workersConf as $workerType => $conf) {
            if (!isset($conf['jobName']) || !isset($conf['threadNum']) || !isset($conf['lifeTime']) || !isset($conf['maxHandleNum']) || !isset($conf['handler'])) {
                $this->_log("worker config error. workerType={$workerType}");
                continue;
            }


            //控制测试环境的进程数
            if (in_array(LWM_ENV, ['dev', 'test'])) {
                $jobName = $conf['jobName'];
                $jWorkers = 0;
                if (isset($this->_queueWorkers[$jobName])) {
                    $jWorkers = $this->_queueWorkers[$jobName];
                }
                else {
                    $this->_queueWorkers[$jobName] = 1;
                }
                if ($jWorkers > 0) {
                    continue;
                }
                //默认启动一个进程用于测试
                $conf['threadNum'] = 1;
            }

            $workers = $this->_getWorkers($workerType);
            if ($workers >= $conf['threadNum']) {
                continue;
            }

            $hasWorkers = $conf['threadNum'] - $workers;
            //启动worker
            for ($i=0; $i < $hasWorkers; $i++) {
                $workerProcess = new Process(function (Process $worker) use ($workerType) {
                    $this->_log("start worker, workerType={$workerType}, pid={$worker->pid}");
                    $cmd = $this->_conf['php'];
                    $worker->exec($cmd,  ['worker.php', '-t', $workerType]);
                },false, false);

                $pid = $workerProcess->start();
                if ($pid === false) {
                    $this->_log("start worker failure. workerType={$workerType}");
                    continue;
                }
                //注册worker
                $this->_addWorker($workerType, $pid);
            }
        }
    }

    /**
     * 处理进程信号
     * @param int $sig  - 信号类型
     */
    public function doSignal($sig) {
        switch ($sig) {
            case SIGCHLD:
                //回收子进程资源
                //必须为false,非阻塞模式
                while($ret =  Process::wait(false)) {
                    $pid = $ret['pid'];
                    $this->_delWorkerByPid($pid);
                    $this->_log("回收进程资源, pid={$ret['pid']}");
                }

                if ($this->_getTotalWorkers() == 0) {
                    //当子进程都退出后,结束masker进程
                    @unlink($this->_conf['pid']);
                    exit(0);
                }
                break;
            case SIGTERM:
                //进程退出处理
                //关闭监控
                Timer::clear($this->_monitorTimerId);
                if (!empty($this->_pidMapToWorkerType)) {
                    $this->_log("worker server shutdown...");
                    foreach (array_keys($this->_pidMapToWorkerType) as $pid) {
                        Process::kill($pid, SIGTERM);
                    }
                }
                break;
        }
    }

    /**
     * 初始化消息队列
     */
    private function _initWorkerMessageQueue()
    {
        if (empty($this->_conf)) {
            return;
        }

        $messageServer = MessageServer::getInstance();
        foreach ($this->_conf['workers'] as $wokerType => $workerConfig) {
            $queueName = $messageServer->getQueueNameByWorkerType($wokerType);
            if (empty($queueName)) {
                $this->_log("creare worker message queue failure, get queue name failure. workerType={$wokerType}");
                continue;
            }
            $ret = $messageServer->createQueue($queueName);
            if (!$ret) {
                $this->_log("creare worker message queue failure. workerType={$wokerType}");
            }

            if (isset($workerConfig['visibilityTimeout'])) {
                //设置队列属性
                $messageServer->setQueueAttributes($queueName, $workerConfig['visibilityTimeout']);
            }
        }
    }


    /**
     * 添加worker
     * @param string $workerType
     * @param  int $pid - 进程id
     */
    private function _addWorker($workerType, $pid)
    {
        if (!isset($this->_runningWorkers[$workerType])) {
            $this->_runningWorkers[$workerType] = [];
        }
        $this->_runningWorkers[$workerType][$pid] = true;
        $this->_pidMapToWorkerType[$pid] = $workerType;
    }

    /**
     * 根据worker type返回指定worker type目前正在允许的worker数量
     * @param $workerType
     * @return int
     */
    private function _getWorkers($workerType)
    {
        if (!isset($this->_runningWorkers[$workerType])) {
            return 0;
        }
        return count($this->_runningWorkers[$workerType]);
    }

    /**
     * 删除worker
     * @param int $pid      - 进程id
     * @return bool
     */
    private function _delWorkerByPid($pid) {
        if (!isset($this->_pidMapToWorkerType[$pid])) {
            return false;
        }
        $workerType = $this->_pidMapToWorkerType[$pid];
        unset($this->_pidMapToWorkerType[$pid]);
        if (isset($this->_runningWorkers[$workerType]) && isset($this->_runningWorkers[$workerType][$pid])) {
            unset($this->_runningWorkers[$workerType][$pid]);
        }
        return true;
    }

    /**
     * 返回workers总数
     * @return int
     */
    private function _getTotalWorkers()
    {
        if (empty($this->_runningWorkers)) {
            return 0;
        }
        $total = 0;
        foreach (array_keys($this->_runningWorkers) as $workerType) {
            $total += count($this->_runningWorkers[$workerType]);
        }
        return $total;
    }

    /**
     * 输出日志
     * @param $msg
     */
    private function _log($msg)
    {
        $dateStr = date("Y-m-d H:i:s");
        $pid = posix_getpid();
        echo "[{$dateStr}] [pid={$pid}] {$msg}\n";
    }
}

 

 

<?php
namespace \base;
require 'BaseApplication.php';
/**
 * 控制台应用 - 仅仅用于启动框架不做命令路由处理
 */
class WorkerApplication extends ConsoleApplication
{

    /**
     * Runs the application.
     * This method loads static application components. Derived classes usually overrides this
     * method to do more application-specific tasks.
     * Remember to call the parent implementation so that static application components are loaded.
     */
    public function run()
    {
        $this->_ended = false;
        $this->_preEnded = false;
        if($this->hasEventHandler('onBeginRequest'))
            $this->onBeginRequest(new CEvent($this));
        register_shutdown_function(array($this,'end'),0,false);
    }
}

 

<?php
namespace \base;
use AliyunMNS\Client;
use AliyunMNS\Exception\MessageNotExistException;
use AliyunMNS\Exception\MnsException;
use AliyunMNS\Exception\QueueAlreadyExistException;
use AliyunMNS\Model\QueueAttributes;
use AliyunMNS\Requests\CreateQueueRequest;
use AliyunMNS\Requests\SendMessageRequest;
use AliyunMNS\Config as AlyConfig;
use config\constants\WorkerTypes;
use \base\worker\WorkerMessage;
use \services\ServiceFactory;

/**
 * 消息服务
 */
class MessageServer
{
    /**
     * @var MessageServer
     */
    private static $_instance;

    private $_conf;

    /**
     * @var Client
     */
    private $_client;

    private function __construct()
    {
        $this->_conf = Config::getInstance()->get('', 'mns');
        $aliConfig = new AlyConfig();
        $this->_client = new Client($this->_conf['endPoint'], $this->_conf['accessKeyId'], $this->_conf['accessSecret']);
    }

    /**
     * 获取消息服务
     * @return MessageServer
     */
    public static function getInstance()
    {
        if (self::$_instance == null) {
            self::$_instance = new MessageServer();
        }
        return self::$_instance;
    }

    /**
     * 创建队列
     * @param string $queueName     - 队列名
     *
     * @return bool 成功返回true, 失败返回false
     */
    public function createQueue($queueName)
    {
        if (empty($queueName)) {
            return false;
        }
        $request = new CreateQueueRequest($queueName);
        try {
            $res = $this->_client->createQueue($request);
            return true;
        }
        catch (QueueAlreadyExistException $e) {
            //队列已经存在
            return true;
        }
        catch (MnsException $e) {
            Logger::error("create message queue failure. queue={$queueName}, mns code={$e->getMnsErrorCode()}, msg={$e->getMessage()}");
        }
        return false;
    }

    /**
     * 设置队列属性
     * @param int $visibilityTimeout - worker获取消息后,多长时间内其他worker不能消费同一条消息,单位秒,最长12小时内
     * @return bool
     */
    public function setQueueAttributes($queueName, $visibilityTimeout)
    {
        if (empty($queueName)) {
            return false;
        }

        $attr = new QueueAttributes();
        $attr->setVisibilityTimeout($visibilityTimeout);
        $queue = $this->_client->getQueueRef($queueName);
        try {
            $queue->setAttribute($attr);
            return true;
        }
        catch (MnsException $e) {
            Logger::error("set message queue attr failure. queue={$queueName}, mns code={$e->getMnsErrorCode()}, msg={$e->getMessage()}");
        }
        return false;
    }

    /**
     * 发送消息
     * @param string $queueName     - 队列名
     * @param string $msgBody       - 消息内容
     * @param int $delaySeconds     - 延迟时间,单位秒 0-604800秒(7天)范围内某个整数值
     * @return bool
     * 成功返回消息id, 失败返回false
     */
    public function send($queueName, $msgBody, $delaySeconds=null)
    {
        if (empty($queueName)) {
            return false;
        }

        $queue = $this->_client->getQueueRef($queueName);
        $request = new SendMessageRequest($msgBody,$delaySeconds);
        try {
            $res = $queue->sendMessage($request);
            return $res->getMessageId();
        }
        catch (MnsException $e) {
            Logger::error("send message failure. queue={$queueName}, mns code={$e->getMnsErrorCode()}, msg={$e->getMessage()}");
        }
        return false;
    }

    /**
     * 获取消息
     * @param string $queueName     - 队列名
     * @param int $waitSeconds      - 队列消息为空时等待多长时间,非0表示这次receiveMessage是一次http long polling,如果queue内刚好没有message,那么这次request会在server端等到queue内有消息才返回。最长等待时间为waitSeconds的值,最大为30。
     * @return \AliyunMNS\Responses\ReceiveMessageResponse
     * 成功返回消息对象,失败返回false
     */
    public function receive($queueName, $waitSeconds = 0)
    {
        $queue = $this->_client->getQueueRef($queueName);
        try {
            return $queue->receiveMessage($waitSeconds);
        }
        catch (MessageNotExistException $e) {
            //没有消息不抛异常
            return false;
        }
        catch (MnsException $e) {
            Logger::error("receive message failure. queue={$queueName}, mns code={$e->getMnsErrorCode()}, msg={$e->getMessage()}");
            return false;
        }
    }

    /**
     * 修改消息可见时间, 既从现在到下次可被用来消费的时间间隔
     * @param string $queueName     - 队列名
     * @param string $receiptHandle   - 消息句柄
     * @param int $visibilityTimeout  - 从现在到下次可被用来消费的时间间隔,单位为秒
     * @return bool 成功返回true, 失败false
     */
    public function changeMessageVisibility($queueName, $receiptHandle, $visibilityTimeout)
    {
        $queue = $this->_client->getQueueRef($queueName);
        try {
            $queue->changeMessageVisibility($receiptHandle,$visibilityTimeout);
            return true;
        }
        catch (MnsException $e) {
            Logger::error("change message visibility failure. queue={$queueName}, msg={$e->getMessage()}");
            return false;
        }
    }

    /**
     * 删除消息
     * @param string $queueName     - 队列名
     * @param mixed $receiptHandle  - 消息句柄
     * @return bool 删除成功返回true, 失败返回false
     */
    public function delete($queueName, $receiptHandle)
    {
        $queue = $this->_client->getQueueRef($queueName);
        try {
            $res = $queue->deleteMessage($receiptHandle);
            return true;
        }
        catch (MnsException $e)
        {
            Logger::error("delete message failure. queue={$queueName}, mns code={$e->getMnsErrorCode()}, msg={$e->getMessage()}");
            return false;
        }
    }

    /**
     * 把消息发送给指定的worker执行
     * @param string $workerType - worker type
     * @param array $params - 任务参数
     * @param int $delaySeconds - 延迟时间,单位秒 0-604800秒(7天)范围内某个整数值
     * @return bool 成功返回true, 失败返回false
     * @throws \CDbException
     */
    public function dispatch($workerType, array $params = [], $delaySeconds = null)
    {
        //dev环境不走消息队列, 直接调用消息处理器
        if (LWM_ENV == 'dev') {
            $workerConfig = null;
            try {
                $workerConfig = Config::getInstance()->get("workers.{$workerType}", 'worker');
            } catch (\CDbException $e) {
                throw new \CDbException("worker config not found, workerType={$workerType}");
            }

            if (!isset($workerConfig['handler']) || empty($workerConfig['handler'])) {
                throw new \CDbException("worker config invalid, workerType={$workerType}");
            }

            $handler = $workerConfig['handler'];
            $srvObj = ServiceFactory::getService($handler[0]);
            $ret = call_user_func_array([$srvObj, $handler[1]], $params);
            if (!empty($ret)) {
                return true;
            }
            return false;
        }

        $queueName = $this->getQueueNameByWorkerType($workerType);
        if (empty($queueName)) {
            return false;
        }

        $msg = new WorkerMessage();
        $msg->setWorkerType($workerType);
        $msg->setParams($params);
        $ret = $this->send($queueName, $msg->serialize(), $delaySeconds);
        if ($ret !== false) {
            return true;
        }
        return false;
    }

    /**
     * 根据worker type获取队列名
     * @param string $workerType        - worker type
     * @return bool|string 成功返回队列名, 失败返回false
     */
    public function getQueueNameByWorkerType($workerType)
    {
        if (empty($workerType)) {
            return false;
        }
        //获取worker配置
        $config = Config::getInstance()->get("workers.{$workerType}", 'worker');
        if (empty($config)) {
            Logger::error("worker config not found. workerType={$workerType}");
            return false;
        }

        $env = Config::getInstance()->get('env');
        $name = "{$env}-lwmWorker-{$config['jobName']}";
        return $name;
    }
}

 

<?php
declare(ticks=1);
namespace \base\worker;
use AliyunMNS\Exception\MnsException;
use \base\Config;
use \base\Logger;
use \base\MessageServer;
use \base\WorkerApplication;
use \services\ServiceFactory;

/**
 * Worker 工作进程, 主要用于执行异步任务
 */
class Worker
{
    /**
     * 当前实例
     * @var Worker
     */
    private static $_instance = null;


    /**
     * worker任务配置
     */
    private $_conf;

    /**
     * @var WorkerApplication
     */
    private $_app;

    /**
     * 当前 worker队列名
     * @var string
     */
    private $_workerQueueName = '';

    /**
     * 初始化workerType, 任务执行过程,如果共享队列,同一个worker可以执行多个worker type 任务
     * @var string
     */
    private $_workerType = '';

    /**
     * 是否结束worker
     * @var bool
     */
    private $_flgWorkerExit = false;

    private function __construct($workerType)
    {
        $this->_app = \YII::app();
        $this->_app->run();
        $this->_workerType = $workerType;
        //记录路由信息
        Logger::$route = $workerType;
        $this->_workerQueueName = MessageServer::getInstance()->getQueueNameByWorkerType($workerType);
        if (empty($this->_workerQueueName)) {
            Logger::error("worker get queue name failure, config invalid. workerType={$workerType}");
            throw new \CDbException("worker get queue name failure, config invalid. workerType={$workerType}");
        }

        //获取worker配置
        $this->_conf = Config::getInstance()->get('', 'worker');

        //注册信号处理
        pcntl_signal(SIGTERM, [$this, 'doSignal']);
        pcntl_signal(SIGQUIT, [$this, 'doSignal']);
    }

    /**
     * 获取定时任务服务
     * @param string $workerType    - worker类型
     * @return Worker
     */
    public static function getInstance($workerType)
    {
        if (self::$_instance == null) {
            self::$_instance = new Worker($workerType);
        }
        return self::$_instance;
    }

    /**
     * worker进程入口
     */
    public function run()
    {
        set_time_limit(0);

        //设置用户组
        $userName = $this->_conf['user'];
        $userInfo = posix_getpwnam($userName);
        if (empty($userName)) {
            Logger::error("start worker failure, get userinfo failure. user={$userName}");
            return;
        }
        posix_setuid($userInfo['uid']);
        posix_setgid($userInfo['gid']);
        $config = $this->_conf['workers'][$this->_workerType];
        $progName = "lwm-worker: {$this->_workerQueueName}";
        \swoole_set_process_name($progName);

        //启动时间
        $startTime = time();
        //当前worker处理任务数
        $currentExcutedTasks = 0;
        $this->_flgWorkerExit = false;
        while (!$this->_flgWorkerExit) {
            $currentTime = time();

            $this->_app->run();
            //处理任务
            $this->_doWorkerTask($this->_workerQueueName);
            $this->_app->end(0, false);

            $currentExcutedTasks++;
            if (($currentTime - $startTime) > $config['lifeTime']) {
                //超出存活时间,自动退出
                $this->_flgWorkerExit = true;
                Logger::info("worker (workerType={$this->_workerType}) run time exceed lifetime, exit worker.");
                break;
            }

            //超出最大任务处理次数, 自动退出
            if ($currentExcutedTasks > $config['maxHandleNum']) {
                $this->_flgWorkerExit = true;
                Logger::info("worker (workerType={$this->_workerType}) done tasks exceed maxHandleNum, exit worker.");
                break;
            }
        }
    }

    /**
     * 处理进程信号
     * @param int $sig  - 信号类型
     */
    public function doSignal($sig) {
        switch ($sig) {
            case SIGTERM:
                //进程退出处理
                $this->_flgWorkerExit = true;
                Logger::info("worker recv terminate signal. pid=" . posix_getpid());
                break;
        }
    }

    /**
     * 处理worker任务
     * @param string $workerMsgQueueName - 队列名
     * @throws \CDbException
     * @throws \RedisException
     */
    private function _doWorkerTask($workerMsgQueueName)
    {
        $response = null;
        try {
            $waitSeconds = $this->_conf['pollingWaitSeconds'];
            $response = MessageServer::getInstance()->receive($workerMsgQueueName,$waitSeconds);
            if ($response === false) {
                //没有消息休眠1秒
                sleep(1);
                return;
            }

            Logger::info("worker recv message msgId={$response->getMessageId()}, msg={$response->getMessageBody()}");

            $workerMsg = new WorkerMessage($response->getMessageBody());
            $workerType = $workerMsg->getWorkerType();
            if (!isset($this->_conf['workers'][$workerType])) {
                Logger::error("invalid message, worker config not found. worker type={$workerType}");
                MessageServer::getInstance()->delete($workerMsgQueueName, $response->getReceiptHandle());
                return;
            }

            $config = $this->_conf['workers'][$workerType];
            if ($config['preConsume']) {
                //预先删除消息
                MessageServer::getInstance()->delete($workerMsgQueueName, $response->getReceiptHandle());
                Logger::debug("pre delete message. msgId={$response->getMessageId()}");
            }

            $hander = $config['handler'];
            $srvObj = ServiceFactory::getService($hander[0]);
            Logger::debug("worker execute message handler=" . json_encode($hander) . ", msgId={$response->getMessageId()}");
            $ret = call_user_func_array([$srvObj, $hander[1]], $workerMsg->getParams());
            Logger::debug("worker execute message handler result=" . json_encode($ret) .", msgId={$response->getMessageId()}");

            if (!empty($ret) && !$config['preConsume']) {
                //任务处理成功,删除消息
                MessageServer::getInstance()->delete($workerMsgQueueName, $response->getReceiptHandle());
                Logger::debug("finish task delete message. msgId={$response->getMessageId()}");
            }

        } catch (WorkerMessageInvalidException $e) {
            //消息格式不正确
            if ($response) {
                //删除消息
                MessageServer::getInstance()->delete($workerMsgQueueName, $response->getReceiptHandle());
                Logger::error("worker error, error={$e->getMessage()}");
            }
        } catch (\CDbException $e) {
            //如果出现数据库异常直接抛出异常退出worker
            throw $e;
        } catch (\RedisException $e) {
            //redis异常直接退出
            throw $e;
        } catch (\Exception $e) {
            Logger::error("worker error, msg={$e->getMessage()}");
            //异常休眠1秒
            sleep(1);
        }
    }
}

 

<?php
namespace \base\worker;

/**
 * worker消息封装
 */
class WorkerMessage
{
    //worker类型
    private $_workerType = '';
    //worker参数
    private $_params = [];

    public function __construct($srcData = '')
    {
        if (!empty($srcData)) {
            $this->unSerialize($srcData);
        }
    }

    /**
     * @return mixed
     */
    public function getWorkerType()
    {
        return $this->_workerType;
    }

    /**
     * @param mixed $workerType
     */
    public function setWorkerType($workerType)
    {
        $this->_workerType = $workerType;
    }

    /**
     * @return mixed
     */
    public function getParams()
    {
        return $this->_params;
    }

    /**
     * @param mixed $params
     */
    public function setParams($params)
    {
        $this->_params = $params;
    }

    /**
     * 序列化
     * @return string
     */
    public function serialize()
    {
        $data = [
            'workerType' => $this->_workerType,
            'params' => $this->_params
        ];
        return json_encode($data);
    }

    /**
     * 返序列化
     * @param string $srcData      - 原始数据
     * @throws \Exception
     */
    public function unSerialize($srcData)
    {
        if (empty($srcData)) {
            throw new WorkerMessageInvalidException("worker msg is empty");
        }
        $data = json_decode($srcData, true);
        if (empty($data)) {
            throw new WorkerMessageInvalidException("WorkerMessage invalid. data={$srcData}");
        }
        if (!isset($data['workerType']) || !isset($data['params'])) {
            throw new WorkerMessageInvalidException("WorkerMessage invalid. data={$srcData}");
        }
        $this->setWorkerType($data['workerType']);
        $this->setParams($data['params']);
    }
}

 

<?php
use Swoole\Process;
use Swoole\Timer;

/**
 * 定时任务服务
 */
class Crond
{
    /**
     * 当前实例
     * @var Crond
     */
    private static $_instance = null;

    /**
     * 定时任务配置
     */
    private $_conf;

    /**
     * @var WorkerApplication
     */
    private $_app;

    private $_runningTasks = [];

    /**
     * 退出状态
     * @var bool
     */
    private $_flgExit = false;

    private function __construct()
    {
        $this->_conf = Config::getInstance()->get("", 'cron');//取出配置信息
        $this->_app = \YII::app();
        //注册子进程回收信号处理
        Process::signal(SIGCHLD, [$this, 'doSignal']);
        Process::signal(SIGTERM, [$this, 'doSignal']);
    }

    /**
     * 获取定时任务服务
     * @return Crond
     */
    public static function getInstance()
    {
        if (self::$_instance == null) {
            self::$_instance = new Crond();
        }
        return self::$_instance;
    }

    public function start()
    {
        $options = getopt('d');
        $this->_log("start cron server...");
        if (isset($options['d'])) {
            Process::daemon();
            file_put_contents($this->_conf['pid'], posix_getpid());
        }

        Timer::tick(1000, [$this, 'doTask']);
        //10s 加载一次配置
        Timer::tick(10000, function () {
            $this->_conf = Config::getInstance()->get("", 'cron', true);
        });
    }

    /**
     * 定时器每秒回调函数
     * @param int $timer_id     - 定时器的ID
     * @param mixed $params
     */
    public function doTask($timer_id, $params = null)
    {
        //开始任务
        $currentTime = time();
        if (isset($this->_conf['jobs']) && !empty($this->_conf['jobs'])) {
            foreach ($this->_conf['jobs'] as $jobId => $job) {
                if (!isset($job['title']) || !isset($job['cron']) || !isset($job['command']) || !isset($job['id'])) {
                    $this->_log("crontab job config error");
                    continue;
                }

                if ($this->_isTimeByCron($currentTime, $job['cron'])) {
                    if (isset($this->_runningTasks[$job['id']])) {
                        $this->_log("last cron worker not exit. job id={$job['id']}");
                        continue;  
                    }

                    //启动任务
                    $cronWorker =  new Process(function (Process $worker) use($job) {
                        $this->doCronTask($worker, $job);
                    });

                    $pid = $cronWorker ->start();
                    if ($pid === false) {
                        $this->_log("start cron worker failure.");
                        continue;
                    }
                    $this->_runningTasks[$job['id']] = $pid;
                    $cronWorker->write(json_encode($job));
                }
            }
        }
    }

    /**
     * do cron worker
     */
    public function doCronTask($worker, $job)
    {
        //clear log
        \Yii::getLogger()->flush();
        $this->_log("cron worker running task={$job['title']}, jobId={$job['id']}");
        $command = PROJECT_PATH . '/scripts/yiic';
         set_time_limit(0);
         $cmdArgs = explode(' ',  $job['command']);
         $worker->exec($command,  $cmdArgs);
    }

    /**
     * 处理进程信号
     * @param int $sig  - 信号类型
     */
    public function doSignal($sig) {
        $pidToJobId = array_flip($this->_runningTasks);
        switch ($sig) {
            case SIGCHLD:
                //必须为false,非阻塞模式
                while($ret =  Process::wait(false)) {
//                    echo "recycle child process PID={$ret['pid']}\n";
                    $exitPid = $ret['pid'];
                    if (isset($pidToJobId[$exitPid])) {
                        $jobId = $pidToJobId[$exitPid];
                        unset($this->_runningTasks[$jobId]);
                    }
                }
                //当子进程都退出后,结束masker进程
                if (empty($this->_runningTasks) && $this->_flgExit) {
                    @unlink($this->_conf['pid']);
                    exit(0);
                }

                break;
            case SIGTERM:
                $this->_log("recv terminate signal, exit crond.");
                $this->_flgExit = true;
                break;
        }
    }

    /**
     * 根据定时任务时间配置,检测当前时间是否在指定时间内
     * @param int $time     - 当前时间
     * @param string $cron  - 定时任务配置
     * @return bool 不在指定时间内返回false, 否则返回true
     */
    private function _isTimeByCron($time, $cron)
    {
        $cronParts = explode(' ', $cron);
        if (count($cronParts) != 6) {
            return false;
        }

        list($sec, $min, $hour, $day, $mon, $week) = $cronParts;

        $checks = array('sec' => 's', 'min' => 'i', 'hour' => 'G', 'day' => 'j', 'mon' => 'n', 'week' => 'w');

        $ranges = array(
            'sec' => '0-59',
            'min' => '0-59',
            'hour' => '0-23',
            'day' => '1-31',
            'mon' => '1-12',
            'week' => '0-6',
        );

        foreach ($checks as $part => $c) {
            $val = $$part;
            $values = array();

            /*
                For patters like 0-23/2
            */
            if (strpos($val, '/') !== false) {
                //Get the range and step
                list($range, $steps) = explode('/', $val);

                //Now get the start and stop
                if ($range == '*') {
                    $range = $ranges[$part];
                }
                list($start, $stop) = explode('-', $range);

                for ($i = $start; $i <= $stop; $i = $i + $steps) {
                    $values[] = $i;
                }
            } /*
                For patters like :
                2
                2,5,8
                2-23
            */
            else {
                $k = explode(',', $val);

                foreach ($k as $v) {
                    if (strpos($v, '-') !== false) {
                        list($start, $stop) = explode('-', $v);

                        for ($i = $start; $i <= $stop; $i++) {
                            $values[] = $i;
                        }
                    } else {
                        $values[] = $v;
                    }
                }
            }

            if (!in_array(date($c, $time), $values) and (strval($val) != '*')) {
                return false;
            }
        }

        return true;
    }

    /**
     * 输出日志
     * @param $msg
     */
    private function _log($msg)
    {
            $dateStr = date("Y-m-d H:i:s");
            echo "[{$dateStr}] {$msg}\n";
    }
}

 

posted @ 2018-08-16 09:57  凌雨尘  阅读(1847)  评论(0编辑  收藏  举报