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

Go语言VPN环境下IP扫描请求耗时递增问题问询

扫描IP请求耗时递增的原因分析与解决方案

场景概述

通过VPN扫描内网IP,使用Go语言编写的扫描接口代码如下,执行时发现请求耗时从最初的2-3秒逐渐增加到10秒以上,扫描300个IP时最后一个请求耗时甚至超过90秒。已排除目标限流、固定IP异常、超时参数无效等情况。

func scanTestHandler(c *gin.Context) {
    var ipRanges ScanApiBody
    decoder := json.NewDecoder(c.Request.Body)

    if err := decoder.Decode(&ipRanges); err != nil {
        http.Error(c.Writer, err.Error(), http.StatusBadRequest)
        c.Writer.WriteHeader(http.StatusBadRequest)
        return
    }
    if len(ipRanges.Ranges) == 0 {
        http.Error(c.Writer, "No Ranges Provided", http.StatusBadRequest)
        c.Writer.WriteHeader(http.StatusBadRequest)
        return
    }
    client := &http.Client{
        Transport: &digest.Transport{
            Username: "root",
            Password: "root",
        },
        // Timeout: time.Second * 3,
    }

    wg := sync.WaitGroup{}
    ctx := context.TODO()
    count := atomic.Int32{}
    sem := semaphore.NewWeighted(200)

    streamChan := make(chan ScanApiRes)

    for ri := 0; ri < len(ipRanges.Ranges); ri++ {
        ipRange := ipRanges.Ranges[ri]
        for ip := ipRange.Start; !ip.Equal(ipRange.End); ip = incrementIP(ip) {
            wg.Add(1)
            go func(client *http.Client, ip net.IP) {
                s := time.Now()
                sem.Acquire(ctx, 1)
                defer wg.Done()
                defer sem.Release(1)
                defer count.Add(1)

                _, err := client.Head(fmt.Sprintf("http://%s", ip))
                if err != nil {
                    log.Println(err)
                }

                scannedIp := ScanApiRes{
                    // TODO: Add result of scan
                }
                streamChan <- scannedIp
                fmt.Printf("%v - %v - %v\n", count.Load(), ip.String(), time.Since(s))
            }(client, ip)
        }
    }
    go func(s chan ScanApiRes) {
        for item := range s {
            c.SSEvent("ip-summary", item)
        }
    }(streamChan)
    wg.Wait()
    close(streamChan)
}

终端日志示例:

0 - 10.0.108.72 - 3.354931667s
1 - 10.0.108.16 - 3.678023666s
...
92 - 10.0.108.57 - 10.213736708s
93 - 10.0.108.24 - 10.245989041s
94 - 10.0.108.60 - 11.855088958s

核心问题原因

  1. 信号量获取时机错误:当前在goroutine内部调用sem.Acquire,循环会瞬间创建所有IP对应的goroutine,这些goroutine全部阻塞在信号量获取步骤,占用大量系统资源,导致后续请求的等待时间被持续拉长。
  2. 无缓冲通道阻塞生产端:streamChan是无缓冲通道,当SSE事件发送速度跟不上扫描完成速度时,goroutine会阻塞在streamChan <- scannedIp,无法释放信号量,进一步加剧排队等待的情况。
  3. 未启用请求超时:代码中注释了Timeout配置,单个请求的超时时间由系统默认值决定,部分请求可能长时间挂起,占用并发资源。

解决方案

1. 调整信号量获取逻辑

在创建goroutine之前先获取信号量,严格控制并发数量,避免瞬间创建大量goroutine。

2. 给通道添加缓冲

设置与信号量大小匹配的通道缓冲,避免生产端因消费速度慢而阻塞。

3. 启用http.Client超时

设置合理的连接和请求超时,防止单个请求占用资源过久。

修改后的代码示例

func scanTestHandler(c *gin.Context) {
    var ipRanges ScanApiBody
    decoder := json.NewDecoder(c.Request.Body)

    if err := decoder.Decode(&ipRanges); err != nil {
        http.Error(c.Writer, err.Error(), http.StatusBadRequest)
        c.Writer.WriteHeader(http.StatusBadRequest)
        return
    }
    if len(ipRanges.Ranges) == 0 {
        http.Error(c.Writer, "No Ranges Provided", http.StatusBadRequest)
        c.Writer.WriteHeader(http.StatusBadRequest)
        return
    }
    // 启用超时配置,设置合理的超时时间
    client := &http.Client{
        Transport: &digest.Transport{
            Username: "root",
            Password: "root",
        },
        Timeout: time.Second * 5, // 可根据实际场景调整
    }

    wg := sync.WaitGroup{}
    ctx := context.TODO()
    count := atomic.Int32{}
    sem := semaphore.NewWeighted(200)
    // 设置与信号量大小匹配的缓冲通道
    streamChan := make(chan ScanApiRes, 200)

    for ri := 0; ri < len(ipRanges.Ranges); ri++ {
        ipRange := ipRanges.Ranges[ri]
        for ip := ipRange.Start; !ip.Equal(ipRange.End); ip = incrementIP(ip) {
            // 在创建goroutine前先获取信号量,控制并发数
            if err := sem.Acquire(ctx, 1); err != nil {
                log.Printf("Failed to acquire semaphore: %v", err)
                continue
            }
            wg.Add(1)
            go func(client *http.Client, ip net.IP) {
                defer wg.Done()
                defer sem.Release(1)
                s := time.Now()

                _, err := client.Head(fmt.Sprintf("http://%s", ip))
                if err != nil {
                    log.Println(err)
                }

                scannedIp := ScanApiRes{
                    // 补充扫描结果字段
                }
                streamChan <- scannedIp
                count.Add(1)
                fmt.Printf("%v - %v - %v\n", count.Load(), ip.String(), time.Since(s))
            }(client, ip)
        }
    }
    go func(s chan ScanApiRes) {
        for item := range s {
            c.SSEvent("ip-summary", item)
        }
    }(streamChan)
    wg.Wait()
    close(streamChan)
}

额外优化建议

  • 给sem.Acquire添加超时逻辑,避免因上下文问题导致永久阻塞。
  • 监控SSE发送速度,若消费端确实较慢,可考虑批量发送或调整并发数。
  • 使用context.WithTimeout控制整个扫描过程的总超时时间。

内容的提问来源于stack exchange,提问作者muezz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 04:48:12