真削峰需在gin handler中建立背压信号:handler只做序列化并写入带缓冲channel(如make(chan []byte, 400)),由独立goroutine异步publish;直接调amqp.publish会阻塞http连接、无缓冲、无重试,导致雪崩。

真削峰不是把请求塞进 RabbitMQ 就完事,而是在 Gin handler 里就建立背压信号——上游不控速、不缓冲,下游再怎么配 MQ 都是假削峰。
为什么 Gin 直接调 publish 不算削峰
常见错误是:用户请求一来,Gin handler 立刻调用 amqp.Publish() 发消息,然后返回 200。这看起来“异步了”,但实际没解决任何问题:
- HTTP 连接仍被长期占用(直到 AMQP publish 完成或超时),QPS 上不去
- RabbitMQ 的
basic.qos只管消费者端,对 Gin 这个生产者完全无感 - 网络抖动或 MQ 拒绝连接时,
publish报错直接 panic 或阻塞,接口雪崩 - 没有缓冲区,瞬时 5000 请求进来,哪怕平均 QPS 只有 500,也会瞬间打穿
用 buffered channel 做首道蓄水池
核心是把“接收请求”和“发消息”解耦,让 Gin handler 快速返回,同时可控积压:
-
ch := make(chan *Request, 400)中的 400 不是并发数,而是按公式算出的最大积压量:P99 耗时 × 峰值 QPS × 可容忍积压时长(例如 50ms × 200 × 2s ≈ 400) - 上游必须用
select { case ch ,绝不能直接 <code>ch 阻塞 - 写入成功后,启动 goroutine 异步
publish;失败则立即降级(如写本地磁盘、返回 429 或走兜底逻辑) - 下游消费 goroutine 要
recoverpanic,并检测amqp.Connection.IsClosed(),连不上就暂停读ch并告警
RabbitMQ 端必须打开的几个配置
不调这些,削峰就是纸糊的:
- 启用
publisher confirms:确保每条消息真正落盘,否则网络抖动会丢消息,上游还浑然不觉 - 队列声明时设
durable: true和delivery_mode: 2,避免服务重启后消息丢失 - 消费者端设置
qos(prefetch_count: 1),防止一个消费者堆积太多未确认消息,拖慢整体吞吐 - 禁用
auto_ack,改用手动ack,确保消息处理失败可重回队列重试
别忽略 Gin 中间件与 channel 的生命周期绑定
channel 是内存对象,不能随请求创建/销毁,必须全局复用;但它又不能无限增长,所以要注意:
- 不要在每个 handler 里
make(chan),channel 应作为服务级变量初始化一次 - goroutine 消费
ch时要加for range+recover,防止 panic 导致 channel 永久阻塞 - 如果用 go-zero,
rate.Limiter只能限速,不能缓冲,它和 buffered channel 是互补关系,不是替代关系 - 真实压测中,积压阈值常被低估——P99 耗时在高负载下会上浮,建议留 20% 余量











