docker與k8s深層理解(2)

Job Controller 的工作原理

首先,Job Controller 控制的對象,直接就是 Pod。

其次,Job Controller 在控制循環中進行的調諧(Reconcile)操作,是根據實際在 Running 狀態 Pod 的數目、已經成功退出的 Pod 的數目,以及 parallelism、completions 參數的值共 同計算出在這個週期裏,應該創建或者刪除的 Pod 數目,然後調用 Kubernetes API 來執行這 個操作。

以創建 Pod 爲例。在上面計算 Pi 值的這個例子中,當 Job 一開始創建出來時,實際處於 Running 狀態的 Pod 數目 =0,已經成功退出的 Pod 數目 =0,而用戶定義的 completions, 也就是最終用戶需要的 Pod 數目 =4。

所以,在這個時刻,需要創建的 Pod 數目 = 最終需要的 Pod 數目 - 實際在 Running 狀態 Pod 數目 - 已經成功退出的 Pod 數目 = 4 - 0 - 0= 4。也就是說,Job Controller 需要創建 4 個 Pod 來糾正這個不一致狀態。

 

三種常用的、使用 Job 對象的方法

第一種用法,也是最簡單粗暴的用法:外部管理器 +Job 模板。

在控制這種 Job 時,我們只要注意如下兩個方面即可:

1. 創建 Job 時,替換掉 $ITEM 這樣的變量;

2. 所有來自於同一個模板的 Job,都有一個 jobgroup: jobexample 標籤,也就是說這一組 Job 使用這樣一個相同的標識。

很容易理解,在這種模式下使用 Job 對象,completions 和 parallelism 這兩個字段都應該使 用默認值 1,而不應該由我們自行設置。而作業 Pod 的並行控制,應該完全交由外部工具來進 行管理(比如,KubeFlow)。

 

第二種用法:擁有固定任務數目的並行 Job。

這種模式下,我只關心最後是否有指定數目(spec.completions)個任務成功退出。至於執行 時的並行度是多少,我並不關心。

一旦你用 kubectl create 創建了這個 Job,它就會以併發度爲 2 的方式,每兩個 Pod 一 組,創建出 8 個 Pod。每個 Pod 都會去連接 BROKER_URL,從 RabbitMQ 裏讀取任務,然後 各自進行處理。

 

第三種用法,也是很常用的一個用法:指定並行度(parallelism),但不設置固定的 completions 的值。

 

你就必須自己想辦法,來決定什麼時候啓動新 Pod,什麼時候 Job 纔算執行完成。在這 種情況下,任務的總數是未知的,所以你不僅需要一個工作隊列來負責任務分發,還需要能夠判斷工作隊列已經爲空(即:所有的工作已經結束了)。

 

CronJob 與 Job 的關係,正如同 Deployment 與 Pod 的關係一樣。CronJob 是一個專 門用來管理 Job 對象的控制器。只不過,它創建和刪除 Job 的依據,是 schedule 字段定義 的、一個標準的Unix Cron格式的表達式。

可以通過 spec.concurrencyPolicy 字段來定義具體的處理策略。

比如: 1. concurrencyPolicy=Allow,這也是默認情況,這意味着這些 Job 可以同時存在;

2. concurrencyPolicy=Forbid,這意味着不會創建新的 Pod,該創建週期被跳過;

3. concurrencyPolicy=Replace,這意味着新產生的 Job 會替換舊的、沒有執行完的 Job。

 

Kubernetes“聲明式 API”的獨特之處:

首先,所謂“聲明式”,指的就是我只需要提交一個定義好的 API 對象來“聲明”,我所期 望的狀態是什麼樣子。

其次,“聲明式 API”允許有多個 API 寫端,以 PATCH 的方式對 API 對象進行修改,而無 需關心本地原始 YAML 文件的內容。

最後,也是最重要的,有了上述兩個能力,Kubernetes 項目纔可以基於對 API 對象的增、 刪、改、查,在完全無需外界干預的情況下,完成對“實際狀態”和“期望狀態”的調諧 (Reconcile)過程。

所以說,聲明式 API,纔是 Kubernetes 項目編排能力“賴以生存”的核心所在

首先,Kubernetes 會匹配 API 對象的組

需要明確的是,對於 Kubernetes 裏的核心 API 對象,比如:Pod、Node 等,是不需要 Group 的(即:它們 Group 是“”)。所以,對於這些 API 對象來說,Kubernetes 會直接在 /api 這個層級進行下一步的匹配過程。

而對於 CronJob 等非核心 API 對象來說,Kubernetes 就必須在 /apis 這個層級裏查找它對應 的 Group,進而根據“batch”這個 Group 的名字,找到 /apis/batch。

這些 API Group 的分類是以對象功能爲依據的,比如 Job 和 CronJob 就都屬 於“batch” (離線業務)這個 Group。

