Go服务器性能调优:避坑选型,QPS翻倍实战
|
跟人吵架,当场撕的——就上个月二十二号,暴雨,雨刷器在玻璃上刮出三道歪斜的水痕,我盯着它看进神,直到李哲把半凉的麻辣烫盒盖掀开,油花溅到我的键盘膜上。他说“你这压测脚本没打满网卡”,我说“你连 runtime.GC() 都塞在 HTTP handler 里还谈什么网卡”。他接了个电话,声音压得很低:“王总说……对,那个……先放一放。”我盯着他左耳垂上那颗痣动了两下。 QPS翻倍?翻得最野那次,是砍掉所有 middleware 包装层,把 zap logger 直接塞进 context.Value,结果 pprof 里 runtime.mcall 占比从十八秒飙到三百七十二毫秒。我原以为是锁竞争,结果抓栈发现是 context.WithValue 那个 iface 逃逸触发的 GC 扫描链。删掉它,QPS没翻倍,但毛刺从 99.9th 到 99.99th 缩了一半。这个我敢打包票。 机房空调坏了三天,湿度计停在 78%,风扇噪音像一百台老式硬盘在同步寻道。张薇蹲在机柜后头拿万用表测 UPS 接地,袖口沾了灰,没擦。她问:“你真不用 gRPC-Gateway?”我说:“不用。JSON over HTTP 比 proto over HTTP 快三十纳秒——不是编的,是 net/http.ServeHTTP 路径上少一层 codec.Dispatch,这个我说不准,但 trace 里能看见调用栈深一度,就是快一度。” 选型避坑?优胜劣汰不是玄学。上周三凌晨两点十七分,我们砍掉 go-kit,换成直接用 stdlib + httprouter(不是 gin,gin 的 panic recovery 默认启用了 recover,且不设 limit,压测时 goroutine 泄露到两万三,OOM 前它还在 defer recover)。我们留着那个 logrus hook 三个月没动——因为当时值班的是陈默,他写了注释:“别碰,这 hook 里混了 prometheus.CounterVec,改了要重打 metrics tag 索引”。后来没人动。现在还在跑。那个 hook 里的 time.Now().UnixNano() 其实多算了三十七纳秒,但没人改。不重要。 有个细节挺滑稽:我们切 stream 模式时,把 bufio.NewReaderSize 改成 4096,结果发现某次批量上传失败率跳高 0.3%。查了两天,是 client 侧 nginx buffer 设置太小,回包分片导致 header 和 body 被拆在两个 TCP 包里,而我们的 ReaderSize 太整,刚好卡住读头。最后解决办法是加一行 debug log —— 就写 “read header len: %d”,光这行日志让延迟涨了一微秒,但暴露了问题。后来把 Size 改成 4095,故障没了。这个真没想到。
文章配图,仅供参考 我桌上那杯美式冷透了,底面积出一圈褐色圈,像年轮。张薇路过顺手倒掉,换了个新杯子,没洗旧杯,就搁在饮水机旁。饮水机嗡嗡响,音调比平时低半度,像是压缩机里进了湿气。再说个反常识的:别信 benchmark,尤其别信别人贴出来的 benchmark。他们测的是 echo hello world,而你压的是带事务上下文+jaeger inject+etcd watch 回调+gorm.Preload 三层嵌套的 POST /v2/order/submit。他们跑出 12 万 QPS,你连 8000 都不到,这不是语言问题,是你代码里写了 for _, u := range users { db.Where("id = ?", u.ID).First(&u) }——这个我见过三次,每次都说“业务需要”,每次压测都崩在 db.QueryContext timeout,不是连接池爆了,是 PG 后端进程数满了。崩前一分钟,DBA 老刘发来微信:“你们又扫表?”我没回,他撤回了两条。 有人问我为什么不用 eBPF 做 trace 注入。我说我们线上禁用 bpf_probe_read_kernel——怕和内核版本打架。去年七月,CentOS 7.9 一个 kernel 补丁让 bpf_prog_run 丢掉第四个参数,我们线上跑了两周才定位,最后靠 perf record -e 'sched:sched_switch' 猜出来的。那天下午我在公司楼下修共享单车,链条掉了,修了十七分钟。手机震了四次,都是告警。我没接。 还有个事儿没搞明白:我们上线后监控告警阈值设成了 P99 < 180ms,但实际 P95 是 142ms,P99 是 179ms,P99.9 突然跳到 640ms。查了三小时,发现是 Jaeger client 在 flush batch 时卡住了——不是网络卡,是 protobuf marshal 过程中某个 struct 字段用了 time.Time 且为空,proto.Marshal 把 nil 指针当成合法值序列化进去,然后 jaeger-agent 解析失败、静默丢弃整个 batch,造成 trace 断流、metric 覆盖稀疏,最终 Prometheus 认为服务“健康”,而真实延迟已经失控。这事还没定论,文档里也没写清楚空 time pointer 是否合法。所以现在我们统一用 NullTime 自定义类型。但代价是每个字段多一次 if nil 判断。无所谓。 哦对,上星期五我看见运维小杨在工位吃糖醋排骨,汁水滴到键盘 F4 键上,他拿纸巾按了按,没擦干净,F4 就一直重复触发 ctrl+F,IDE 界面闪了八次。我没提醒他。 下一步?今晚我把 zap 的 encoder 从 json 换成 console,不是为了性能——console encoder 不做结构化 escape,但输出可读,debug 方便。压测环境先不动。生产?等下周三再切。或者不下,等它自己崩一次。反正崩了也不过是重起一个 pod,十六秒内恢复。这次我不守着了。 (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |





