追蹤 Kubernetes 中的網路流量

語言: CN / TW / HK

譯者注:

這篇文章很全面的羅列出了 Kubernetes 中涉及的網路知識,從 Linux 核心的網路內容,到容器、Kubernetes,一一進行了詳細的說明。

​文章篇幅有點長,不得不說,網路是很複雜很麻煩的一層,但恰恰這層多年來變化不大。希望翻譯的內容對大家能有所幫助,有誤的地方,也歡迎大家指正。

本文翻譯獲得 Learnk8s 的授權,原文 Tracing the path of network traffic in Kubernetes 作者 Kristijan Mitevski。

TL;DR: 本文將代理了解 Kubernetes 叢集內外的資料流轉。從最初的 Web 請求開始,一直到託管應用程式的容器。

目錄

  • Linux 網路名稱空間如果在 pod 中工作
  • Pause 容器建立 Pod 中的網路名稱空間
  • 為 Pod 分配了 IP 地址
  • 檢查叢集中 pod 到 pod 的流量
  • Pod 名稱空間連線到乙太網橋接器
  • 跟蹤同一節點上 pod 間的流量
  • 跟蹤不同節點上 pod 間的通訊
  • 檢查 pod 到服務的流量
  • 使用 Netfilter 和 Iptables 攔截和重寫流量

Kubernetes 網路要求

在深入瞭解 Kubernetes 中的資料流轉之前,讓我們先澄清下 Kubernetes 網路的要求。

Kubernetes 網路模型定義了一套基本規則:

  • 叢集中的 pod 應該能夠與任何其他 pod 自由通訊 ,而無需使用網路地址轉換(NAT)。
  • 在不使用 NAT 的情況下, 叢集節點上執行的任意程式都應該能夠與同一節點上的任意 pod 通訊
  • 每個 pod 都有自己的 IP 地址 (IP-per Pod),其他 pod 都可以使用同一個地址進行訪問。

這些要求不會將實現限制在單一方案上。

相反,他們概括了叢集網路的特性。

在滿足這些限制時,必須解決如下 挑戰

  1. 如何保證同一 pod 中的容器間的訪問就像在同一主機上一樣?
  2. Pod 能否訪問叢集中的其他 pod?
  3. Pod 能否訪問服務(service)?以及服務可以負載均衡請求嗎?
  4. Pod 可以接收來自叢集外的流量嗎?

本文將專注於前三點,從 pod 內部網路或者容器間的通訊說起。

Linux 網路名稱空間如果在 pod 中工作

我們想象下,有一個承載應用程式的主容器和另一個與它一起執行的容器。

在示例 Pod 中有一個 Nginx 容器和 busybox 容器:

apiVersion: v1
kind: Pod
metadata:
  name: multi-container-pod
spec:
  containers:
    - name: container-1
      image: busybox
      command: ['/bin/sh', '-c', 'sleep 1d']
    - name: container-2
      image: nginx

在部署時,會出現如下情況:

  1. Pod 在節點上得到 自己的網路名稱空間
  2. Pod 分配到一個 IP 地址 ,兩個容器間共享埠。
  3. 兩個容器共享同一個網路名稱空間 ,在本地互相可見。

網路配置在後臺很快完成。

然後,我們退後一步,是這理解 為什麼 上面是容器執行所必須的。

在 Linux 中,網路名稱空間是獨立的、隔離的邏輯空間。

可以將網路名稱空間堪稱將物理網路介面分割成更小的獨立部分。

每部分都可以單獨配置,並使用自己的網路規則和資源。

這些可以包括防火牆規則、介面(虛擬或物理)、路由和其他所有與網路相關的內容。

  1. 物理介面持有根名稱空間。

  2. 可以使用 Linux 網路名稱空間建立隔離的網路。每個網路都是獨立的,除非進行配置否則不會與其他名稱空間通訊。

物理介面必須處理最後的所有 真實 資料包,因此所有的虛擬介面都是從中建立的。

網路名稱空間可以通過 ip-netns 管理工具 來管理,可以使用 ip netns list 列出主機上的名稱空間。

請注意,建立的網路名稱空間將會出現在 /var/run/netns 目錄下,但Docker 並沒有遵循這一點。

例如,下面是 Kubernetes 節點的名稱空間:

$ ip netns list
cni-0f226515-e28b-df13-9f16-dd79456825ac (id: 3)
cni-4e4dfaac-89a6-2034-6098-dd8b2ee51dcd (id: 4)
cni-7e94f0cc-9ee8-6a46-178a-55c73ce58f2e (id: 2)
cni-7619c818-5b66-5d45-91c1-1c516f559291 (id: 1)
cni-3004ec2c-9ac2-2928-b556-82c7fb37a4d8 (id: 0)

注意 cni- 字首意味著名稱空間的建立由 CNI 來完成。

當建立 pod 並分配給節點時, CNI 會:

  1. 為其建立網路名稱空間。
  2. 分配 IP 地址。
  3. 將容器連線到網路。

如果 pod 像上面的示例一樣包含多個容器,則所有容器都被置於同一個名稱空間中。

  1. 建立 pod 時,CNI 為容器建立網路名稱空間

  2. 然後分配 IP 地址

  3. 最後將容器連線到網路的其餘部分

那麼當列出節點上的容器時會看到什麼?

可以 SSH 到 Kubernetes 節點來檢視名稱空間:

$ lsns -t net
        NS TYPE NPROCS   PID USER     NETNSID NSFS                           COMMAND
4026531992 net     171     1 root  unassigned /run/docker/netns/default      /sbin/init noembed norestore
4026532286 net       2  4808 65535          0 /run/docker/netns/56c020051c3b /pause
4026532414 net       5  5489 65535          1 /run/docker/netns/7db647b9b187 /pause

lsns 命令會列出主機上 所有 的名稱空間。

記住 Linux 中有 多種名稱空間型別

Nginx 容器在哪?

那麼 pause 容器又是什麼?

Pause 容器建立 Pod 中的網路名稱空間

從節點上的所有程序中找出 Nginx 容器:

$ lsns
        NS TYPE   NPROCS   PID USER            COMMAND
# truncated output
4026532414 net         5  5489 65535           /pause
4026532513 mnt         1  5599 root            sleep 1d
4026532514 uts         1  5599 root            sleep 1d
4026532515 pid         1  5599 root            sleep 1d
4026532516 mnt         3  5777 root            nginx: master process nginx -g daemon off;
4026532517 uts         3  5777 root            nginx: master process nginx -g daemon off;
4026532518 pid         3  5777 root            nginx: master process nginx -g daemon off;

該容器出現在了掛在(mount mnt )、Unix 分時系統(Unix time-sharing uts )和 PID( pid )名稱空間中,但是並不在網路名稱空間( net )中。

不幸的是, lsns 只顯示了每個程序最低的 PID,不過可以根據程序 ID 進一步過濾。

可以通過以下內容檢索Nginx 容器的所有名稱空間:

$ sudo lsns -p 5777
       NS TYPE   NPROCS   PID USER  COMMAND
4026531835 cgroup    178     1 root  /sbin/init noembed norestore
4026531837 user      178     1 root  /sbin/init noembed norestore
4026532411 ipc         5  5489 65535 /pause
4026532414 net         5  5489 65535 /pause
4026532516 mnt         3  5777 root  nginx: master process nginx -g daemon off;
4026532517 uts         3  5777 root  nginx: master process nginx -g daemon off;
4026532518 pid         3  5777 root  nginx: master process nginx -g daemon off;

pause 程序再次出現,這次它劫持了網路名稱空間。

那是什麼?

叢集中的每個 pod 都有一個在後臺執行的隱藏容器,被稱為 pause。

列出節點上的所有容器並過濾出 pause 容器:

$ docker ps | grep pause
fa9666c1d9c6   k8s.gcr.io/pause:3.4.1  "/pause"  k8s_POD_kube-dns-599484b884-sv2js…
44218e010aeb   k8s.gcr.io/pause:3.4.1  "/pause"  k8s_POD_blackbox-exporter-55c457d…
5fb4b5942c66   k8s.gcr.io/pause:3.4.1  "/pause"  k8s_POD_kube-dns-599484b884-cq99x…
8007db79dcf2   k8s.gcr.io/pause:3.4.1  "/pause"  k8s_POD_konnectivity-agent-84f87c…

將看到對於節點分配到的每個 pod,都有一個匹配的 pause 容器。

pause 容器負責建立和維持網路名稱空間。

它包含的程式碼極少,部署後立即進入睡眠狀態。

然而, 它在 Kubernetes 生態中的首當其衝,發揮著至關重要的作用。

  1. 建立 pod 時,CNI 會建立一個帶有 睡眠 容器的網路名稱空間

  2. Pod 中的所有容器都會加入到它建立的網路名稱空間中

  3. 此時 CNI 分配 IP 地址並將容器連線到網路

進入睡眠狀態的容器有什麼用?

要了解它的實用性,我們可以想象下如示例一樣有兩個容器的 pod,但沒有 pause 容器。

容器啟動,CNI:

  1. 為 Nginx 容器建立一個網路名稱空間。
  2. 把 busybox 容器加入到前面建立的網路名稱空間中。
  3. 為 pod 分配 IP 地址。
  4. 將容器連線到網路。

假如 Nginx 容器崩潰了會發生什麼?

CNI 將不得不 再次 完成所有流程,兩個容器的網路都會中斷。

由於 sleep 容器不太可能有任何 bug,因此建立網路名稱空間通常是一個更保險、更健壯的選擇。

如果 pod 中的一個容器崩潰,其餘的仍可以處理網路請求。

為 Pod 分配了 IP 地址

前面提到 pod 和所有容器獲得了同樣的 IP。

這是怎麼配置的?

在 pod 網路名稱空間中,建立一個介面並分配 IP 地址

我們來驗證下。

首先,找到 pod 的 IP 地址:

$ kubectl get pod multi-container-pod -o jsonpath={.status.podIP}
10.244.4.40

接下來,找到相關的網路名稱空間。

由於網路名稱空間是從物理介面建立的,需要訪問叢集節點。

如果你執行的是 minikube,可以通過 minikube ssh 訪問節點。如果在雲提供商中執行,應該有某種方法通過 SSH 訪問節點。

進入後,可以找到建立的最新的網路名稱空間:

$ ls -lt /var/run/netns
total 0
-r--r--r-- 1 root root 0 Sep 25 13:34 cni-0f226515-e28b-df13-9f16-dd79456825ac
-r--r--r-- 1 root root 0 Sep 24 09:39 cni-4e4dfaac-89a6-2034-6098-dd8b2ee51dcd
-r--r--r-- 1 root root 0 Sep 24 09:39 cni-7e94f0cc-9ee8-6a46-178a-55c73ce58f2e
-r--r--r-- 1 root root 0 Sep 24 09:39 cni-7619c818-5b66-5d45-91c1-1c516f559291
-r--r--r-- 1 root root 0 Sep 24 09:39 cni-3004ec2c-9ac2-2928-b556-82c7fb37a4d8

本示例中,它是 cni-0f226515-e28b-df13-9f16-dd79456825ac 。此時,可以在該名稱空間總執行 exec 命令:

$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip a
# output truncated
3: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 16:a4:f8:4f:56:77 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.4.40/32 brd 10.244.4.40 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::14a4:f8ff:fe4f:5677/64 scope link
       valid_lft forever preferred_lft forever

10.244.4.40 就是 pod 的 IP 地址。

通過查詢 @if12 中的 12 找到網路介面。

$ ip link | grep -A1 ^12
12: [email protected]: mtu 1376 qdisc noqueue master weave state UP mode DEFAULT group default
    link/ether 72:1c:73:d9:d9:f6 brd ff:ff:ff:ff:ff:ff link-netnsid 1

還可以驗證 Nginx 容器是否從該名稱空間中監聽 HTTP 流量:

$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac netstat -lnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      692698/nginx: master
tcp6       0      0 :::80                   :::*                    LISTEN      692698/nginx: master

如果無法通過 SSH 訪問叢集的節點,可以試試 kubectl exec 進入到 busybox 容器,然後使用 ipnetstat 命令。

太棒了!

現在我們已經介紹了容器間的通訊,接下來看看 Pod 與 Pod 直接如何建立通訊。

檢查叢集中 pod 到 pod 的流量

