最近新项目上线,自己写了一个测试程序来做压测。
测试刚开始使用小并发的请求没有啥问题,但是加大并发后,发现每次请求到1.6 万笔左右的时候就连接不上服务器了,而监控服务器这边显示 cpu、内存都很正常,所以猜测是客户端这边某些资源到达了瓶颈。
再次发起测试,同时在客户端这边通过 netstat -anpt 命令 查看 tcp网络状态,发现存在大量 time_wait 状态 的连接。问题找到了,因为压测程序这边使用 tcp 短连接,每完成一次请求后就 主动断开 了连接。
我们知道正常调用 close 会经历 tcp 四次挥手,同时主动断开连接的一方会维护 time_wait 状态来保证连接正确的断开,time_wait 状态会持续 2MSL 时间。而每台物理机的端口是有限的,在大并发的情况下,如果端口一直被这些状态的连接占用,就没有端口可以用于新的连接。
问题找到了,怎么解决?
现在问题是客户端这边维持的 time_wait 状态的连接过多,导致端口都被占用无法用于新的请求。
从客户端的角度解决 time_wai 状态过多可以用以下方法:
设置 tcp_tw_reuse 开启端口重用设置 SO_LINGER 选项 跳过 time_wait状态想着我这只是一个测试程序,就不去改系统参数了,因此,选择第二种方法。
TCP 提供的 SO_LINGER 选项可以用来改变调用 close 后默认的行为:
l_onoff 为 0,则该选项关闭,l_linger 的值被忽略;l_onoff 非 0,l_linger 为 0,调用 close 将会直接发送 RST 分组来关闭该连接,不会经历四次挥手过程;l_onoff 非 0,l_linger 非 0,调用 close 将会拖延 l_linger 大小的时间用来将发送缓冲区中残留的数据都发送给对端。golang 对应的结构体在 syscall 包:
type Linger struct {
Onoff int32
Linger int32
}
net 包好似没有提供相关的方法来设置这一选项,因此只能采用 syscall 包提供的原生 tcp 来实现。
1、首先创建 socket,设置 SO_LINGER 选项
syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
// 设置SO_LINGER
linger := syscall.Linger{Onoff: 1, Linger: 0}
syscall.SetsockoptLinger(fd, syscall.SOL_SOCKET, syscall.SO_LINGER, &linger)
2、连接服务器
sa := &syscall.SockaddrInet4{Port: port, Addr: inet_addr(ip)}
syscall.Connect(fd, sa)
3、发送消息(本人是 windows 平台)
func Send(fd syscall.Handle, msg string) error{
var buf syscall.WSABuf
var written uint32
buf.Buf,_ = syscall.BytePtrFromString(msg)
buf.Len = uint32(len(msg))
err := syscall.WSASend(fd, &buf, 1, &written,0, nil, nil)
if err != nil{
log.Printf("write error [%s]\n", err)
return err
}
return nil
}
4、接收消息
func Recv(fd syscall.Handle)([]byte, error){
var (
buffer = make([]byte, 1024*4)
readBytes uint32
flags uint32
buf syscall.WSABuf
)
buf.Buf = &buffer[0]
buf.Len = uint32(len(buffer))
err := syscall.WSARecv(fd, &buf, 1, &readBytes, &flags, nil, nil)
if err != nil {
log.Printf("recv error [%s]\n", err)
return nil, err
}
n := int(readBytes)
if n <= 0 {
return []byte{}, errors.New("recv 0 byte")
}
return buffer[:n], nil
}
5、关闭连接
func Close(fd syscall.Handle){
if err := syscall.Closesocket(fd); err!=nil{
log.Printf("close error [%s]\n", err)
}
log.Printf("close success\n")
}
完整代码如下(注:这里只是一个测试客户端,有需要生产使用的请酌情修改)
package client
import (
"errors"
"log"
"net"
"strconv"
"strings"
"syscall"
"time"
)
func init(){
var wsadata syscall.WSAData
if err := syscall.WSAStartup(MAKEWORD(2, 2), &wsadata); err != nil {
log.Printf("Startup error, [%s]\n", err)
return
}
}
func MAKEWORD(low, high uint8) uint32 {
var ret uint16 = uint16(high)<<8 + uint16(low)
return uint32(ret)
}
func inet_addr(ipaddr string) [4]byte {
var (
ips = strings.Split(ipaddr, ".")
ip [4]uint64
ret [4]byte
)
for i := 0; i < 4; i++ {
ip[i], _ = strconv.ParseUint(ips[i], 10, 8)
}
for i := 0; i < 4; i++ {
ret[i] = byte(ip[i])
}
return ret
}
func Connect(ip string, port int) (syscall.Handle, error){
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
if err != nil{
log.Printf("init socket error [%s]\n", err)
return syscall.InvalidHandle, err
}
// 直接关闭,不经历四次挥手
err = syscall.SetsockoptLinger(fd, syscall.SOL_SOCKET, syscall.SO_LINGER, &syscall.Linger{Onoff: 1, Linger: 0})
if err != nil{
log.Printf("SetsockoptLinger error [%s]\n", err)
return syscall.InvalidHandle, err
}
sa := &syscall.SockaddrInet4{Port: port, Addr: inet_addr(ip)}
err = syscall.Connect(fd, sa)
if err != nil{
log.Printf("connect error [%s]\n", err)
return syscall.InvalidHandle, err
}
return fd, nil
}
func Send(fd syscall.Handle, msg string) error{
var buf syscall.WSABuf
var written uint32
buf.Buf,_ = syscall.BytePtrFromString(msg)
buf.Len = uint32(len(msg))
err := syscall.WSASend(fd, &buf, 1, &written,0, nil, nil)
if err != nil{
log.Printf("write error [%s]\n", err)
return err
}
return nil
}
func Recv(fd syscall.Handle)([]byte, error){
var (
buffer = make([]byte, 1024*4)
readBytes uint32
flags uint32
buf syscall.WSABuf
)
buf.Buf = &buffer[0]
buf.Len = uint32(len(buffer))
err := syscall.WSARecv(fd, &buf, 1, &readBytes, &flags, nil, nil)
if err != nil {
log.Printf("recv error [%s]\n", err)
return nil, err
}
n := int(readBytes)
if n <= 0 {
return []byte{}, errors.New("recv 0 byte")
}
return buffer[:n], nil
}
func Close(fd syscall.Handle){
if err := syscall.Closesocket(fd); err!=nil{
log.Printf("close error [%s]\n", err)
}
log.Printf("close success\n")
}
目前网上对于 golang 原生 tcp 的介绍还是比较少的, 好在函数定义和 C 标准库没有太大区别,只要去源码包找到对应的方法即可。当然,没有写过 C 代码的童鞋可能会有点头大,所以本人也是把代码都提供出来给家参考,希望对大家有帮助哈。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)