天天看點

記一次容器環境下出現 Address not available

作者:技術聯盟總壇

鄭明泉、餘凱 阿裡雲雲原生 2023-07-16 19:08 發表于浙江

記一次容器環境下出現 Address not available

困惑的源位址

Cloud Native

pod 建立後一段時間一直是正常運作,突然有一天發現沒有新的連接配接建立了,業務上是通過 pod A 通路 svc B 的 svc name 的方式,進入 pod 手動去 wget 一下,發現報錯了 Address not available,為何會報錯這個呢?

大概示例圖如下:

記一次容器環境下出現 Address not available

為什麼會出現 Address not available,是什麼位址不可用,查了很多資料,根據 POSIX(Portable Operating System Interface for UNIX)标準的錯誤定義中找到了相關的定義,同樣說的還不是很清楚。

錯誤代碼參考連接配接:[errno.3[1]]
EADDRNOTAVAIL               

    Address not available (POSIX.1-2001).           

容易被忽視的核心參數

Cloud Native

通過 netstat -an 檢視到連接配接 svc 的位址,其中 estab 狀态的連接配接數,已經到達了可用的随機端口數量門檻值,無法在建立連接配接了。

記一次容器環境下出現 Address not available

最後通過修改了核心參數随機端口 net.ipv4.ip_local_port_range 端口範圍才得以解決的。

我們可以知道 Linux 的核心定義的随機端口 32768 ~ 60999,可能在業務設計場景中,比較容易被忽略的,我們都知道,每一個 TCP 連接配接都是由四元組(源 IP,源端口,目的 IP,目的端口)構成的,隻要四元組中其中一個元組發生了變化,就可以建立一個 TCP 連接配接的。當一個 POD 要通路一個固定的目的 IP + 目的端口的時候,那麼每一個 TCP 連接配接的變量就隻剩下源端口是随機的了,是以如果在需求就是需要建立大量長連接配接的話,要麼就調大核心随機端口,要麼就調整業務。

相關核心參考連接配接:[ip-sysctl.txt[2]]
ip_local_port_range - 2 INTEGERS
  Defines the local port range that is used by TCP and UDP to
  choose the local port. The first number is the first, the
  second the last local port number.
  If possible, it is better these numbers have different parity
  (one even and one odd value).
  Must be greater than or equal to ip_unprivileged_port_start.
  The default values are 32768 and 60999 respectively.           

同樣的問題還可能出現什麼類型的報錯呢?

Cloud Native

手動調小了 net.ipv4.ip_local_port_range,之後進行複現。

同樣的問題,分别嘗試了 curl,nc,wget 指令,報錯都不一樣,這就犯難了。

難道就不能統一一下嗎?

  • curl: (7) Couldn't connect to server
  • nc: bind: Address in use
  • wget: can't connect to remote host (1.1.1.1): Address not available
記一次容器環境下出現 Address not available

那麼就通過 strace 指令程序分析一下看看,跟蹤指定系統調用名稱 它們都會建立 socket(), 然後發現 wget/curl 指令是通過 connect() 函數,而 nc 指令先是是通過 bind() 函數調用, 如果報錯就不會繼續調用 connect() 函數了。

記一次容器環境下出現 Address not available

如圖,通過對 B/S 架構的分析如下,connect() 是在用戶端建立 socket 後建立的。

記一次容器環境下出現 Address not available

引發思考

Cloud Native

為什麼 wget/curl 同樣調用的是 connect() 函數報錯的,為何報錯還是不一樣的?

  • 每一個用戶端程式都會有自定義的 errorcode,在同樣的 connect() 函數報錯後 ,wget 是直接輸出了 POSIX 标準的錯誤定義 Address not available,而 curl 會輸出自己的定義錯誤碼和對應的提示資訊 curl: (7) Couldn't connect to server,錯誤代碼是 7,curl 的報錯定義在 lib/strerror.c。
記一次容器環境下出現 Address not available
記一次容器環境下出現 Address not available

