一次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()在执行时直接将写入的值取出,解除阻塞。