You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用SOCKS5代理时Go协程泄漏问题排查求助

解决Go中SOCKS5代理检测的Goroutine泄漏问题

从你提供的协程栈信息来看,泄漏的核心原因很明确:SOCKS5代理的握手过程没有设置超时,导致IO操作无限阻塞,进而卡住了Transport内部的goroutine。虽然你配置了http.Client和net.Dialer的超时,但这些超时并没有覆盖到SOCKS5代理的握手环节——默认的socks5.Dialer(来自golang.org/x/net/proxy)不会自动继承底层Dialer的超时设置,导致在读取SOCKS5代理响应时无限等待。

问题拆解

  1. 第一个泄漏的goroutine卡在socks5.go:113的ReadFull调用:这是SOCKS5代理握手时,等待代理返回响应的环节,没有设置ReadDeadline,所以一直阻塞在IO wait。
  2. 第二个泄漏的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 10:05:25