當說起 pod 間通訊時,會有兩種可能:

  1. Pod 流量流向同一節點上的 pod。
  2. Pod 流量流量另一個節點上的 pod。

為了使整個設定正常工作,我們需要之前討論過的虛擬介面和乙太網橋接。

在繼續之前,我們先討論下他們的功能以及為什麼他們時必需的。

要完成 pod 與其他 pod 的通訊,它必須先訪問節點的根名稱空間。

這是使用連線 pod 和根名稱空間的虛擬乙太網對來實現的。

這些 虛擬介面裝置veth 中的 v )連線並充當兩個名稱空間間的隧道。

使用此 veth 裝置,將一端連線到 pod 的名稱空間,另一端連線到根名稱空間。

這些 CNI 可以替你做,也可以手動操作:

$ ip link add veth1 netns pod-namespace type veth peer veth2 netns root

現在 pod 的名稱空間有了可以訪問根名稱空間的隧道。

節點上每個新建的 pod 都會設定如下所示的 veth 對。

建立介面對時其中一部分。

其他的就是為乙太網裝置分配地址,並建立預設路由。

來看下如何在 pod 的名稱空間中設定 veth1 介面:

$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip addr add 10.244.4.40/24 dev veth1
$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip link set veth1 up
$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip route add default via 10.244.4.40

在節點側,我們建立另一個 veth2 對:

$ ip addr add 169.254.132.141/16 dev veth2
$ ip link set veth2 up

可以像以前一樣檢查現有的 veth 對。

在 pod 的名稱空間中,檢查 eth0 介面的字尾。

$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip link show type veth
3: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default
    link/ether 16:a4:f8:4f:56:77 brd ff:ff:ff:ff:ff:ff link-netnsid 0

這種情況下可以使用 grep -A1 ^12 進行查詢(或者滾動到目標所在):

$ ip link show type veth
# output truncated
12: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netns cni-0f226515-e28b-df13-9f16-dd79456825ac

也可以使用 ip -n cni-0f226515-e28b-df13-9f16-dd79456825ac link show type veth 命令。

注意 3: [email protected]12: [email protected] 介面上的符號。

在 pod 名稱空間中, eth0 介面連線到根名稱空間中編號為 12 的介面。因此是 @if12

veth 對的另一端,根名稱空間連線到 pod 名稱空間的 3 號介面。

接下來是連線 veth 對兩端的橋接器(bridge)。

Pod 名稱空間連線到乙太網橋接器

橋接器將位於根名稱空間中的虛擬介面的每一端“繫結”。

該橋接器將允許流量在虛擬對之間流動,並通過公共根名稱空間。

理論時間。

乙太網橋接器位於 OSI 網路模型 的第二層。

可以將橋接器看作一個虛擬交換機,接受來自不同名稱空間和介面的連線。

乙太網橋接器允許連線同一個節點上的多個可用網路。

因此,可以使用該設定連線兩個介面:從 pod 名稱空間的 veth 連線到同一節點上另一個 pod 的 veth

我們繼續看下乙太網橋接器和 veth 對的作用。

跟蹤同一節點上 pod 間的流量

假設同一個節點上有兩個 pod,Pod-A 想向 Pod-B 傳送訊息。

  1. 由於目標不是同命名空間的容器,Pod-A 向其預設介面 eth0 傳送資料包。這個介面與 veth 對的一端繫結,作為隧道。因此資料包將被轉發到節點的根名稱空間。

  2. 乙太網橋接器作為虛擬交換機,必須以某種方式將目標 pod IP(Pod-B)解析為其 MAC 地址。

  3. 輪到ARP 協議上場了。當幀到達橋接器時,會向所有連線的裝置傳送 ARP 廣播。橋接器喊道 誰有 Pod-B 的 IP 地址

  4. 收到帶有連線 Pod-B 介面的 MAC 地址的回覆,然後此資訊儲存在橋接器 ARP 快取(查詢表)中。

  5. IP 和 MAC 地址的對映儲存完成後,橋接器在表中查詢,並將資料包轉發到正確的短點。資料包到達根名稱空間中 Pod- B 的 veth ,然後從那快速到達 Pod-B 名稱空間內的 eth0 介面。

有了這個,Pod-A 和 Pod-B 之間的通訊取得了成功。