然後,Kubernetes 會進一步匹配到 API 對象的版本號。

首先,當我們發起了創建 CronJob 的 POST 請求之後,我們編寫的 YAML 的信息就被提交給 了 APIServer。

而 APIServer 的第一個功能,就是過濾這個請求,並完成一些前置性的工作,比如授權、超時 處理、審計等。

然後,請求會進入 MUX 和 Routes 流程。

如果你編寫過 Web Server 的話就會知道,MUX 和 Routes 是 APIServer 完成 URL 和 Handler 綁定的場所。而 APIServer 的 Handler 要做的事 情,就是按照我剛剛介紹的匹配過程,找到對應的 CronJob 類型定義。

接着,APIServer 最重要的職責就來了:根據這個 CronJob 類型定義,使用用戶提交的 YAML 文件裏的字段,創建一個 CronJob 對象。

而在這個過程中,APIServer 會進行一個 Convert 工作,即:把用戶提交的 YAML 文件,轉換 成一個叫作 Super Version 的對象,它正是該 API 資源類型所有版本的字段全集。

這樣用戶提 交的不同版本的 YAML 文件,就都可以用這個 Super Version 對象來進行處理了。 接下來,APIServer 會先後進行 Admission() 和 Validation() 操作。

而 Validation,則負責驗證這個對象裏的各個字段是否合法。這個被驗證過的 API 對象,都保 存在了 APIServer 裏一個叫作 Registry 的數據結構中。

也就是說,只要一個 API 對象的定義能 在 Registry 裏查到,它就是一個有效的 Kubernetes API 對象。 最後,APIServer 會把驗證過的 API 對象轉換成用戶最初提交的版本,進行序列化操作,並調 用 Etcd 的 API 把它保存起來。

基於角色的權限控制之 RBAC

負責完成授權(Authorization)工作的機制,就是 RBAC:基於角 色的訪問控制(Role-Based Access Control)

1. Role:角色,它其實是一組規則,定義了一組對 Kubernetes API 對象的操作權限。

2. Subject:被作用者,既可以是“人”,也可以是“機器”,也可以使你在 Kubernetes 裏 定義的“用戶”。

3. RoleBinding:定義了“被作用者”和“角色”的綁定關係

 

Role 本身就是一個 Kubernetes 的 API 對象,定義如下所示:

這個 Role 對象指定了它能產生作用的 Namepace 是:mynamespace

Namespace 是 Kubernetes 項目裏的一個邏輯管理單位。不同 Namespace 的 API 對象,在 通過 kubectl 命令進行操作的時候,是互相隔離開的。

比如,kubectl get pods -n mynamespace。

當然,這僅限於邏輯上的“隔離”,Namespace 並不會提供任何實際的隔離或者多租戶能力。 而在前面文章中用到的大多數例子裏,我都沒有指定 Namespace,那就是使用的是默認 Namespace:default。

RoleBinding 本身也是一個 Kubernetes 的 API 對象。它的定義如下所示:

 

這個 RoleBinding 對象裏定義了一個 subjects 字段,即“被作用者”。它的類型是 User,即 Kubernetes 裏的用戶。這個用戶的名字是 example-user。

在 Kubernetes 中,其實並沒有一個叫作“User”的 API 對象。而且,我們在前面和部 署使用 Kubernetes 的流程裏,既不需要 User,也沒有創建過 User。

實際上,Kubernetes 裏的“User”,也就是“用戶”,只是一個授權系統裏的邏輯概念。它需 要通過外部認證服務,比如 Keystone,來提供。或者,你也可以直接給 APIServer 指定一個用 戶名、密碼文件。那麼 Kubernetes 的授權系統,就能夠從這個文件裏找到對應的“用戶”了。 當然,在大多數私有的使用環境中,我們只要使用 Kubernetes 提供的內置“用戶”,就足夠 了。

roleRef 字段。正是通過這個字段,RoleBinding 對象就可以直接通 過名字,來引用我們前面定義的 Role 對象(example-role),從而定義了“被作用者 (Subject)”和“角色(Role)”之間的綁定關係。

Role 和 RoleBinding 對象都是 Namespaced 對象(Namespaced Object),它們對權限的限制規則僅在它們自己的 Namespace 內有效,roleRef 也只能引用當 前 Namespace 裏的 Role 對象。

 

 

對於非 Namespaced(Non-namespaced)對象(比如:Node),或者,某一個 Role 想要作用於所有的 Namespace 的時候,我們又該如何去做授權

 

Kubernetes 還提供了四個預先定義好的 ClusterRole 來供用戶直接使用:

1. cluster-amdin; 2. admin; 3. edit; 4. view。

 

