做Laravel项目的同学,应该都碰到过这种情况:运营同事突然喊“用户提交订单后,短信通知、优惠券发放等操作半天没动静,后台任务堆成山了”——这大概率就是队列堆积搞的鬼。今天就来聊聊Laravel队列系统怎么排查堆积,以及生产环境出故障时的解决办法,全是实际踩坑后总结的干货,不管你是刚入门的小白还是有经验的开发者都能看懂。

一、先搞懂:Laravel队列到底是啥?

我总把Laravel队列比作餐厅的“外卖出餐区”:主线程就是餐厅厨师,负责做核心的做菜、上餐等硬需求;而发短信、发优惠券、生成订单报表这类耗时间又不影响主线业务的任务,就会先放到队列(外卖出餐的取餐台),由专门的服务员(消费进程)慢慢处理,不用让客人(用户)一直等着,能大幅提升系统的响应速度。简单说,队列就是把“慢任务”和主线业务拆分开,避免阻塞整个系统的工具。

二、队列堆积最常出现的场景

队列堆积本质就是“取餐台的订单太多,服务员忙不过来”,常见触发场景有这几类:

  1. 突发流量涌入:比如618、双11促销,几万用户同时提交订单,每个订单都要生成通知、发放优惠券,短时间内任务量远超系统能处理的上限;
  2. 消费进程未正常运行:比如服务器重启后,忘记重启队列进程,相当于服务员都下班了,订单全堆在取餐台;
  3. 单个任务卡住堵塞整个队列:比如某个发送短信的任务,调用第三方API超时或报错,又没做失败重试,就会一直卡在队列最前面,后面所有任务都没法执行;
  4. 进程配置太少:比如默认只开了2个消费进程,但服务器每秒要处理100个任务,进程肯定跟不上流量。

三、排查队列堆积的三步法

碰到队列堆积不要慌,按这三步走,10分钟就能定位问题。

3.1 先查当前队列的积压量

第一步肯定是先搞清楚“到底堆了多少任务”,用Laravel自带的artisan命令就能直接看:

# 查看所有队列的积压任务数量(默认队列是default)
php artisan queue:size

执行后会输出类似default | 156的结果,意思是default这个队列有156个还没处理的任务,数字越大说明堆积越严重。如果有多个自定义队列(比如短信队列、订单队列),可以在命令后指定队列名,比如php artisan queue:size sms专门看短信队列的积压情况。

3.2 检查消费进程的运行状态

队列的任务是靠消费进程处理的,要是进程没跑,再多人也没法干活,用这个命令查进程:

# 查看当前系统中运行的队列消费进程
ps aux | grep queue:work

如果执行结果里只有最后一行grep queue:work,说明队列进程根本没启动;要是有两三行php artisan queue:work,说明是进程数不够。正常生产环境应该至少开3个以上进程,根据服务器性能调整。

3.3 看是不是有失败任务卡住

有时候队列里的任务不是被堆积,而是前面有任务执行失败,导致后面的任务排队等待,这时候要看失败队列:

# 查看所有失败队列的任务(每个失败任务会记录原因和时间)
php artisan queue:failed

输出结果会有任务ID、失败时间、异常信息,比如某个ID为27的任务,失败原因是“短信API超时”,那这个任务就是卡住的“罪魁祸首”,单独处理它就能恢复后面的任务。

四、生产环境故障的应急解决方案

排查清楚问题后,要快速解决,别让用户一直等,这里分享几个实际用的应急方案。

4.1 紧急扩容消费进程数

最常用的应急方案就是多启动几个消费进程,帮着消化堆积的任务,命令如下(默认队列):

# 启动10个default队列的消费进程,每个任务最多尝试3次,超时60秒
php artisan queue:work default --daemon --tries=3 --timeout=60 --queue=default

这里解释下参数:--daemon是后台运行,适合生产环境;--tries=3是任务失败后自动重试3次;--timeout=60是每个任务最多处理60秒,避免任务卡死进程。不过这种临时启动的进程,服务器重启后会失效,生产环境建议用supervisor管理进程,配置示例:

