在Liunx服务器上发现有 10倍于 LISTEN 服务的 time_wait 状态;服务并非高并发;日常的连接数也比较少;因此该现象明显异常
回顾下 time_wait 状态处于 TCP 通信的哪个阶段;
在TCP四次挥手过程中;主动断开连接的一方会在发送完最后一个 ACK 包后;等待 2MSL;Maximum Segment Lifetime;的时间;这个阶段就处于 time_wait 状态
time_wait 状态是为了确保;当被动断开连接的一方没有收到最后一个 ACK 包时;会再次发送 FIN 包;如果此时已经建立了新连接;可能被该 FIN 包影响从而导致连接终止
一般在高并发、短连接;单个连接时长超过time_wait时间;的服务端容易出现大量time_wait并存的情况;但在此服务器应不存在
首先查看服务器 2MSL 的设置;是正常范围
同时发现在 LISTEN 端口上同时存在多个处于 time_wait 状态的本机端口;此时确认应该是另一个本机的扫描程序导致的
因为此情况下;TCP 连接的两方都在同一台机器上;无法规避 time_wait 状态的存在;因此首先将探测程序改为长连接
这是之前的探测连接代码
// TCP连接端口
func Ping(host string, timeout int) bool {
_timeout := time.Duration(timeout) * time.Millisecond
if conn, err := net.DialTimeout(;tcp;, host, _timeout); err != nil {
return false
} else {
conn.Close()
return true
}
}
修改后选择 HTTP 长连接的方式;这样可以最大程度规避 time_wait 状态
唯一需要注意的是;HTTP 长连接如果想要复用上一次的连接;哪怕不需要读取数据;也需要调用 ioutil.ReadAll(resp.Body)清空buffer里的数据;否则该连接不会被复用
p := net.TCPAddr{IP: net.ParseIP(addr), Port: port}
// 通过Transport设置最大连接、timeout、
client = &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout, // transport timeout
KeepAlive: time.Duration(IdleConnTimeout) * time.Second,
LocalAddr: &p,
}).DialContext,
IdleConnTimeout: time.Duration(IdleConnTimeout) * time.Second,
ResponseHeaderTimeout: timeout,
},
Timeout: timeout,
}
if req, err := http.NewRequest(;POST;, addr, nil); err == nil {
// 创建请求
q := req.URL.Query()
req.URL.RawQuery = q.Encode()
// 进行请求
if resp, err := client.Do(req); err == nil {
_, e := ioutil.ReadAll(resp.Body) // 必须读取response后才能复用连接
if err = resp.Body.Close(); err != nil {
log.Info(;resp body close err: ;, err, ; ;, e)
}
}
}
;PS;这里直接用 TCP 连接也可达到同样效果;
而且上述探测功能会固定占用和 LISTEN 端口一样数量的端口;如果和动态分配范围内的端口重合会存在问题
查看机器上动态分配端口的范围;一般为32768-61000
所以额外在 Transport 里指定了 LocalAddr;这一步可以绑定固定的端口;将探测端口绑定到61000以上;可以避免端口冲突的问题
如果 time_wait 状态过多影响剩余端口的分配;可以设置预留端口;来保证time_wait状态不会影响其他功能的使用
Linux 的 net.ipv4.ip_local_port_range参数可以规划出一段端口段预留作为服务端口;可以将服务监听的端口以逗号分隔全部添加到ip_local_reserved_ports中;或直接设置一个端口范围段
这样当 Linux 调用 bind(0) 或者 connect 从ip_local_port_range;前面说的32768-61000;中随机选取源端口时;会排除ip_local_reserved_ports中定义的端口;因此就不会出现端口被占用了服务无法启动
vim /etc/sysctl.conf
# 加入下面这行
# net.ipv4.ip_local_reserved_ports=42310,51000-52000
sysctl -p
关于这两个参数的概念理解并不是本篇的重点;大家可以参考SO_REUSEADDR和SO_REUSEPORT作用这篇博文的解释
对于time_wait状态较多;但又无法解决的情况下;比如就是需要服务端主动断开连接or服务端还需要请求下游;;可以通过设置 SO_REUSEADDR和SO_REUSEPORT 参数;让 time_wait 状态不要影响正常的服务
可以通过以下方式来进行设置;
;Golang版本可以用syscall来调用系统方法设置;其他语言也有类似方法可以设置;
import (
;syscall;
;golang.org/x/sys/unix;
)
if fd, err := syscall.Socket(syscall.AF_INET, syscall.O_NONBLOCK|syscall.SOCK_STREAM, 0); err == nil && port > 0 {
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEADDR, 1) // 设置复用端口
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
addr := syscall.SockaddrInet4{Port: port}
copy(addr.Addr[:], net.ParseIP(;0.0.0.0;).To4())
syscall.Bind(fd, &addr)
}
以上是本篇文章的全部内容;下一篇会总结当服务端口频繁被其他随机分配端口占用的情况下;可以如何通过 Golang或其他代码来解决