PVC 描述的,是 Pod 想要使用的持久化存儲的屬性,比如存儲的大小、讀寫權限等。 PV 描述的,則是一個具體的 Volume 的屬性,比如 Volume 的類型、掛載目錄、遠程存儲 服務器地址等。

而 StorageClass 的作用,則是充當 PV 的模板。並且,只有同屬於一個 StorageClass 的 PV 和 PVC,纔可以綁定在一起。

本文章提到的“Volume”,指的就 是一個遠程存儲服務掛載在宿主機上的持久化目錄;而“PV”,指的是這個 Volume 在 Kubernetes 裏的 API 對象。

在刪除 PV 時需要按 如下流程執行操作:

1. 刪除使用這個 PV 的 Pod; 2. 從宿主機移除本地磁盤(比如,umount 它); 3. 刪除 PVC; 4. 刪除 PV。

 

一個 Linux 容器能看見的“網絡棧”,實際上是被隔離 在它自己的 Network Namespace 當中的

就包括了:網卡(Network Interface)、迴環設備(Loopback Device)、路由表(Routing Table)和 iptables 規則。對於一個進程來說,這些要素,其實 就構成了它發起和響應網絡請求的基本環境。

 

聲明直接使用宿主機的網絡棧(–net=host),即:不開 啓 Network Namespace

這個被隔離的容器進程,該如何跟其他 Network Namespace 裏的容器進程進行交互呢?

 

夠起到虛擬交換機作用的網絡設備,是網橋(Bridge)。它是一個工作在數據 鏈路層(Data Link)的設備,主要功能是根據 MAC 地址學習來將數據包轉發到網橋的不同端 口(Port)上。

Docker 項目會默認在宿主機上創建一個名叫 docker0 的網橋,凡是連 接在 docker0 網橋上的容器,就可以通過它來進行通信。該如何把這些容器“連接”到 docker0 網橋上呢?

 

使用一種名叫Veth Pair的虛擬設備了。

 

Veth Pair 設備的特點是:它被創建出來後,總是以兩張虛擬網卡(Veth Peer)的形式成對出 現的。並且,從其中一個“網卡”發出的數據包,可以直接出現在與它對應的另一張“網 卡”上,哪怕這兩個“網卡”在不同的 Network Namespace 裏。

通過 route 命令查看 nginx-1 容器的路由表,我們可以看到,這個 eth0 網卡是這個容器裏的 默認路由設備;所有對 172.17.0.0/16 網段的請求,也會被交給 eth0 來處理(第二條 172.17.0.0 路由規則)。

 

而這個 Veth Pair 設備的另一端,則在宿主機上。你可以通過查看宿主機的網絡設備看到它,如 下所示:

通過 ifconfig 命令的輸出,你可以看到,nginx-1 容器對應的 Veth Pair 設備,在宿主機上是 一張虛擬網卡。它的名字叫作 veth9c02e56。並且,通過 brctl show 的輸出,你可以看到這張 網卡被“插”在了 docker0 上。

 

這其中的原理

你就會發現一個新的、名叫 vethb4963f3 的虛擬網卡,也被“插”在了 docker0 網橋上。 這時候,如果你在 nginx-1 容器裏 ping 一下 nginx-2 容器的 IP 地址(172.17.0.3),就會發 現同一宿主機上的兩個容器默認就是相互連通的。

 

當你在 nginx-1 容器裏訪問 nginx-2 容器的 IP 地址(比如 ping 172.17.0.3)的時候,這個目 的 IP 地址會匹配到 nginx-1 容器裏的第二條路由規則。可以看到,這條路由規則的網關 (Gateway)是 0.0.0.0,這就意味着這是一條直連規則,即:凡是匹配到這條規則的 IP 包,應 該經過本機的 eth0 網卡,通過二層網絡直接發往目的主機。 而要通過二層網絡到達 nginx-2 容器,就需要有 172.17.0.3 這個 IP 地址對應的 MAC 地址。 所以 nginx-1 容器的網絡協議棧,就需要通過 eth0 網卡發送一個 ARP 廣播,來通過 IP 地址查 找對應的 MAC 地址。

 

這個 eth0 網卡,是一個 Veth Pair,它的一端在這個 nginx-1 容器的 Network Namespace 裏,而另一端則位於宿主機上(Host Namespace),並且被“插”在 了宿主機的 docker0 網橋上。

一旦一張虛擬網卡被“插”在網橋上,它就會變成該網橋的“從設備”。從設備會被“剝奪”調 用網絡協議棧處理數據包的資格,從而“降級”成爲網橋上的一個端口。而這個端口唯一的作 用,就是接收流入的數據包,然後把這些數據包的“生殺大權”(比如轉發或者丟棄),全部交 給對應的網橋。

 

