Go语言实现的APNs推送服务:一次稳定性问题的排查与优化实录
在生产环境中部署 APNs 推送服务时,我们遇到了一个让人头疼的问题:服务运行约一周后,会突然停止推送消息,且腾讯云实例的内存占用达到 100%。本文将详细记录从最初的异常现象,到最后稳定运行的整个排查与优化过程,希望对你构建稳定、高效的 Go 推送服务有所帮助。
异步问题
生产环境下,程序运行约一周后推送功能会失效。梳理相关代码流程如下:
生产者将消息放入队列A,Consumer-A消费后同步将响应写入队列B。Consumer-B同步消费队列B,将推送结果写入缓存,随后通过定时任务批量保存到数据库。
问题出在队列B:高峰期Consumer-B消费速度慢于Consumer-A,导致队列B被填满。此时,推送响应阻塞在写入队列B的操作上,程序不断创建新的goroutine,最终内存耗尽导致服务崩溃。
优化
简化代码如下:
import "time"
// 优化塞入消费队列B的操作
select {
case channelB <- data:
case <-time.After(time.Second):
return
}
这样,当channelB已满时,1秒超时后直接丢弃响应,避免Consumer-A因阻塞而无限制创建goroutine。
一、问题现象与初步排查
- 初始配置:服务启动时会根据配置创建
200 个 httpClient,每个 client 启动400 个 goroutine,总计约 8 万个 goroutine。 - 问题表现:服务运行一周左右后 完全停止推送,同时腾讯云实例内存占用达到 100%。
- 第一步尝试:简单地提升云主机内存,但问题并未解决,仅仅延后了推送停滞的时间。
二、深入阅读源码,找出并发模型的问题
- 阅读代码后发现,每个 HTTP client 会配套启动 goroutine 队列,导致 goroutine 数量极度膨胀。
- 初步怀疑 goroutine 与连接数的组合导致了内存膨胀与资源抢占。
🔧 动作:调整配置
- 将配置修改为:
10 个 httpClient,每个 client 启动100 个 goroutine。 - 效果:内存占用明显降低,但 推送停止问题依然存在,只是发生时间变长了。
三、提高日志等级,捕获 APNs 的 GOAWAY 信号
- 将日志级别调为
Error,观察关键错误。 - 发现日志中频繁出现:
http2: received GOAWAY的错误。
🧠 理解 GOAWAY:
- GOAWAY 是 HTTP/2 协议中服务端通知关闭连接的帧。
- APNs 服务器可能因连接使用过度或资源占用过多而主动断开。
四、实现连接重建逻辑
🛠 初步方案:
- 在捕获到
http2.GoAwayError时,调用replaceTransport()逻辑,重建 HTTP 客户端连接。
🐞 遇到问题:
- 日志中发现:
interface conversion: error is *errors.errorString, not http2.GoAwayError - 原因是代码错误地将
error类型直接断言为http2.GoAwayError,导致 panic。
✅ 修复方式:
使用 errors.As() 安全判断底层错误类型:
var goAwayErr http2.GoAwayError
if errors.As(e.Err, &goAwayErr) {
// 正确捕获 GOAWAY 错误
s.replaceHttpClient()
}
🟢 发布后验证:
- 日志中确认
replaceTransport()被触发。 - GOAWAY 后能自动重连,服务持续运行未中断。
五、当前配置与运行状态
- httpClient 数量:50
- 每个 Client goroutine 数量:200
- 状态:服务稳定运行,内存占用合理,GOAWAY 可自愈处理。
六、后续优化建议
| 优化方向 | 建议 | 目的 |
|---|---|---|
| 连接池管理 | - 统一使用 http.Transport,设置 MaxIdleConnsPerHost 充足- 避免每个 client 自建 Transport | 提升连接复用率,减少建连开销 |
| 并发动态调整 | - 监控 GOAWAY、重试次数- 高负载时自动降并发 | 降低 APNs 断连接概率 |
| 内存与 GC 监控 | - 启用 pprof- 定期抓取堆快照 | 及时发现 goroutine 或对象泄漏 |
| 错误分类重试 | - 区分 GOAWAY / 网络失败 / 非重试错误 | 提高重试成功率、减少资源浪费 |
| 指标监控告警 | - 集成 Prometheus/Grafana- 监控连接数、错误率、推送成功率等 | 快速发现问题趋势,防患未然 |
七、小结
通过本次稳定性问题的排查,你已经:
- 识别出 goroutine 与内存暴涨之间的因果关系;
- 正确理解并处理了 HTTP/2 的 GOAWAY 机制;
- 用日志 + 错误类型匹配的方式,实现了连接重建的自愈逻辑;
稳定的系统不是不出错,而是能及时感知和快速恢复。这次经历非常有代表性,也为你构建更健壮的推送服务打下了基础。
如果你有类似服务,也欢迎参考这份经验流程,或与我交流进一步的系统设计和优化策略。