Go服务器性能调优:避坑选型,QPS翻倍实战
|
告警响的那一刻,我正把最后一口泡面汤搅匀——没加醋,嫌酸。手机在桌上震,不是静音,是调成了“仅重要联系人”,结果它连震三下,像在骂我。屏幕还开着 Grafana,CPU 曲线刚翘起尾巴,Prometheus 就把 QPS 红标打到了 127,底下一行小字写着「backend-go-api-03:no response for 9.8s」。 我原以为是 TLS 握手卡死,毕竟上周刚换过证书,nginx 层加了 OCSP stapling,还让开发改了 Go 的 crypto/tls 默认 cipher suites……结果抓包一看,ClientHello 都没发出去。三台机器里,两台压根没监听 443,第三台监听了,但 listen backlog 设成 128,而 netstat -s 里「times the listen queue of a socket overflowed」已经跳到三千多。这数字我盯着看了两秒,关了——不查了,太晚,查不动。 机房很冷,空调风口正对着我后颈吹,头发都结霜了。同事老张裹着羽绒服进来递热茶,说:“你那台服务器,root 分区昨儿满过一次,我没敢清,怕你半夜回滚要日志。”我点头,其实压根没备份那台的日志——上个月说要做冷备,结果值班表排到我头上那天,家里小孩发高烧三十九度四,我在医院输液室改完 deployment.yaml,回来就忘了。反正现在也没人问日志。 跟开发吵了一架。他站我身后看 top,指着 go process 里那个 RSS 1.2G 的实例说:“你没开 pprof 吗?是不是内存泄漏?”我说:“你先看看你写的 http.TimeoutHandler 里那个 time.AfterFunc 调用了几次。”他愣住,掏出手机翻 PR 记录,三分钟后低声说:“哦……我写了个 goroutine 池,但没回收 timer。”我笑了一下,没接话。这事我真不知道根因是不是这个 timer,也懒得重跑 pprof;QPS 翻倍是在换掉那个池子之后发生的,可之前我们也动过 runtime.GOMAXPROCS 和 net/http.Server.ReadTimeout——哪个起效?谁说得清。优胜劣汰罢了。 窗外天快亮了,灰蓝泛白,路灯还亮着。我删掉一个 service 配置里的 readinessProbe.httpGet.path = "/healthz",换成 "/ping",就这一个字符。Go 的 http.ServeMux 不区分大小写?不。但 /healthz 里我们写了段代码:每次请求都去查 pg_conn.State() + exec “SELECT 1”。/ping 就 return 200。上线三分钟,QPS 从 820 涨到 1650。隔壁监控组老李凑过来扫了一眼,嘟囔:“你们健康检查还连 DB?当心主从延迟把探针拖垮。”我嗯了一声,没反驳。他昨天自己把 Redis Sentinel 配错成哨兵+主从双写,导致两个分片数据不一致,凌晨一点半还在敲命令,我不提,他也不提。 其实最坑的是选型阶段没人拦。当时开会,PPT 第三页写“gRPC vs HTTP/1.1”,第四页就跳到“选用 gin”,第五页列 Benchmark:gin 处理 JSON 比 fasthttp 快 12%——结果他们用的 benchmark 是空 handler,连中间件都不开。上线半年才发现,gin 的 recover 中间件每请求新建 stack trace,gc 压力全在分配侧。换成 chi,加个自定义 panicHandler 不 new errors,GC Pause 直接砍掉 60%。但我没说是 chi 的锅,是我们的错——没压测真实路径,光信文档里的 Hello World。失败案例?上个月迁移服务到新集群,同一套镜像、同一配置,QPS 掉一半,最后发现是 kubelet 的 cpu.cfs_quota_us 设低了,但谁会想到 k8s 这层还能吃掉 Go 的调度红利?认了。 可你说怪 Go 吗?不怪。runtime.scheduler.trace 开出来,goroutine wait time 短得像假的;pprof cpu 就是平的,说明没卡在调度上;netpoller 也安分。真正崩的是我们自己:log.Println 在每条 API 入口都打,还塞在 defer 里,等 response 写完才 flush;redis client 用的是默认 NewClient(),没设 pool size;就连 HTTP header 解析,我们都让 Gin 自动 parse Content-Type,却从来不管它怎么解析 multipart boundary——直到某次前端传了个含中文的 filename,边界被截断,整个 body 读到 EOF,connection 直接 hang 住,三台机器轮流变红。这事儿我原以为是编码问题,结果 grep 了一圈源码,发现是 net/textproto.NewReader 里的 readLineSlice 没超时……算了,补 timeout 去了,没深挖。
文章配图,仅供参考 晚饭是楼下兰州拉面,老板记得我口味,葱花多放。他说最近生意差,抖音团购券卖得太狠,单子爆了出不来,后面二十单全是退款。“哎,系统扛不住啊。”他擦着桌子叹气。我低头嗦面,突然想起上礼拜线上有个 POST /v2/order 提交接口,ResponseTime 90th 百分位 3.8 秒,Trace 显示 3.2 秒耗在“等待 DB commit”——结果查慢日志,那条 SQL 本身只 80ms,剩下全是 context.WithTimeout 里 time.Timer 触发前的等待。我们给整个事务设了 5 秒,Go 的 timer 有 jitter,又碰上 GC stop-the-world…算了,反正下周换 ent,加自动 query timeout。这事本来能避免:评审时我说“别拿 context 包全局超时”,开发回我:“那你要我每个 db.QueryContext 单独传 context.WithTimeout?你来写?”我认了。 下一步嘛……先把 log 改成 zap 生产模式,再把所有 net/http.Server.WriteTimeout 拆出来单独设,不和 ReadTimeout 绑死。还有,找个时间,把所有 redis.NewClient() 全搜出来,替换成 &redis.Options{PoolSize: 50}。不指望一次全改完——今天早上七点,我看 git blame,发现那个害死人的 timer goroutine 池,是我自己去年十一月写的。PR 标题叫「优化并发吞吐」。真讽刺。 (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |



