记time_wait状态引起的端口占用排查


0. 问题背景

在Liunx服务器上发现有 10倍于 LISTEN 服务的 time_wait 状态,服务并非高并发,日常的连接数也比较少,因此该现象明显异常
服务器time_wait状态

1. 问题定位

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 状态的本机端口,此时确认应该是另一个本机的扫描程序导致的

2. 解决过程

长连接探测

因为此情况下,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

关于这两个参数的概念理解并不是本篇的重点,大家可以参考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或其他代码来解决

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值