CNI
容器網路介面,就是在網路解決方案由網路外掛提供,這些外掛配置容器網路則透過CNI定義的介面來完成,也就是CNI定義的是容器執行環境與網路外掛之間的介面規範。這個介面只關心容器的網路連線,在建立容器是分配網路,刪除容器是移除網路。外掛就是對CNI的規範的具體實現。
Network Namespace提供的是什麼
這裡我們簡要回顧一下,容器具有自己的網路協議棧而且被隔離在它自己的網路名稱空間內,在這個隔離的網路空間裡會為容器提供網路卡、迴環裝置、IP地址、路由表、防火牆規則等這些基本的網路環境。
為什麼POD中的容器共享同一網路名稱空間
每一個POD中都有一個特殊的容器且永遠是POD中建立的第一個容器,只是我們檢視的時候看不到,因為它完成作用就暫停了。這個容器就是Infra容器,使用匯編語言編寫。當建立POD的時kubernetes先建立名稱空間,然後把其中的網路名稱空間和這個Infra關聯,後面屬於你的建立的容器都是透過Join的方式與這個Infra容器關聯在一起,這樣這些容器和Infra都屬於一個網路名稱空間,這也就是為什麼POD中多個容器可以使用本地通訊的原因,所以嚴格意義來說網路名稱空間屬於Infra。我們看下面的演示
我們啟動POD
登陸Srv03
檢視這個程式所屬的名稱空間,如下圖:
上圖說容器名稱空間,其實是那個程式的名稱空間,名稱空間名稱-->該名稱空間的檔案描述符。
就拿net來說,它為程式提供了一個完全獨立的網路協議棧檢視,包括網路介面、IPV4/6協議棧、IP路由表、防火牆規則等,也就是為程式提供一個獨立的網路環境。
進入POD中的容器,檢視這個名稱空間
可以看到連結的檔案的指向的都是一樣的,我這裡的POD只有一個容器,如果設定2個你就可以分別進入檢視,指向都是一樣的。
上面的結果其實為程式建立的名稱空間,Infra容器抓住網路名稱空間,然後POD中的容器JOIN到Infra中,這樣POD中的容器就能使用這個網路名稱空間。所以Kubernetes的網路外掛考慮的不是POD中容器的網路配置,而是配置這個POD的Network Namespace。
Infra是為了給容器共享網路名稱空間,當然也可以共享volume。
理解了為什麼POD裡面的容器可以進行本地通訊的原因,我們就要向下看一層,是誰給這個Infra使用的網路名稱空間提供的各種網路棧配置呢?
誰為容器提供的網路配置
我們這裡以網橋模式為例來說明。容器就是程式,容器擁有隔離的網路名稱空間,就意味著程式所在的網路名稱空間也是隔離的且和外界不能聯絡,那麼如何讓實現和其他容器互動呢?你就理解為2臺獨立的主機要想通訊就把它們連線到一個交換機上,透過一箇中間裝置來連線兩個獨立的網路。這個就是能起到交換機作用的網橋,Linux中就有這樣一個虛擬裝置。有了這樣一個裝置那麼就要解決誰來建立、誰來連線兩個容器的網路名稱空間的問題。
Docker專案
在Dcoker專案中,這個網橋是由docker來建立的且安裝完之後就會預設建立一個這樣的裝置,預設叫做docker0,這種網橋並不是docker的專利,是屬於Linux核心就支援的功能,docker只是透過某些介面呼叫建立這樣一個網橋並且命名為docker0。
網橋是一個二層裝置,傳統網橋在處理報文時只有2個操作,一個是轉發;一個是丟棄。當然網橋也會做MAC地址學習就像交換機一樣。但是Linux中的虛擬網橋除了具有傳統網橋的功能外還有特殊的地方,因為執行虛擬網橋的是一個執行著Linux核心的物理主機,有可能網橋收到的報文的目的地址就是這個物理主機本身,所以這時候處理轉發和丟棄之外,還需要一種處理方式就是交給網路協議棧的上層也就是網路層,從而被主機本身消化,所以Linux的虛擬網橋可以說是一個二層裝置也可以說是一個三層裝置。另外還有一個不同的地方是虛擬網橋可以有IP地址。
充當連線容器到網橋的虛擬介質叫做Veth Pair。這個東西一頭連線在容器的eth0上,一頭連線在網橋上,而且連線在網橋上的這一端沒有網路協議棧功能可以理解為就是一個普通交換機的埠,只有一個MAC地址;而連線在容器eth0上的一端則具有網站的網路協議棧功能。
完成上述功能就是透過Libnetwork來實現的:
Libnetwork是CNM原生實現的,它為Docker daemon和網路驅動之間提供了介面,網路控制器負責將驅動和一個網路對接,每個驅動程式負責管理它擁有的網路以及為網路提供各種負責,比如IPAM,這個IPAM就是IP地址管理。
Docker daemon啟動也就是dockerd這個程式,它會建立docker0,而docker0預設用的驅動就是bridge,也就是建立一個虛擬網橋裝置,且具有IP地址,所以dockerd啟動後你透過ifconfig 或者 ip addr
可以看到docker0的IP地址。
接下來就可以建立容器,我們使用docker
這個客戶端工具來執行容器,容器的網路棧會在容器啟動前建立完成,這一系列的工作都由Libnetwork的某些介面(底層還是Linux系統呼叫)來實現的,比如建立虛擬網路卡對(Veth pair),一端連線容器、一端連線網橋、建立網路名稱空間、關聯容器中的程式到其自己的網路名稱空間中、設定IP地址、路由表、iptables等工作。
在dockerd這個程式中有一個引數叫做--bridige和--bip,它就是設定使用哪個網橋以及網橋IP,如下:
dockerd --bridge=docker0 --bip=X.X.X.X/XX
透過上面的描述以及這樣的設定,啟動的容器就會連線到docker0網橋,不寫--bridge預設也是使用docker0,--bip則是網橋的IP地址,而容器就會獲得和網橋IP同網段的IP地址。
上述關於網路的東西也沒有什麼神秘的,其實透過Linux命令都可以實現,如下就是在主機上建立網橋和名稱空間,然後進入名稱空間配置該空間的網路卡對、IP等,最後退出空間,將虛擬網路卡對的一端連線在網橋上。
# 建立網橋
brctl addbr
# 建立網路名稱空間
ip netns add
# 進入名稱空間
ip netns exec bash
# 啟動lo
ip link set lo up
# 在建立的名稱空間中建立veth pair
ip link add eth0 type veth peer name veth00001
# 為eth0設定IP地址
ip addr add x.x.x.x/xx dev eth0
# 啟動eth0
ip link set eth0 up
# 把網路名稱空間的veth pair的一端放到主機名稱空間中
ip link set veth00001 netns
# 退出名稱空間
exit
# 把主機名稱空間的虛擬網路卡連線到網橋上
brctl addif veth0001
# 在主機上ping網路名稱空間的IP地址
ping x.x.x.x
Kubernetes專案
但是對於Kubernetes專案稍微有點不同,它自己不提供網路功能(早期版本有,後來取消了)。所以就導致了有些人的迷惑尤其是初學者,自己安裝的Kubernetes叢集使用的cni0這個網橋,有些人則使用docker0這個網橋。其實這就是因為Kubernetes專案本身不負責網路管理也不為容器提供具體的網路設定。而網路外掛的目的就是為了給容器設定網路環境。
這就意味著在Kubernetes中我可以使用docker作為容器引擎,然後也可以使用docker的網路驅動來為容器提供網路設定。所以也就是說當你的POD啟動時,由於infra是第一個POD中的容器,那麼該容器的網路棧設定是由Docker daemon呼叫Libnetwork介面來做的。那麼我也可以使用其他的能夠完成為infra設定網路的其他程式來執行這個操作。所以CNI Plugin它的目的就是一個可執行程式,在容器需要為容器建立或者刪除網路是進行呼叫來完成具體的操作。
所以這就是為什麼之前要說一下POD中多容器是如何進行本地通訊的原因,你設定POD的網路其實就是設定Infra這個容器的Network Namespace的網路棧。
在Kubernetes中,kubelet主要負責和容器打交道,而kubelet並不是直接去操作容器,它和容器引擎互動使用的是CRI(Container Runtime Interface,它規定了執行一個容器所必須的引數和標準)介面,所以只要容器引擎提供了符合CRI標準的介面那麼可以被Kubernetes使用。那麼容器引擎會把介面傳遞過來的資料進行翻譯,翻譯成對Linux的系統呼叫操作,比如建立各種名稱空間、Cgroups等。而kubelet預設使用的容器執行時是透過kubelet命令中的
--container-runtime=
來設定的,預設就是docker。
如果我們不使用docker為容器設定網路棧的話還可以怎麼做呢?這就是CNI,kubelet使用CNI規範來配置容器網路,那麼同理使用CNI這種規範也是透過設定Infra容器的網路棧實現POD內容器共享網路的方式。那麼具體怎麼做呢?在kubelet啟動的命令中有如下引數:
-
--cni-bin-dir=STRING
這個是用於搜尋CNI外掛目錄,預設/opt/cni/bin -
--cni-conf-dir=STRING
這個是用於搜尋CNI外掛配置檔案路徑,預設是/opt/cni/net.d -
--network-plugin=STRING
這個是要使用的CNI外掛名,它就是去--cni-bin-dir
目錄去搜尋
我們先看看CNI外掛有哪些,官網分了三大類,如下圖:
-
Main外掛:這就是具體建立網路裝置的二進位制程式檔案,比如birdge就是建立Linux網橋的程式、ptp就是建立Veth Pair裝置的程式、loopback就是建立lo裝置的程式,等等。
-
IPAM外掛:負責分配IP地址的程式檔案,比如dhcp就是會向DHCP伺服器發起地址申請、host-local就是會使用預先設定的IP地址段來進行分配,就像在dockerd中設定--bip一樣。
-
Meta其他外掛:這個是由CNI社群維護的,比如flannel就是為Flannel專案提供的CNI外掛,不過這種外掛不能獨立使用,必須呼叫Main外掛使用。
所以要用這些東西就要解決2個問題,我們以flannel為例,一個是網路方案本身如何解決跨主機通訊;另外一個就是如何配置Infra網路棧並連線到CNI網橋上。
下面我們以flannel為例的網橋模式為例說一下流程
結合使用docker容器引擎以及flannel外掛來說就是,kubelet透過CRI介面建立POD,它會第一個建立Infra容器,這一步是呼叫Dokcer對CRI的實現(dockershim),dockershim呼叫docker api來完成,然後就會設定網路,首選需要準備CNI外掛引數、其次就是傳遞引數並呼叫這個CNI外掛去設定Infra網路。對於flannel外掛所需要的引數包含2個部分:
-
kubelet透過CRI呼叫dockershim時候傳遞的一組資訊,也就是具體動作比如ADD或者DEL以及這些動作所需要的引數,如果是ADD的話則包括容器網路卡名稱、POD的Network namespace路徑、容器的ID等。
-
dockershim從CNI配置檔案中載入到的(在CNI中叫做Network Configuration),也就是從
--cni-conf-dir
目錄中載入的預設配置資訊。
有了上面2部分資訊之後,dockershim就會把這個資訊給flannel外掛,外掛會對第二部分資訊做補充,然後這個外掛來做ADD操作,而這個操作是由CNI bridge這個外掛來完成的。這個bridge使用全部的引數資訊來執行把容器加入到CNI網路的操作了。它會做如下內容:
-
檢查是否有CNI網橋,如果沒有就建立並UP這個網橋,相當於在宿主機上執行
ip link add cni0 type bridge
、ip link set cni0 up
命令 -
接下來bridge會透過傳遞過來的Infra容器的網路名稱空間檔案進入這個網路名稱空間中,然後建立Veth Pair裝置,然後UP容器的這一端eth0,然後將另外一端放到宿主機名稱空間裡並UP這個裝置。
-
bridge把宿主機的一端連線到網橋cni0上
-
bridge會呼叫IPAM外掛為容器的eht0分配IP地址,並設定預設路由。
-
bridge會為CNI網橋新增IP地址,如果是第一次的話。
-
最後CNI外掛會把容器的IP地址等資訊返回給dockershim,然後被kubelet新增到POD的status欄位中。
現在我們再來回顧一下之前那個問題,我的環境是kubernetes、flannel、docker都是二進位制程式安裝透過系統服務形式啟動。我沒有/opt/cni/bin目錄,也就是沒有任何CNI外掛,所以也就沒有設定--network-plugin。那透過kubernetes部署的POD是如何被設定網路的呢?
透過之前的介紹我們知道了kubelet透過CRI與容器引擎打交道來操作容器,那麼kubelet預設是用容器執行時就是docker,因為kubelet啟動引數--container-runtime就是docker,而docker又提供了符合CRI規範的介面那麼kubelet就自然可以讓容器執行在docker引擎上。接下來說網路,--network-plugin沒有設定,那kubelet到底用的什麼網路外掛呢?而CNI外掛裡面也沒有docker外掛這個東西。我們看一下這個引數說明如下:
The name of the network plugin to be invoked for various events in kubelet/pod lifecycle. This docker-specific flag only works when container-runtime is set to docker.
前半句意思是說這個引數定義POD使用的網路外掛名稱,但是後面半句的意思是如果使用的容器執行時是docker,那麼預設網路外掛就是docker。
二進位制安裝環境下如何使用CNI
我的環境所有元件都是二進位制安裝,以系統服務形式執行。最初是使用docker0網橋。這裡說一下過程:
-
你需要向etcd寫入子網資訊
{"Network":"'${CLUSTER_CIDR}'", "SubnetLen": 24, "Backend": {"Type": "vxlan"}}
。 -
flanneld服務啟動後會去etcd中申請本機使用的子網網段,然後就會寫入到
/run/flannel/subnet.env
檔案中 -
然後flanneld的service檔案中有一條
ExecStartPost=
指令,就是在flanneld啟動後執行的,作用就是呼叫一個指令碼去修改dockerd的--bip引數把獲取的子網資訊寫入 -
啟動docker服務,然後dockerd會根據--bip的資訊設定docker0網橋
在上面這個過程中部署的POD容器的網路協議棧都是docker來設定的,當然是kubelet透過呼叫CRI介面來去和docker互動的,跨主機通訊是由flannel來完成的。
現在我們要在某一臺主機上切換成CNI,需要做如下修改:
-
建立目錄
mkdir -p /opt/cni/{bin,net.d}
-
下載CNI外掛到上面的bin目錄中,然後解壓。
-
在上面的net.d目錄中建立10-flannel.conflist檔案,內容後面再說。
-
停止該主機的docker、kubelet服務,這樣該主機的容器就會被master排程到其他可用主機,不過DaemonSet型別的則不行。
-
修改docker服務檔案的dockerd的啟動引數,去掉那個環境變數,增加--bip=10.0.0.1/16,這個IP隨便,主要是為了不佔用etcd中要使用的地址段,因為最初它用就是etcd中分配的。
-
修改kubelet服務檔案,增加3個引數
--cni-bin-dir=/opt/cni/bin --cni-conf-dir=/opt/cni/net.d --network-plugin=cni
,最後這個一定要寫cni,不要寫flannel,因為這裡我們要用的就是cni,而至於cni會使用什麼外掛,這個在配置檔案中寫。因為在kubernetes中只有2類外掛,一個是cni一個是kubenet,這裡設定的是用哪一類外掛。 -
然後啟動之前停止的服務
10-flannel.conflist檔案內容
{
"name": "cni0",
"cniVersion": "0.4.0",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
還可以簡化為這樣
{
"name": "cni0",
"type": "flannel",
"subnetFile": "/run/flannel/subnet.env",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
}
name: 網路名稱而不是網橋的名字
cniVersion: 外掛版本,可以不寫。
subnetFile: 本機flanneld從etcd中申請的本機網段,這個資訊存放在哪個檔案中,預設就是/run/flannel/subnet.env,我沒有改位置,所以可以省略。
plugins:表示使用什麼外掛
type: 外掛型別,這裡是flannel
delegate:這個CNI外掛並不是自己完成,而是需要呼叫某中內建的CNI外掛完成,對於flannel來說,就是呼叫bridge。flannel只是實現了網路方案,而網路方案對應的CNI外掛是bridge,我們下載的外掛裡面就包括了bridge,所有flannel對應的CNI外掛就已經被內建了,因為flannel方案本身就是使用網橋來做的。
執行過程如下:
kubelet呼叫CRI介面也就是dockershim,它呼叫CNI外掛,也就是/opt/cni/bin中的flannel程式,這個程式需要2部分引數,一部分是dockershim的CNI環境變數也就是具體動作ADD或者DEL,如果是ADD則是把容器新增到CNI網路裡,ADD會有一些引數;另外一部分就是dockershim載入的JSON檔案,然後/opt/cni/bin/flannel程式會對2部分引數做整合以及填充其他資訊比如ipam資訊也就是具體的子網資訊,ipMasp資訊,mtu資訊,這些都是從/run/flannel/subnet.env檔案中讀取的,然後flannel會呼叫CNI的birdge外掛,也就是執行/opt/cni/bin/bridge這個命令,由它來完成將容器加入CNI網路的具體操作。