背景
最近在开发一个golang reactor模型的网络库,底层采用epoll多路复用网络IO。我编写了一个测试脚本用于对程序进行压测,压测脚本的内容是运行一个tcp client,client会大批量的向server发送数据包,但是当我进行实测的时候,奇怪的现象发生了,一旦我运行一个压测脚本几秒后,其他后续的测试脚本就无法再与服务端建立连接,报错如下:
1 | Error connecting: dial tcp 127.0.0.1:10101: connect: connection refused |
使用netstat命令查看,程序监听的10101端口已经被释放,说明socket已经被回收
1 | root@0cf669f12840:/var/www/go/src/sophonn# netstat -ntlp |
涉及到的代码如下:
1 | func main() { |
排错过程
是否主动close?
首先排查,是不是程序是否有主动的close操作,对监听的文件描述符进行了释放。在对程序进行debug之后排除了这种可能
gc?
既然不是主动对端口进行的释放,那么大概率是gc对资源的回收导致的。
打开gctrace对程序的gc进行监控,命令如下
1 | GODEBUG=gctrace=1 go run ./example/server/server.go |
同时使用netstat对端口监听状况进行持续监控
1 | netstat -ntlpc |
监控发现,端口的回收和程序运行后的第一次gc刚好是同时发生的,所以可以断定是gc引发的问题。
问题解决
既然发现是gc将listener回收,那么解决办法就是防止listener被gc回收,所以把listener注入到Eventpoller结构体中,重新编译运行server,发现问题被成功解决了
继续深入
其实排错过程中我依然有几个疑问:
why gc ?
listener在函数体内分配,理论上应该分配在栈空间,那么为什么还会被gc回收呢?我们都知道gc并不会涉及栈空间的内存,那么这个问题唯一的可能就是变量发生了内存逃逸,listener被编译器分配到了堆中,才发生了后续的gc问题。
golang内存逃逸是在编译的时候决定的,内存逃逸查看的指令如下
1 | go build -gcflags '-m' xxx.go |
gc是如何实现对资源的回收?
我们知道gc是对堆内存的回收,那么golang的gc是如何实现在回收内存的同时对系统资源进行释放的呢?
答案是finalizer,我们顺着net.Listen的源码探索,最后可以在setAddr方法中找到如下代码
1 | func (fd *netFD) setAddr(laddr, raddr Addr) { |
gc回收资源的关键就在于runtime.SetFinalizer(fd, (*netFD).Close)这一行。
那么这个函数的作用是什么呢,finalizer是与对象关联的一个函数,通过runtime.SetFinalizer 来设置,它在对象被GC的时候,这个finalizer会被调用,golang通过Finalizer完成一些类似于资源释放的操作。而listener fd的资源回收就是通过finalizer设置了对应的Close方法实现了资源的回收。
关于finalizer的详情可以阅读文章深入理解Go-runtime.SetFinalizer原理剖析