在收到這些 ARP 請求之後,docker0 網橋就會扮演二層交換機的角色,把 ARP 廣播轉 發到其他被“插”在 docker0 上的虛擬網卡上。這樣,同樣連接在 docker0 上的 nginx-2 容 器的網絡協議棧就會收到這個 ARP 請求,從而將 172.17.0.3 所對應的 MAC 地址回覆給 nginx-1 容器。

此時,對宿主機來說,docker0 網橋就是一個普通的網卡。

在實際的數據傳遞時,上述數據的傳遞過程在網絡協議棧的不同層次,都有 Linux 內核 Netfilter 參與其中。

 

我們整個集羣裏的容器網絡就會類似於下圖所示的樣子:

構建這種容器網絡的核心在於:我們需要在已有的宿主機網絡上,再通過軟件構建一 個覆蓋在已有宿主機網絡之上的、可以把所有容器連通在一起的虛擬網絡。所以,這種技術就被 稱爲:Overlay Network(覆蓋網絡)。

 

深入解析容器跨主機網絡

 

Flannel 支持三種後端實現

1. VXLAN; 2. host-gw; 3. UDP。

UDP 模式,是 Flannel 項目最早支持的一種方式,卻也是性能最差的一種方式。所以,這個模 式目前已經被棄用。不過,Flannel 之所以最先選擇 UDP 模式,就是因爲這種模式是最直接、也是 最容易理解的容器跨主網絡實現。

 

 

會先從 UDP 模式開始

在這個例子中,我有兩臺宿主機。

宿主機 Node 1 上有一個容器 container-1,它的 IP 地址是 100.96.1.2,對應的 docker0 網橋 的地址是:100.96.1.1/24。

宿主機 Node 2 上有一個容器 container-2,它的 IP 地址是 100.96.2.3,對應的 docker0 網橋 的地址是:100.96.2.1/24。

 

這種情況下,container-1 容器裏的進程發起的 IP 包,其源地址就是 100.96.1.2,目的地址就是 100.96.2.3。由於目的地址 100.96.2.3 並不在 Node 1 的 docker0 網橋的網段裏,所以這個 IP 包 會被交給默認路由規則,通過容器的網關進入 docker0 網橋(如果是同一臺宿主機上的容器間通 信,走的是直連規則),從而出現在宿主機上。

可以看到,由於我們的 IP 包的目的地址是 100.96.2.3,它匹配不到本機 docker0 網橋對應的 100.96.1.0/24 網段,只能匹配到第二條、也就是 100.96.0.0/16 對應的這條路由規則,從而進入 到一個叫作 flannel0 的設備中。

這個 flannel0 設備的類型就比較有意思了:它是一個 TUN 設備(Tunnel 設備)。

TUN 設備是一種工作在三層(Network Layer)的虛擬網絡設備。TUN 設備的功能 非常簡單,即:在操作系統內核和用戶應用程序之間傳遞 IP 包。

以 flannel0 設備爲例: 像上面提到的情況,當操作系統將一個 IP 包發送給 flannel0 設備之後,flannel0 就會把這個 IP 包,交給創建這個設備的應用程序,也就是 Flannel 進程。這是一個從內核態(Linux 操作系統) 向用戶態(Flannel 進程)的流動方向。 反之,如果 Flannel 進程向 flannel0 設備發送了一個 IP 包,那麼這個 IP 包就會出現在宿主機網絡 棧中,然後根據宿主機的路由表進行下一步處理。這是一個從用戶態向內核態的流動方向。 所以,當 IP 包從容器經過 docker0 出現在宿主機,然後又根據路由表進入 flannel0 設備後,宿主 機上的 flanneld 進程(Flannel 項目在每個宿主機上的主進程),就會收到這個 IP 包。然後, flanneld 看到了這個 IP 包的目的地址,是 100.96.2.3,就把它發送給了 Node 2 宿主機。

 

Flannel 項目裏一個非常重要的概念:子網(Subnet)。

在由 Flannel 管理的容器網絡裏,一臺宿主機上的所有容器,都屬於該宿主機被分配的一 個“子網”。

在我們的例子中,Node 1 的子網是 100.96.1.0/24,container-1 的 IP 地址是 100.96.1.2。

Node 2 的子網是 100.96.2.0/24,container-2 的 IP 地址是 100.96.2.3。

flanneld 進程在處理由 flannel0 傳入的 IP 包時,就可以根據目的 IP 的地址(比如 100.96.2.3),匹配到對應的子網(比如 100.96.2.0/24),從 Etcd 中找到這個子網對應的宿主機 的 IP 地址是 10.168.0.3

 

flanneld 進程在處理由 flannel0 傳入的 IP 包時,就可以根據目的 IP 的地址(比如 100.96.2.3),匹配到對應的子網(比如 100.96.2.0/24),從 Etcd 中找到這個子網對應的宿主機 的 IP 地址是 10.168.0.3

