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