一次Golang Socket泄露问题排查
公司内部使用的云管理平台,查看Pod状态时发现有多次restart的记录:
由于早就超过Events的保存时间,describe pod一下,查看Last State:
OOMKilled,但由于环境的prometheus问题,之前的监控数据找不到,因此参考重启后的监控信息,发现内存整体具有上涨趋势,但是在一定时间段还是有所下降,go tool pprof heap 也看不到什么有用的信息。
而观察Pod的文件描述符与socket连接数便发现了问题:
在工作时间(有用户使用),Pod的文件描述符打开数的上涨趋势几乎和Socket连接数的上涨趋势一样。那么结论便显而易见,最起码socket是有泄露的。
在该系统中,调用网络连接的地方有很多,比如client-go与集群的交互、prometheus客户端、websocket等等。因为之前处理过一次Pod文件描述符打开过多的问题,因此本次首先排查websocket的连接问题。使用 go tool pprof 查看goroutine,果然发现了问题:
协程都阻塞在websocket的Close上了,这也符合监控的预期,大概率是打开log或者terminal的时候,关闭页面后(前端发来websocket的关闭信号)websocket的go client并没有按照预期关闭客户端以及相关io。
type Client struct {
conn *websocket.Conn
mtx sync.RWMutex
// send message to client
Send chan interface{}
// receive message from client
Read chan []byte
closed bool
// close signal
closeSignal chan bool
}
func (c *Client) Close() {
if c.getCloseStatus() {
return
}
c.closeSignal <- true
c.close()
close(c.closeSignal)
close(c.Send)
close(c.Read)
if err := c.conn.Close(); err != nil {
log.SetContext(log.Context{}).Errorf("websocket close failed. err:%v", err)
}
}
写得不好,问题显而易见,closeSignal为一个无缓冲通道,在传入一个true变量后,这个Close()函数便阻塞了!根本不会继续执行后续的多个close方法。
解决的方法有很多,在不改变现有代码的情况下,有个方法便是在启动Client的时候再启动一个协程,监控closeSignal通道,Close()在执行时直接将写入的值取出,解除阻塞。