Linux 下让 ThinkPHP 8 队列脱离终端后台常驻(start/status/stop 三脚本版)

在服务器上跑队列消费时,如果直接前台执行 php think queue:work,一旦断开 SSH 会话进程就可能被挂断,导致队列停止消费。本文提供一套轻量的脚本方案(基于 nohup + &),实现队列 脱离终端常驻 + 可启停 + 可查状态


目标效果

  • 脱离终端:SSH 断开后队列仍继续运行
  • 可配置:队列名、重试次数、worker 数量、日志路径等均可变量控制
  • 防误杀:可选按项目目录严格匹配进程(避免误杀同机器其它项目的队列 worker)
  • 可观测:标准输出/错误写入日志文件
  • 可运维:一键启动、查看、停止

完整脚本代码(直接可用)

把下面三段分别保存为 start-queue.shstatus-queue.shstop-queue.sh(建议放项目根目录或 bin/ 目录)。

1) start-queue.sh - 启动队列(支持多 worker)

#!/usr/bin/env bash
#============= 用户配置区(改这里即可) =============
PHP_BIN=/www/server/php/81/bin/php        # PHP 可执行文件路径
PROJECT=/www/wwwroot/example.com          # 项目根目录
QUEUE_NAME=default                         # 队列名
TRIES=3                                    # 任务失败重试次数(根据业务调整)
WORKER_NUM=1                               # worker 进程数量(多个 worker 可并行消费,建议 1-4)
LOG_DIR=$PROJECT/runtime/queue             # 日志目录(会自动创建)
STRICT_MATCH=1                             # 1=按工作目录严格匹配(推荐),0=只匹配 queue:work
#===================================================

cd "$PROJECT" || exit

# 自动按日期生成日志文件名(格式:queue-20260126.log)
LOG_FILE="$LOG_DIR/queue-$(date +%Y%m%d).log"

# 确保日志目录存在
if [ ! -d "$LOG_DIR" ]; then
    mkdir -p "$LOG_DIR"
    echo "Created log directory: $LOG_DIR"
fi

# 查找并杀掉旧进程(通过工作目录匹配,更精确)
if [ "$STRICT_MATCH" -eq 1 ]; then
    # 找到所有 queue:work 进程,然后检查工作目录是否匹配
    OLD_PIDS=$(ps -ef | grep '[q]ueue:work' | awk '{print $2}')
    if [ -n "$OLD_PIDS" ]; then
        for pid in $OLD_PIDS; do
            # 获取进程的工作目录
            if [ -d "/proc/$pid" ]; then
                PWD_PATH=$(readlink /proc/$pid/cwd 2>/dev/null)
                if [ "$PWD_PATH" = "$PROJECT" ]; then
                    echo "Killing old queue worker in $PROJECT (PID $pid) ..."
                    kill "$pid" 2>/dev/null
                fi
            fi
        done
        sleep 2
    fi
else
    # 简单匹配模式:杀掉所有 queue:work
    OLD_PIDS=$(ps -ef | grep '[q]ueue:work' | awk '{print $2}')
    if [ -n "$OLD_PIDS" ]; then
        echo "Killing old queue workers (PIDs: $OLD_PIDS) ..."
        echo "$OLD_PIDS" | xargs kill 2>/dev/null
        sleep 2
    fi
fi

# 启动指定数量的 worker 进程
echo "Starting $WORKER_NUM queue worker(s) in $PROJECT ..."
for i in $(seq 1 "$WORKER_NUM"); do
    nohup "$PHP_BIN" think queue:work --queue "$QUEUE_NAME" --tries "$TRIES" 
          >>"$LOG_FILE" 2>&1 &
    echo "  - Worker #$i started, PID: $!"
done

echo "All workers started successfully!"
echo "Log file: $LOG_FILE"

2) status-queue.sh - 查看队列运行状态(显示队列名、重试次数等)

#!/usr/bin/env bash
#============= 用户配置区(与 start 保持一致) =============
PROJECT=/www/wwwroot/example.com          # 项目根目录
STRICT_MATCH=1                             # 1=按工作目录严格匹配,0=只匹配 queue:work
#=========================================================

# 查找所有 queue:work 进程
ALL_PIDS=$(ps -ef | grep '[q]ueue:work' | grep -v grep)

if [ -z "$ALL_PIDS" ]; then
    echo "Queue worker is NOT running"
    exit 0
fi

# 根据匹配模式过滤
MATCHED_COUNT=0
echo "Queue worker status:"
echo "----------------------------------------"