為什麼 connect() 函數和 bind() 函數報錯不一樣?

  • 函數不同,錯誤的定義也就不同,從 POSIX 标準的錯誤定義都能找到。
EADDRINUSE               

    Address already in use (POSIX.1-2001).        

EADDRNOTAVAIL               

    Address not available (POSIX.1-2001).           

是不是所有情況下都是這樣輸出呢?

Cloud Native

那麼直接找了一台 Centos7.9 的系統,安裝 curl 、wget、 nc 等工具,同樣改小端口範圍的情況下會出現如下報錯 Cannot assign requested address,從這裡可以得知某些鏡像(alpine、busybox) 裡,使用相同的指令工具對相同的情況下報錯會不同。因為這些鏡像裡可能為了縮小整個鏡像大小,對于一些基礎指令都會選擇 busybox 工具箱(上面的 wget 和 nc 就來自于 busybox 工具箱裡的,參考 busybox 文檔:Busybox Command Help[3])來使用,是以就造成在問題定位方面困擾了。

Linux 系統中用于包含與錯誤碼相關的定義:/usr/include/asm-generic/errno.h
記一次容器環境下出現 Address not available
#define  EADDRNOTAVAIL  99  /* Cannot assign requested address */           

容器環境下,端口配置最佳實踐

Cloud Native

可修改範圍

理論上來是 0~65535 都能使用, 但是 0~1023 是特權端口,已經預留給一下标準服務,如 HTTP:80,SSH:22 等,隻能特權使用者使用,同時也避免未授權的使用者通過流量特征攻擊等是以建議端口調大的話可以将随機端口範圍限制在 1024-65535 之間。

如何正确配置 Pod 源端口

普通 Pod 源端口修改方法

從 kubernetes 社群得知可以通過安全上下文修改 securityContext[4],還有可以通過 initContainers 容器給特權模式 mount -o remount rw /proc/sys 的方式修改,此修改方式隻會在 pod 的網絡命名空間中生效。

  • securityContext
...

securityContext:

  sysctls:       

    - name: net.ipv4.ip_local_port_range           

      value: 1024 65535           
  • initContainers
initContainers:

        - command:

            - /bin/sh

            - '-c'

            - |

              sysctl -w net.core.somaxconn=65535

              sysctl -w net.ipv4.ip_local_port_range="1024 65535"

          securityContext:

            privileged: true

...           

hostnetwork 模式 pod 修改注意事項

1.22+ 叢集以上就不建議修改 net.ipv4.ip_local_port_range,因為這會和 ServiceNodePortRange 産生沖突。

Kubernetes 的 ServiceNodePortRange 預設是 30000~32767,Kubernetes 1.22 及以後的版本,去除了 kube-proxy 監聽 NodePort 的邏輯,如果有監聽的話,應用程式在選用随機端口的時候,會避開這些監聽中的端口。如果 net.ipv4.ip_local_port_range 的範圍和 ServiceNodePortRange 存在重疊,由于去掉了監聽 NodePort 的邏輯,應用程式在選用随機端口的時候就可能選中重疊部分,比如 30000~32767,在當 NodePort 與核心 net.ipv4.ip_local_port_range 範圍有沖突的情況下,可能會導緻偶發的 TCP 無法連接配接的情況,可能導緻健康檢查失敗、業務通路異常等問題。更多資訊,請參見 Kubernetes 社群 PR[5]。

大量建立 svc 的時候減少建立監聽的步驟隻是送出 ipvs/iptables 規則,這樣可以優化連接配接性能 。另一個就解決某些場景下出現大量的 CLOSE_WAIT 占用 TCP 連接配接等問題。在 1.22 版本之後就去掉了 PortOpener 邏輯。

kubernetes/pkg/proxy/iptables/proxier.go

Line 1304 in f98f27b[6]

1304        proxier.openPort(lp, replacementPortsMap)           
記一次容器環境下出現 Address not available

具體是如何沖突的呢?

測試環境是 k8s 1.22.10,kube-proxy 網絡模式 ipvs。以 kubelet 健康檢查為例,調整了節點的核心參數 net.ipv4.ip_local_port_range 為1 024~65535。

