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
你好,肖恩,
感谢你的回复!
我已经尝试过原问题中提到的那些注册表设置(但没起作用)。
你认为为 net.Dial 函数以某种方式建立连接池有意义吗?
更多关于Golang负载测试程序在Windows中耗尽端口问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我有一个想法:这可能是因为你每次调用 caller 时都在创建新的 *http.Client。*http.Client 会缓存它们的连接,这样新的请求就不总是需要打开新的连接。
根据你进行压力测试的目的,你可能需要:
-
提前创建
*http.Client并重复使用它们(这将主要给 API 端点施加压力,而不会给网络栈的其他部分带来太大压力) -
在从
caller返回之前调用(*http.Client).CloseIdleConnections(这会将打开和关闭连接的性能包含在你的压力测试中,这可能是也可能不是你想要的)。
这个GitHub问题似乎回答了你的问题:
问题标题: 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状态,导致端口耗尽。以下是解决方案:
- 使用连接池和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
}
- 使用单个共享的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
}
- 使用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
}
- 限制并发连接数并添加延迟:
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更严格,因此需要启用连接复用。

