Golang程序在创建约7600个WebSocket连接后崩溃

Golang程序在创建约7600个WebSocket连接后崩溃 大家好,

我正在开发一个用于创建WebSocket的压力测试脚本。 我不知道为什么,但在调用此函数大约7600次后,WebSocket的创建就会中断。

如果我在Docker容器中运行该脚本,我可以累积创建多达90,000个WebSocket,所以这似乎是一个与宿主机相关的问题。

关于资源方面,在每个Docker实例上运行得都相当好,因此我怀疑是编译器的配置问题,但我对其能力还不太了解(目前正在深入研究)。

我知道WebSocket创建的最大数量受可用端口数(客户端)的限制,但我远未达到这个限制,理论上应该大约是64,500。

有什么想法吗?

func getWSConn(u *userdom.User, roomID string) (*websocket.Conn, error) {
	// Create a custom WebSocket dialer with custom TLS settings
	dialer := &websocket.Dialer{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true, // disable certificate verification for testing
		},
	}

	url := url.URL{
		Scheme: "wss",
		Host:   "localhost:8090",
		Path:   "/ws",
		RawQuery: url.Values{
			"uid":                 []string{u.UID},
		}.Encode(),
	}
	conn, _, err := dialer.Dial(url.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("can't connect to %s %w", url.String(), err)
	}
	return conn, err
}

我运行了pprof以获取一些数据,或许可以与某些编译器值关联起来。以下是7,500个连接时的数据(就在中断之前):

image


更多关于Golang程序在创建约7600个WebSocket连接后崩溃的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

累积创建的WebSocket数量高达90,000个,所以这似乎是一个与主机相关的问题。

关于资源,它在每个Docker实例上都运行得相当好,所以我在考虑编译器的配置问题,但我不太确定。

你的机器上配置了文件描述符的数量吗?

更多关于Golang程序在创建约7600个WebSocket连接后崩溃的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你好 @telo_tade

非常好的问题,我现在也在思考这个……你说到点子上了!

在我的本地电脑上 (macOS)

# 文件描述符限制
ulimit -n
10496

# 进程数限制
ulimit -u
2666

# 软限制 / 硬限制
launchctl limit maxfiles
maxfiles    256   unlimited

我通过 Compose 文件更改了 stress test 容器中的软限制和硬限制值

stress-test:
   image: my-go-stress-test-build
   ulimits:
      nproc: 65535
      nofile:
        soft: 30000
        hard: 40000

镜像最初是从 Scratch 构建的,如下所示 (https://hub.docker.com/_/scratch/),所以我没有 shell 来检查这些数值:

FROM golang:1.21-alpine3.17 AS builder
...
FROM scratch AS final
...
ENTRYPOINT ["/app/bin/stress-test"]

我刚刚修改了 Dockerfile,现在镜像基于 Golang/Alpine 镜像: FROM golang:1.21-alpine3.17 AS final

$ ulimit -n
30000

现在它可以正常工作了,我只是遇到了这个 tls 握手错误。尽管设置了忽略某些安全验证的 TLSConfig,但在 18,000 到 22,000 次连接之后,这个错误会随机出现……不过这是另一个问题了。

tls: handshake message of length 2243106 bytes exceeds maximum of 65536 bytes
TLSConfig: &tls.Config{
			Certificates:       []tls.Certificate{conf.HTTPServer.Certificate},
			InsecureSkipVerify: true, // 警告:生产环境请移除
		},

谢谢你的帮助 @telo_tade

问题很可能与文件描述符限制有关。当在宿主机上运行Go程序时,系统默认的文件描述符限制可能较低,而Docker容器通常具有更高的限制或不同的配置。

可以通过以下命令检查当前的文件描述符限制:

ulimit -n

在Go程序中,可以通过设置dialerNetDial字段来更有效地管理连接,并确保及时关闭不再使用的连接。以下是一个改进的示例,其中包含了连接池和资源清理:

import (
    "context"
    "net"
    "sync"
    "time"
)

type ConnPool struct {
    mu      sync.RWMutex
    conns   map[string]*websocket.Conn
    dialer  *websocket.Dialer
}

func NewConnPool() *ConnPool {
    return &ConnPool{
        conns: make(map[string]*websocket.Conn),
        dialer: &websocket.Dialer{
            TLSClientConfig: &tls.Config{
                InsecureSkipVerify: true,
            },
            HandshakeTimeout: 10 * time.Second,
            NetDial: func(network, addr string) (net.Conn, error) {
                d := net.Dialer{
                    Timeout:   5 * time.Second,
                    KeepAlive: 30 * time.Second,
                }
                return d.Dial(network, addr)
            },
        },
    }
}

func (p *ConnPool) GetWSConn(u *userdom.User, roomID string) (*websocket.Conn, error) {
    key := u.UID + ":" + roomID
    
    p.mu.RLock()
    if conn, ok := p.conns[key]; ok {
        p.mu.RUnlock()
        return conn, nil
    }
    p.mu.RUnlock()
    
    url := url.URL{
        Scheme: "wss",
        Host:   "localhost:8090",
        Path:   "/ws",
        RawQuery: url.Values{
            "uid": []string{u.UID},
        }.Encode(),
    }
    
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    conn, _, err := p.dialer.DialContext(ctx, url.String(), nil)
    if err != nil {
        return nil, fmt.Errorf("can't connect to %s: %w", url.String(), err)
    }
    
    p.mu.Lock()
    p.conns[key] = conn
    p.mu.Unlock()
    
    return conn, nil
}

func (p *ConnPool) CloseAll() {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    for key, conn := range p.conns {
        conn.Close()
        delete(p.conns, key)
    }
}

同时,建议在程序启动时增加系统的文件描述符限制。可以在程序开始时调用:

import "golang.org/x/sys/unix"

func increaseFileLimit() error {
    var rLimit unix.Rlimit
    err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rLimit)
    if err != nil {
        return err
    }
    
    if rLimit.Cur < 65535 {
        rLimit.Cur = 65535
        if rLimit.Cur > rLimit.Max {
            rLimit.Cur = rLimit.Max
        }
        err = unix.Setrlimit(unix.RLIMIT_NOFILE, &rLimit)
        if err != nil {
            return err
        }
    }
    return nil
}

main函数中调用:

func main() {
    if err := increaseFileLimit(); err != nil {
        log.Printf("Warning: failed to increase file limit: %v", err)
    }
    // ... rest of your code
}

另外,确保在压力测试完成后正确关闭所有WebSocket连接,避免资源泄漏。可以使用defer语句或在适当的时机调用连接池的CloseAll方法。

回到顶部