Resque队列积压1,480个任务:当worker正在运行、Redis健康时如何诊断任务堆积
发布于 2026年4月29日
从未触发的告警
一家金融科技客户反映快递跟踪队列在积压。截图显示了如下情况:
| 队列 | 任务 | 状态 |
|------------|-------|--------------|
| postbacks | 0 | 空 |
| payments | 0 | 空 |
| test_queue | 0 | 空 |
| tracking | 1480 | 积压中 |
| default | 18 | 积压中 |
| sales | 7 | 处理中 |第一步检查:worker进程在ASG的两个实例上都在运行。systemd服务报告'active (running)'。Redis(ElastiCache)响应正常。日志中没有错误。从监控角度看,一切都是绿色的。
worker在线、Redis健康但队列持续积压,是队列系统中最难排查的情况之一。没有告警闪烁,没有死进程,没有堆栈跟踪。唯一的证据是任务数量在悄悄增长。
诊断:简单的数学揭示了结构性问题
worker被配置为在单个进程中按顺序处理6个队列。'tracking'队列会为每笔订单触发一个对快递跟踪API的HTTP请求——这是一个阻塞式网络调用,没有并行处理。
计算很直接:
触发器(EventBridge):每15分钟一次
每个周期入队任务:~1,480
可用worker:2个(每个ASG实例1个,已烘焙到AMI中)
每个任务耗时:~1.5秒(1次外部API HTTP请求)
2个worker处理1,480个任务需要:
1,480 / 2 / 每次1个任务 = 每个worker处理740个
740 × 1.5秒 = ~18分钟
下一次触发时间:15分钟后
每个周期亏欠:+3分钟的积压worker在下一个周期开始前永远无法完成当前周期。队列并没有卡住——而是在结构性地积压,每个15分钟周期都落后一个周期。
为什么其他队列也受到影响
单线程worker按顺序处理所有6个队列:postbacks → payments → test_queue → tracking → default → sales。当处理到有1,480个任务的'tracking'队列时,worker就被卡在那里进行~18分钟的阻塞HTTP调用。
在此期间,'default'和'sales'队列中的新任务在等待。在这个案例中没有严重的饥饿问题,因为其他队列的量很低,但这个模式很危险:带有阻塞I/O的慢队列会拖住序列中排在它后面的所有队列。
这就是队列系统中的队首阻塞(head-of-line blocking):处理序列前面最慢的任务会阻塞后面的所有内容,即使后续任务处理很快。
如何识别这个模式:诊断命令
得出这个诊断的路径是按顺序检查worker状态、队列深度和日志:
# 1. 在Redis中注册的worker
redis-cli -h <redis端点> smembers resque:workers
# 输出:hostname:PID:队列1,队列2,...,队列N
# 单个worker列出所有队列 = 单线程
# 2. 每个队列的深度
redis-cli -h <redis端点> llen resque:queue:tracking
redis-cli -h <redis端点> llen resque:queue:default
redis-cli -h <redis端点> llen resque:queue:sales
# 3. 失败的任务
redis-cli -h <redis端点> llen resque:failed
redis-cli -h <redis端点> lrange resque:failed 0 -1
# 4. 主机上的worker进程
ps aux | grep resque | grep -v grep
sudo systemctl status app-worker.service关键点:注册为'hostname:PID:postbacks,payments,test_queue,tracking,default,sales'的worker在单个线程中按顺序处理所有这些队列。如果任何队列有慢任务,其他所有队列都要等待。
可观测性缺口:没有队列深度告警
最大的运营问题不是积压本身——而是不知道正在积压。该环境有CPU、内存和HTTP错误告警,但CloudWatch中没有队列深度告警。
通过定期采集脚本将队列指标发布到CloudWatch:
#!/usr/bin/env python3
import boto3
import redis
import time
r = redis.Redis(host='<redis端点>', port=6379)
cw = boto3.client('cloudwatch', region_name='us-east-1')
queues = ['tracking', 'default', 'sales', 'postbacks']
for queue in queues:
depth = r.llen(f'resque:queue:{queue}')
cw.put_metric_data(
Namespace='App/Queues',
MetricData=[{
'MetricName': 'QueueDepth',
'Dimensions': [{'Name': 'QueueName', 'Value': queue}],
'Value': depth,
'Unit': 'Count',
'Timestamp': time.time()
}]
)
print(f'{queue}: {depth}个任务')有了CloudWatch中的指标,告警就很简单:'tracking'队列深度超过500个任务超过10分钟 → 告警。没有这个,积压只能在有人手动查看仪表板时才会被发现。
解决方案:从临时措施到正确方案
方案一——慢队列专用worker(无需修改代码)
生产环境最快的解决方案:为慢队列创建单独的systemd服务,将QUEUE环境变量设置为只处理该队列。
# /etc/systemd/system/app-worker-tracking.service
[Unit]
Description=App Queue Worker — 仅处理快递队列
After=docker.service
Requires=docker.service
[Service]
Type=simple
Restart=always
RestartSec=10
ExecStart=/usr/bin/docker exec myapp-php php app/Workers/resque-worker.php
Environment="QUEUE=tracking"
Environment="REDIS_HOST=<redis端点>"
Environment="REDIS_PORT=6379"
[Install]
WantedBy=multi-user.target有2个ASG实例,每个实例1个专用worker:1,480 / 2个worker / 1.5秒 = ~11分钟。仍然接近15分钟的极限,但解决了阻塞其他队列的问题。要获得更多余量,将ASG最小值从2增加到3个实例。
方案二——任务级并行(需要修改代码)
中期的正确解决方案:在入队时将订单分批,并使用curl_multi_exec(PHP)或asyncio/aiohttp(Python)在单个任务内并行发起N个请求。
以10个为一批并实现真正的并行:148个任务而不是1,480个,每个任务并行发起10个请求。2个worker处理一个完整周期的总时间:不到2分钟。
这个模式不只适用于Resque
这个诊断适用于任何worker按顺序处理多个队列并带有阻塞I/O的队列系统:
Sidekiq(Ruby):在同一线程中处理多个队列的worker——有慢外部API任务的队列会拖慢所有其他队列。
BullMQ(Node.js):没有配置并发数(默认为1)的队列中有awaited HTTP调用——相同的问题,不同的运行时。
AWS SQS + Lambda:这个问题在设计上不存在(Lambda默认水平扩展),但由单线程EC2处理的SQS队列会复现这个模式。
规则很简单:如果你的worker进行外部网络调用并在同一线程中处理多个队列,任何有慢API的队列都会拖住所有其他队列。队列深度监控不是可选的——它是在积压变成事故之前到达的唯一信号。
运营经验
三个配置变更可以消除这类问题:
1. 对有慢外部I/O的队列使用专用worker。 永远不要在同一个worker中混合阻塞式外部API队列和快速处理队列。
2. 在CloudWatch中设置队列深度告警。 阈值:任何主队列深度超过N个任务超过1个触发周期 → 立即告警。
3. ASG最小值与任务量相匹配。 如果每个实例有1个worker,而任务周期需要N个worker才能在触发间隔内清空队列,那么ASG最小值需要为N。
