天天看點

網絡端口占用問題的綜合調研與解決方案

作者:閃念基因

問題背景

去年底資訊安全團隊進行網絡權限治理,要求所有應用執行個體使用靜态IP,公網通路政策與靜态IP綁定;之後執行個體重新開機時偶現“端口被占用”錯誤。通過分析總結應用日志,共有以下4種錯誤類型,實質都是端口被占用。

網絡端口占用問題的綜合調研與解決方案
// Netty架構
Caused by: java.net.BindException: Address already in use


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


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


// Tomcat
The 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_wait
sk_uid socket --> 所屬使用者ID
reuseport_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/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  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,現任技術架構資深專家

來源-微信公衆号:拍碼場

出處:https://mp.weixin.qq.com/s/n8Bf0KBGIpVcr9VebcESnA

繼續閱讀