對於 flanneld 來說,只要 Node 1 和 Node 2 是互通的,那麼 flanneld 作爲 Node 1 上的一個 普通進程,就一定可以通過上述 IP 地址(10.168.0.3)訪問到 Node 2,這沒有任何問題。

 

flanneld 在收到 container-1 發給 container-2 的 IP 包之後,就會把這個 IP 包直接封裝 在一個 UDP 包裏,然後發送給 Node 2。不難理解,這個 UDP 包的源地址,就是 flanneld 所在 的 Node 1 的地址,而目的地址,則是 container-2 所在的宿主機 Node 2 的地址。 當然,這個請求得以完成的原因是,每臺宿主機上的 flanneld,都監聽着一個 8285 端口,所以 flanneld 只要把 UDP 包發往 Node 2 的 8285 端口即可。

 

基於 Flannel UDP 模式的跨主通信的基本原理

 

Flannel UDP 模式提供的其實是一個三層的 Overlay 網絡,即:它首先對發出端的 IP 包進行 UDP 封裝,然後在接收端進行解封裝拿到原始的 IP 包,進而把這個 IP 包轉發給目標容 器。這就好比,Flannel 在不同宿主機上的兩個容器之間打通了一條“隧道”,使得這兩個容器可 以直接使用 IP 地址進行通信,而無需關心容器和宿主機的分佈情況。

 

相比於兩臺宿主機之間的直接通信,基於 Flannel UDP 模式的容器通信多了一個額外的步 驟,即 flanneld 的處理過程。而這個過程,由於使用到了 flannel0 這個 TUN 設備,僅在發出 IP 包的過程中,就需要經過三次用戶態與內核態之間的數據拷貝,如下所示:

我們可以看到:

第一次:用戶態的容器進程發出的 IP 包經過 docker0 網橋進入內核態;

第二次:IP 包根據路由表進入 TUN(flannel0)設備,從而回到用戶態的 flanneld 進程;

第三次:flanneld 進行 UDP 封包之後重新進入內核態,將 UDP 包通過宿主機的 eth0 發出去。

Flannel 進行 UDP 封裝(Encapsulation)和解封裝(Decapsulation) 的過程,也都是在用戶態完成的。在 Linux 操作系統中,上述這些上下文切換和用戶態操作的代價 其實是比較高的,這也正是造成 Flannel UDP 模式性能不好的主要原因。

 

在進行系統級編程的時候,有一個非常重要的優化原則,就是要減少用戶態到內核態 的切換次數,並且把核心的處理邏輯都放在內核態進行。

VXLAN 模式,逐漸成爲了主流的容器網絡方案的原因

VXLAN,即 Virtual Extensible LAN(虛擬可擴展局域網),是 Linux 內核本身就支持的一種網絡 虛似化技術。所以說,VXLAN 可以完全在內核態實現上述封裝和解封裝的工作,從而通過與前面 相似的“隧道”機制,構建出覆蓋網絡(Overlay Network)。

 

VXLAN 的覆蓋網絡的設計思想是:在現有的三層網絡之上,“覆蓋”一層虛擬的、由內核 VXLAN 模塊負責維護的二層網絡,使得連接在這個 VXLAN 二層網絡上的“主機”(虛擬機或者容器都可 以)之間,可以像在同一個局域網(LAN)裏那樣自由通信。當然,實際上,這些“主機”可能分 布在不同的宿主機上,甚至是分佈在不同的物理機房裏。

而爲了能夠在二層網絡上打通“隧道”,VXLAN 會在宿主機上設置一個特殊的網絡設備作爲“隧 道”的兩端。這個設備就叫作 VTEP,即:VXLAN Tunnel End Point(虛擬隧道端點)。

而 VTEP 設備的作用,其實跟前面的 flanneld 進程非常相似。只不過,它進行封裝和解封裝的對 象,是二層數據幀(Ethernet frame);而且這個工作的執行流程,全部是在內核裏完成的(因爲VXLAN 本身就是 Linux 內核中的一個模塊)。

 

現在,我們的 container-1 的 IP 地址是 10.1.15.2,要訪問的 container-2 的 IP 地址是 10.1.16.3。 那麼,與前面 UDP 模式的流程類似,當 container-1 發出請求之後,這個目的地址是 10.1.16.3 的 IP 包,會先出現在 docker0 網橋,然後被路由到本機 flannel.1 設備進行處理。也就是說,來 到了“隧道”的入口。爲了方便敘述,我接下來會把這個 IP 包稱爲“原始 IP 包”。 爲了能夠將“原始 IP 包”封裝並且發送到正確的宿主機,VXLAN 就需要找到這條“隧道”的出 口,即:目的宿主機的 VTEP 設備。 而這個設備的信息,正是每臺宿主機上的 flanneld 進程負責維護的。

 

