网络端口占用问题的综合调研与解决方案

原创 Randy 拍码场

问题背景

去年底信息安全团队进行网络权限治理,要求所有应用实例使用静态IP,公网访问策略与静态IP绑定;之后实例重启时偶现"端口被占用"错误。通过分析总结应用日志,共有以下4种错误类型,实质都是端口被占用。

// Netty框架Caused by: java.net.BindException: Address already in use

// JettyFailed to start jetty server at port 8080, cause: Address already in use

// Embedded TomcatEmbedded servlet container failed to start. Port 8080 was already in use.

// TomcatThe Tomcat connector configured to listen on port 8080 failed to start. The port ay already be in use or the connector may be misconfigured.

原因分析

学过计算机网络的同学应该知道,网络连接的建立需要通过调用操作系统内核函数来实现;查询linux的官方文档,确定端口被占用的校验发生在系统调用bind()阶段。

端口被占用原因

TCP/IP连接断开后,进入TIME_WAIT状态,等待2MSL(Maximum Segment Lifetime)时间后才会释放网络资源,在此过程中重新打开相同端口会报:bind: address already in use错误。

为什么需要等待2MSL时间

1.可靠的实现TCP全双工连接终止,正确处理关闭连接的四次握手。

2.确保迷路的数据包在网络中消失,防止上一次连接中的包影响新连接(数据包及应答均被丢弃。

不同操作系统中MSL默认值

Windows: 120s

Linux(centos, ubuntu): 60s

Unix: 30s

为什么治理前未发生问题

在采用动态IP时,实例每次重启都会从IP池中选取一个未被使用的IP,新建socket的IP与之前socket的IP不同,属于不同的连接,因此不会报错。

linux源码分析

由于不能使用动态IP,为了寻找解决方案,在对端口被占用逻辑有了大致了解后,我们进一步研读源代码了解端口被占用的详细判断逻辑。

// 系统调用bind()对应的入口函数是__sys_bind()// 端口被占用判断逻辑是inet_bind_conflict函数

static bool inet_bind_conflict(const struct sock *sk, struct sock *sk2, kuid_t sk_uid, bool relax, bool reuseport_cb_ok, bool reuseport_ok){ int bound_dev_if2;

if (sk == sk2) return false;

bound_dev_if2 = READ_ONCE(sk2->sk_bound_dev_if);

if (!sk->sk_bound_dev_if || !bound_dev_if2 || sk->sk_bound_dev_if == bound_dev_if2) { if (sk->sk_reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) { if (!relax || (!reuseport_ok && sk->sk_reuseport && sk2->sk_reuseport && reuseport_cb_ok && (sk2->sk_state == TCP_TIME_WAIT || uid_eq(sk_uid, sock_i_uid(sk2))))) return true; } else if (!reuseport_ok || !sk->sk_reuseport || !sk2->sk_reuseport || !reuseport_cb_ok || (sk2->sk_state != TCP_TIME_WAIT && !uid_eq(sk_uid, sock_i_uid(sk2)))) { return true; } } return false;}

可以看到判断端口占用逻辑用到如下字段:

// 端口被占用判断字段sk_bound_dev_if --> 网卡编号sk_reuse --> 套接字复用sk_reuseport --> 端口复用sk_state --> 当前状态listen还是time_waitsk_uid socket --> 所属用户IDreuseport_cb_ok --> 内核是否支持端口复用

这些字段中网卡编号、用户ID、内核是否支持端口复用均无法修改,能够调整的参数是端口复用和超时时间。

解决方案

鉴于公司所有应用都绑定了静态IP,应用重启时创建的socket与上一个socket必定是同一个应用,此时开启端口复用,不会出现超时报文被其他应用接收的情况,因此开启端口复用(sk_reuseport)是比较合理解决方式。

端口复用开启方式

开启端口复用主要有两种方式:

1. 应用级别:每个业务项目在启动时自行开启端口复用,由于需要修改业务代码,并且不同框架实现方式不同,推广难度大

2. 操作系统层次:直接修改系统内核的 net.ipv4.tcp_tw_reuse=1

其中第2种方式对用户无感,便于集中处理,因此我们对第2种方式进行验证。

Node与Pod系统参数相互隔离

在我们研究如何修改tcp_tw_reuse时,发现Node节点的端口复用开关是开启状态,但是运行在上面的Pod中的端口复用开关却是关闭的,而应用容器使用的端口复用状态是Pod中的值,此时问题变成了如何开启pod中的端口复用开关。

Node上的端口复用开启

Pod中的端口复用关闭

开启Pod中的端口复用

默认情况Pod是无法修改内核相关配置的,经过调研得知Pod需要获取系统级权限(securityContext.privileged=true)才能修改内核参数,但是次权限太大存在安全风险,如果直接在应用容器开通此权限可能影响宿主机的稳定性;最终我们决定增加一个init容器,当系统参数修改成功后再退出,这样既能有足够权限修改内核参数,又不扩大业务容器的权限。测试实例如下:

增加一个busybox的init容器,修改完端口复用开关后退出

apiVersion: apps/v1kind: Deploymentmetadata: name: my-deploymentspec: replicas: 2 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-container image: nginx initContainers: - name: sysctl-modifier image: busybox securityContext: privileged: true command: ["sysctl -w net.ipv4.tcp_tw_reuse=1 && exit"]

使用kubectl部署yaml文件之后,使用kubectl exec -it进入Pod,可以看到pod中的端口被占用功能已经开启。

参考文档

linux源码 https://github.com/torvalds/linux

linux man手册:https://www.man7.org/linux/man-pages/man2/bind.2.html

作者介绍

Randy,现任技术架构资深专家

相关推荐
上海云盾-高防顾问3 分钟前
SCDN如何有效防护网站免受CC攻击?——安全加速网络的实战解析
网络·安全
alden_ygq13 分钟前
nginx 出现大量connect reset by peer
服务器·网络·nginx
xiao--xin36 分钟前
计算机网络笔记(二十三)——4.5IPv6
网络·笔记·计算机网络·ipv6
Excuse_lighttime3 小时前
HTTP / HTTPS 协议
网络·网络协议·http·https
z人间防沉迷k3 小时前
TCP核心机制
网络·网络协议·tcp/ip·http
像风一样_5 小时前
TCP首部格式及三次握手四次挥手
网络·网络协议·tcp/ip
{{uname}}10 小时前
利用WebSocket实现实时通知
网络·spring boot·websocket·网络协议
我不想当小卡拉米11 小时前
【Linux】操作系统入门:冯诺依曼体系结构
linux·开发语言·网络·c++
思科小白白11 小时前
【无标题】
网络·智能路由器
yayaer212 小时前
GOOSE 协议中MAC配置
服务器·网络·goose