深入淺出Kubernetes網路:容器網路初探

沃趣科技發表於2019-01-04

前言

隨著雲端計算的興起,各大平臺之爭也落下了帷幕,Kubernetes作為後起之秀已經成為了事實上的PaaS平臺標準,而網路又是雲端計算環境當中最複雜的部分,總是讓人琢磨不透。本文嘗試著圍繞在Kubernetes環境當中同一個節點(work node)上的Pod之間是如何進行網路通訊的這個問題進行展開,暫且不考慮跨節點網路通訊的情況。

| Network Namespace

Namespace

提到容器就不得不提起容器的核心底層技術Namespace,Namespace為Linux核心當中提供的一種隔離機制,最初在2002年引入到Linux 2.4.19當中,且只有Mount Namespace用於檔案系統隔離。截止目前,Linux總共提供了7種Namespace(since Linux 4.6),系統當中執行的每一個程式都與一個Namespace相關聯,且該程式只能看到和使用該Namespace下的資源。可以簡單將Namespace理解為作業系統對程式實現的一種“障眼法”,比如通過UTS Namespace可以實現讓執行在同一臺機器上的程式看到不同的Hostname。正是由於Namespace開創性地隔離方式才讓容器的實現得以變為可能,才能讓我們的軟體真正地實現“build once, running everywhere”。

Network Namespace

Network Namespace是Linux 2.6.24才開始引入的,直到Linux 2.6.29才完成的特性。Network Namespace實際上實現的是對網路棧的虛擬化,且在建立出來時預設只有一個迴環網路介面lo,每一個網路介面(不管是物理介面還是虛擬化介面)都只能存在於一個Network Namespace當中,但可以在不同的Network Namespace之間切換,每一個Network Namespace都有自己獨立的IP地址、路由表、防火牆以及套接字列表等網路相關資源。當刪除一個Network Namespace時,其內部的網路資源(網路介面等)也會同時被刪掉,而物理介面則會被切換回之前的Network Namespace當中。

容器與Pod

在Kubernetes的定義當中,Pod為一組不可分離的容器,且共享同一個Network Namespace,故不存在同一個Pod當中容器間網路通訊的問題,對於同一個Pod當中的容器來講,通過Localhost即可與其他的容器進行網路通訊。

所以同一個節點上的兩個Pod如何進行網路通訊的問題可以轉變為,同一個節點上的兩個容器如何進行網路通訊。

Namespace實操

在回答上面提出的Network Namespace網路通訊的問題前,我們先來做一些簡單的命令列操作,先對Namespace有一個感性地認識,實驗環境如下: 

通過命令lsns可以檢視到宿主機上所有的Namespace(注意需要使用root使用者執行,否則可能會出現有些Namespace看不到的情況): 

lsns預設會輸出所有可以看到的Namespace,簡單解釋一下lsns命令各個輸出列的含義:

與Network Namespace相關性較強的還有另外一個命令 ip netns,主要用於持久化名稱空間的管理,包括Network Namespace的建立、刪除以和配置等。 ip netns命令在建立Network Namespace時預設會在/var/run/netns目錄下建立一個bind mount的掛載點,從而達到持久化Network Namespace的目的,即允許在該名稱空間當中沒有程式的情況下依然保留該名稱空間。Docker當中由於缺少了這一步,玩過Docker的同學就會發現通過Docker建立容器後並不能在宿主機上通過 ip netns檢視到相關的Network Namespace(這個後面會講怎麼才能夠看到,稍微小操作一下就行)。

與Network Namespace相關操作命令:

ip netns add < namespace name > # 新增network namespace
ip netns list  # 檢視Network Namespace
ip netns delete < namespace name > # 刪除Network Namespace
ip netns exec < namespace name > <command> # 進入到Network Namespace當中執行命令

建立名為netA的Network Namespace: 

檢視建立的Network Namespace: 

可以看到Network Namespace netA當中僅有一個環回網路介面lo,且有獨立的路由表(為空)。

宿主機(root network namespace)上有網路介面eth0(10.10.88.170)和eth1(172.16.130.164),此時可以直接ping通IP 172.16.130.164。

嘗試將root network namespace當中的eth0介面新增到network namespce netA當中:

ip link set dev eth0 netns netA

將宿主機上的網路介面eth0(10.10.88.170)加入到網路名稱空間netA後:

1. 宿主機上看不到eth0網路介面了(同一時刻網路介面只能在一個Network Namespace)

2. netA network namespace裡面無法ping通root namespace當中的eth1(網路隔離)

從上面的這些操作我們只是知道了Network Namespace的隔離性,但仍然無法達到我們想要的結果,即讓兩個容器或者說兩個不同的Network Namespace進行網路通訊。在真實的生活場景中,當我們要連線同一個集團兩個相距千里的分公司的區域網時,我們有3種解決方案:第一種是對資料比較隨意的,直接走公網連線,但存在網路安全的問題。第二種是不差錢的,直接拉一根專線將兩個分公司的網路連線起來,這樣雖然遠隔千里,但仍然可以處於一個網路當中。另外一種是兼顧網路安全集和價效比的VPN連線,但存在效能問題。很顯然,不管是哪一種方案都需要有一根“線”將兩端連線起來,不管是虛擬的VPN還是物理的專線。

| vEth(Virtual Ethernet Device)

前面提到了容器通過Network Namespace進行網路隔離,但是又由於Network Namespace的隔離導致兩個不同的Network Namespace無法進行通訊,這個時候我們聯想到了實際生活場景中連線一個集團的兩個分公司區域網的處理方式。實際上Linux當中也存在類似像網線一樣的虛擬裝置vEth(此時是不是覺得Linux簡直無所不能?),全稱為Virtual Ethernet Device,是一種虛擬的類似於乙太網路的裝置。

vEth有以下幾個特點:

  • vEth作為一種虛擬乙太網路裝置,可以連線兩個不同的Network Namespace。

  • vEth總是成對建立,所以一般叫veth pair。(因為沒有隻有一頭的網線)。

  • vEth當中一端收到資料包後另一端也會立馬收到。

  • 可以通過ethtool找到vEth的對端介面。(注意後面會用到)

理解了以上幾點對於我們後面理解容器間的網路通訊就容易多了。


vEth實操

建立vEth:

ip link add < veth name > type veth peer name < veth peer name >

建立名為veth0A,且對端為veth0B的vEth裝置。

可以看到root network namespace當中多出來了兩個網路介面veth0A和veth0B,網路介面名稱@後面的接的正是對端的介面名稱。

建立Network Namespace netA和netB:

分別將介面veth0A加入到netA,將介面veth0B加入到netB:

ip link set veth0A netns netA
ip link set veth0B netns netB

這個時候通過IP a檢視宿主機(root network namespace)網路介面時可以發現,已經看不到介面veth0A和veth0B了(同一時刻一個介面只能處於一個Network Namespace下面)。

再分別到netA和netB兩個Network Namespace當中去檢視,可以看到兩個Network Namespace當中都多了一個網路介面。

分別拉起兩個網路介面並配上IP,這裡將為veth0A配置IP 192.168.100.1,veth0B配置IP 192.168.100.2:

ip netns exec netA ip link set veth0A up
ip netns exec netA ip addr add 192.168.100.1/24 dev veth0A


ip netns exec netB ip addr add 192.168.100.2/24 dev veth0B

測試通過veth pair連線的兩個Network Namespace netA和netB之間的網路連線。

在netA(192.168.100.1)當中ping netB(192.168.100.2): 

在netB(192.168.100.2)當中ping netA(192.168.100.1): 

可以發現netA跟netB這兩個Network Namespace在通過veth pair連線後已經可以進行正常的網路通訊了。

解決了容器Network Namespace隔離的問題,這個時候有云計算經驗或者熟悉OpenStack和ZStack的同學就會發現,現在的場景跟虛擬機器之間的網路互聯是不是簡直一模一樣了?

