Buzeli
buzeliSoluções Digitais
SRE

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。