Swoft 定时任务

Swoft的任务功能是基于Swoole的Task机制,Swoft的Task机制本质上是对SwooleTask机制的封装和加强,Swoft提供了精度为秒的定时任务功能用于替代Linux的Crontab。

Crontab是指需要定期运行的命令列表,以及用于管理该列表的命令的名称,crontab代表cron表,因为它使用作业调度程序cron来执行任务,cron本身是以chronos命名,是希腊语中时间的意思。cron是Linux的系统进程,会根据一个安排的时间表计划为用户自动执行任务,该计划也称为crontab,也用于编辑计划的程序名称。

定时任务进程

环境配置

修改环境配置.env文件中定时任务配置项

$ vim .env
CRONABLE=true

创建任务

一个类就是一个任务组,类中的每个方法就是一个任务。

$ vim app/task/TestTask.php
<?php
namespace App\Tasks;

use Swoft\Task\Bean\Annotation\Scheduled;
use Swoft\Task\Bean\Annotation\Task;

/**
 * @Task(“Test”)
*/
class TestTask
{
    /**
     * @Scheduled(cron="* * * * * *")
    */
    public function run($num)
    {
        echo "{$num} TestTask running...".PHP_EOL;
    }
}
  • @Task("Test") 定义任务名称,名称必须唯一。
  • @Scheduled用于设置触发时间cron
0     1    2    3    4    5
*     *    *    *    *    *
-     -    -    -    -    -
|     |    |    |    |    |
|     |    |    |    |    +----- 星期 (0 - 6) (星期日=0)
|     |    |    |    +----- 月 (1 - 12)
|     |    |    +------- 日 (1 - 31)
|     |    +--------- 时 (0 - 23)
|     +----------- 分 (0 - 59)
+------------- 秒 (0-59)

例如

// 每分钟的第10秒触发
@Scheduled(cron="10 * * * * *")

//每小时50分钟10秒时触发
@Scheduled(cron="10 50 * * *")

//每天21点01分10秒触发
@Scheduled(cron="10 1 21 * *")

任务投递

在控制器中投递任务

$result = Task::deliver("Test", "run", ["3"], Task::TYPE_ASYNC);

Swoft任务投递的实现机制离不开Swoole\Timer::tick(),和\Swoole\Server->task()底层执行机制是一样的,Swoft在实现的时候填了crontab方式,实现在src/Crontab下:

  • ParseContab 解析crontab
  • TableCrontab 使用Swoole\Table实现 用于存储crontab任务
  • Crontab 连接TaskTaskCrontab
Task::deliver(任务组名称, 任务名称, 任务参数, 投递方式);

参数说明

  • 参数1:@Task定义的
  • 参数2:方法名称
  • 参数3:以数组的格式传值
  • 参数4:指定是协程投递Task::TYPE_CO还是异步投递Task::TYPE_ASYNC

任务投递方式

任务投递Task::deliver()将调用参数打包后,根据$type参数使用Swoole的$server->taskCo()$server->task()接口投递到Task进程。Task本身始终是同步执行的,$type仅仅影响投递这个操作行为。

  • Task::TYPE_ASYNC对应的$server->task()是异步投递,Task::deliver()调用后立即返回。
  • Task::TYPE_CO对应的$server->taskCo()是协程投递,投递后让出协程控制,任务完成后或执行超时后Task::deliver()才会从协程返回。

Swoole的Task机制的本质是Worker进程将耗时任务投递给同步的Task进程(TaskWorker进程)处理。换句话说,Swoole的$server->taskCo()$server->task()都只能在Worker进程中使用,这一点限制了使用场景。如何才能在Process中投递任务呢?Swoft为了绕过这个限制提供了Task::deliverByProcess()方法。其实现原理是通过Swoole的$server->sendMessage()方法将调用信息从Process中投递到Worker进程中,然后由Worker进程替其投递到Task进程当中。

