Conn.File方式导致无法close对应旧fd,线程数泄漏
使用go编写server时,有时需要获取当前连接对应的connection属性。而Linux系统中,Connection本质为socket类型的file descriptor。go源码中多种网络类型(比如tcp/udp/unix)的conn均提供了File方法,用于获取连接对应的socket file descriptor。但使用该方法后,会导致close连接时阻塞,进而造成线程增加,引发系统panic。
问题的复现过程也比较简单,逻辑代码如下:
...
conn, err := ln.Accept()
// 获取conn对应fd
connT, ok := conn.(*net.TCPConn)
file, _ := connT.File()
// do some operation using file
...
当http server承载较多流量后,调用Shutdown方法关闭server,会发现服务无法关闭。
- pprof分析
分析go问题,必然先通过pprof神器,尝试能否看出端倪。出现问题时goroutine profile显示如下:
可看出大量goroutine在进行SemacquireMutex
系统调用,即阻塞在锁竞争过程,但无法根据调用栈明确导致锁竞争的调用方。
- dlv调试
为了明确具体的调用方,使用dlv对http.Shutdown方法进行调试。
通过断点调试,发送goroutine最后阻塞在runtime_Semacquire
函数中,且与线上pprof内容吻合。
- 阻塞分析
搜索相关资料,发现use RawConn.Control to get fd instead of Fd()和net: UnixListener blocks forever in Close() if File() is used to get the file descriptor中提到过相关问题,该问题为go升级1.11后的已知问题。
调用TCPConn.File
方法时,会复制底层fd,返回结果称为duplacated fd
,如下:
调用duplacated fd
的file.Fd()
方法时,会通过系统调用fcntl
将duplacated fd和底层fd内容设置为blocking
状态。但对于original fd,仍然为nonblocking
状态,所以产生了死锁。示意如下:
将代码中出现的file.Fd()方法调用改为tcpConn.SyscallConn(),从而避免fd关闭时死锁。
...
conn, err := ln.Accept()
// 获取conn对应fd
connT, ok := conn.(*net.TCPConn)
rawConn, err := tcpConn.SyscallConn()
rawConn.Control(func(fd uintptr) {
// do some operation using file
})
...
验证过程中也发现,在连接可正常关闭后,server占用的线程数并没有随之降低,此为go语言本身的问题(或许不一定能定义成问题),可参考极端情况下收缩 Go 的线程数。