踩坑分享:对接宝蓝得 BCS Redis,后台任务全部卡死不消费
现象
线上 SAQ 队列积压,23 个 job 全部停在 queued 状态,worker 看着活着,但就是不干活,日志里也没有明显报错。
根因
BCS(宝蓝得)的 Redis 是兼容实现,INFO 命令返回的内容里没有 redis_version 这个字段。
而 SAQ 的 RedisQueue.version() 是这么写的:
info = await self.redis.info()
self._version = tuple(int(i) for i in str(info["redis_version"]).split("."))
直接下标取值 → KeyError。
关键在于,这个 version() 是在 dequeue 的时候才调用的(用来判断走 BLMOVE 还是 BRPOPLPUSH),不是连接阶段。所以服务能正常启动、能正常入队,一到取任务就炸 —— 排查起来特别阴间,很容易误判成"worker 挂了"或者"队列有问题"。
修复
继承 RedisQueue 覆写 version(),不要写死,优先取真值、取不到再兜底:
class BcsRedisQueue(RedisQueue):
async def version(self):
if self._version is None:
info = await self.redis.info()
self._version = tuple(
int(p) for p in str(info.get("redis_version", "6.0.0")).split(".")[:3]
)
return self._version
⚠️ 兜底版本号要看 BCS 是否支持
BLMOVE(用COMMAND INFO BLMOVE查一下):
- 支持 → 兜底
(6, 2, 0),走新分支- 不支持 → 兜底
(6, 0, 0),走BRPOPLPUSH老分支
避坑提醒
-
影响面不止 SAQ:任何通过
INFO做能力探测 / 版本判断的组件都可能中招(Celery 部分 backend、各类 SDK 自检逻辑)。后面谁再对接 BCS 的 Redis,建议先手动跑一次INFO看看字段齐不齐。 -
排查这类问题别只盯 worker 死活:job 卡
queued+ worker 存活 = 大概率是消费链路里某一步抛异常被吞了,去翻 worker 的完整堆栈。 -
积压任务别直接全量重放:webhook 类任务积压久了,重放可能把对端打爆或触发幂等拒绝,先按时间戳筛一遍再决定。
-
SAQ 的键结构和常见队列不一样:
键 类型 正确命令 saq:job:{queue}:{id}String(JSON) GETsaq:{queue}:queuedList LLEN/LRANGEsaq:{queue}:activeList LLENsaq:{queue}:incompleteZSet ZCARD用
HGETALL/ZCARD去查 job 会全部报WRONGTYPE,别被误导。
附:排查用得上的命令
# 确认 INFO 里到底有没有 redis_version
INFO server
# 确认 BCS 支持哪个阻塞弹出命令(决定兜底版本号)
COMMAND INFO BLMOVE
COMMAND INFO BRPOPLPUSH
# 看队列积压量(注意是 List,不是 ZSet)
LLEN saq:webhook:queued
LRANGE saq:webhook:queued 0 -1
LLEN saq:webhook:active
# 看单个 job 内容(String/JSON)
GET saq:job:webhook:0b3d0c17-xxxx-xxxx
# 安全遍历所有 job(别用 KEYS,会阻塞单线程)
SCAN 0 MATCH saq:job:* COUNT 1000
一句话总结
国产兼容 Redis 未必 100% 遵循 Redis 协议细节,接入前先跑一遍 INFO / COMMAND INFO 做能力探测,别等到线上积压了才发现。
后续建议:如果公司里对接 BCS 的不止一个项目,可以把这段同步到公共 wiki,或者提给平台侧让 BCS 补上
redis_version字段 —— 那才是根治,不然每个接入方都得踩一遍。