本文已發布超過一年。較舊的文章可能包含過時的內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
kube-proxy 細微之處:偵錯間歇性連線重設
我最近遇到一個導致間歇性連線重設的錯誤。經過一番挖掘,我發現它是由幾個不同網路子系統的微妙組合引起的。它幫助我更好地理解 Kubernetes 網路,並且我認為值得與對相同主題感興趣的更廣泛受眾分享。
症狀
我們收到使用者報告,聲稱他們在使用 ClusterIP 類型的 Kubernetes 服務向在同一個叢集中運行的 Pod 提供大型檔案時,遇到連線重設。對叢集的初步偵錯沒有產生任何有趣的結果:網路連線良好,下載檔案也沒有遇到任何問題。但是,當我們跨多個用戶端並行運行工作負載時,我們能夠重現問題。更令人費解的是,當使用沒有 Kubernetes 的 VM 運行工作負載時,無法重現該問題。這個問題可以透過一個簡單的應用程式輕鬆重現,顯然與 Kubernetes 網路有關,但究竟是什麼?
Kubernetes 網路基礎知識
在深入研究這個問題之前,讓我們稍微談談 Kubernetes 網路的一些基礎知識,因為 Kubernetes 處理來自 Pod 的網路流量的方式,會根據不同的目的地而有很大的不同。
Pod 到 Pod
在 Kubernetes 中,每個 Pod 都有自己的 IP 位址。這樣做的好處是,在 Pod 內部運行的應用程式可以使用其標準連接埠,而不是重新對應到不同的隨機連接埠。Pod 彼此之間具有 L3 連線能力。它們可以互相 ping,並互相發送 TCP 或 UDP 封包。CNI 是為在不同主機上運行的容器解決這個問題的標準。有大量的不同外掛程式支援 CNI。
Pod 到外部
對於從 Pod 到外部位址的流量,Kubernetes 僅使用 SNAT。它的作用是將 Pod 的內部來源 IP:連接埠替換為主機的 IP:連接埠。當回程封包返回主機時,它會將 Pod 的 IP:連接埠重寫為目的地,並將其發送回原始 Pod。整個過程對原始 Pod 是透明的,原始 Pod 完全不知道位址轉譯。
Pod 到服務
Pod 是會消亡的。大多數情況下,人們想要的是可靠的服務。否則,它幾乎毫無用處。因此,Kubernetes 有一個稱為「服務」的概念,它只是一個位於 Pod 前面的 L4 負載平衡器。服務有幾種不同的類型。最基本的類型稱為 ClusterIP。對於這種服務類型,它有一個唯一的 VIP 位址,該位址僅可在叢集內部路由。
Kubernetes 中實現此功能的組件稱為 kube-proxy。它位於每個節點上,並編寫複雜的 iptables 規則,以在 Pod 和服務之間執行各種過濾和 NAT。如果您前往 Kubernetes 節點並鍵入 iptables-save
,您將看到由 Kubernetes 或其他程式插入的規則。最重要的鏈是 KUBE-SERVICES
、KUBE-SVC-*
和 KUBE-SEP-*
。
KUBE-SERVICES
是服務封包的入口點。它的作用是比對目的地 IP:連接埠,並將封包分派到對應的KUBE-SVC-*
鏈。KUBE-SVC-*
鏈充當負載平衡器,並將封包平均分配到KUBE-SEP-*
鏈。每個KUBE-SVC-*
的KUBE-SEP-*
鏈數量與其後端的端點數量相同。KUBE-SEP-*
鏈代表服務端點。它僅執行 DNAT,將服務 IP:連接埠替換為 Pod 的端點 IP:連接埠。
對於 DNAT,conntrack 會啟動並使用狀態機追蹤連線狀態。之所以需要狀態,是因為它需要記住它更改為的目的地位址,並在回程封包返回時將其改回。Iptables 也可以依賴 conntrack 狀態 (ctstate) 來決定封包的目標。以下 4 個 conntrack 狀態尤其重要
- NEW:conntrack 對於此封包一無所知,這發生在收到 SYN 封包時。
- ESTABLISHED:conntrack 知道封包屬於已建立的連線,這發生在握手完成後。
- RELATED:封包不屬於任何連線,但它隸屬於另一個連線,這對於 FTP 等協定尤其有用。
- INVALID:封包有問題,conntrack 不知道如何處理它。此狀態在此 Kubernetes 問題中扮演核心角色。
以下是 Pod 和服務之間 TCP 連線如何運作的圖表。事件順序如下
- 來自左側的用戶端 Pod 向服務發送封包:192.168.0.2:80
- 封包正在通過用戶端節點中的 iptables 規則,並且目的地已更改為 Pod IP,10.0.1.2:80
- 伺服器 Pod 處理封包並發送回目的地為 10.0.0.2 的封包
- 封包正在返回用戶端節點,conntrack 識別封包並將來源位址重寫回 192.169.0.2:80
- 用戶端 Pod 接收回應封包

良好的封包流程
是什麼導致連線重設?
背景知識足夠了,那麼真正出錯並導致意外連線重設的原因是什麼?
如下圖所示,問題出在封包 3。當 conntrack 無法識別回程封包時,並將其標記為 INVALID。最常見的原因包括:conntrack 無法追蹤連線,因為它超出容量,封包本身超出 TCP 視窗等。對於那些被 conntrack 標記為 INVALID 狀態的封包,我們沒有 iptables 規則來丟棄它,因此它將被轉發到用戶端 Pod,而來源 IP 位址未重寫(如封包 4 所示)!用戶端 Pod 無法識別此封包,因為它具有不同的來源 IP,即 Pod IP,而不是服務 IP。因此,用戶端 Pod 會說:「等一下,我不記得曾經與這個 IP 建立過連線,為什麼這個傢伙一直向我發送這個封包?」基本上,用戶端所做的只是向伺服器 Pod IP 發送 RST 封包,即封包 5。不幸的是,這是一個完全合法的 Pod 到 Pod 封包,可以傳遞到伺服器 Pod。伺服器 Pod 不知道用戶端發生的所有位址轉譯。從它的角度來看,封包 5 是一個完全合法的封包,就像封包 2 和 3 一樣。伺服器 Pod 只知道:「好吧,用戶端 Pod 不想與我交談,所以讓我們關閉連線!」砰!當然,為了讓所有這些發生,RST 封包也必須是合法的,具有正確的 TCP 序號等。但當它發生時,雙方都同意關閉連線。

連線重設封包流程
如何解決它?
一旦我們了解根本原因,修復並不難。至少有 2 種方法可以解決它。
- 讓 conntrack 在封包上更寬鬆,不要將封包標記為 INVALID。在 Linux 中,您可以透過
echo 1 > /proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_be_liberal
來執行此操作。 - 特別新增一個 iptables 規則來丟棄標記為 INVALID 的封包,這樣它就不會到達用戶端 Pod 並造成損害。
修復程式已在 v1.15+ 中提供。但是,對於受此錯誤影響的使用者,有一種方法可以透過在您的叢集中套用以下規則來緩解問題。
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: startup-script
labels:
app: startup-script
spec:
template:
metadata:
labels:
app: startup-script
spec:
hostPID: true
containers:
- name: startup-script
image: gcr.io/google-containers/startup-script:v1
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
env:
- name: STARTUP_SCRIPT
value: |
#! /bin/bash
echo 1 > /proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_be_liberal
echo done
摘要
顯然,這個錯誤幾乎一直存在。我很驚訝直到最近才被注意到。我相信原因可能是:(1) 這在擁塞的伺服器提供大型酬載時更容易發生,這可能不是常見的使用案例;(2) 應用程式層處理重試以容忍這種重設。無論如何,儘管 Kubernetes 的成長速度有多快,它仍然是一個年輕的專案。沒有其他秘訣,只有密切傾聽客戶的回饋、不將任何事情視為理所當然而是深入挖掘,我們才能使其成為運行應用程式的最佳平台。