跟蹤不同節點上 pod 間的通訊

對於需要跨不同節點通訊的 pod,需要額外的通訊跳轉。

  1. 前幾個步驟保持不變,直到資料包到達根名稱空間並需要傳送到 Pod- B。

  2. 當目標地址不在本地網路中,資料包將被轉發到本節點的預設閘道器。節點上退出或預設閘道器通常位於 eth0 介面上 – 將節點連線到網路的物理介面。

這次並不會發生 ARP 解析,因為源和目標 IP 在不同網路上。

檢查使用位運算(Bitwise)操作完成。

當目標 IP 不在當前網路上時,資料包將被轉發到節點的預設閘道器。

位運算的工作原理

在確定資料包的轉發位置時,源節點必須執行位運算。

這也被稱為與操作。

作為複習,位與操作產生如下結果:

0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1

除了 1 與 1 以外的都是 false。

如果源節點的 IP 為 192.168.1.1,子網掩碼為 /24,目標 IP 為 172.16.1.1/16,則按位與操作將確認他們不在同一網路上。

這意味著目標 IP 與資料包的源在不同的網路上,因此資料包將在預設閘道器中轉發。

數學時間。

我們必須從二進位制檔案中的 32 位地址執行與操作開始。

先找出源和目標 IP 的網路。

| Type             | Binary                              | Converted          |
| ---------------- | ----------------------------------- | ------------------ |
| Src. IP Address  | 11000000.10101000.00000001.00000001 | 192.168.1.1        |
| Src. Subnet Mask | 11111111.11111111.11111111.00000000 | 255.255.255.0(/24) |
| Src. Network     | 11000000.10101000.00000001.00000000 | 192.168.1.0        |
|                  |                                     |                    |
| Dst. IP Address  | 10101100.00010000.00000001.00000001 | 172.16.1.1         |
| Dst. Subnet Mask | 11111111.11111111.00000000.00000000 | 255.255.0.0(/16)   |
| Dst. Network     | 10101100.00010000.00000000.00000000 | 172.16.0.0         |

位運算操作後,需要將目標 IP 與資料包源節點的子網進行比較。

| Type             | Binary                              | Converted          |
| ---------------- | ----------------------------------- | ------------------ |
| Dst. IP Address  | 10101100.00010000.00000001.00000001 | 172.16.1.1         |
| Src. Subnet Mask | 11111111.11111111.11111111.00000000 | 255.255.255.0(/24) |
| Network  Result  | 10101100.00010000.00000001.00000000 | 172.16.1.0

進行位比較後,ARP 會檢查其查詢表來查詢預設閘道器的 MAC 地址。

如果有條目,將立即轉發資料包。

否則,先進行廣播以確定閘道器的 MAC 地址。

  1. 資料包現在路由到另一個節點的預設介面,我們叫它 Node-B。

  2. 以相反的順序。資料包現在位與 Node-B 的根名稱空間,併到達橋接器,這裡會進行 ARP 解析。

  3. 收到帶有連線 Pod-B 的介面 MAC地址的回覆。

  4. 這次橋接器通過 Pod-B 的 veth 裝置將幀轉發,併到達 Pod-B 自己的名稱空間。

此時應該已經熟悉了 pod 之間的流量如何流轉,接下來再探索下 CNI 如何建立上述內容。

容器網路介面 - CNI

容器網路介面(CNI)關注當前節點的網路。

可以將 CNI 看作網路外掛在解決 Kubernetes 某些 需求時要遵循的一套規則。

然而,它不僅僅與 Kubernetes 或者特定網路外掛關聯。

可以使用如下 CNI:

他們都實現相同的 CNI 標準。

如果沒有 CNI,你需要手動完成如下操作:

  • 建立 pod(容器)的網路名稱空間
  • 建立介面
  • 建立 veth 對
  • 設定名稱空間網路
  • 設定靜態路由
  • 配置乙太網橋接器
  • 分配 IP 地址
  • 建立 NAT 規則

還有太多其他需要手動完成的工作。

更不用說刪除或重新啟動 pod 時刪除或調整上述所有內容了。

