不能直接用http.client发请求,因其缺乏重试退避、渠道健康探测、熔断降级、并发控制与trace链路追踪等关键能力,易导致雪崩、oom及故障难定位。

为什么不能直接用 http.Client 发请求就完事
第三方推送(比如极光、华为 HMS、苹果 APNs)失败率高、响应延迟波动大、限流策略不透明。直接裸用 http.Client 会把业务逻辑和网络抖动、重试、降级逻辑耦合在一起,一旦推送服务不可用,上游调用方容易被拖慢甚至雪崩。
核心问题不是“怎么发”,而是“发失败了怎么办”“并发突增时怎么扛”“某个渠道挂了要不要切走”。所以模块必须自带:可配置的重试退避、失败回调钩子、渠道健康度探测、降级开关。
-
http.Client必须设置Timeout和MaxIdleConnsPerHost,否则默认无限等待 + 连接池无上限,压测时极易耗尽文件描述符 - 不要复用同一个
http.Client实例打所有渠道——不同厂商 API 的超时/重试策略差异很大,混用会导致某些渠道误判为超时 - 避免在发送函数里做 JSON 序列化或签名计算等耗时操作;这些应提前完成,进队列前就准备好
payload字节流
如何用 channel + worker pool 控制并发与背压
推送请求本质是 I/O 密集型任务,但第三方接口通常有 QPS 限制(如极光单应用 600 QPS)。硬限流靠 time.Sleep 或 rate.Limiter 不够稳——它无法感知下游真实响应时间,容易在突发流量下堆积大量待发消息,OOM 风险高。
更稳妥的方式是:用带缓冲的 chan *PushTask 做入口队列,固定数量 goroutine 从队列取任务执行,并对每个渠道单独配 buffer size 和 worker 数。
type PushTask struct {
Channel string // "jpush", "apns", "hms"
Payload []byte
Timeout time.Duration
Retry int
}
// 每个渠道独立 worker pool
jpushPool := NewWorkerPool(5, 1000) // 5 workers, 1000 queue capacity
hmsPool := NewWorkerPool(3, 500)
- 缓冲区大小要小于下游渠道允许的“最大积压请求数”,例如华为 HMS 推荐单设备每秒最多 10 条,那整个应用缓冲区不宜超过 5000
- worker 数量 ≠ CPU 核心数,而是根据渠道平均 RT 调整:RT 越长,worker 越少,否则大量 goroutine 卡在
http.Do上,调度开销飙升 - 入队列前必须做快速校验(如 token 是否为空、payload 是否超长),失败直接返回错误,别让无效任务占着缓冲区
怎么实现渠道健康度自动探测与故障隔离
不能等用户投诉才发现某渠道挂了。需要主动探测:定时发轻量 probe 请求(如极光的 /v3/push/validate),记录成功率、P95 延迟、连接错误率。
探测结果驱动两个动作:一是动态调整该渠道的 worker 数(连续 3 次失败率 > 30%,worker 减半);二是触发熔断——当失败率超阈值且持续 60 秒,自动关闭该渠道写入,只允许读取状态(比如查送达回执)。
- 探测间隔不能太密( 5min),来不及响应突发故障
- 健康状态必须跨进程共享(比如存 Redis),否则多实例部署时各节点判断不一致,可能部分实例还在往已挂渠道发请求
- 熔断后要提供手动恢复入口(如 HTTP 管理接口
POST /admin/channel/jpush/enable),不能只依赖自动恢复
为什么日志和 trace ID 必须贯穿整个推送链路
推送失败时,开发最常问的是:“这条消息到底走到哪一步了?是签名错了?还是 token 过期?还是网络超时?” 如果日志分散在不同 goroutine、没带唯一 trace ID,排查就得翻五六个日志文件,再拼时间戳。
解决方案很简单:所有 PushTask 初始化时生成 traceID,并通过 context.WithValue 透传到每个环节(序列化、签名、HTTP 请求、回调处理),最终所有日志都打上这个 ID。
- 不要用
uuid.New(),而用fastuuid.MustNew()或github.com/google/uuid的NewSHA1变体,减少 GC 压力 - HTTP 请求头必须带上
X-Trace-ID,方便和第三方平台日志对齐(极光、个推都支持该 header) - 失败日志里除了 error,一定要输出原始
Payload的前 100 字节(脱敏手机号、token 后),否则根本没法复现
最麻烦的从来不是发出去,而是发出去之后没人知道它去了哪、卡在哪、为什么失败。trace ID 不是锦上添花,是定位问题的最低成本路径。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