while read -r line; do
    PID=$(echo "$line" | awk '{print $2}')
    
    # 严格匹配模式:检查工作目录
    if [ "$STRICT_MATCH" -eq 1 ]; then
        if [ -d "/proc/$PID" ]; then
            PWD_PATH=$(readlink /proc/$PID/cwd 2>/dev/null)
            if [ "$PWD_PATH" != "$PROJECT" ]; then
                continue  # 工作目录不匹配,跳过
            fi
        else
            continue
        fi
    fi
    
    # 提取队列名称
    QUEUE_NAME=$(echo "$line" | grep -oP '(?<=--queue )S+' || echo "unknown")
    
    # 提取重试次数
    TRIES=$(echo "$line" | grep -oP '(?<=--tries )S+' || echo "N/A")
    
    echo "PID: $PID"
    echo "  队列名: $QUEUE_NAME"
    echo "  重试次数: $TRIES"
    echo "  工作目录: ${PWD_PATH:-$PROJECT}"
    echo "----------------------------------------"
    
    ((MATCHED_COUNT++))
done <<< "$ALL_PIDS"

if [ $MATCHED_COUNT -gt 0 ]; then
    echo "共 $MATCHED_COUNT 个 worker 进程在运行"
else
    echo "Queue worker is NOT running in $PROJECT"
fi

3) stop-queue.sh - 停止队列

#!/usr/bin/env bash
#============= 用户配置区(与 start 保持一致) =============
PROJECT=/www/wwwroot/example.com          # 项目根目录
STRICT_MATCH=1                             # 1=按工作目录严格匹配,0=只匹配 queue:work
#=========================================================

# 查找所有 queue:work 进程
ALL_PIDS=$(ps -ef | grep '[q]ueue:work' | awk '{print $2}')

if [ -z "$ALL_PIDS" ]; then
    echo "Queue worker not found, nothing to stop"
    exit 0
fi

# 根据匹配模式过滤
MATCHED_PIDS=()
if [ "$STRICT_MATCH" -eq 1 ]; then
    # 严格匹配:检查工作目录
    for pid in $ALL_PIDS; do
        if [ -d "/proc/$pid" ]; then
            PWD_PATH=$(readlink /proc/$pid/cwd 2>/dev/null)
            if [ "$PWD_PATH" = "$PROJECT" ]; then
                MATCHED_PIDS+=("$pid")
            fi
        fi
    done
else
    # 简单匹配:所有 queue:work
    MATCHED_PIDS=($ALL_PIDS)
fi

if [ ${#MATCHED_PIDS[@]} -gt 0 ]; then
    echo "Stopping queue workers in $PROJECT (PIDs: ${MATCHED_PIDS[*]}) ..."
    for pid in "${MATCHED_PIDS[@]}"; do
        kill "$pid" 2>/dev/null
    done
    echo "All queue workers stopped (${#MATCHED_PIDS[@]} process(es))"
else
    echo "Queue worker not found in $PROJECT, nothing to stop"
fi

使用方法

1) 上传脚本并赋权

chmod +x start-queue.sh status-queue.sh stop-queue.sh

2) 修改三个脚本的"用户配置区"

每个脚本顶部都有 #============= 用户配置区 =============,按你的实际环境修改:

start-queue.sh 配置说明(最重要)

变量名说明示例
PHP_BINPHP 可执行文件路径/usr/bin/php/www/server/php/81/bin/php
PROJECT项目根目录(必须包含 think 命令)/www/wwwroot/example.com
QUEUE_NAME要消费的队列名default / mail / sms
TRIES任务失败重试次数3(建议 2-5,根据业务容错度调整)
WORKER_NUM同时启动几个 worker 进程1(单机建议 1-4,太多会增加数据库压力)
LOG_DIR日志目录(自动按日期分割)$PROJECT/runtime/queue(日志文件自动按 queue-20260126.log 格式)
STRICT_MATCH是否按工作目录严格匹配(通过 /proc/$pid/cwd 检查)1(推荐,只操作本项目进程)或 0(操作所有 queue:work)

status-queue.shstop-queue.sh 配置

只需保证 PROJECTSTRICT_MATCHstart-queue.sh 一致即可。

3) 启动 / 查看 / 停止

# 启动队列
./start-queue.sh

# 查看状态(会显示队列名、重试次数等详细信息)
./status-queue.sh

# 停止队列
./stop-queue.sh

# 查看今天的日志(实时追踪)
tail -f /www/wwwroot/example.com/runtime/queue/queue-$(date +%Y%m%d).log

# 查看所有日志文件
ls -lh /www/wwwroot/example.com/runtime/queue/

status-queue.sh 输出示例

Queue worker status:
----------------------------------------
PID: 12345
  队列名: default
  重试次数: 3
  工作目录: /www/wwwroot/example.com
----------------------------------------
PID: 12346
  队列名: mail
  重试次数: 5
  工作目录: /www/wwwroot/example.com
----------------------------------------
共 2 个 worker 进程在运行

核心配置项详解

LOG_DIR:日志自动按日期分割

脚本会自动按日期生成日志文件,格式为 queue-YYYYMMDD.log,所有日志统一放在 LOG_DIR 目录下:

LOG_DIR=$PROJECT/runtime/queue   # 配置日志目录

实际效果

runtime/queue/
├── queue-20260124.log  (前天的日志)
├── queue-20260125.log  (昨天的日志)
└── queue-20260126.log  (今天的日志,正在写入)

