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 机制;
  • 用日志 + 错误类型匹配的方式,实现了连接重建的自愈逻辑;

稳定的系统不是不出错,而是能及时感知和快速恢复。这次经历非常有代表性,也为你构建更健壮的推送服务打下了基础。


如果你有类似服务,也欢迎参考这份经验流程,或与我交流进一步的系统设计和优化策略。