Golang负载测试程序在Windows中耗尽端口问题

Golang负载测试程序在Windows中耗尽端口问题 我编写了一个对Web服务进行压力测试的负载测试程序,在Linux系统上运行良好。

然而,在Windows系统上运行时,net.Dial 会耗尽端口并出现以下错误:

Post http://localhost:9090: dial tcp [::1]:9090: connectex: Only one usage of each socket address (protocol/network address/port) is normally permitted.

我已尝试按照这篇文章所述,修改注册表以增加可用端口数量并减少等待时间。

有没有人知道是否有办法复用端口,以避免Windows系统耗尽端口(或者是否有其他方法可以实现这个负载测试)?

我的代码如下:

package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

type args struct {
	url     string
	body    []byte
	threads int
}

func main() {
	// 06-hammer url body threads
	if len(os.Args) != 4 {
		showUsage()
		return
	}

	var arg args
	arg.url = os.Args[1]
	body := os.Args[2]
	threads, err := strconv.Atoi(os.Args[3])
	if err != nil || len(arg.url) < 1 || len(body) < 1 {
		showUsage()
		return
	}
	arg.threads = threads
	arg.body, err = os.ReadFile(body)
	if err != nil {
		fmt.Printf("error reading '%v': %v \n", body, err)
		return
	}
	call(arg)
}

func showUsage() {
	fmt.Println("Usage:")
	fmt.Println("06-hammer url body threads")
	fmt.Println("Example:")
	fmt.Println(`06-hammer http://localhost:9090 "./body.json" 10`)
}

func call(arg args) {
	start := time.Now()
	var succ int
	var fail int
	var attempt int

	for {
		c := parallel(arg)

		attempt++
		for i := 0; i < arg.threads; i++ {
			result := <-c
			if result {
				succ++
			} else {
				fail++
			}
		}

		if time.Since(start) >= time.Duration(time.Second*5) {
			log.Printf("success: %v fail: %v elapsed: %v, avg: %v \n", succ, fail, time.Since(start), time.Duration(int64(time.Since(start))/int64(attempt)))
			start = time.Now()
			succ = 0
			fail = 0
			attempt = 0
		}
	}
}

func parallel(arg args) chan bool {
	out := make(chan bool)

	for i := 0; i < arg.threads; i++ {
		go caller(arg, out)
	}

	return out
}

func caller(arg args, c chan bool) {
	var defaultTransport http.RoundTripper = &http.Transport{Proxy: nil, DisableKeepAlives: true}
	client := &http.Client{Transport: defaultTransport}

	r, err := client.Post(arg.url, "application/json", bytes.NewReader(arg.body))

	if err != nil {
		c <- false
		return
	}
	defer r.Body.Close()

	_, err = ioutil.ReadAll(r.Body)
	if err != nil {
		c <- false
		return
	}

	c <- true
}


更多关于Golang负载测试程序在Windows中耗尽端口问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

你好,肖恩,

感谢你的回复!

我已经尝试过原问题中提到的那些注册表设置(但没起作用)。

你认为为 net.Dial 函数以某种方式建立连接池有意义吗?

更多关于Golang负载测试程序在Windows中耗尽端口问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我有一个想法:这可能是因为你每次调用 caller 时都在创建新的 *http.Client*http.Client 会缓存它们的连接,这样新的请求就不总是需要打开新的连接。

根据你进行压力测试的目的,你可能需要:

  1. 提前创建 *http.Client 并重复使用它们(这将主要给 API 端点施加压力,而不会给网络栈的其他部分带来太大压力)

  2. 在从 caller 返回之前调用 (*http.Client).CloseIdleConnections(这会将打开和关闭连接的性能包含在你的压力测试中,这可能是也可能不是你想要的)。

这个GitHub问题似乎回答了你的问题:

github.com/go-gitea/gitea

问题标题: connectex: Only one usage of each socket address (protocol/network address/port) is normally permitted

描述:

  • Gitea版本(或提交引用):1.8.3
  • Git版本:2.22.0.windows.1
  • 操作系统:Windows server 2012 R2
  • 数据库(使用 [x]):
    • [ ] PostgreSQL
    • [x] MySQL
    • [ ] MSSQL
    • [ ] SQLite
  • 能否在 https://try.gitea.io 重现此错误:
    • [ ] 是(提供示例URL)
    • [x] 否
    • [ ] 不相关
  • 日志要点:

问题描述

Gitea每隔200毫秒就在日志文件中写入此消息:

