Go 队列消费者:并发数不是配置越大越能扛
一、消费者扩太猛,会把下游一起拖下水
队列系统常被当成削峰工具。请求进队列,消费者慢慢处理,看起来很稳。但消费者并发数如果随便拉大,就会把数据库、第三方 API、对象存储一起打满。队列只是把压力换了个地方,不会凭空消灭压力。
消费者设计要按下游容量反推并发。吞吐、失败率、重试策略、单任务耗时都要一起看。并发越大,未必越快,可能只是失败得更快。
我处理过一起典型事故:监控看到消息队列积压 80 万条,运维第一反应是"加消费者"。消费者并发从 50 拉到 200,CPU 和内存都不是问题。但五分钟之后数据库连接池打满——每条消息处理都要查两次数据库、写一次,200 个并发意味着可能同时有 600 个数据库请求。连接池只有 100 个连接,瞬间耗尽。然后消息开始大量 ack 超时,回到队列,消费者重新拉取,造成重复消费,数据库写入冲突更多。加消费者的结果是:积压没减少,数据库差点被打挂。真正的解决方案是先限流队列的推送速率,等数据库连接池恢复后,再逐步放开消费者。
二、消费者链路要包含限流、重试和死信
一个生产消费者至少要有并发限制、处理超时、重试次数和死信队列。
flowchart TD A[消息队列] --> B[消费者拉取] B --> C[并发限制] C --> D[处理任务] D --> E{成功} E -->|是| F[确认消息] E -->|否| G{可重试} G -->|是| H[延迟重试] G -->|否| I[死信队列]没有死信队列,坏消息会在队列里无限转圈。系统看起来很努力,实际在原地打滑。
重试策略不能是简单的"失败就等一下再试"。重试次数要有限制(通常 3-5 次),重试间隔要指数退避(1s → 2s → 4s → 8s),并且区分可重试错误和不可重试错误。参数校验失败不应该重试,下游临时超时可以重试。重试策略写清后,队列的"假忙碌"会大幅减少。
三、用 semaphore 控制本地并发
下面示例用信号量限制处理并发。真实项目还要处理 ack、nack 和 shutdown。
func Consume(ctx context.Context, msgs <-chan Message, limit int, handle func(context.Context, Message) error) { sem := make(chan struct{}, limit) for { select { case <-ctx.Done(): return case msg := <-msgs: sem <- struct{}{} go func(m Message) { defer func() { <-sem }() _ = handle(ctx, m) }(msg) } } }这里的limit不是随便拍。要根据下游能力、任务耗时和机器资源压测出来。压测时不仅要看"多少并发时下游 CPU 到 80%",还要看"并发每增加 20%,P99 延迟增加多少"。延迟增长到不可接受时就应该是并发上限,而不是等 CPU 打满才停。
另外,处理超时要独立于并发限制。并发数只控制同时处理的数量,但如果某些消息处理超过预期时间(比如下游慢查询),就应该主动超时取消。goroutine 泄漏最常见的来源就是消费者处理超时后没有及时退出。
四、消费者要能优雅停机
部署升级时,消费者不能直接杀。正在处理的消息要么完成 ack,要么安全 nack 回队列。否则会出现重复处理或消息丢失。
还要处理幂等。队列消费者天然可能重复消费。业务侧必须有幂等键,尤其是写数据库、扣库存、发通知这类动作。只靠队列"至少一次"语义,不能保证业务正确。
最后,监控要看积压、处理耗时、重试次数和死信数量。只看消费者进程活着没有意义。积压持续升高,说明消费能力或下游能力不够。
消息可见性超时也要设置合理。处理时间超过可见性超时,消息会被其他消费者重复拿到。超时太长,失败恢复慢;超时太短,重复消费多。这个值要按 P99 处理耗时和重试策略来定。
还要区分重试队列和主队列。失败消息如果立刻回到主队列,会影响正常消息。延迟重试可以放到单独队列,避免坏消息一直插队。死信队列也要有人处理,不是丢进去就完事。建议给死信队列配置监控,死信数量增长要触发告警,并安排定期 Review。
消费者扩容要看下游指标。队列积压时第一反应不是加消费者,而是确认下游是否还有容量。否则扩容只是把事故推进下一层。
消费者还要处理消息顺序。有些业务要求同一用户或同一订单按顺序处理,此时不能简单并发拉满。可以按 key 分区,让同一 key 落到同一处理通道,既保留局部顺序,又不牺牲全局吞吐。
批量消费也要谨慎。批量能提高吞吐,但失败时要知道是哪条消息失败。整批重试会放大重复处理,逐条确认又会增加开销。批量大小要结合业务幂等和失败率来定。如果失败率低于 1%,批量模式可以显著提升吞吐;如果失败率超过 5%,批量重试的成本可能高于收益。
五、总结
Go 队列消费者的并发数要按下游容量反推。并发限制、处理超时、重试和死信队列是基本盘。消费者还要支持优雅停机和业务幂等。队列能削峰,但不能替系统承受无限压力。消费者设计的目标是有节制地消费,不是疯狂地消费然后更快地失败。