vEth作為一個二層網路裝置,當需要跟別的網路裝置相連時該怎麼處理呢?在現實生活場景當中我們會拿一個交換機將不同的網線連線起來。實際上在虛擬化場景下也是如此,Linux Bridge和Open vSwith(OVS)是當下比較常用的兩種連線二層網路的解決方案,而Docker當中採用的是Linux Bridge。

| Docker與Kubernetes

Kubernetes作為一個容器編排平臺,在容器引擎方面既可以選擇Docker也可以選擇rkt,這裡直接分別通過Docker和Kubernetes建立容器來進行簡單比對。 Kubernetes在建立Pod時首先會建立一個pause容器,它的唯一作用就是保留和佔據一個被Pod當中所有容器所共享的網路名稱空間(Network Namespace),就這樣,一個Pod IP並不會隨著Pod當中的一個容器的起停而改變。

Docker下的容器網路

我們先來看一下在沒有Kubernetes的情況下是什麼樣子的。在Docker啟動的時候預設會建立一個名為docker0的網橋,且預設配置下采用的網路段為172.17.0.0/16,每一個在該節點上建立的容器都會被分配一個在該網段下的IP。容器通過連線到docker0上進行相互通訊。

手動建立兩個容器:

docker run -it --name testA busybox sh
docker run -it --name testB busybox sh

檢視網路介面狀況。

容器testA: 

容器testB: 

檢視網橋狀態: 

可以發現docker0上面已經連線了兩個虛擬網路介面(vEth)。

在docker0上通過tcpdump抓包:

tcpdump -n -i docker0

可以發現容器testA和容器testB正是通過docker0網橋進行網路包轉發的。

加入Kubernetes後的容器網路

其實加入Kubernetes後本質上容器網路通訊模式並沒有發生變更,但Kubernetes出於網路地址規劃的考慮,重新建立了一個網橋cni0用於取代docker0,來負責本節點上網路地址的分配,而實際的網路段管理由Flannel處理。

下面還是以建立2個執行BusyBox映象的Pod作為例子進行說明。

先給Kubernetes叢集當中的兩個work node打上label以方便將Pod排程到相同的節點上面進行測試:

[root@10-10-88-192 network]# kubectl get node --show-labels
NAME STATUS ROLES AGE VERSION LABELS
10-10-88-170 Ready <none> 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10-10-88-170
10-10-88-192 Ready master 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10-10-88-192,node-role.kubernetes.io/master=
10-10-88-195 Ready <none> 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10-10-88-195
[root@10-10-88-192 network]#
[root@10-10-88-192 network]# kubectl label --overwrite node 10-10-88-170 host=node1node "10-10-88-170" labeled
[root@10-10-88-192 network]#
[root@10-10-88-192 network]# kubectl label --overwrite node 10-10-88-195 host=node2node "10-10-88-195" labeled
[root@10-10-88-192 network]#
[root@10-10-88-192 network]# kubectl get node --show-labels
NAME STATUS ROLES AGE VERSION LABELS
10-10-88-170 Ready <none> 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,host=node1,kubernetes.io/hostname=10-10-88-170
10-10-88-192 Ready master 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10-10-88-192,node-role.kubernetes.io/master=
10-10-88-195 Ready <none> 47d v1.10.5-28+187e1312d40a02 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,host=node2,kubernetes.io/hostname=10-10-88-195
[root@10-10-88-192 network]#

建立兩個Pod並通過新增nodeSelector使其排程到同一個節點(host1)。

編輯Pod的yaml配置檔案:

基於yaml檔案建立Pod:

可以看到兩個Pod都按照預期排程到了10-10-88-170這個節點上面。

通過IP a命令可以看到在Pod的宿主機上多出來了2個vethXXX樣式的網路介面:

[root@10-10-88-170 ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
   valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
   valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether fa:35:b6:5e:ac:00 brd ff:ff:ff:ff:ff:ff
inet 10.10.88.170/24 brd 10.10.88.255 scope global eth0
   valid_lft forever preferred_lft forever
inet6 fe80::f835:b6ff:fe5e:ac00/64 scope link
   valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether fa:88:2a:44:2b:01 brd ff:ff:ff:ff:ff:ff
inet 172.16.130.164/24 brd 172.16.130.255 scope global eth1
   valid_lft forever preferred_lft forever
inet6 fe80::f888:2aff:fe44:2b01/64 scope link
   valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
link/ether 02:42:43:a1:fc:ad brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 scope global docker0
   valid_lft forever preferred_lft forever
5: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN
link/ether 0e:c4:2c:84:a5:ea brd ff:ff:ff:ff:ff:ff
inet 10.244.2.0/32 scope global flannel.1
   valid_lft forever preferred_lft forever
inet6 fe80::cc4:2cff:fe84:a5ea/64 scope link
   valid_lft forever preferred_lft forever
6: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP qlen 1000
link/ether 0a:58:0a:f4:02:01 brd ff:ff:ff:ff:ff:ff
inet 10.244.2.1/24 scope global cni0
   valid_lft forever preferred_lft forever
inet6 fe80::f0a0:7dff:feec:3ffd/64 scope link
   valid_lft forever preferred_lft forever
9: veth2a69de99@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP
link/ether 86:70:76:4f:de:2b brd ff:ff:ff:ff:ff:ff link-netnsid 2
inet6 fe80::8470:76ff:fe4f:de2b/64 scope link
   valid_lft forever preferred_lft forever
10: vethc8ca82e9@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP
link/ether 76:ad:89:ae:21:68 brd ff:ff:ff:ff:ff:ff link-netnsid 3
inet6 fe80::74ad:89ff:feae:2168/64 scope link
   valid_lft forever preferred_lft forever
39: veth686e1634@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP
link/ether 66:99:fe:30:d2:e1 brd ff:ff:ff:ff:ff:ff link-netnsid 4
inet6 fe80::6499:feff:fe30:d2e1/64 scope link
   valid_lft forever preferred_lft forever
40: vethef16d6b0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP
link/ether c2:7f:73:93:85:fc brd ff:ff:ff:ff:ff:ff link-netnsid 5
inet6 fe80::c07f:73ff:fe93:85fc/64 scope link
   valid_lft forever preferred_lft forever
[root@10-10-88-170 ~]#
[root@10-10-88-170 ~]# brctl show
bridge name    bridge id   STP enabled interfaces
cni0    8000.0a580af40201   no  veth2a69de99
   veth686e1634
   vethc8ca82e9
   vethef16d6b0
docker0    8000.024243a1fcad   no
[root@10-10-88-170 ~]#

此時兩個Pod的網路連線如圖所示:

網路包從Container A傳送到Container B的過程如下:

1. 網路包從busybox1的eth0發出,並通過vethef16d6b0進入到root netns(網路包從vEth的一端傳送後另一端會立馬收到)。

2. 網路包被傳到網橋cni0,網橋通過傳送“who has this IP?”的ARP請求來發現網路包需要轉發到的目的地(10.244.2.208)。

3. busybox2回答到它有這個IP,所以網橋知道應該把網路包轉發到veth686e1634(busybox2)。

4. 網路包到達veth686e1634介面,並通過vEth進入到busybox2的netns,從而完成網路包從一個容器busybox1到另一個容器busybox2的過程。

對於以上流程有疑問的同學也可以自己動手驗證一下結論,最好的方式就是通過tcpdump命令在各個網路介面上進行抓包驗證,看網路包是如何經過網橋再由veth pair流轉到另一個容器網路當中的。

| 結語

容器網路在很大程度上依託於虛擬網路的發展,這也正是技術發展的趨勢所在,正所謂站在巨人的肩膀上。

| 作者簡介

葉龍宇·沃趣科技研發工程師

主要負責公司基於Kubernetes的RDS平臺QFusion的研發, 熟悉雲端計算及Kubernetes技術體系。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28218939/viewspace-2286287/,如需轉載,請註明出處,否則將追究法律責任。

相關文章