2019/07/10 07:32:18 [...s/context/context.go:238 func1()] [E] UserSignIn: dial tcp 127.0.0.1:3306: connectex: Only one usage of each socket address (protocol/network address/port) is normally permitted.
2019/07/10 07:32:18 [...s/context/context.go:238 func1()] [E] GetAccessTokenBySha: dial tcp 127.0.0.1:3306: connectex: Only one usage of each socket address (protocol/network address/port) is normally permitted.
20

有什么办法可以解决这个问题?

答案似乎是将MaxUserPort注册表项设置为65535,并将TcpTimedWaitDelay项设置为30:可修改以提高网络性能的设置 - BizTalk Server | Microsoft Docs

这个问题是由于Windows的TCP连接处理方式与Linux不同导致的。在Windows上,当连接关闭后,端口会进入TIME_WAIT状态,导致端口耗尽。以下是解决方案:

  1. 使用连接池和KeepAlive
func caller(arg args, c chan bool) {
    // 创建可复用的HTTP客户端
    tr := &http.Transport{
        MaxIdleConns:        arg.threads,
        MaxIdleConnsPerHost: arg.threads,
        IdleConnTimeout:     30 * time.Second,
        DisableKeepAlives:   false, // 启用KeepAlive
    }
    
    client := &http.Client{
        Transport: tr,
        Timeout:   10 * time.Second,
    }

    r, err := client.Post(arg.url, "application/json", bytes.NewReader(arg.body))
    
    if err != nil {
        c <- false
        return
    }
    defer r.Body.Close()

    _, err = ioutil.ReadAll(r.Body)
    if err != nil {
        c <- false
        return
    }

    c <- true
}
  1. 使用单个共享的HTTP客户端
var client *http.Client
var clientOnce sync.Once

func getClient(threads int) *http.Client {
    clientOnce.Do(func() {
        tr := &http.Transport{
            MaxIdleConns:        threads,
            MaxIdleConnsPerHost: threads,
            IdleConnTimeout:     30 * time.Second,
            DisableKeepAlives:   false,
        }
        client = &http.Client{
            Transport: tr,
            Timeout:   10 * time.Second,
        }
    })
    return client
}

func caller(arg args, c chan bool) {
    client := getClient(arg.threads)
    
    r, err := client.Post(arg.url, "application/json", bytes.NewReader(arg.body))
    
    if err != nil {
        c <- false
        return
    }
    defer r.Body.Close()

    _, err = ioutil.ReadAll(r.Body)
    if err != nil {
        c <- false
        return
    }

    c <- true
}
  1. 使用SO_REUSEADDR选项(需要syscall):
import (
    "syscall"
    "golang.org/x/sys/windows"
)

func dialerWithReuseAddr(network, addr string) (net.Conn, error) {
    dialer := &net.Dialer{
        Control: func(network, address string, c syscall.RawConn) error {
            var operr error
            err := c.Control(func(fd uintptr) {
                operr = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
            })
            if err != nil {
                return err
            }
            return operr
        },
    }
    return dialer.Dial(network, addr)
}

func caller(arg args, c chan bool) {
    tr := &http.Transport{
        DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
            return dialerWithReuseAddr(network, addr)
        },
        MaxIdleConns:        arg.threads,
        MaxIdleConnsPerHost: arg.threads,
        DisableKeepAlives:   false,
    }
    
    client := &http.Client{Transport: tr}
    
    r, err := client.Post(arg.url, "application/json", bytes.NewReader(arg.body))
    
    if err != nil {
        c <- false
        return
    }
    defer r.Body.Close()

    _, err = ioutil.ReadAll(r.Body)
    if err != nil {
        c <- false
        return
    }

    c <- true
}
  1. 限制并发连接数并添加延迟
func parallel(arg args) chan bool {
    out := make(chan bool)
    sem := make(chan struct{}, 1000) // 限制最大并发连接数
    
    for i := 0; i < arg.threads; i++ {
        go func() {
            sem <- struct{}{}
            caller(arg, out)
            <-sem
        }()
    }
    
    return out
}

func caller(arg args, c chan bool) {
    time.Sleep(time.Millisecond * 10) // 添加微小延迟
    
    tr := &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 100,
        DisableKeepAlives:   false,
    }
    
    client := &http.Client{Transport: tr}
    
    r, err := client.Post(arg.url, "application/json", bytes.NewReader(arg.body))
    
    if err != nil {
        c <- false
        return
    }
    defer r.Body.Close()

    _, err = ioutil.ReadAll(r.Body)
    if err != nil {
        c <- false
        return
    }

    c <- true
}

主要问题是你在代码中设置了DisableKeepAlives: true,这会导致每个请求都创建新的连接而不复用。Windows对TIME_WAIT状态的处理比Linux更严格,因此需要启用连接复用。

回到顶部