CNI 必須支援 四個不同的操作

  • ADD - 將容器新增到網路
  • DEL - 從網路中刪除容器
  • CHECK - 如果容器的網路出現問題,則返回錯誤
  • VERSION - 顯示外掛的版本

讓我們在實踐中看看它是如何工作的。

當 pod 分配到特定節點時,kubelet 本身不會初始化網路。

相反,它將任務交給了 CNI。

然後,它指定了配置,並以 JSON 格式將其傳送給 CNI 外掛。

可以在節點的 /etc/cni/net.d 目錄中,找到當前 CNI 的配置檔案:

$ cat 10-calico.conflist
{
  "name": "k8s-pod-network",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "calico",
      "datastore_type": "kubernetes",
      "mtu": 0,
      "nodename_file_optional": false,
      "log_level": "Info",
      "log_file_path": "/var/log/calico/cni/cni.log",
      "ipam": { "type": "calico-ipam", "assign_ipv4" : "true", "assign_ipv6" : "false"},
      "container_settings": {
          "allow_ip_forwarding": false
      },
      "policy": {
          "type": "k8s"
      },
      "kubernetes": {
          "k8s_api_root":"https://10.96.0.1:443",
          "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
      }
    },
    {
      "type": "bandwidth",
      "capabilities": {"bandwidth": true}
    },
    {"type": "portmap", "snat": true, "capabilities": {"portMappings": true}}
  ]
}

每個外掛都使用不同型別的配置來設定網路。

例如,Calico 使用 BGP 路由協議配對的第 3 層網路來連線 pod。

Cilium 在第 3 到 7 層使用 eBPF 配置覆蓋網路。

與 Calico 一起,Cilium 支援設定網路策略來限制流量。

那該如何選擇呢?

這取決於。

CNI 主要有兩組。

第一組中,可以找到使用基本網路設定(也稱為扁平網路)的CNI,並將叢集 IP 池 中的IP 地址分配給 pod。

這可能會因為快速用盡可用的 IP 地址而成為負擔。

相反,另一種方法是使用覆蓋網路。

簡而言之,覆蓋網路是主(底層)網路之上的輔助網路。

覆蓋網路的工作原理是封裝來自底層網路的所有資料包,這些資料包指向另一個節點上的 pod。

覆蓋網路的一項流行技術是 VXLAN ,它允許在 L3 網路上隧道傳輸 L2 域。

那麼哪種更好?

沒有唯一的答案,通常取決於你的需求。

你是在構建一個擁有數萬個節點的大叢集嗎?

可能覆蓋網路更好。

你是否在意更簡單的設定和在巢狀網路中不失去檢查網路流量的能力。

扁平網路更適合你。

現在已經討論了 CNI,讓我們繼續探索 Pod 到服務(service)的通訊是如何完成的。

檢查 pod 到服務的流量

由於 Kubernetes 環境下 pod 的動態特性,分配給 pod 的 IP 地址不是靜態的。

這些 IP 地址是短暫的,每次建立或者刪除 pod 時都會發生變化。

服務解決了這個問題,為連線到一組 pod 提供了穩定的機制。

預設情況下,在 Kubernetes 中建立服務時,會 為其預定並分配虛擬 IP

使用選擇器將服務於目標 pod 進行管理。

當刪除 pod 並新增新 pod 時會發生什麼?

該服務的虛擬 IP 保持不變。

然而,無需敢於,流量將到達新建立的 pod。

換句話說,Kubernetes 中的服務類似於負載均衡器。

但他們時如何工作的?

使用 Netfilter 和 Iptables 攔截和重寫流量

Kubernetes 中的服務基於兩個 Linux 核心元件:

  1. Netfilter
  2. Iptables

Netfilter 是一個框架,允許配置資料包過濾、建立 NAT或埠翻譯規則,並管理網路中的流量。

此外,它還遮蔽和阻止不請自來的連線訪問服務。

另一方面,Iptables 是一個使用者空間程式,允許你配置 Linux 核心防火牆的 IP 資料包過濾器規則。

iptables 使用不同的 Netfilter 模組實現。

你可以使用 iptables CLI 實時更改過濾規則,並將其插入 netfilters 的掛點。

過濾器組織在不同的表中,其中包含處理網路流量資料包的鏈。

每個協議都使用不同的核心模組和程式。