比如,當 Node 2 啓動並加入 Flannel 網絡之後,在 Node 1(以及所有其他節點)上,flanneld 就會添加一條如下所示的路由規則:

:凡是發往 10.1.16.0/24 網段的 IP 包,都需要經過 flannel.1 設備發出,並 且,它最後被髮往的網關地址是:10.1.16.0。

,10.1.16.0 正是 Node 2 上的 VTEP 設 備(也就是 flannel.1 設備)的 IP 地址。

需要想辦法組成一個虛擬的二層網絡,即:通過二層數據幀進行通信。

 

“源 VTEP 設備”收到“原始 IP 包”後,就要想辦法把“原始 IP 包”加上 一個目的 MAC 地址,封裝成一個二層數據幀,然後發送給“目的 VTEP 設備”(當然,這麼做還 是因爲這個 IP 包的目的地址不是本機)。

 

此時,根據前面的路由記錄,我們已經知道了“目的 VTEP 設備”的 IP 地址。而要根據三層 IP 地 址查詢對應的二層 MAC 地址,這正是 ARP(Address Resolution Protocol )表的功能。

 

:IP 地址 10.1.16.0,對應的 MAC 地址是 5e:f8:4f:00:e3:37。

有了這個“目的 VTEP 設備”的 MAC 地址,Linux 內核就可以開始二層封包工作了。這個二層幀 的格式,如下所示:

,Linux 內核會把“目的 VTEP 設備”的 MAC 地址,填寫在圖中的 Inner Ethernet Header 字段,得到一個二層數據幀。

上述封包過程只是加一個二層頭,不會改變“原始 IP 包”的內容。所以圖中的 Inner IP Header 字段,依然是 container-2 的 IP 地址,即 10.1.16.3。

但是,上面提到的這些 VTEP 設備的 MAC 地址,對於宿主機網絡來說並沒有什麼實際意義。所以 上面封裝出來的這個數據幀,並不能在我們的宿主機二層網絡裏傳輸。爲了方便敘述,我們把它稱 爲“內部數據幀”(Inner Ethernet Frame)。

所以接下來,Linux 內核還需要再把“內部數據幀”進一步封裝成爲宿主機網絡裏的一個普通的數 據幀,好讓它“載着”“內部數據幀”,通過宿主機的 eth0 網卡進行傳輸。 我們把這次要封裝出來的、宿主機對應的數據幀稱爲“外部數據幀”(Outer Ethernet Frame)。

爲了實現這個“搭便車”的機制,Linux 內核會在“內部數據幀”前面,加上一個特殊的 VXLAN 頭,用來表示這個“乘客”實際上是一個 VXLAN 要使用的數據幀。

而這個 VXLAN 頭裏有一個重要的標誌叫作VNI,它是 VTEP 設備識別某個數據幀是不是應該歸自 己處理的重要標識。而在 Flannel 中,VNI 的默認值是 1,這也是爲何,宿主機上的 VTEP 設備都 叫作 flannel.1 的原因,這裏的“1”,其實就是 VNI 的值。

Linux 內核會把這個數據幀封裝進一個 UDP 包裏發出去

這個 UDP 包該發給哪臺宿主機呢? 在這種場景下,flannel.1 設備實際上要扮演一個“網橋”的角色,在二層網絡進行 UDP 包的轉 發。而在 Linux 內核裏面,“網橋”設備進行轉發的依據,來自於一個叫作 FDB(Forwarding Database)的轉發數據庫。

不難想到,這個 flannel.1“網橋”對應的 FDB 信息,也是 flanneld 進程負責維護的。它的內容可 以通過 bridge fdb 命令查看到,如下所示:

->>發往我們前面提到的“目的 VTEP 設備”(MAC 地址是 5e:f8:4f:00:e3:37)的二層數據幀,應該 通過 flannel.1 設備,發往 IP 地址爲 10.168.0.3 的主機。顯然,這臺主機正是 Node 2,UDP 包 要發往的目的地就找到了。

 

接下來的流程,就是一個正常的、宿主機網絡上的封包工作。

 

UDP 包是一個四層數據包,所以 Linux 內核會在它前面加上一個 IP 頭,即原理圖中的 Outer IP Header,組成一個 IP 包。並且,在這個 IP 頭裏,會填上前面通過 FDB 查詢出來的目的 主機的 IP 地址,即 Node 2 的 IP 地址 10.168.0.3。

然後,Linux 內核再在這個 IP 包前面加上二層數據幀頭,即原理圖中的 Outer Ethernet Header, 並把 Node 2 的 MAC 地址填進去。這個 MAC 地址本身,是 Node 1 的 ARP 表要學習的內容, 無需 Flannel 維護。這時候,我們封裝出來的“外部數據幀”的格式,如下所示:

接下來,Node 1 上的 flannel.1 設備就可以把這個數據幀從 Node 1 的 eth0 網卡發出去。顯然, 這個幀會經過宿主機網絡來到 Node 2 的 eth0 網卡。

這時候,Node 2 的內核網絡棧會發現這個數據幀裏有 VXLAN Header,並且 VNI=1。

所以 Linux 內核會對它進行拆包,拿到裏面的內部數據幀,然後根據 VNI 的值,把它交給 Node 2 上的 flannel.1 設備。

而 flannel.1 設備則會進一步拆包,取出“原始 IP 包”。

最終,IP 包就進入到了 container-2 容器的 Network Namespace 裏。 以上,就是 Flannel VXLAN 模式的具體工作原理了。

這兩種模式其實都可 以稱作“隧道”機制.

 

(UDP 模式創建的是 TUN 設備,VXLAN 模式創建的則是 VTEP 設備),docker0 與這個設備之間,通過 IP 轉發(路由表)進行協作。

 

Kubernetes 是通過一個叫作 CNI 的接口,維護了一個單獨的網橋來代替 docker0。這個網橋 的名字就叫作:CNI 網橋,它在宿主機上的設備名稱默認是:cni0。

docker0 網橋被替換成了 CNI 網橋而已

假設 Infra-container-1 要訪問 Infra-container-2(也就是 Pod-1 要訪問 Pod-2), 這個 IP 包的源地址就是 10.244.0.2,目的 IP 地址是 10.244.1.3。而此時,Infra-container-1 裏的 eth0 設備,同樣是以 Veth Pair 的方式連接在 Node 1 的 cni0 網橋上。所以這個 IP 包就 會經過 cni0 網橋出現在宿主機上。

CNI 網橋只是接管所有 CNI 插件負責的、即 Kubernetes 創建的容器 (Pod)。而此時,如果你用 docker run 單獨啓動一個容器,那麼 Docker 項目還是會把這個 容器連接到 docker0 網橋上。所以這個容器的 IP 地址,一定是屬於 docker0 網橋的 172.17.0.0/16 網段。

有一個步驟是安裝 kubernetes-cni 包,它的目的就是在宿主 機上安裝CNI 插件所需的基礎可執行文件。

這些 CNI 的基礎可執行文件,按照功能可以分爲三類:

第一類,叫作 Main 插件,它是用來創建具體網絡設備的二進制文件。比如,bridge(網橋設 備)、ipvlan、loopback(lo 設備)、macvlan、ptp(Veth Pair 設備),以及 vlan。

第二類,叫作 IPAM(IP Address Management)插件,它是負責分配 IP 地址的二進制文 件。比如,dhcp,這個文件會向 DHCP 服務器發起請求;host-local,則會使用預先配置的 IP 地址段來進行分配

第三類,是由 CNI 社區維護的內置 CNI 插件。比如:flannel,就是專門爲 Flannel 項目提供的 CNI 插件;tuning,是一個通過 sysctl 調整網絡設備參數的二進制文件;portmap,是一個通 過 iptables 配置端口映射的二進制文件;bandwidth,是一個使用 Token Bucket Filter (TBF) 來進行限流的二進制文件。

首先,實現這個網絡方案本身。這一部分需要編寫的,其實就是 flanneld 進程裏的主要邏輯。 比如,創建和配置 flannel.1 設備、配置宿主機路由、配置 ARP 和 FDB 表裏的信息等等。 然後,實現該網絡方案對應的 CNI 插件。這一部分主要需要做的,就是配置 Infra 容器裏面的 網絡棧,並把它連接在 CNI 網橋上。

CNI(container network interface)

CRI(Container Runtime Interface,容器運行時接口)

在 Kubernetes 中,處理容器網絡相關的邏輯並不會在 kubelet 主幹代碼裏執 行,而是會在具體的 CRI(Container Runtime Interface,容器運行時接口)實現裏完成。對 於 Docker 項目來說,它的 CRI 實現叫作 dockershim,你可以在 kubelet 的代碼裏找到它。

 

Kubernetes 中 CNI 網絡的實現原理

1. 所有容器都可以直接使用 IP 地址與其他容器通信,而無需使用 NAT。

2. 所有宿主機都可以直接使用 IP 地址與所有容器通信,而無需使用 NAT。反之亦然。

3. 容器自己“看到”的自己的 IP 地址,和別人(宿主機或者容器)看到的地址是完全一樣 的。

 

Kubernetes 三層網絡方案

host-gw 示意圖

host-gw 模式的工作原理,其實就是將每個 Flannel 子網(Flannel Subnet,比 如:10.244.1.0/24)的“下一跳”,設置成了該子網對應的宿主機的 IP 地址

host-gw 的性能損失大約在 10% 左右,而其他所有基於 VXLAN“隧道”機制的網絡方 案,性能損失都在 20%~30% 左右。

