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个连接时的数据(就在中断之前):

更多关于Golang程序在创建约7600个WebSocket连接后崩溃的实战教程也可以访问 https://www.itying.com/category-94-b0.html
累积创建的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程序中,可以通过设置dialer的NetDial字段来更有效地管理连接,并确保及时关闭不再使用的连接。以下是一个改进的示例,其中包含了连接池和资源清理:
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方法。

