使用SOCKS5代理时Go协程泄漏问题排查求助
解决Go中SOCKS5代理检测的Goroutine泄漏问题
从你提供的协程栈信息来看,泄漏的核心原因很明确:SOCKS5代理的握手过程没有设置超时,导致IO操作无限阻塞,进而卡住了Transport内部的goroutine。虽然你配置了http.Client和net.Dialer的超时,但这些超时并没有覆盖到SOCKS5代理的握手环节——默认的socks5.Dialer(来自golang.org/x/net/proxy)不会自动继承底层Dialer的超时设置,导致在读取SOCKS5代理响应时无限等待。
问题拆解
- 第一个泄漏的goroutine卡在
socks5.go:113的ReadFull调用:这是SOCKS5代理握手时,等待代理返回响应的环节,没有设置ReadDeadline,所以一直阻塞在IO wait。 - 第二个泄漏的goroutine卡在Transport的
getConn的chan接收:这是因为Transport在尝试创建连接时,前面的SOCKS5握手卡住了,导致内部协程一直等待连接建立完成。
你的DisableKeepAlive: true配置只能避免连接复用的问题,但无法解决连接建立阶段的阻塞。
解决方案:为SOCKS5代理全链路添加超时
要彻底解决泄漏,需要确保所有网络环节都有超时覆盖,包括SOCKS5握手、TCP连接建立、HTTP请求传输。下面是具体的代码修复方案:
1. 包装SOCKS5 Dialer,添加读写超时
我们需要自定义一个Dialer,在建立连接后立即设置Deadline,确保SOCKS5握手和后续操作不会无限阻塞:
import ( "context" "net" "time" "golang.org/x/net/proxy" ) // timeoutDialer 包装底层Dialer,为连接添加超时 type timeoutDialer struct { proxy.Dialer timeout time.Duration } func (d *timeoutDialer) Dial(network, addr string) (net.Conn, error) { conn, err := d.Dialer.Dial(network, addr) if err != nil { return nil, err } // 设置连接的读写超时,覆盖SOCKS5握手和后续HTTP传输 if err := conn.SetDeadline(time.Now().Add(d.timeout)); err != nil { conn.Close() return nil, err } return conn, nil } // 支持DialContext的版本(推荐Go 1.10+使用) func (d *timeoutDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { // 尝试将底层Dialer转为DialContext接口 if dc, ok := d.Dialer.(interface{ DialContext(context.Context, string, string) (net.Conn, error) }); ok { conn, err := dc.DialContext(ctx, network, addr) if err != nil { return nil, err } // 优先使用ctx的超时 if deadline, ok := ctx.Deadline(); ok { conn.SetDeadline(deadline) } else { conn.SetDeadline(time.Now().Add(d.timeout)) } return conn, nil } // fallback到普通Dial方法 select { case <-ctx.Done(): return nil, ctx.Err() default: conn, err := d.Dial(network, addr) if err != nil { return nil, err } if deadline, ok := ctx.Deadline(); ok { conn.SetDeadline(deadline) } return conn, nil } } // createSocks5Dialer 创建带超时的SOCKS5 Dialer func createSocks5Dialer(proxyAddr string, timeout time.Duration) (proxy.Dialer, error) { baseDialer := &net.Dialer{ Timeout: timeout, // TCP连接建立超时 KeepAlive: -1, // 禁用KeepAlive,和你的Transport配置一致 } socks5Dialer, err := proxy.SOCKS5("tcp", proxyAddr, nil, baseDialer) if err != nil { return nil, err } return &timeoutDialer{ Dialer: socks5Dialer, timeout: timeout, }, nil }
2. 配置HTTP Client和Transport
在创建http.Client时,使用上面的带超时SOCKS5 Dialer,并确保Transport使用DialContext来传递上下文超时:
func createProxyHTTPClient(proxyAddr string, timeout time.Duration) *http.Client { socks5Dialer, err := createSocks5Dialer(proxyAddr, timeout) if err != nil { // 这里根据你的业务处理错误,比如返回nil或者panic return nil } transport := &http.Transport{ DisableKeepAlive: true, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { // 使用带超时的SOCKS5 Dialer,传递上下文 if dc, ok := socks5Dialer.(interface{ DialContext(context.Context, string, string) (net.Conn, error) }); ok { return dc.DialContext(ctx, network, addr) } // 兼容不支持DialContext的情况 select { case <-ctx.Done(): return nil, ctx.Err() default: return socks5Dialer.Dial(network, addr) } }, // 可选:添加其他Transport配置,比如TLS设置 TLSHandshakeTimeout: timeout, } return &http.Client{ Transport: transport, Timeout: timeout, // 整体请求超时,包括连接、握手、响应读取 } }
3. 在Worker中使用正确的Client
在你的readProxyWorker中,为每个代理创建对应的带超时HTTP Client,然后调用GetAndCheckURL:
func readProxyWorker(DB *storm.DB, workerNum int, conf config.CheckTaskConfig, ownIP string, proxyWaitChan <-chan proxy.Proxy, m proxy.CheckMetrics) { requestTimeout := time.Duration(conf.RequestTimeout) * time.Second // 从配置获取超时,比如10秒 for i := 0; i < workerNum; i++ { go func() { for p := range proxyWaitChan { // 根据代理类型创建对应的Client,这里以SOCKS5为例 client := createProxyHTTPClient(p.Addr, requestTimeout) if client == nil { // 处理创建失败的情况,比如标记代理无效 continue } _, err := GetAndCheckURL(client, conf.TestURL) // 后续处理结果:更新Metrics、写入DB等 // ... // 注意:如果不再使用client,可以关闭Transport的闲置连接,但因为DisableKeepAlive是true,连接会自动关闭 // client.Transport.(*http.Transport).CloseIdleConnections() } }() } }
关键注意事项
- 不要依赖单一超时:同时设置TCP连接超时、SOCKS5握手超时、HTTP请求整体超时,确保每个环节都有兜底。
- 使用DialContext:Go 1.10及以后版本推荐使用
DialContext,它能更好地传递上下文超时和取消信号,避免goroutine泄漏。 - 及时关闭连接:虽然
DisableKeepAlive会让连接在请求后关闭,但如果请求中途阻塞,超时设置会触发连接关闭,释放资源。
这样修改后,SOCKS5代理的握手环节会被超时限制,不会再出现无限阻塞的goroutine,泄漏问题就能解决。
内容的提问来源于stack exchange,提问作者Valera
相关产品推荐
相关产品推荐