host-gw 模式能夠正常工作的核心,就在於 IP 包在封 裝成幀發送出去的時候,會使用路由表裏的“下一跳”來設置目的 MAC 地址。這樣,它就會經 過二層網絡到達目的宿主機。

Flannel host-gw 模式必須要求集羣宿主機之間是二層連通的。

BGP 的全稱是 Border Gateway Protocol,即:邊界網關協議。它是一個 Linux 內核原生就支 持的、專門用在大規模數據中心裏維護不同的“自治系統”之間路由信息的、無中心的路由協 議

我們有兩個自治系統(Autonomous System,簡稱爲 AS):AS 1 和 AS 2。而 所謂的一個自治系統,指的是一個組織管轄下的所有 IP 網絡和路由器的全體。你可以把它想象 成一個小公司裏的所有主機和路由器。在正常情況下,自治系統之間不會有任何“來往”。

比如,AS 1 裏面的主機 10.10.0.2,要訪問 AS 2 裏面的主機 172.17.0.3 的話。它發出的 IP 包,就會先到達自治系統 AS 1 上的路由器 Router 1。

而在此時,Router 1 的路由表裏,有這樣一條規則,即:目的地址是 172.17.0.2 包,應該經過 Router 1 的 C 接口,發往網關 Router 2(即:自治系統 AS 2 上的路由器)。 所以 IP 包就會到達 Router 2 上,然後經過 Router 2 的路由表,從 B 接口出來到達目的主機 172.17.0.3。

所謂 BGP,就是在大規模網絡中實現節點路由信息共享的一種協議。

Calico 項目與 Flannel 的 host-gw 模式的另一個不同之處

 

 

爲 Calico 打開 IPIP 模式。

儘管這條規則的下一跳地址仍然是 Node 2 的 IP 地址,但這一次,要負責將 IP 包 發出去的設備,變成了 tunl0。注意,是 T-U-N-L-0,而不是 Flannel UDP 模式使用的 T-UN-0(tun0),這兩種設備的功能是完全不一樣的。

兩種將宿主機網關設置成 BGP Peer 的解決方案

 

第一種方案,就是所有宿主機都跟宿主機網關建立 BGP Peer 關係。 這種方案下,Node 1 和 Node 2 就需要主動跟宿主機網關 Router 1 和 Router 2 建立 BGP 連 接。從而將類似於 10.233.2.0/24 這樣的路由信息同步到網關上去。 需要注意的是,這種方式下,Calico 要求宿主機網關必須支持一種叫作 Dynamic Neighbors 的 BGP 配置方式。這是因爲,在常規的路由器 BGP 配置裏,運維人員必須明確給出所有 BGP Peer 的 IP 地址。考慮到 Kubernetes 集羣可能會有成百上千個宿主機,而且還會動態地添加和 刪除節點,這時候再手動管理路由器的 BGP 配置就非常麻煩了。而 Dynamic Neighbors 則允 許你給路由器配置一個網段,然後路由器就會自動跟該網段裏的主機建立起 BGP Peer 關係。 不過,相比之下,我更願意推薦第二種方案。

 

這種方案,是使用一個或多個獨立組件負責蒐集整個集羣裏的所有路由信息,然後通過 BGP 協 議同步給網關。而我們前面提到,在大規模集羣中,Calico 本身就推薦使用 Route Reflector 節點的方式進行組網。所以,這裏負責跟宿主機網關進行溝通的獨立組件,直接由 Route Reflector 兼任即可。 更重要的是,這種情況下網關的 BGP Peer 個數是有限並且固定的。所以我們就可以直接把這些 獨立組件配置成路由器的 BGP Peer,而無需 Dynamic Neighbors 的支持。 當然,這些獨立組件的工作原理也很簡單:它們只需要 WATCH Etcd 裏的宿主機和對應網段的 變化信息,然後把這些信息通過 BGP 協議分發給網關即可。

 

Kubernetes 裏的 Pod 默認都是“允許所有”(Accept All)的,即:Pod 可以接收來自任何發送方的請求;或者,向任何接收方發送請求。而如果你 要對這個情況作出限制,就必須通過 NetworkPolicy 對象來指定。

NetworkPolicy 定義的規則,其實就是“白名單”

三種並列的情況,分別是:ipBlock、 namespaceSelector 和 podSelector。

安裝 Flannel + Calico 的流程非常簡單

https://docs.projectcalico.org/v3.2/getting-started/kubernetes/installation/flannel

 

 

一 組 Pod 實例之間總會有負載均衡的需求

 

IPVS 模塊只負責上述的負載均衡和代理功能。而一個完整的 Service 流程 正常工作所需要的包過濾、SNAT 等操作,還是要靠 iptables 來實現。只不過,這些輔助性的 iptables 規則數量有限,也不會隨着 Pod 數量的增加而增加。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章