优势

  • 每天自动生成新文件,单个文件不会过大
  • 方便按日期查看和归档
  • 可以轻松实现"只保留最近 N 天"的清理策略

查看今天的日志

tail -f /www/wwwroot/example.com/runtime/queue/queue-$(date +%Y%m%d).log

TRIES:任务失败重试次数

--tries 3 表示任务执行失败后会再重试 2 次(总共尝试 3 次)。根据你的业务场景调整:

  • 幂等任务(如发邮件、推送通知):建议 2-3
  • 非幂等任务(如扣款、生成订单):建议 1(失败立即进失败队列,人工介入)
  • 允许多次重试的任务:可设 5 或更高

WORKER_NUM:多 worker 并行消费

设置为 2 或更多时,会同时启动多个进程并行消费队列(提高吞吐量):

WORKER_NUM=4   # 同时启动 4 个 worker

适用场景

  • 队列积压严重,单 worker 消费太慢
  • 任务执行时间长(如调用外部 API、文件处理)

注意事项

  • 多 worker 会增加数据库/Redis 连接数,建议结合连接池配置
  • 建议不超过 CPU 核心数(一般 2-4 个即可)
  • 如果任务有顺序要求,保持 WORKER_NUM=1

STRICT_MATCH:防误杀开关(推荐开启)

当你服务器上有多个 ThinkPHP 项目都在跑队列时:

  • STRICT_MATCH=1(推荐):只操作"工作目录是当前项目"的进程(安全)
  • STRICT_MATCH=0:操作所有 queue:work 进程(可能误杀其它项目)

原理

  1. 启用后,脚本会检查每个 queue:work 进程的工作目录(通过 readlink /proc/$pid/cwd
  2. 只有工作目录 = $PROJECT 的进程才会被操作
  3. 这比匹配命令行更精确(因为 ps -ef 看到的命令行里不一定包含完整项目路径)

为什么不用命令行匹配?
因为启动时已经 cd $PROJECT,执行的是相对路径 think,所以 ps -ef 输出里看不到完整项目路径。使用工作目录匹配更可靠。


原理解释:为什么能"脱离终端"

脚本启动时用的命令是:

nohup php think queue:work --queue default --tries 3 >> queue.log 2>&1 &
  • nohup:让进程忽略挂断信号(SIGHUP),SSH 断开也不会被杀
  • >> queue.log 2>&1:把标准输出/错误都追加到日志文件
  • &:进程在后台运行,终端立即返回

注意事项与最佳实践

1) 日志自动按日期分割(已内置)

脚本已经内置了按日期分割日志的功能(每天自动生成新文件,格式:queue-20260126.log),避免单个日志文件过大。

查看日志文件列表

ls -lh /www/wwwroot/example.com/runtime/queue/
# 输出示例:
# queue-20260124.log  (3.2M)
# queue-20260125.log  (5.1M)
# queue-20260126.log  (1.8M)

自动清理 7 天前的旧日志(可加到 crontab):

# 每天凌晨 2 点清理 7 天前的日志
0 2 * * * find /www/wwwroot/example.com/runtime/queue/ -name "queue-*.log" -mtime +7 -delete

手动清理示例

# 只保留最近 7 天
find /www/wwwroot/example.com/runtime/queue/ -name "queue-*.log" -mtime +7 -delete

# 压缩 3 天前的日志(节省空间)
find /www/wwwroot/example.com/runtime/queue/ -name "queue-*.log" -mtime +3 ! -name "*.gz" -exec gzip {} ;

常见问题

Q1:启动后立即停止 / 查看状态显示未运行?

检查:

  1. PHP_BIN 路径是否正确(执行 which php/www/server/php/81/bin/php -v 确认)
  2. PROJECT 路径是否正确(能否 cd $PROJECT && ls think
  3. 查看今天的日志:tail -50 /www/wwwroot/example.com/runtime/queue/queue-$(date +%Y%m%d).log 看报错

Q2:多 worker 会重复消费同一任务吗?

不会。think-queue 底层(基于 Redis/数据库)有锁机制,多个 worker 会自动分配任务,不会重复消费。

Q3:如何监控队列是否还活着?

可以写个定时任务(cron)每 5 分钟执行 status-queue.sh,如果未运行就自动执行 start-queue.sh 或发告警。

Q4:如何查看历史日志或搜索错误?

查看昨天的日志

tail -100 /www/wwwroot/example.com/runtime/queue/queue-$(date -d "yesterday" +%Y%m%d).log

搜索最近 3 天内的所有错误

grep -i "error|exception|fail" /www/wwwroot/example.com/runtime/queue/queue-*.log | tail -50

查看某个日期的日志

cat /www/wwwroot/example.com/runtime/queue/queue-20260125.log

统计今天处理了多少任务(假设日志里有 "Processing job" 字样):

grep -c "Processing" /www/wwwroot/example.com/runtime/queue/queue-$(date +%Y%m%d).log

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com