一篇聊透 Kubernetes CNI 基礎知識
前言
在介紹宿主機上的容器網絡時,更多是關注如何解決同一主機上容器的相互訪問和容器對外暴露服務的問題。但是,并沒有涉及怎么解決跨主機的容器之間的互相訪問問題。
對于 Kubernetes 來說,除了實現同一主機上 Pod 的互相通信之外,它還要實現跨主機 Pod 之間的互相通信的問題。針對網絡問題,Kubernetes 定義了一個合格集群網絡的基本要求:
- 所有 Pod 應該可以直接使用 IP 地址與其他 Pod 通信,而無需使用 NAT。
- 所有宿主機都可以直接使用 IP 地址與所有 Pod 通信,而無需使用 NAT。反之亦然。
- Pod 自己“看到”的自己的 IP 地址,和別人(宿主機或者 Pod)看到的地址是完全一樣的。
這個要求除了網絡互通的基本要求外,還有一個要求就是必須直接基于容器和宿主機的 IP 地址來實現的(也就是說 Pod 作為 Kubernetes 中的一等公民,那么它的 IP 地址也是同樣是作為“一等”,可直接訪問的)。為此,Kubernetes 定義了自己的網絡模型,并有一套自己的網絡標準,叫 CNI(Container Network Interface) 。任何人按照這套接口規范,實現一個 CNI 插件,并部署到 Kubernetes 中即可實現 Kubernetes 中 Pod 的互相訪問。總的來說,CNI 插件的最終目的是讓 Kubernetes 中的 Pod 實現網絡互通,它會根據自己的實現創建相應的 bridge 虛擬網絡設備,或者其他虛擬網絡設備,然后再配置相應的路由等方式,最終實現 Pod 間的網絡互通。
“
Docker 采用了 CNM(Container Network Model) 網絡模型。對于 Docker 來說,任何按照 CNM 網絡模型實現了一個驅動的話,就可以應用到 Docker 中實現容器間的通信。CNM 和 CNI 其實本質上并沒有區別,它們相當于實現容器網絡的兩套規范,最終目的都是實現容器的網絡互通。所以 CNI 和 CNM 是獨立的,不相互依賴,所以使用 CNI 插件的時候,我們會看到 CNI 插件可能并不會用 Docker 創建的那些虛擬設備等。從針對網絡模型的實現角度來看的話,CNI 相對于對開發者的約束更少、更開放,不依賴于容器運行時,而 CNM 跟容器運行時綁定嚴重。
而實現一個 CNI 網絡插件只需要一個配置文件和一個可執行文件:
- 配置文件描述 CNI 插件的版本、名稱、描述等基本信息。
- 可執行文件會被上層的容器管理平臺調用。一個 CNI 可執行文件需要實現將容器加入到網絡的 ADD 操作以及將容器從網絡中刪除的 DEL 操作即可(以及一個可選的 VERSION 查看版本操作)。
CNI 插件創建網絡流程
在 Kubernetes 中,CNI 網絡插件的基本工作流程如下,
- Kubelet 組件創建 Pod 的時候,它首先調用 CRI 接口創建并啟動 pause 容器(也會創建對應的網絡命名空間)。
- 在啟動 pause 容器之后,CRI 接口的具體實現(比如 dockershim)中會執行 SetUpPod() 方法。這個方法的主要作用是為 CNI 插件準備參數,并調用 CNI 插件給 pause 容器配置符合預期的網絡棧(Pod 中的其他容器都是復用 pause 容器的網絡),比如網卡(network interface)、路由表(routing table)和 iptables 規則等,可能還需要涉及宿主機上路由等信息的配置。
dockershim 設置的 CNI 環境變量。其中最重要的環境變量參數叫作:CNI_COMMAND。它的取值只有兩種:ADD 和 DEL(ADD 和 DEL 是 CNI 插件唯一需要實現的兩個方法)。
在 CNI 環境變量里,還有一個叫做 CNI_ARGS 的參數。這個參數用于以 key-value 的格式傳遞自定義的信息給 CNI 插件,比如 CNI 插件需要額外的變量就可以使用這個參數。
- dockershim 從 CNI 配置文件里加載到的配置信息(這個配置信息在 CNI 中被叫作 Network Configuration,完整定義可查看:https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration)。dockershim 會把 Network Configuration 以 JSON 數據的格式,通過標準輸入(stdin)的方式傳遞給 CNI 插件。
- 注意:Kubernetes 目前不支持多個 CNI 插件混用,所以如果在
/etc/cni/net.d
(CNI 配置目錄)里放置了多個 CNI 配置文件的話,dockershim 只會加載按字母順序排序的第一個 CNI 配置文件。但是,CNI 允許你在一個 CNI 配置文件里,通過 plugins 字段,定義多個插件進行協作。比如,配置文件中指定 flannel 和 portmap 這兩個插件,那么在之后的執行中,flannel 和 portmap 插件會按照定義順序被調用,從而依次完成配置容器網絡和配置端口映射。
$ cat /etc/cni/net.d/10-flannel.conflist
{
"name": "cbr0",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
- 容器里網卡的名字 CNI_IFNAME(如 eth0)。
- Pod 的 Network Namespace 文件的路徑(CNI_NETNS,即 /proc/<容器進程的PID>/ns/net)。
- 容器的 ID(CNI_CONTAINERID)等。
- ADD 操作的含義是:把容器添加到 CNI 網絡里。對于網橋類型的 CNI 插件來說,意味著把容器以 Veth Pair 的方式插到 CNI 網橋上。CNI 的 ADD 可能還需要的變量有:
- DEL 操作的含義是把容器從 CNI 網絡里移除掉。對于網橋類型的 CNI 插件來說,意味著把容易以 Veth Pair 的方式從網橋上“拔掉”。
- 先為 CNI 插件準備參數。參數分為兩部分,
- 然后調用 CNI 插件為 pause 容器配置網絡(比如調用 /opt/cni/bin/flannel)。
從上面可以看到,在 Kubernetes 中,處理容器網絡相關的邏輯,比如調用 CNI 插件,其實并不在 kubelet 主干代碼里執行,而是會在具體的 CRI(Container Runtime Interface,容器運行時接口)實現里完成。
- 對于 Docker 項目來說,它的 CRI 實現叫作 dockershim,你可以在 kubelet 的代碼里找到它。
- 對于 containerd 來說,是在 cri-plugin 里。
主流的 CNI 網絡實現方案
開篇的時候,我們曾提到 CNI 通過虛擬設備、iptables 規則、路由等方式,最終實現 Pod 與 Pod 之間互相通信的問題。目前,主流的 CNI 網絡實現方案有兩種:
- Overlay:在已有的宿主機網絡之上,借助隧道傳輸技術,比如 VxLAN、ipip 等,構建一層可以將所有容器連通在一起的虛擬網絡。比如將容器的數據包封裝到原宿主機網絡的三層或者四層數據包中,然后使用宿主機網絡的 IP 或者 TCP/UDP 傳輸到目標主機,目標主機拆包后再轉發給目標容器。目前使用隧道傳輸技術的主流 Overlay 容器網絡有 Flannel 等。
圖片
- Underlay:不借助隧道傳輸技術。把容器網絡加到宿主機路由表中,把宿主機網絡設備當作容器網關,通過路由規則轉發到指定的主機,直接實現容器的三層互通。目前通過路由技術實現容器互通的 Underlay 網絡方案有 Flannel host-gw、Calico 等。
圖片
CNI 插件所需的基礎可執行文件
在部署 Kubernetes 的時候,有一個步驟是安裝 kubernetes-cni 包,它的目的就是在宿主機上安裝 CNI 插件所需的基礎可執行文件。這些可執行文件包括(查看 /opt/cni/bin 目錄可以看到):
$ ls -al /opt/cni/bin/
total 73088
-rwxr-xr-x 1 root root 3890407 Aug 17 2017 bridge
-rwxr-xr-x 1 root root 9921982 Aug 17 2017 dhcp
-rwxr-xr-x 1 root root 2814104 Aug 17 2017 flannel
-rwxr-xr-x 1 root root 2991965 Aug 17 2017 host-local
-rwxr-xr-x 1 root root 3475802 Aug 17 2017 ipvlan
-rwxr-xr-x 1 root root 3026388 Aug 17 2017 loopback
-rwxr-xr-x 1 root root 3520724 Aug 17 2017 macvlan
-rwxr-xr-x 1 root root 3470464 Aug 17 2017 portmap
-rwxr-xr-x 1 root root 3877986 Aug 17 2017 ptp
-rwxr-xr-x 1 root root 2605279 Aug 17 2017 sample
-rwxr-xr-x 1 root root 2808402 Aug 17 2017 tuning
-rwxr-xr-x 1 root root 3475750 Aug 17 2017 vlan
按照功能可以分為以下三類:
- 第一類,叫做 Main 插件,它是用來創建具體網絡設備的二進制文件。比如,bridge(網橋設備)、ipvlan、loopback(lo設備)、macvlan、ptp(Veth Pari 設備),以及 vlan。
Flannel、Weave 等項目都屬于網橋類型的 CNI 插件。所以在具體實現中,它們往往會調用 bridge 這個二進制文件。 - 第二類,叫做 IPAM(IP Address Management)插件,它是負責分配 IP 地址的二進制文件。比如,
dhcp 會向 DHCP 服務器發起請求;
- host-local 會使用預先配置的 IP 地址段來進行分配。
- 第三類,是由 CNI 社區維護的內置的 CNI 插件,比如
flannel,這就是專門為 Flannel 項目提供的 CNI 插件;
tunning,是一個通過 sysctl 調整網絡設備參數的二進制文件;
portmap 是一個通過 iptables 配置端口映射的二進制文件;
bandwidth 是一個使用 Token Bucket Filter(TBF)來進行限流的二進制文件。
Flannel 項目對應的 CNI 插件已經被內置了,所以不需要再單獨安裝 CNI 插件(這里的意思是 Flannel 所需要的插件已經在這個安裝包中了)。然而,對于 Weave、Calico 等項目來說,這些并沒有內置,因此如果需要使用它們的話,則必須要把對應的 CNI 插件的可執行文件放到 /opt/cni/bin 目錄中。
Flannel 插件調用流程
- flanneld 啟動后會在每臺宿主機上生成對應的 CNI 配置文件,如下所示:
$ cat /etc/cni/net.d/10-flannel.conflist
{
"name": "cbr0",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
- dockershim 啟動 pause 容器之后,在給 pause 容器配置網絡的時候,會將上述參數傳給 flannel CNI 插件。對于 flannel 說,它會對 dockershim 傳來的 Network Configuration 進行補充。比如將 Delegate 的 IPAM 字段設置為如下所示的內容。10.244.1.0/24 等內容讀取自 Flannel 在宿主機上生成的 Flannel 配置文件(/run/flannel/subnet.env)。
{
"hairpinMode":true,
"ipMasq":false,
"ipam":{
"routes":[
{
"dst":"10.244.0.0/16"
}
],
"subnet":"10.244.1.0/24",
"type":"host-local"
},
"isDefaultGateway":true,
"isGateway":true,
"mtu":1410,
"name":"cbr0",
"type":"bridge"
}
- 接下來,Flannel CNI 插件會調用 bridge 插件(Delegate 字段中的 type 字段),也就是執行:/opt/cni/bin/bridge 二進制文件。并且給 bridge 插件傳兩部分參數,
同時,Flannel CNI 插件還會把 Delegate 字段以 JSON 文件的方式,保存在 /var/lib/cni/flannel 目錄下(給后面刪除容器調用 DEL 操作時使用)。Delegate 這個字段更多是表明要傳給 CNI 插件要調用的另一個 CNI 插件的參數,有這個字段一般表明這個 CNI 插件并不會自己做事兒,而是會調用另外指定的某種內置插件來完成。
第一個部分,還是 CNI 環境變量沒有變化。
第二部分 Network Configuration,正好是上面補充的 Delegate 字段。
- 之后 bridge 插件會在宿主機上檢查 CNI 網橋是否存在,如果沒有,那就創建。類似下面命令的作用,
# 在宿主機上
$ ip link add cni0 type bridge
$ ip link set cni0 up
- 接下來 bridge 插件會通過 pause 容器的 Network Namespace 文件,進入到這個 Network Namespace 中,然后創建一對 Veth Pair 設備。
- 緊接著,它會把這個 Veth Pair 的其中一端,移動到宿主機上。這相當于在容器里執行如下命令,
#在容器里
# 創建一對Veth Pair設備。其中一個叫作eth0,另一個叫作vethb4963f3
$ ip link add eth0 type veth peer name vethb4963f3
# 啟動eth0設備
$ ip link set eth0 up
# 將Veth Pair設備的另一端(也就是vethb4963f3設備)放到宿主機(也就是Host Namespace)里
$ ip link set vethb4963f3 netns $HOST_NS
# 通過Host Namespace,啟動宿主機上的vethb4963f3設備
$ ip netns exec $HOST_NS ip link set vethb4963f3 up
經過上述的操作之后,vethb4963f3 就出現在了宿主機上,而這個 Veth Pair 的另一端,就是容器里的 eth0。上述創建 Veth pair 設備的操作,其實在宿主機上也可以執行,然后再把 Veth Pair 的一端放到容器的 Network Space 里,原理是一樣的。而之所以這樣反著來,是因為 CNI 里對 Namespace 操作函數的設計就是這樣反著來的。而這樣反著來的原因是因為在編程時,容器的 Namespace 是可以直接通過 Namespace 文件拿到的,而 Host Namespace,則是一個隱含在上下文的參數。所以這樣反著來,就是先進入到容器 namespace 里面,然后再反向操作 host namespace,對于編程來說更加方便。
- 接下來,bridge 插件就可以把 vethb4963f3 設備連接在 CNI 網橋上。相當于在宿主機中執行
# 在宿主機上
$ ip link set vethb4963f3 master cni0
- 在將 vethb4963f3 設備連接在 cni0 之后,bridge 插件還會為它設置 Hairpin Mode(發夾模式)(Flannel 插件要在 CNI 配置文件里聲明 hairpinMode=true)。這樣,將來這個集群里的 Pod 才可以通過它自己的 Service 訪問到自己。
默認情況下,網橋設備是不允許一個數據包從一個端口進來后,再從這個端口發出去的,而設置 Hairpin Mode 模式就可以取消這個限制。為什么呢?主要是考慮到容器中通過 NAT (端口映射)的方式,“自己訪問自己”的情況。比如執行docker run -p 8080:80
,就是在宿主機上通過 iptables 設置了一條DNAT(目的地址轉換)轉發規則。這條規則的作用是,當宿主機上的進程訪問“<宿主機的 IP 地址 >:8080”時,iptables 會把該請求直接轉發到“<容器的 IP 地址 >:80”上。如果此時在容器里面訪問宿主機的 8080 端口,那么這個容器里發出的 IP 包會經過 vethb4963f3 設備(端口)和 docker0 網橋,來到宿主機上。此時,根據上述 DNAT 規則,這個 IP 包又需要回到 docker0 網橋,并且還是通過 vethb4963f3 端口進入到容器里。所以,這種情況下,就需要開啟 vethb4963f3 端口的 Hairpin Mode 了。 - 接下來,bridge 插件會調用 ipam 插件,從 ipam.subnet 字段規定的網段里為容器分配一個可用的 IP 地址。然后,bridge 插件就會把這個 IP 地址添加到容器的 eth0 網卡上,同時為容器設置默認路由。相當于在容器中執行:
# 在容器里
$ ip addr add 10.244.0.2/24 dev eth0
$ ip route add default via 10.244.0.1 dev eth0
- 最后 bridge 插件會為 CNI 網橋添加 IP 地址,這相當于在宿主機上執行:
# 在宿主機上
$ ip addr add 10.244.0.1/24 dev cni0
- 在執行完上述操作之后,Flannel 插件會把容器的 IP 地址等信息返回給 dockershim,然后被 kubelet 添加到 Pod 的 Status 字段。
至此,Flannel 插件的 ADD 方法的執行流程結束。總結下,
- 對于網橋類型的 CNI 插件來說,基本是兩個步驟,
給 pause 容器配置相應的網絡棧,比如創建 veth pair,連接到 cni0 bridge 上。
網絡互通方案的實現,比如創建和配置 flannel.1 設備、配置宿主機路由、配置 ARP 和 FDB 表里的信息等。
- 對于非網橋類型的 CNI 插件來說,上述“將容器添加到 CNI 網絡”的操作流程,以及網絡方案本身的工作原理會不太一樣。
注意:cni0 只是接管由上述 Flannel 插件負責創建的容器。如果此時用 docker run 單獨啟動一個容器,Docker 項目是把這個容器連接到 docker0 網橋上。
相關鏈接
- The Layers of the OSI Model Illustrated:https://www.lifewire.com/layers-of-the-osi-model-illustrated-818017
- Container Network Interface (CNI) Specification:https://github.com/containernetworking/cni/blob/main/SPEC.md#container-network-interface-cni-specification
- 使用 Go 從零開始實現 CNI:https://morven.life/posts/create-your-own-cni-with-golang/
- 極客時間.張磊.《深入剖析Kubernetes》