掘金 后端 ( ) • 2024-05-16 09:55

前言

最近工作遇到了一个疑似端口耗尽的问题,系统整理下这类问题如何排查、如何避免。

基础知识

每个TCP或者UDP连接由一个四元组构成唯一标识:源IP地址、源端口号、目的IP、目的端口号,因此当四元组中其他三个都保持不变时(通常源ip、目的ip、目的端口不变),源端口号可能会耗尽。例如:当一台服务器作为客户端请求某一个外部主机上的服务时,产生的每个连接的源IP地址、目标IP地址、目的端口都不会变,源端口号都是系统自动分配的一个端口号。因此如果对外建立的连接数,超过了本机可分配的端口数,则这台服务器无法再与外部服务建立新的请求。

需要注意的是,一个端口可以同时用于源端口和目的端口,比如服务器可以有一个 TCP 连接到外部服务器的 80 端口,同时本地端口可能是 12345。在这种情况下,四元组可能是这样的:(你的服务器的 IP, 12345,外部服务器的 IP, 80)。同时,你的服务器可以监听端口 12345 来等待来自不同服务器的传入连接。如果有另一个连接发起,其四元组是:(外部服务器的 IP, 端口N, 你的服务器的 IP, 12345),即使端口 12345 正在被上述的连接作为源端口使用,这个新的连接也是允许的,因为它的四元组是唯一的。此时12345被分别用于两个连接的源端口和目的端口。

检测方法

既然一般来说是源端口号耗尽,则观察每个链接,其目的ip、目的端口应该都是一致的。而源ip则一般都是本机ip,变化不大,因此可以忽略。我们只关注目的ip+目的端口即可。

下面是检测脚本,注意下面的脚本只检测了ipv4的连接。

 #!/bin/bash
 ​
 # 获取本地端口分配范围
 LOW_PORT=$(sysctl net.ipv4.ip_local_port_range | awk '{print $3}')
 HIGH_PORT=$(sysctl net.ipv4.ip_local_port_range | awk '{print $4}')
 TOTAL_PORTS=$((HIGH_PORT - LOW_PORT + 1))
 ​
 echo "Local port range is: $LOW_PORT - $HIGH_PORT (Total: $TOTAL_PORTS ports)"
 ​
 # 使用 ss 获取源IP、目的IP、目的端口完全相同的所有连接,可以仅仅看了ipv4
 conn_count=$(ss -tan4 |
             grep -v 'Local' |
             awk -F'[ :]+' '{printf "%-14s %s:%-8s\n", $4, $6, $7}'  |
             sort |
             uniq -c |
             sort -nr |
             awk '$1>1 {printf "%-14s %-22s count:%s\n", $2, $3, $1}' |
             head -10)
 ​
 # 如果没有找到任何重复的连接,输出相应信息并退出
 if [ -z "$conn_count" ]; then
   echo "No duplicate connections found."
   exit 0
 fi
 ​
 echo "Top duplicate connections (Destination IP:Port count):\n$conn_count"

解决办法

连接复用

比如http2,或者使用http2协议的grpc协议,都会链接复用,即同时发起的多次调用,都只会使用一个连接。

限制连接数

golang中,可以通过如下的方式来限制连接数。

 func main() {
     // 新建一个client,限制连接数为1024
     client := http.Client{
         Transport: &http.Transport{
             MaxConnsPerHost: 1024,
         },
     }
     // 修改全局的默认client
     http.DefaultClient.Transport = &http.Transport{
         MaxConnsPerHost: 1024,
     }
 }

因此可以看到,默认的Client,或者一个不经过任何配置的Client,很有可能在大压力下将自己的连接数用尽,要警惕这一点!

设置系统参数

在一个连接四次挥手的时候,client最后会处于TIME_WAIT状态等待一段时间,最后再关闭连接。因此Linux提供了一些配置,可以让处于TIME_WAIT状态的连接被内核快速回收,进而使用其端口号建立新的连接。

下面的都是系统配置,对于这些配置,都可以通过sysctl {变量名}来查看配置,通过修改/etc/sysctl.conf 文件并使用sysctl -p来应用配置,下面不再重复这两个步骤。

net.ipv4.tcp_tw_reuse

/etc/sysctl.conf 文件中添加:

 net.ipv4.tcp_tw_reuse = 1

然后运行以下命令以应用配置:

 sysctl -p

net.ipv4.tcp_tw_recycle

这个配置允许内核回收处于TIME_WAIT状态的连接。但是在较新的 Linux 内核中已被弃用,因为它可能会导致 NAT 问题。修改配置方法和上述一致。

net.ipv4.ip_local_port_range

这个配置决定了可用端口的范围,一般都是10000 65535,一般不太需要调整。

net.ipv4.tcp_tw_timeout

还有一种办法是修改client处于TIME_WAIT状态的时间,即让连接自动尽快释放。不过一般不建议调整这些参数,这里仅仅做知识补全。

net.ipv4.tcp_tw_timeout 参数控制 TIME_WAIT 状态的超时时间的参数,但这个参数并不是所有的linux发行版都支持。

net.ipv4.tcp_max_tw_buckets

该参数设置了系统中 TIME_WAIT 状态的最大连接数。如果超过这个值,TIME_WAIT 连接将立即被清除并输出警告信息。