ThinkPHP项目定时任务与守护进程

wen PHP项目 4

ThinkPHP项目定时任务与守护进程:从入门到生产级实践指南

目录导读

  1. 为什么ThinkPHP项目需要定时任务与守护进程?
  2. 基础篇:使用Crontab实现ThinkPHP定时任务
  3. 进阶篇:自定义守护进程的完整实现
  4. 生产级方案:Supervisor与ThinkPHP的完美结合
  5. 常见问题与性能优化(QA)
  6. 总结与最佳实践

为什么ThinkPHP项目需要定时任务与守护进程?

在实际业务场景中,我们经常遇到需要周期性执行的任务:如订单超时自动关闭、用户积分结算、报表生成、邮件/短信发送队列消费等,如果依赖用户请求触发,不仅效率低下,还会造成极差的用户体验。

ThinkPHP项目定时任务与守护进程

ThinkPHP作为国内最流行的PHP框架之一,官方提供了 think 命令行工具,但多数开发者仅停留在手动执行层面。真正的生产环境必须解决两个核心问题

  • 定时调度:在指定时间点或间隔执行特定ThinkPHP命令。
  • 常驻进程:保证任务不间断执行(如队列消费者),并具备崩溃自恢复能力。

本文将综合现有最佳实践,带你从零构建一套稳定、可维护的任务体系。


基础篇:使用Crontab实现ThinkPHP定时任务

1 传统Crontab方案

Linux系统的Crontab是最简单的定时方案,假设你的ThinkPHP项目位于 /var/www/html,那么一个典型任务配置如下:

* * * * * cd /var/www/html && php think order:close >> /var/log/order_close.log 2>&1

注意点

  • 必须使用绝对路径或先进入项目目录。
  • 需要为PHP设置环境变量(可在crontab顶部添加 PATH=/usr/local/bin:/usr/bin:/bin)。
  • 日志重定向务必设置,否则错误信息会丢失。

2 单入口Shell脚本优化

直接写多条crontab规则容易混乱,建议封装成脚本 task.sh

#!/bin/bash
PHP_BIN=$(which php)
PROJECT_DIR="/var/www/html"
cd $PROJECT_DIR
case $1 in
  close)
    $PHP_BIN think order:close ;;
  report)
    $PHP_BIN think report:generate ;;
  *)
    echo "Usage: $0 {close|report}" ;;
esac

然后在crontab中调用:* * * * * /var/www/html/task.sh close

3 局限性

Crontab最小粒度是分钟级,且无法处理并发冲突、无法自动重试失败任务,对于要求较高的场景(如秒级轮询、队列消费),需要更高级的守护进程方案。


进阶篇:自定义守护进程的完整实现

ThinkPHP允许你完全自定义一个命令行程序作为长期运行的守护进程,这通常应用于消息队列的worker。

1 创建守护进程命令类

application/command 目录下创建 Worker.php

<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
class Worker extends Command
{
    protected function configure()
    {
        $this->setName('worker:start')
             ->addOption('daemon', 'd', Option::VALUE_NONE, '后台运行')
             ->setDescription('启动队列消费者守护进程');
    }
    protected function execute(Input $input, Output $output)
    {
        $output->writeln("Worker started.");
        while (true) {
            // 从队列取任务 (示例为Redis)
            $job = \think\facade\Cache::store('redis')->lPop('task_queue');
            if ($job) {
                $this->processJob($job);
            } else {
                usleep(100000); // 无任务时休眠0.1秒
            }
        }
    }
    private function processJob($job)
    {
        // 处理业务逻辑...
    }
}

2 后台守护化实现

一个简单的守护化fork实现(Unix环境):

protected function execute(Input $input, Output $output)
{
    if ($input->getOption('daemon')) {
        $pid = pcntl_fork();
        if ($pid == -1) {
            die('fork failed');
        } else if ($pid) {
            // 父进程退出
            exit(0);
        }
        // 子进程成为会话组长
        if (posix_setsid() == -1) {
            die('setsid failed');
        }
        // 第二次fork,防止获取终端
        $pid = pcntl_fork();
        if ($pid == -1) {
            die('fork failed');
        } else if ($pid) {
            exit(0);
        }
        // 重定向标准输入输出
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);
        $stdin = fopen('/dev/null', 'r');
        $stdout = fopen('/var/log/worker.log', 'a');
        $stderr = fopen('/var/log/worker_error.log', 'a');
    }
    // ... 业务循环
}

