公司一个同事使用 Go Websocket 开发了 k8s 在线调试服务,该服务也部署在 k8s 集群中,没几天运维那边通告说 cpu 100% 高负载了,还把限制的范围内的 cpu core 都干满了。由于那人休假,我帮忙处理下。
通常来说这类 cpu 高负载的问题相对好排查,多是 bug 造成的。像这个调试服务在一个量级请求完毕后,cpu 使用率居然还是爆满。? 不用想,肯定是协程泄露了,造成了某个逻辑的忙轮询。
该服务在 k8s 集群中部署,没有接入 ingress,所以在有外网的 k8s node 上做了一个端口映射。这样开发机就可以 go tool pprof 外网地址。
还有一个好方法是我以前常用的,在 k8s node 上把 golang 的 pprof 可分析数据导入到文件里,然后把文件 post 到一个支持上传的服务,比如 http upload,后面大家就知道该怎么操作了。
下面是pprof生成的火焰图和调用链耗时图,很明显的看到 writeLoop 不断的调用 Close() 方法。
既然确定了问题,通过 pprof source 定位热点代码,为什么会不断调用 wsConn.Close()
呢 ?因为没有 return,既然已感知连接关闭,那么就应该 return 出去!!!
// xiaorui.cc
Total: 2.31s 1.35mins (flat, cum) 99.25%
76 . . }
77 . . }
78 . .
79 . . // 发送协程
80 . . func (wsConn *WsConnection) wsWriteLoop() {
81 330ms 330ms defer fmt.Println("write exited")
82 . . for {
83 740ms 1.05mins select {
84 720ms 730ms case msg = <-wsConn.outChan:
85 . . if err = wsConn.socket.WriteMessage(msg.MessageType, msg.Data); err != nil {
86 . . log.Error("websocket write message error", err)
87 . . }
88 220ms 220ms case <-wsConn.closeChan:
89 300ms 16.21s wsConn.Close()
90 . . }
91 . . }
92 . . }
相比性能调优,这类由于 bug 引起的 cpu 高负载问题反而特别容易处理,基本上通过 pprof 看火焰图就可以快速定位问题。