前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记time_wait状态引起的端口占用排查

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

作者头像
siri
发布2022-11-18 14:22:17
1.1K0
发布2022-11-18 14:22:17
举报
文章被收录于专栏:siri的开发之路siri的开发之路

文章目录


0. 问题背景

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

服务器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 状态的存在,因此首先将探测程序改为长连接

这是之前的探测连接代码

代码语言:javascript
复制
// 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里的数据,否则该连接不会被复用

代码语言:javascript
复制
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中定义的端口,因此就不会出现端口被占用了服务无法启动

代码语言:javascript
复制
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来调用系统方法设置,其他语言也有类似方法可以设置)

代码语言:javascript
复制
		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或其他代码来解决

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-10-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 0. 问题背景
  • 1. 问题定位
    • time_wait状态
      • 确认原因
      • 2. 解决过程
        • 长连接探测
          • 预留端口
            • SO_REUSEADDR和SO_REUSEPORT
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档