3 运行与停止

启动:php think worker:start -d
停止:复杂,推荐使用 Supervisor 管理生命周期。


生产级方案:Supervisor与ThinkPHP的完美结合

Supervisor 是Linux下最常用的进程管理工具,能监控进程状态,崩溃自动重启,且支持日志轮转。

1 安装与配置Supervisor

# Ubuntu/Debian
apt-get install supervisor
# CentOS
yum install supervisor

配置 /etc/supervisor/conf.d/think-worker.conf

[program:think_worker]
command=php /var/www/html/think worker:start   ; 这里不用-d,交给supervisor管理
process_name=%(program_name)s_%(process_num)02d
numprocs=2               ; 启动2个进程实例,提高消费能力
directory=/var/www/html
autostart=true           ; 随supervisor启动
autorestart=true         ; 崩溃自动重启
user=www-data            ; 降低权限运行
redirect_stderr=true
stdout_logfile=/var/log/think_worker.out.log
stderr_logfile=/var/log/think_worker.err.log
stdout_logfile_maxbytes=10MB
stopasgroup=true         ; 重要,确保子进程被kill
killasgroup=true

2 关键配置说明

  • numprocs:多进程并发,解决单进程阻塞问题。
  • stopasgroupkillasgroup:ThinkPHP命令中可能存在子进程(如异步任务),设置后确保整个进程组被杀掉,防止僵尸进程。

3 管理命令

supervisorctl reread
supervisorctl update
supervisorctl status
supervisorctl restart think_worker:*

优势对比:Crontab无法解决常驻进程,纯PHP自守护易内存泄漏,Supervisor提供最大可靠性。


常见问题与性能优化(QA)

Q1: 定时任务与守护进程如何区分使用?

  • 定时任务:适合非实时、周期性任务(如每天凌晨清缓存),用Crontab即可。
  • 守护进程:适合实时、高频率任务(如消息队列消费),若任务有延迟容忍度,也可使用Crontab低频触发(但并发需加锁)。

Q2: 多个实例同时执行定时任务造成重复处理怎么办?

:必须引入互斥锁,ThinkPHP中可使用 Cache::set($lockKey, 1, 60) 结合 Cache::has 实现原子性,或者使用Mysql GET_LOCK() 函数作为分布式锁。

Q3: ThinkPHP的 Cache::store('redis')->lPop 守护进程,如果任务执行一半崩溃怎么办?

:这涉及任务确认机制,更安全的做法是使用 brPop 或者先 lrem 后处理,处理失败重新入队,或者引入简单的消息队列实现(如RabbitMQ)提供 ack 机制。

Q4: 守护进程内存增长,出现内存溢出?

:长期循环运行PHP脚本,必须在每次迭代后释放大变量、关闭连接,使用 gc_collect_cycles() 强制垃圾回收,建议每处理N条任务后 die 并由Supervisor自动拉起,这是一种防内存泄漏的常见“自愈”策略。

$processed = 0;
while (true) {
    $job = $redis->lPop('queue');
    if ($job) {
        // 处理...
        if (++$processed >= 1000) {
            $output->writeln("Recycling...");
            exit(0); // 安全退出,supervisor自动重启
        }
    } else {
        usleep(100000);
    }
}

Q5: 如何监控守护进程的健康状态?

  • 利用Supervisor的 memcpu 指标。
  • 在业务代码中记录心跳到Redis(key带过期时间),监控系统定期检查。
  • 使用 pm2(如果迁移Node.js)或 Docker 容器编排。

总结与最佳实践

  1. 简单定时任务:首选Crontab + Shell脚本。
  2. 复杂常驻任务:优先Supervisor管理 php think xxx:start
  3. 业务代码应无状态:保证进程重启不产生副作用(如幂等性设计)。
  4. 日志必须隔离:任务日志与业务日志分开,并开启文件轮转。
  5. 多进程考虑资源使用:按CPU核数合理设置 numprocs
  6. 测试环境预演:在使用 daemon 模式前,务必在CLI前台运行验证逻辑。

通过上述方案,你的ThinkPHP项目将具备媲美Java/Go微服务的后台任务处理能力,掌握定时任务与守护进程,是PHP工程师走向高级架构的必经之路。

抱歉,评论功能暂时关闭!