加入收藏 | 设为首页 | 会员中心 | 我要投稿 站长网 (https://www.haoxinwen.cn/)- 应用程序、AI行业应用、CDN、低代码、区块链!
当前位置: 首页 > 服务器 > 系统 > 正文

Go服务器性能调优:13年前端站长的QPS翻倍实战

发布时间:2026-10-27 11:17:57 所属栏目:系统 来源:DaWei
导读:  凌晨三点十七分,告警声像指甲刮黑板,第七次。DBA老周直接把咖啡泼在了自己工牌上——那块蓝底白字的塑料片现在吸饱了褐色液体,边缘起翘,印着“周立群 2019.03入职”,咖啡渍顺着“群”字最后一撇往下拖了三毫米。我盯

  凌晨三点十七分,告警声像指甲刮黑板,第七次。DBA老周直接把咖啡泼在了自己工牌上——那块蓝底白字的塑料片现在吸饱了褐色液体,边缘起翘,印着“周立群 2019.03入职”,咖啡渍顺着“群”字最后一撇往下拖了三毫米。我盯着屏幕右下角的 QPS 曲线,它正往下栽,栽得比上个月离职那个后端改的中间件还利索。值班表贴在显示器背面,墨迹没干透,我划掉“张工(已调休)”那一行时,笔尖卡顿了一下。


  我原以为是 goroutine 泄漏。查了十八秒 pprof /goroutines 页面,堆叠里全是 runtime.netpoll、net.(conn).Read、http.serverHandler.ServeHTTP——全是正经路径。可 HTTP 处理函数里嵌着一个 defer recover(),recover 里又包了一层 log.Printf,log 写的是 stdout,stdout 被重定向到 systemd-journald,journald 在刷磁盘时卡住……这链路我敢打包票,三年前就在测试环境复现过,当时运维说“先用着”。现在 journald 的日志缓冲区满了,所有写日志的 goroutine 全卡在 write(2) syscall 上,连带 HTTP handler 一起吊着,HTTP/1.1 keep-alive 长连接一多,就变成排队等打烊的奶茶店——不是人少,是收银员蹲厕所去了。空调嗡的一声,突然加大风量,机柜顶部的冷风喷口歪了三十度,斜吹在交换机散热格栅上,滋滋响。


文章配图,仅供参考

  删掉那行 log.Printf,QPS 从四百出头跳到六百二——但没翻倍。老板进站时穿的是灰色羊绒衫,袖口磨出了毛球,他问我“是不是该上 Service Mesh”,我没接话,手指在键盘上敲了两下:go tool trace -http=localhost:8081,然后把 trace 文件拖进 Chrome。trace 里看到 runtime.findrunnable 等待时间暴涨,而 P 的状态图里有三分之一在 idle ——不是 CPU 没活干,是 G 在等 netpoll。我换了个姿势,左脚踩右脚踝,手机响了一声,没人接,响了三秒断掉。对面工位那个后端换了双拖鞋,绿色,带兔子耳朵,左耳掉了,胶布缠了两圈。


  接着把 net/http.Server 的 ReadTimeout 和 WriteTimeout 全设成 15 秒,看着好像防了慢客户端,实则把长连接全掐死在 handshake 后半秒。反而更糟——大量 TCP 连接卡在 ESTABLISHED → CLOSE_WAIT 状态,netstat -ant | grep :8080 | wc -l 跑出来三百七十二条。这时候我才想起那句老话:优胜劣汰。不是机器挑你,是你亲手筛掉不扛压的请求模式。我把超时干掉了,换成 SetKeepAlivesEnabled(true),再加一句 http.DefaultClient.Transport = &http.Transport{IdleConnTimeout: 30 time.Second}——等等,这是客户端,不是服务端。我笑了一下,删掉。重读文档才发现:Server 侧的 IdleTimeout 才管 keep-alive 空闲连接回收,而 ReadHeaderTimeout 才是真正救场的:它防的是慢发 header 的扫描器或恶意 client,不影响业务逻辑耗时。这个我说不准是不是主因,但加上之后,p95 延迟毛刺少了百分之六十。


  还有个细节:JSON 序列化。我们用了标准 encoding/json,但 struct tag 里全塞了 omitempty,导致每次 marshal 都要反射判断字段值是否为零——而大部分结构体字段都带指针、map、slice,它们的 zero 值判断比 int 还慢。我改用 jsoniter.ConfigCompatibleWithStandardLibrary,编译时加了 -tags=jsoniter。没动一行业务代码,只是替换 import。上线前三小时,压测平台报错:“Connection reset by peer”,抓包一看是客户端主动 RST——原来 jsoniter 默认开启 stream 缓冲,buffer 太小,连续 flush 把 client 给挤爆了。加了一句 Config.SetPrivateFields(true) 就好了?不对,是 SetEscapeHTML(false) 和 DisableHTMLEscape() 搞混了,试了五次。最后一次,我抄了隔壁组的 init 函数,黏过去,跑了。咖啡渍干了,在工牌上缩成一块硬痂,颜色比原来深一点。


  最后是 context。所有 handler 都套了一层 withTimeout,但 timeout 值是从 config.yaml 读的,而 config 里写的是 “timeout_ms: 2000”。我查了下,我们项目里所有 timeout 变量都是 int64,但 context.WithTimeout 第二个参数是 time.Duration——而 time.Duration 是 int64 类型别名。所以其实……什么都没错,就是读配置时 parse 成了 2000 纳秒。对,两千纳秒。不是毫秒。不是微秒。就是 2000ns。这个我敢打包票。改完,压测峰值从 621 跳到 1307。没到翻倍?不对,是 1307 对 621,翻了一倍零九。可老板说“1300 是整数,算翻倍”。我点头,没拆穿。他的羊绒衫袖口毛球,被他自己用指甲揪下来一颗,弹到了地上。


  调休申请驳回邮件是昨天下午四点十九分发的,理由是“系统稳定性关键期”。我顺手把值班表撕了,贴新一张,把“王工(产假中)”改成“王工(未排班)”,铅笔字有点抖。现在得去查下那个 rabbitmq connector 是不是还在用 v1.2.7 版本,它底层有个 sync.Pool 被错误复用了两次,第二次 Get 出来的对象里 channel 已 close。这事跟 QPS 没关系,但我刚看见监控里有个队列积压在涨。先记下来吧。下一轮上线前再说。

(编辑:站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!