数据打包后会使用$server->sendMessage()投递给Worker,$server->sendMessage后Worker进程收到数据时会触发一个swoole.pipeMessage事件回调,Swoft会将其转换为自己的swoft.pipeMessage事件并触发。swoft.pipeMessage事件最终由PipeMessageListener处理,在相关的监听中如果发现swoft.pipeMessage事件由Task::deliverByProceess()产生,Worker进程会提替其执行一次Task::deliver(),最终将任务数据投递到TaskWorker进程中。

任务投递流程

  1. 当框架启动后会启动定时器每秒去更新执行一次任务,更新任务之前需要先去队列内存表中清理已完成的队列数据。
  2. 然后获取出所有的任务中的队列,可理解为获取所有Task类中的方法,任务规则以TaskClass、分钟、时间戳这些数据以md5方式加密得到每个任务队列的key值,保存到runTimeTable中。

任务执行

Swoole的Task任务机制的本质是Worker进程将耗时任务投递给同步的TaskWorker进程处理,所以swoole.onTask的事件回调是在Task进程中执行的。

$ vim vendor/swoft/task/src/Bootstrap/Listeners/TaskEventListener.php

此处是swoole.onTask的事件回调,其职责仅仅是将Worker进程投递来打包后的数据转发给TaskExecutor

/**
 * @param \Swoole\Server $server
 * @param int            $taskId
 * @param int            $workerId
 * @param mixed          $data
 * @return mixed
 * @throws \InvalidArgumentException
 */
public function onTask(Server $server, int $taskId, int $workerId, $data)
{
    try {
        /* @var TaskExecutor $taskExecutor*/
        $taskExecutor = App::getBean(TaskExecutor::class);
        $result = $taskExecutor->run($data);
    } catch (\Throwable $throwable) {
        App::error(sprintf('TaskExecutor->run %s file=%s line=%d ', $throwable->getMessage(), $throwable->getFile(), $throwable->getLine()));
        $result = false;

        // Release system resources
        App::trigger(AppEvent::RESOURCE_RELEASE);

        App::trigger(TaskEvent::AFTER_TASK);
    }
    return $result;
}

Worker进程是大部分HTTP服务代码执行的环境,但从TaskEventListener.onTask()方法开始,代码的执行环境都是Task进程,也就是说,TaskExecutor和具体的TaskBean都是执行在Task进程中的。

$ vim vendor/swoft/task/src/TaskExecutor.php

任务执行的思路是将Worker进程发过来的数据解包并还原为原来的调用参数,根据$name参数找到对应的TaskBean并调用其对应的Task方法,其中TaskBean使用类级别注解@Task(name="TaskName")@Task("TaskName")声明。注意@Task注解除了name属性外还有一个coroutine属性。

/**
 * @return mixed
 */
public function run(string $data)
{
    $data = TaskHelper::unpack($data);

    $name = $data['name'];
    $type = $data['type'];
    $method = $data['method'];
    $params = $data['params'];
    $logid = $data['logid'] ?? uniqid('', true);
    $spanid = $data['spanid'] ?? 0;


    $collector = TaskCollector::getCollector();
    if (! isset($collector['task'][$name])) {
        return false;
    }

    list(, $coroutine) = $collector['task'][$name];
    $task = bean($name);

    if ($coroutine) {
        $result = $this->runCoTask($task, $method, $params, $logid, $spanid, $name, $type);
    } else {
        $result = $this->runAsyncTask($task, $method, $params, $logid, $spanid, $name, $type);
    }

    return $result;
}

任务执行流程

  1. 通过getExecTasks方法将所有满足条件的队列放入到一个数组,遍历数组将runStatus修改为self::START
  2. 执行所有runStatus值为self::START的队列任务
  3. 将执行后的队列任务的runStatus值修改为self::FINISH
  4. runStatus值修改为self::FINISH的剔除掉

任务进程

Swoft使用两个前置进程

  1. 任务计划进程CronTimerProcess