# 写入supervisor的配置文件,路径/etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/你的项目路径/artisan queue:work default --sleep=3 --tries=3 --timeout=90
autostart=true # 服务器启动自动运行
autorestart=true # 进程挂了自动重启
user=www-data # 运行用户,和web服务保持一致
numprocs=10 # 启动10个进程,根据积压量调整
redirect_stderr=true
stdout_logfile=/var/www/你的项目路径/storage/logs/worker.log # 日志路径,方便排查

配置后执行supervisorctl update和supervisorctl start laravel-worker:*就能生效,进程会一直保持运行,不会因为服务器重启或者进程崩溃而中断。

4.2 手动处理卡住的单个任务

如果是前面说的失败任务,直接手动重试或删除,快速释放队列,比如重试ID为27的任务:

# 重试失败队列里ID为27的任务
php artisan queue:retry 27

要是这个任务本身有问题(比如第三方API永久下线),就直接删除,别让它一直占着位置:

# 删除失败队列里ID为27的任务
php artisan queue:forget 27

这两步操作完,被卡住的队列就能正常处理后面的任务了。

4.3 拆分高优先级队列

如果堆积的任务里有高优先级的(比如短信通知、支付回调),又有低优先级的(比如生成报表、清洗数据),可以把高优先级任务分到单独的队列,这样高优先级任务不会被低优先级的慢任务卡住,配置步骤:

// 打开config/queue.php,修改队列配置,新增sms专属队列
'connections' => [
    'redis' => [
        'driver' => 'redis',
        'queue' => env('REDIS_QUEUE', 'default'),
        'retry_after' => 90,
        'block_for' => null,
    ],
    'sms' => [ // 新增短信专用队列
        'driver' => 'redis',
        'queue' => env('SMS_QUEUE', 'sms'),
        'retry_after' => 90,
        'block_for' => null,
    ],
],

然后在发送短信的任务里指定队列:

// 发送短信的任务类中,dispatch时指定sms队列
use App\Jobs\SendSmsJob;

// 比如在控制器里调用:
dispatch(new SendSmsJob($user))->onQueue('sms');

最后启动sms队列的消费进程,这样短信任务会优先被处理,不会被其他任务拖慢。

五、日常避免队列堆积的小技巧

应急方案是“亡羊补牢”,日常做好预防才不会出故障,分享几个实用的技巧:

5.1 用Laravel Horizon做监控

Laravel官方自带的Horizon工具,能可视化看所有队列的任务、进程数、失败任务,还能设置告警,比如队列积压超过50个就发钉钉或邮件到开发群,不用等用户投诉才发现问题,安装步骤:

# 安装Horizon(技术栈:Laravel)
composer require laravel/horizon
# 发布配置文件
php artisan vendor:publish --provider="Laravel\Horizon\HorizonServiceProvider"
# 启动Horizon
php artisan horizon

配置好后,访问你的域名/horizon就能看到所有队列的实时状态,还能设置告警规则,非常适合生产环境监控。

5.2 合理配置进程数

消费进程的数量要根据服务器CPU和内存来定,一般来说,每个队列进程大概占1个CPU核心,所以如果服务器是4核,开6-8个进程就够了,开太多会导致进程之间竞争资源,反而降低处理速度,得不偿失。

5.3 优化耗时任务

能优化单个任务的处理时间,比如发送短信时用批量接口,不要一条一条调用;生成报表时用异步分块处理,不要一次性生成超大文件,减少每个任务的耗时,从根源降低队列堆积的概率。

六、总结

Laravel队列是生产环境中优化用户体验的核心工具,碰到队列堆积不用慌,按“查积压、查进程、查失败任务”的步骤排查,应急时用扩容进程、处理卡住任务、拆分高优先级队列快速解决,日常用Horizon监控、合理配置进程数、优化任务耗时,就能有效避免生产环境的队列故障,提升系统的稳定性和用户体验。