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
核心问题原因
- 信号量获取时机错误:当前在goroutine内部调用
sem.Acquire,循环会瞬间创建所有IP对应的goroutine,这些goroutine全部阻塞在信号量获取步骤,占用大量系统资源,导致后续请求的等待时间被持续拉长。 - 无缓冲通道阻塞生产端:
streamChan是无缓冲通道,当SSE事件发送速度跟不上扫描完成速度时,goroutine会阻塞在streamChan <- scannedIp,无法释放信号量,进一步加剧排队等待的情况。 - 未启用请求超时:代码中注释了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
相关产品推荐
相关产品推荐