CronTimerProcess进程是Swoft的定时任务调度进程,其核心方法是Crontab->initRunTimeTableData(),该进程使用了Swoole的定时器功能,通过Swoole\Timer在每分钟首秒时执行的回调,CronTimerProcess每次被唤醒后都会遍历任务表,计算出当前这一分钟内的60秒分别需要执行的任务清单,写入执行表并标记为未执行。

  1. 任务执行进程CronExecProcess

CronExecProcess作为定时任务的执行者,通过Swoole\Timer每0.5秒唤醒自身一次,然后把执行表遍历一次,挑选当下需要执行的任务,通过sendMessage()投递出去并更新该任务执行表中的状态。该执行进程只负责任务的投递,任务的实际执行仍然在Task进程中由TaskExecutor处理。

定时任务机制

内存表

Swoft使用两张内存数据表

在定时器中会使用到两个内存表Table,一个是用于存储任务实例originTable,一个是存储任务队列实例runTimeTable,也就是存储需要执行的任务实例。

为什么要使用Swoole的内存表呢?

Swoft的定时任务管理分别由任务计划进程和任务执行进程负责,两个进程的运行共同管理定时任务,如果使用进程间独立的数组等结构,两个进程必然需要频繁地进程间通信。而使用跨进程的Swoole\Table结构直接进行进程间数据共享,不仅性能高,操作简单还能解耦两个进程。为了让Table能够在两个进程间共同使用,Table必须在Swoole Server启动前创建并分配内存。

Table底层是建立在共享内存之上的HashTable数据结构,$size参数指定了Table的最大行数,最大行数决定了HashTable的总行数,由于HashTable是在共享内存之上,所以无法动态扩容,因此$size必须在创建前设置好。

$size若不是2的N次方,如1024、8196、65536等,底层会自动调整为接近的一个数字,如果小于1024则默认为1024,即1024为最小值。

$ vim vendor/swoft/task/src/Crontab/TableCrontab.php
  1. 任务配置表OriginTable

任务表用于记录用户配置的任务信息,任务表每行记录包含的字段包括

  • rule 定时任务执行规则,对应@Scheduled注解的cron属性。
  • taskClass 任务名称,对应@Taskname属性,默认为类名。
  • taskMethod Task方法,对应@Scheduled注解所在的方法
  • add_time 初始化表内容时的10位时间戳

ruletaskClasstaskMethod是生成key,唯一确定一条记录。

/**
 * @var \Swoft\Memory\Table $originTable 内存任务表
 */
private $originTable;

/**
 * @var array $originStruct 任务表结构
 */
private $originStruct = [
    'rule'       => [\Swoole\Table::TYPE_STRING, 100],
    'taskClass'  => [\Swoole\Table::TYPE_STRING, 255],
    'taskMethod' => [\Swoole\Table::TYPE_STRING, 255],
    'add_time'   => [\Swoole\Table::TYPE_STRING, 11],
];
  1. 任务执行表RunTimeTable

这里的执行并非指任务本身的执行,而是指任务投递这个操作的执行。

执行表记录短时间内要执行的任务列表及其执行状态,表每行记录包含字段:

  • taskClass
  • taskMethod
  • minute 需要执行任务的时间,精确到分钟,格式为date("YmdHi")
  • sec 需要执行任务的时间,精确到分钟,10位时间戳。
  • runStatus 任务状态包括0未执行、1已执行、2执行中三种
/**
 * @var \Swoft\Memory\Table $runTimeTable 内存运行表
 */
private $runTimeTable;
/**
 * @var array $runTimeStruct 运行表结构
 */
private $runTimeStruct = [
    'taskClass'  => [\Swoole\Table::TYPE_STRING, 255],
    'taskMethod' => [\Swoole\Table::TYPE_STRING, 255],
    'minute'      => [\Swoole\Table::TYPE_STRING, 20],
    'sec'        => [\Swoole\Table::TYPE_STRING, 20],
    'runStatus'  => [\Swoole\TABLE::TYPE_INT, 4],
];

推荐阅读更多精彩内容