當提到 iptables 時,通常說的是 IPV4。對於 IPV6 的規則,CLI 是 ip6tables。

Iptables 有五種型別的鏈,每種鏈都直接對映到 Netfilter 鉤子。

從 iptables 角度看是:

PRE_ROUTING
INPUT
FORWARD
OUTPUT
POST_ROUTING

對應對映到 Netfilter 鉤子:

NF_IP_PRE_ROUTING
NF_IP_LOCAL_IN
NF_IP_FORWARD
NF_IP_LOCAL_OUT
NF_IP_POST_ROUTING

當資料包到達時,根據所處的階段,會“出發” Netfilter 鉤子,該鉤子應用特定的 iptables 過濾。

哎呀,看起來很複雜!

不過不需要擔心。

這就是為什麼我們使用 Kubernetes,上面的所有內容都是通過使用服務來抽象的,一個簡單的 YAML 定義就可以自動完成這些規則的設定。

如果對這些 iptables 規則感興趣,可以登陸到節點並執行:

iptables-save

也可以使用 視覺化工具 瀏覽節點上的 iptables 鏈。

記住,可能會有數百條規則。想象下手動建立的難度。

我們已經解釋了相同和不同節點上的 pod 間如何通訊。

在 Pod-to-Service 中,通訊的前半部分保持不變。

當 Pod-A 發出請求時,希望到達 Pod-B(這種情況下,Pod-B 位與服務之後),轉移的過程中會發生其他變化。

原始請求從 Pod-A 名稱空間中的 eth0 接口出來。

從那裡穿過 veth 對,到達根名稱空間的乙太網橋。

一旦到達橋接器,資料包立即通過預設閘道器轉發。

與 Pod-to-Pod 部分一樣,主機進行位比較,由於服務的 vIP 不是節點 CIDR 的一部分,資料包將立即通過預設閘道器轉發出去。

如果查詢表中尚沒有預設閘道器的 MAC 地址,則會進行相同的 ARP 解析。

現在魔法發生了。

在資料包經過節點的路由處理之前,Netfilter 鉤子 NF_IP_PRE_ROUTING 被觸發並應用一條 iptables 規則。規則進行了 DNAT 轉換,重寫了 POD-A 資料包的目標 IP 地址。

原來服務 vIP 地址被重寫稱 POD-B 的IP 地址。

從那裡,路由就像 Pod-to-Pod 直接通訊一樣。

然而,在所有這些通訊之間,使用了第三個功能。

這個功能被稱為 conntrack ,或連線跟蹤。

Conntrack 將資料包與連線關聯起來,並在 Pod-B 傳送迴響應時跟蹤其來源。

NAT 嚴重依賴 contrack 工作。

如果沒有連線跟蹤,它將不知道將包含響應的資料包傳送回哪裡。

使用 conntrack 時,資料包的返回路徑可以輕鬆設定相同的源或目標 NAT 更改。

另一半使用相反的順序執行。

Pod-B 接收並處理了請求,現在將資料傳送回 Pod-A。

此時會發生什麼?

檢查服務的響應

現在 Pod-B 傳送響應,將其 IP 地址設定為源地址,Pod-A IP 地址設定為目標地址。

  1. 當資料包到達 Pod-A 所在節點的介面時,就會發生另一個 NAT

  2. 這次,使用 conntrack 更改源 IP 地址,iptables 規則執行 SNAT 將 Pod-B IP 地址替換為原始服務的 VIP 地址。

  3. 從 Pod-A 來看像是服務發回的響應,而不是 Pod-B。

其他部分都一樣;一旦 SNAT 完成,資料包到達根名稱空間中的乙太網橋接器,並通過 veth 對轉發到 Pod-A。

回顧

讓我們來總結下你在本文中學到的東西:

  • 容器如何在本地或 pod 內通訊。
  • 當 pod 位於相同和不同的節點上時,Pod-to- Pod 如何通訊。
  • Pod-to-Service - 當 pod 向 Kubernetes 服務背後的 pod 傳送流量時。
  • Kubernetes 網路工具箱中有效通訊所需的名稱空間、veth、iptables、鏈、Netfilter、CNI、覆蓋網路以及所有其他內容。