記一次容器環境下出現 Address not available

部署 tcpdump 抓包,抓到有健康檢查失敗的事件後,停止抓包。

看到 kubelet 是用節點 IP(192.168.66.27)+随機端口 32582 向 pod 發起了 TCP 握手 podIP(192.168.66.65)+80,但是 pod 在 TCP 握手時回 SYN ACK 給 kubelet 的時候,目标端口是 32582,卻一直在重傳。因為這個随機端口剛好是某一個服務的nodeport,是以優先被 IPVS 攔截給規則後端的服務,但這個後端服務 (192.168.66.9) 并沒有發起和 podIP(192.168.66.65)TCP 建連,是以後端服務 (192.168.66.9) 直接是丢棄的。那麼 kubelet 就不會收到 SYN ACK 回應,TCP 無法建聯,是以導緻健康檢查失敗。

這個封包看 kubelet 發起 TCP 握手,pod 回 syn ack 的時候一直重傳。

記一次容器環境下出現 Address not available

實際是發送到了 32582 這個 svc 的後端 pod 了,直接是丢棄。

記一次容器環境下出現 Address not available
記一次容器環境下出現 Address not available

增加前置判斷

是以 hostnework 可以加上一個判斷,通過 initContainers 容器修改的時候,如果 podIP 和 hostIP 不相等才修改 net.ipv4.ip_local_port_range 參數,避免誤操作導緻修改節點的核心參數。

initContainers:
        - command:
            - /bin/sh
            - '-c'
            - |
              if [ "$POD_IP" != "$HOST_IP" ]; then
              mount -o remount rw /proc/sys
              sysctl -w net.ipv4.ip_local_port_range="1024 65535"
              fi
          env:
            - name: POD_IP
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: status.podIP
            - name: HOST_IP
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: status.hostIP
          securityContext:
            privileged: true
...           

如何正确配置 NodePort 範圍

在 Kubernetes中,APIServer 提供了 ServiceNodePortRange 參數(指令行參數 --service-node-port-range),該參數是用于限制 NodePort 或 LoadBalancer 類型的 Service 在節點上所監聽的 NodePort 端口範圍,該參數預設值為 30000~32767。在 ACK Pro 叢集中,您可以通過自定義 Pro 叢集的管控面參數修改該端口範圍。具體操作,請參見自定義 ACK Pro 叢集的管控面參數[7]。

記一次容器環境下出現 Address not available
記一次容器環境下出現 Address not available
  • 在修改 NodePort 端口範圍時必須十分謹慎。務必保證 NodePort 端口範圍與叢集節點上 Linux 核心提供的 net.ipv4.ip_local_port_range 參數中的端口範圍不沖突。該核心參數 ip_local_port_range 控制了 Linux 系統上任意應用程式可以使用的本地端口号範圍。ip_local_port_range 的預設值為 32768~60999,Nodeport 預設值為 30000~32767。
  • ACK 叢集在預設配置情況下,ServiceNodePortRange 參數和 ip_local_port_range 參數不會産生沖突。如果您此前為了提升端口數量限制調整了這兩個參數中任意一個,導緻兩者範圍出現重合,則可能會産生節點上的偶發網絡異常,嚴重時會導緻業務健康檢查失敗、叢集節點離線等。建議您恢複預設值或同時調整兩個端口範圍到完全不重合。
  • 調整端口範圍後,叢集中可能存在部分 NodePort 或 LoadBalancer 類型的 Service 仍在使用 ip_local_port_range 參數端口範圍内的端口作為 NodePort。此時您需要對這部分 Service 進行重新配置以避免沖突,可通過 kubectl edit <service-name> 的方式直接将 spec.ports.nodePort 字段的值更改為未被占用的 NodePort。

相關連結:

[1] errno.3

[2] ip-sysctl.txt

[3] Busybox Command Help

[4] securityContext

[5] Kubernetes社群PR

[6] f98f27b

[7] 自定義ACK Pro叢集的管控面參數

繼續閱讀