Go服务器性能调优实战:13年运维经验谈QPS翻倍路径
|
线上报警那一刻,我正把冷掉的咖啡泼进绿植土里——那盆发财树其实早死了,就剩几根硬枝插着,上周运维老张路过还啧了一声说“比咱监控系统活得都倔”。警报声没响,是手机震了三下,钉钉弹窗直接糊满屏幕:“/api/order/list 耗时 P99 突破 2.8s,QPS 滑坡 40%”。我盯着那个“2.8”,手指悬在“已读”上停了七秒。不是不敢点,是怕点完就得翻昨天刚合并的 PR —— 那个叫“order_v3_refactor”的分支,前端小李昨晚联调时说“逻辑太绕”,我回他“你传个字段名别带下划线我就谢天谢地”。 其实翻代码前我先去茶水间晃了一圈。键盘声太响,隔壁工位新来的后端敲 Go test 用的是 Caps Lock 切大小写,哒哒哒像啄木鸟刨树心。我原以为又是 GC 峰值捅穿了 pause time,结果 pprof 看了一半,发现 runtime.goroutines 居然卡在 1.2w 不动,而 HTTP server 的 active conn 才 800 出头。这不对劲。goroutine 数跟连接数根本对不上。我说不对劲,运维老张叼着没点着的烟凑过来扫了一眼,吐出一句:“你那个 defer recover 写在 middleware 里了吧?——哎,烟借我打个火。” 对,就是那个 recover。就两行,包在 http.HandlerFunc 闭包里,捕获 panic 后写日志、返回 500。写它的人我认识,去年跳槽去某云做 SRE 的阿哲,我们喝过三顿散伙酒,他走前提醒过我:“defer 在中间件里套太深,log.Printf 会 block goroutine。”我当时想,Log 啥时候成瓶颈了?又不是往磁盘 flush。结果跑大半年,日志库早被替换成 zap,但那行 defer recover 还在,连带着 log.Sync() 被锁在 sync.Pool 之外某个冷门路径里,每来一个 panic 就新起 goroutine 跑 sync,最后堆到调度器喘不过气。不是它吃 CPU,是它吃调度器耐心。我删了它,加了个全局 panic handler,QPS 当场回来一半。说白了,不是调优,是断肢保命。 窗外天黑得快,路灯一盏接一盏亮起来,光晕糊在玻璃上像没擦干净的油渍。接着往下查,发现 /api/order/list 的超时设的是 context.WithTimeout(ctx, 3time.Second),可底层 Redis Client 的 DialTimeout 是 1.5s,ReadTimeout 却是 5s。产品上周改需求时非要“兜底更稳”,逼我们把 HTTP 超时拉长——可他们不知道,当 Go net/http server 拿着 3 秒 deadline 去等 Redis 返回,而 Redis 本身在 read 阶段能憋足 5 秒,那这个请求在 etcd 里的租约早就续了三回,下游服务自己都重试完了两次。这不是容错,是纵容上游失联。我改回 2s,ReadTimeout 强制同步降成 2s,接口耗时立刻压下去,P99 变成“挺高”,具体数字我没细看,反正告警消了,钉钉消息也终于停止弹窗。 老张后来站我身后看了五分钟,忽然说:“你们是不是把灰度开关放 configmap 里了?” 我点头。他说:“configmap 每次 reload 都要全量重载结构体,你们那个 order_config 有二十多个字段,其中三个是 json.RawMessage……每次变更触发 reflect.DeepEqual 对比,对比失败才 reload。” 我当时手一抖,键盘按错键——原来每次前端发版改个灰度比例,kubelet 更新 configmap,我们服务就默默跑一遍 deep equal,而那三个 RawMessage 里嵌了 base64,解码再对比。我原以为这玩意儿不占资源,结果 perf top 一看,runtime.mapassign_faststr 占 CPU 第二高。我把那三字段抽成单独 configmap,加了 hash 注解控制 reload 触发。做完之后——嗯,QPS 又爬上去一截。至于到底多少,没人测。上线后前端小李发来截图:“List 接口滑动不卡了。” 他配了张动图,页面丝滑到反光。我关掉页面,顺手点了静音——工单系统又弹了条消息,说是“用户反馈订单状态延迟”,时间戳是三分钟前。
文章配图,仅供参考 说真的,现在回头看,翻倍哪是什么技术胜利。就是老代码一块块剥开,扒出当年谁图省事写的裸 channel、谁为赶排期留的 sleep(100 time.Millisecond)、还有那些明面上叫 “utils” 实际叫 “shitpile” 的包。上个月招聘面试,我问候选人:“如果让你砍掉一个最该删的依赖,你砍哪个?” 有人答 Prometheus client,有人答 Gin middleware stack。我没吭声。真轮到自己动手,第一个砍的永远是那个没人敢动的 log wrapper —— 它连版本号都没有,作者邮箱早失效了,但所有 error 都得过它手。优胜劣汰?呵,淘汰的哪是代码,是当初写代码时那份心照不宣的敷衍。我甚至不确定它是不是真被淘汰了,可能只是藏得更深了。手机亮了一下,是我妈发来的微信,问“今晚回家吃饭吗”,我回了个“可能加班”。然后切回终端,敲了 go tool trace ./trace.out,还没点开,就想起来 trace 采样需要改编译参数,而我们部署脚本里写死的 -gcflags 是 "-l -m"。所以——接下来得先改 CI。或者……先找老张借根烟? (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |

