Golang HTTP服务运行12小时后无响应,CLOSE_WAIT连接过多排查求助
问题背景
我们用Golang开发的HTTP Web服务(监听8000端口),持续运行约12小时后会停止响应HTTP请求,无法返回200响应。登录服务所在节点执行netstat命令后,发现myserver进程存在大量CLOSE_WAIT状态的TCP连接。问题发生后,本地curl 8000端口会挂起,无任何响应返回。
netstat命令输出如下:
# netstat -alpn | grep -i 8000 tcp 0 0 127.0.0.1:60502 127.0.0.1:8000 TIME_WAIT - tcp6 1 0 :::8000 :::* LISTEN 20987/myserver tcp6 1 0 100.10.10.2:8000 200.10.10.2:38436 CLOSE_WAIT 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:32800 LAST_ACK - tcp6 91 0 100.10.10.2:8000 200.10.10.2:42204 ESTABLISHED - tcp6 0 0 100.10.10.2:8000 200.10.10.2:46250 CLOSE_WAIT 20987/myserver tcp6 0 0 100.10.10.2:8000 200.10.10.2:44042 ESTABLISHED 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:40876 LAST_ACK - tcp6 0 0 100.10.10.2:8000 200.10.10.2:32774 CLOSE_WAIT 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:32792 LAST_ACK - tcp6 0 1 100.10.10.2:8000 200.10.10.2:33088 LAST_ACK - tcp6 92 0 100.10.10.2:8000 200.10.10.2:39122 CLOSE_WAIT 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:32788 LAST_ACK - tcp6 0 0 100.10.10.2:8000 200.10.10.2:32768 CLOSE_WAIT 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:38262 LAST_ACK - tcp6 0 1 100.10.10.2:8000 200.10.10.2:32790 LAST_ACK - tcp6 0 0 100.10.10.2:8000 200.10.10.2:32772 CLOSE_WAIT 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:32776 LAST_ACK - tcp6 0 1 100.10.10.2:8000 200.10.10.2:34102 LAST_ACK - tcp6 0 1 100.10.10.2:8000 200.10.10.2:38872 LAST_ACK - tcp6 0 0 100.10.10.2:8000 200.10.10.2:32798 CLOSE_WAIT 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:32786 LAST_ACK - tcp6 0 0 100.10.10.2:8000 200.10.10.2:46248 CLOSE_WAIT 20987/myserver tcp6 0 0 100.10.10.2:8000 200.10.10.2:38430 CLOSE_WAIT 20987/myserver tcp6 0 0 100.10.10.2:8000 200.10.10.2:39092 CLOSE_WAIT 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:38866 LAST_ACK - tcp6 0 1 100.10.10.2:8000 200.10.10.2:34126 LAST_ACK - tcp6 91 0 100.10.10.2:8000 200.10.10.2:32768 ESTABLISHED 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:38246 LAST_ACK - tcp6 0 0 100.10.10.2:8000 200.10.10.2:45316 CLOSE_WAIT 20987/myserver tcp6 0 0 100.10.10.2:8000 200.10.10.2:45328 CLOSE_WAIT 20987/myserver tcp6 0 0 100.10.10.2:8000 200.10.10.2:32782 CLOSE_WAIT 20987/myserver tcp6 0 0 100.10.10.2:8000 200.10.10.2:32794 CLOSE_WAIT 20987/myserver tcp6 0 1 100.10.10.2:8000 200.10.10.2:32772 LAST_ACK - tcp6 0 1 100.10.10.2:8000 200.10.10.2:33078 LAST_ACK -
服务核心代码如下:
mux := http.NewServeMux() mux.HandleFunc("/", taskHandler) // New method to start a customized server with timeouts myServer := &http.Server{ Addr: ":8000", Handler: mux, ReadTimeout: 3 * time.Second, WriteTimeout: 3 * time.Second, } // Here we will disable keep alives for the server connections myServer.SetKeepAlivesEnabled(false) if err = myServer.ListenAndServe(); err != nil { log.Fatal("server failed to start %v", err) }
请求处理函数:
func taskHandler(w http.ResponseWriter, r *http.Request) { response := "200" value := getSomeDBValue() if value == "0" { log.Info("Response sent back -> %s", response) io.WriteString(w, response) } }
疑问:是否是服务程序未正确关闭TCP连接导致?希望得到问题原因及排查方向的建议。
问题原因分析
CLOSE_WAIT连接堆积的直接原因:CLOSE_WAIT状态表示对方(客户端)已经主动关闭了TCP连接,但我方(服务端)尚未调用close()关闭连接。结合代码来看,问题出在
taskHandler的逻辑漏洞:- 当
getSomeDBValue()返回的结果不等于"0"时,处理函数没有向客户端返回任何响应,也没有调用w.WriteHeader()或结束请求的操作。 - 虽然设置了
WriteTimeout: 3 * time.Second,但Golang的WriteTimeout是从请求读取完成后开始计时,直到响应写入完成。如果处理函数没有触发任何写入操作,这个超时不会生效,连接会一直处于打开状态。 - 当客户端超时后主动关闭连接,服务端就会进入CLOSE_WAIT状态,且因为代码没有正确结束请求,连接资源无法被释放,长期堆积后耗尽服务端的文件描述符,导致新请求无法被处理,最终服务挂起。
- 当
服务挂起的连锁反应:系统可用文件描述符被耗尽后,新的TCP连接请求无法被服务端接收,本地
curl会因为无法建立连接或连接后无法被处理而挂起。
排查与修复建议
修复步骤
- 补全处理函数的分支逻辑:确保无论
getSomeDBValue()返回什么结果,都向客户端返回合法的HTTP响应,比如:func taskHandler(w http.ResponseWriter, r *http.Request) { response := "200" value := getSomeDBValue() if value == "0" { log.Info("Response sent back -> %s", response) io.WriteString(w, response) } else { // 返回合适的状态码,比如400或500,确保请求被正确结束 http.Error(w, "invalid value", http.StatusBadRequest) } } - 添加请求超时兜底:除了
ReadTimeout和WriteTimeout,建议设置IdleTimeout(即使禁用了keepalive,也能覆盖异常未关闭的连接),同时可以考虑设置MaxHeaderBytes限制请求头大小,增强服务稳定性:myServer := &http.Server{ Addr: ":8000", Handler: mux, ReadTimeout: 3 * time.Second, WriteTimeout: 3 * time.Second, IdleTimeout: 5 * time.Second, // 兜底超时,确保空闲连接被关闭 MaxHeaderBytes: 1 << 20, }
排查方向
- 监控文件描述符使用情况:在服务运行期间,用
lsof -p <pid> | wc -l或cat /proc/<pid>/limits查看进程的文件描述符消耗趋势,确认是否是CLOSE_WAIT连接耗尽了资源。 - 日志补充:在
taskHandler中添加全分支的日志输出,记录getSomeDBValue()的返回值,确认非"0"的场景是否频繁出现,导致连接堆积。 - TCP连接状态监控:定时执行
netstat -alpn | grep -i 8000 | awk '{print $6}' | sort | uniq -c,统计各状态连接的数量变化,验证修复后CLOSE_WAIT连接是否不再堆积。
内容的提问来源于stack exchange,提问作者Rohan Parulekar
相关产品推荐
相关产品推荐

