K8s 中跨主機 Pod 之間是如何通信的(SDN 使用 Calico)?



K8s 中 Pod 之間是如何通信的(SDN 使用 Calico)?
目前只接觸過 calico,所以默認 SDN 實現為 calico,下文不在贅述。

簡單介紹
在 Kubernetes 中,每個 Pod 都會被分配一個唯一的 IP 地址,并且這些 IP 地址將用于在 Pod 之間進行通信。

Pod 通信本質上是 不同機器上的兩個 network namespace 通信, network namespace  通過 veth pair 會在容器內部和宿主機映射一對虛擬網卡(veth pair)。這對虛擬網卡類似一個通道一樣,一端在容器,一端在宿主機,可以直接通信,在部署的好的 K8s 集群中,可以在節點上看到好多虛擬網卡,這些就是 veth pair  宿主機的虛擬網卡。

Calico 使用 Linux 內核的網絡堆棧來實現網絡功能(宿主機的 calico 組件的 Felix 程序會在內核的路由表里面寫入數據,注明這個IP出去時下一跳地址和進來時的由那個網卡解析), 同時路由程序會獲取ip變換,通過 BPG 路由協議擴散到其他宿主機上,這里也包括使用代理 ARP 來處理 Pod 到節點的 ARP 請求。

宿主機,也就是工作節點,可以看做是一個路由器。pod 可以看做是連接到路由器上的網絡終端。

報文路徑跟蹤
下面的思維導圖為 兩個不同節點 Pod 報文的訪問路徑


248325bk-1.png


這里創建兩個 Pod ,簡單分析一下,編寫 YAML 文件通過拓撲分布約束調度在不同的節點。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-deployment
  labels:
    app: os
spec:
  replicas: 2
  selector:
    matchLabels:
      app: os
  template:
    metadata:
      labels:
        app: os
    spec:
      containers:
      - name: centos
        image: centos:latest
        args:
         - tail
         - -f
         - /dev/null
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: os
應用之后,查看 Pod 信息

┌──[root@vms100.liruilongs.github.io]-[~/docker]
└─$kubectl get pods -n demo -o wide
NAME                               READY   STATUS    RESTARTS   AGE   IP              NODE                          NOMINATED NODE   READINESS GATES
demo-deployment-6cbdbd86d5-fbt9d   1/1     Running   0          17m   10.244.169.66   vms105.liruilongs.github.io   <none>           <none>
demo-deployment-6cbdbd86d5-nm467   1/1     Running   0          16m   10.244.38.174   vms103.liruilongs.github.io   <none>           <none>
┌──[root@vms100.liruilongs.github.io]-[~/docker]
└─$
分別調度到了不同節點:

vms105.liruilongs.github.io :demo-deployment-6cbdbd86d5-fbt9d
vms103.liruilongs.github.io :demo-deployment-6cbdbd86d5-nm467
進入容器查看 Pod IP 信息

demo-deployment-6cbdbd86d5-fbt9d Pod 對應 IP 為 :10.244.169.66, 生成的 veth pair容器側的虛擬網卡為 eth0@if16

┌──[root@vms100.liruilongs.github.io]-[~/docker]
└─$kubectl exec -it demo-deployment-6cbdbd86d5-fbt9d -n demo -- bash
[root@demo-deployment-6cbdbd86d5-fbt9d /]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    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
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP group default
    link/ether 42:17:dd:38:6a:10 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.169.66/32 scope global eth0
       valid_lft forever preferred_lft forever
[root@demo-deployment-6cbdbd86d5-fbt9d /]# exit
exit
demo-deployment-6cbdbd86d5-nm467 Pod 對應 IP 為 10.244.38.174 ,生成的 veth pair容器側的虛擬網卡為 eth0@if34

┌──[root@vms100.liruilongs.github.io]-[~/docker]
└─$kubectl exec -it demo-deployment-6cbdbd86d5-nm467 -n demo -- bash
[root@demo-deployment-6cbdbd86d5-nm467 /]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    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
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP group default
    link/ether 36:a2:81:c4:84:f9 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.38.174/32 scope global eth0
       valid_lft forever preferred_lft forever
[root@demo-deployment-6cbdbd86d5-nm467 /]# exit
exit
進入 demo-deployment-6cbdbd86d5-nm467 Pod 簡單做 ping 測試,來看一下這個 ICMP 包是如何出去的。

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$kubectl exec -it demo-deployment-6cbdbd86d5-nm467 -n demo -- bash
[root@demo-deployment-6cbdbd86d5-nm467 /]# ping -c 3  10.244.169.66
PING 10.244.169.66 (10.244.169.66) 56(84) bytes of data.
64 bytes from 10.244.169.66: icmp_seq=1 ttl=62 time=0.497 ms
64 bytes from 10.244.169.66: icmp_seq=2 ttl=62 time=0.460 ms
64 bytes from 10.244.169.66: icmp_seq=3 ttl=62 time=0.391 ms

--- 10.244.169.66 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2044ms
rtt min/avg/max/mdev = 0.391/0.449/0.497/0.047 ms
當前容器 IP 為 10.244.38.174 , ping 側的容器 IP 為 10.244.169.66,不在同一個網絡內,所以當前容器會在路由表獲取一下跳地址.

查看容器路由信息

[root@demo-deployment-6cbdbd86d5-nm467 /]# ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
下一跳地址為 169.254.1.1 ,這是預留的本地 IP 網段,這里的容器里的路由規則在所有的容器都是一樣的,不需要動態更新.

容器會查詢下一跳 168.254.1.1 的 MAC 地址,這個 ARP 請求(查找目標設備的 MAC 地址)會如何發出?

這里通過 veth pair 發出,容器內部的虛擬網卡eth0@if34 發到宿主節點的對應的虛擬網卡cali7a4b00317e6

如何確定一對 veth pair 虛擬網卡?

[root@demo-deployment-6cbdbd86d5-nm467 /]# ethtool -S eth0
NIC statistics:
     peer_ifindex: 34
     rx_queue_0_xdp_packets: 0
     rx_queue_0_xdp_bytes: 0
     rx_queue_0_xdp_drops: 0
[root@demo-deployment-6cbdbd86d5-nm467 /]#
在容器內部我們通過 ethtool -S eth0 可以查看到網卡索引為 34。

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$ansible-console -i host.yaml --limit 192.168.26.103
Welcome to the ansible console.
Type help or ? to list commands.

root@all (1)[f:5]# ip a | grep 34
192.168.26.103 | CHANGED | rc=0 >>
34: cali7a4b00317e6@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP
root@all (1)[f:5]# ifconfig cali7a4b00317e6
192.168.26.103 | CHANGED | rc=0 >>
cali7a4b00317e6: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1480
        inet6 fe80::ecee:eeff:feee:eeee  prefixlen 64  scopeid 0x20<link>
        ether ee:ee:ee:ee:ee:ee  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0      
       
在宿主節點上對應的索引的虛擬網卡即為 veth pair 的另一端,由前面可知,這個pod 調度到了  192.168.26.103 節點,進入節點可以獲取到索引對應的虛擬網卡。

這里小伙伴會發現這個虛擬網卡沒有隨機 MAC 地址,所有的 MAC 地址為 ee:ee:ee:ee:ee:ee , 也沒有IP地址。






向所在的節點發送 ARP 請求后,節點上的代理 ARP 進程將接收到這個請求,應答報文中MAC地址是自己的MAC地址,容器的后續報文 IP 地址還是 目的容器,但是 MAC 地址就變成了主機上該網卡的地址,也就是說,所有的報文都會發給主機,主機根據IP地址再進行轉發.(這里不是特別清晰,和書里的有些出入,書的這部分感覺有點問題)

主機上這塊網卡不管 ARP 請求的內容,直接用自己的 MAC 地址作為應答的行為被稱為 ARP proxy ,可以通過以下內核參數檢查

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$ansible-console -i host.yaml --limit 192.168.26.103
Welcome to the ansible console.
Type help or ? to list commands.

root@all (1)[f:5]# cat /proc/sys/net/ipv4/conf/cali7a4b00317e6/proxy_arp
192.168.26.103 | CHANGED | rc=0 >>
1
root@all (1)[f:5]#
可以認為 Calico 把主機作為容器的默認網關使用,所有的報文發到主機,主機根據路由表進行轉發。和經典的網絡架構不同的是,Calico 并沒有給默認網關配置一個 IP 地址,而是通過 ARP proxy 和修改容器路由表的機制實現。

主機上的 cali7a4b00317e6 網卡接收到報文之后,所有的報文會根據路由表轉發,查看節點的路由表

root@all (1)[f:5]# route
192.168.26.103 | CHANGED | rc=0 >>
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         gateway         0.0.0.0         UG    0      0        0 ens32
10.244.31.64    vms106.liruilon 255.255.255.192 UG    0      0        0 tunl0
10.244.38.128   0.0.0.0         255.255.255.192 U     0      0        0 *
10.244.38.151   0.0.0.0         255.255.255.255 UH    0      0        0 cali39ccd735ea3
10.244.38.154   0.0.0.0         255.255.255.255 UH    0      0        0 calif39455d9c24
10.244.38.174   0.0.0.0         255.255.255.255 UH    0      0        0 cali7a4b00317e6
10.244.63.64    vms102.liruilon 255.255.255.192 UG    0      0        0 tunl0
10.244.169.64   vms105.liruilon 255.255.255.192 UG    0      0        0 tunl0
10.244.198.0    vms101.liruilon 255.255.255.192 UG    0      0        0 tunl0
10.244.239.128  vms100.liruilon 255.255.255.192 UG    0      0        0 tunl0
link-local      0.0.0.0         255.255.0.0     U     1002   0        0 ens32
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
192.168.26.0    0.0.0.0         255.255.255.0   U     0      0        0 ens32
訪問 IP 為 10.244.169.66,可以看到匹配這一條路由

root@all (1)[f:5]# route | grep 169
192.168.26.103 | CHANGED | rc=0 >>
10.244.169.64   vms105.liruilon 255.255.255.192 UG    0      0        0 tunl0
root@all (1)[f:5]# ip route | grep 169
192.168.26.103 | CHANGED | rc=0 >>
10.244.169.64/26 via 192.168.26.105 dev tunl0 proto bird onlink
。。。。
10.244.169.64/26: 表示一個 IP 地址范圍,其中 /26(255.255.255.192) 表示子網掩碼,確定了網絡的大小。具體來說,這個 IP 地址范圍包括從 10.244.169.64 到 10.244.169.127 的所有 IP 地址。

通過設備 tunl0 使用協議 bird 和 onlink 選項發送,下一跳IP地址為  192.168.26.105(vms105.liruilon...)。

這里的 bird 和 onlink 是路由表中的兩個選項:

bird:是一種路由協議,它可以幫助路由器動態地學習和適應網絡拓撲結構的變化。Calico 使用 BGP 協議來實現網絡功能,Bird 可以用于實現 BGP 路由器.
onlink:選項表示下一跳 IP 地址是直接可達的,也就是說,它是在同一子網內的。如果下一跳 IP 地址不在同一子網內,則需要使用網關來轉發數據包。
然后我們來到下一跳 IP 地址對應的工作節點 192.168.26.105

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$kubectl get pods -n demo -o wide
NAME                               READY   STATUS    RESTARTS   AGE     IP              NODE                          NOMINATED NODE   READINESS GATES
demo-deployment-6cbdbd86d5-fbt9d   1/1     Running   0          4h36m   10.244.169.66   vms105.liruilongs.github.io   <none>           <none>
demo-deployment-6cbdbd86d5-nm467   1/1     Running   0          4h36m   10.244.38.174   vms103.liruilongs.github.io   <none>           <none>
┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$
這個地址實際上是 目標 Pod 所在節點的 IP 地址,查看節點的路由信息。

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$ansible-console -i host.yaml --limit 192.168.26.105
Welcome to the ansible console.
Type help or ? to list commands.

root@all (1)[f:5]# ip route
192.168.26.105 | CHANGED | rc=0 >>
default via 192.168.26.2 dev ens32
10.244.31.64/26 via 192.168.26.106 dev tunl0 proto bird onlink
10.244.38.128/26 via 192.168.26.103 dev tunl0 proto bird onlink
10.244.63.64/26 via 192.168.26.102 dev tunl0 proto bird onlink
blackhole 10.244.169.64/26 proto bird
10.244.169.65 dev calid5e76ad523e scope link
10.244.169.66 dev cali39888f400bd scope link
10.244.169.70 dev cali0fdeca04347 scope link
10.244.169.73 dev cali7cf9eedbe64 scope link
10.244.169.77 dev calia011d753862 scope link
10.244.169.78 dev caliaa36e67b275 scope link
10.244.169.90 dev califc24aa4e3bd scope link
10.244.169.119 dev calibdca950861e scope link
10.244.169.120 dev cali40813694dd6 scope link
10.244.169.121 dev calie1fd05af50d scope link
10.244.198.0/26 via 192.168.26.101 dev tunl0 proto bird onlink
10.244.239.128/26 via 192.168.26.100 dev tunl0 proto bird onlink
169.254.0.0/16 dev ens32 scope link metric 1002
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.26.0/24 dev ens32 proto kernel scope link src 192.168.26.105
通過路由信息 會匹配10.244.169.66 dev cali39888f400bd scope link 這個路由規則

root@all (1)[f:5]# ip route | grep 66
192.168.26.105 | CHANGED | rc=0 >>
10.244.169.66 dev cali39888f400bd scope link
root@all (1)[f:5]#
這個規則匹配的是一個IP地址,而不是網段。也就是說,主機上的每個容器都會有一個對應的路由表項。報文被發送到 cali39888f400bd 這個 veth pair

root@all (1)[f:5]# ip a | grep -A 4 cali39888f400bd
192.168.26.105 | CHANGED | rc=0 >>
16: cali39888f400bd@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 9
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever
root@all (1)[f:5]#
然后從 cali39888f400bd 一端發送給目標容器的 eth0@if16。目標容器接收到報文之后,回復 ICMP 報文,應答報文原路返回。

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$kubectl exec -it demo-deployment-6cbdbd86d5-fbt9d -n demo -- bash
[root@demo-deployment-6cbdbd86d5-fbt9d /]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    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
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP group default
    link/ether 42:17:dd:38:6a:10 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.169.66/32 scope global eth0
       valid_lft forever preferred_lft forever
簡單來回顧一下,跨主機的 Pod 如何通信?

本質是 Linux 中兩個不同機器網絡命名空間(network namespace)的通信,通過當前容器(網絡命名空間)和宿主機內的一對虛擬網卡 veth pair ,把數據包發送到宿主節點上(這里涉及到ARP 代理),然后通過匹配路由表(這里是網段匹配),下一跳到 目標Pod 所在宿主節點,到達 目標Pod 宿主節點 上之后,在通過 路由匹配(IP 匹配)到宿主節點上的虛擬網卡(和容器對應的一對虛擬網卡 veth pair ),然后從這個虛擬網卡到容器內部的虛擬網卡,實現請求,之后原路返回。

所以從網絡命名空間角度理解,是容器 network namespace 到節點 root network namespace,然后 節點 root network namespace 到 容器 network namespace

network namespace 簡單介紹
network namespace 在Linux內核 2.6 版本引入,作用是隔離 Linux 系統的網絡設備,以及 IP地址、端口、路由表、防火墻規則等網絡資源。因此,每個網絡 namespace 里都有自己的網絡設備(如IP地址、路由表、端口范圍、/proc/net目錄等) .

從網絡的角度看,network namespace 使得容器非常有用,一個直觀的例子就是:由于每個容器都有自己的(虛擬)網絡設備,并且容器里的進程可以放心地綁定在端口上而不必擔心端口沖突,這就使得在一個主機上同時運行多個監聽80端口的Web服務器變為可能.

初識network namespace
network namespace 可以通過系統調用來創建, 當前 network namespace 的增刪改查功能已經集成到 Linux的 ip 工具的 netns 子命令中。

創建一個名為 netns_demo 的network namespace

┌──[root@liruilongs.github.io]-[~]
└─$ip netns add netns_demo
查看當前系統的 network namespace

┌──[root@liruilongs.github.io]-[~]
└─$ip netns list
netns_demo
創建了一個 network namespace 時,系統會在 /var/run/netns 路徑下面生成一個掛載點

┌──[root@liruilongs.github.io]-[~]
└─$ls /var/run/netns/netns_demo
/var/run/netns/netns_demo
掛載點的作用

方便對 namespace 的管理
使 namespace 即使沒有進程運行也能繼續存在
一個 network namespace 被創建出來后,可以使用 ip netns exec 命令進入,做一些網絡 查詢/配置 的工作。

查看網絡命名空間信息,沒有任何配置,因此只有一塊系統默認的本地回環設備lo。

┌──[root@liruilongs.github.io]-[~]
└─$ip netns exec netns_demo ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
對于刪除 network namespace,可以通過以下命令實現:

┌──[root@liruilongs.github.io]-[~]
└─$ip netns delete netns_demo
┌──[root@liruilongs.github.io]-[~]
└─$ip netns list
┌──[root@liruilongs.github.io]-[~]
└─$
注意,上面這條命令實際上并沒有刪除netns_demo這個network namespace,它只是移除了這個network  namespace對應的掛載點。只要里面還有進程運行著,network  namespace便會一直存在。

配置 network namespace
當嘗試訪問創建的 網絡 命名空間的本地回環地址時,網絡是不通的

┌──[root@liruilongs.github.io]-[~]
└─$ip netns exec netns_demo ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
這是因為本地回環地址對應的網卡設備連接默認是禁用的,需要下面的方式激活。

┌──[root@liruilongs.github.io]-[~]
└─$ip netns exec netns_demo ip  link set dev lo up
激活之后,查看網絡命名空間網卡的詳細信息

┌──[root@liruilongs.github.io]-[~]
└─$ip netns exec netns_demo 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
做 ping 測試

┌──[root@liruilongs.github.io]-[~]
└─$ip netns exec netns_demo ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.047 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.047 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.048 ms
^C
--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2022ms
rtt min/avg/max/mdev = 0.047/0.047/0.048/0.005 ms
┌──[root@liruilongs.github.io]-[~]
└─$
僅有一個本地回環設備是沒法與外界通信的。如果我們想與外界(比如主機上的網卡)進行通信,需要構建一個通道,就需要在 namespace 里再 創建一對虛擬的以太網卡,即所謂的 veth pair。






顧名思義,veth pair 總是成對出現且相互連接,它就像 Linux 的雙向管道(pipe),報文從 veth pair一端進去就會由另一端收到

創建 veth0 和 veth1 這么一對虛擬以太網卡

┌──[root@liruilongs.github.io]-[~]
└─$ip link add veth0 type veth peer name veth1
在默認情況下,它們都在主機的  root network namespce 中,將其中一塊虛擬網卡 veth1 通過 ip link set 命令移動到 之前創建的 network namespace`。

┌──[root@liruilongs.github.io]-[~]
└─$ip link set veth1 netns  netns_demo
查看 創建的網絡命名空間內的網絡設備信息,添加了一塊新的網卡  veth1@if4

┌──[root@liruilongs.github.io]-[~]
└─$ip netns exec netns_demo 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
3: veth1@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether 5a:af:75:80:a6:ba brd ff:ff:ff:ff:ff:ff link-netnsid 0
主機網卡信息查看,多了一塊 veth0@if3 的虛擬網卡

┌──[root@liruilongs.github.io]-[~]
└─$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: ens32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:0c:29:e5:68:68 brd ff:ff:ff:ff:ff:ff
    inet 192.168.26.152/24 brd 192.168.26.255 scope global dynamic ens32
       valid_lft 1377sec preferred_lft 1377sec
    inet6 fe80::20c:29ff:fee5:6868/64 scope link
       valid_lft forever preferred_lft forever
4: veth0@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether be:2a:81:c1:e4:24 brd ff:ff:ff:ff:ff:ff link-netnsid 0
┌──[root@liruilongs.github.io]-[~]
└─$
這兩塊網卡剛創建出來還都是 DOWN 狀態,需要手動把狀態設置成 UP ,這里同時設置 IP 地址

┌──[root@liruilongs.github.io]-[~]
└─$ip netns exec netns_demo ifconfig veth1 10.1.1.1/24 up
┌──[root@liruilongs.github.io]-[~]
└─$ifconfig veth0 10.1.1.2/24 up
ping 測試。在網絡命名空間內 ping 主機的 veth pair 對應的網卡 IP 。

┌──[root@liruilongs.github.io]-[~]
└─$ip netns exec  netns_demo ping 10.1.1.2 -c 3
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.058 ms
64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=0.060 ms
64 bytes from 10.1.1.2: icmp_seq=3 ttl=64 time=0.060 ms

--- 10.1.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2020ms
rtt min/avg/max/mdev = 0.058/0.059/0.060/0.006 ms
┌──[root@liruilongs.github.io]-[~]
└─$
不同 network namespace 之間的 路由表和防火墻規則 等也是隔離的,因此我們剛剛創建的 network namespace 沒法和主機共享路由表和防火墻

需要注意的是,用戶可以隨意將虛擬網絡設備分配到自定義的 network namespace 里,而連接真實硬件的物理設備則只能放在系統的根  network namesapce 中。并且,任何一個網絡設備最多只能存在于一個 network namespace 中。

veth pair 簡單介紹
在 Docker 或 Kubernetes 的環境中,如果在主機上查詢網卡信息的時候,總會出來一大堆虛擬網卡,這些虛擬網卡就是Docker/Kubernetes為容器而創建的。

veth 是虛擬以太網卡(Virtual  Ethernet)的縮寫,veth 設備總是成對的,因此我們稱之為 veth pair

veth pair 一端發送的數據會在另外一端接收,非常像 Linux 的雙向管道。根據這一特性,veth pair 常被用于跨network namespace 之間的通信,即分別將 veth pair 的兩端放在不同的 namespace 里。

僅有 veth  pair 設備,容器是無法訪問外部網絡的。為什么呢?因為從容器發出的數據包,實際上是直接進了 veth pair 設備的協議棧。

如果容器需要訪問網絡,則需要使用網橋等技術將 veth pair 設備接收的數據包通過某種方式轉發出去。在 docker 中,我們通過 網橋的方式訪問,docker 在啟動時會自動創建一個 docker0 的 Linux 網橋.

veth pair的創建和使用
通過下面的命令創建一對 veth pair

┌──[root@liruilongs.github.io]-[~]
└─$ip  link add veth3 type veth peer name veth4
創建的veth pair在主機上表現為兩塊網卡,我們可以通過ip link 命令查看:

┌──[root@liruilongs.github.io]-[~]
└─$ip link list
........
5: veth4@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether de:3a:5e:42:2d:ff brd ff:ff:ff:ff:ff:ff
6: veth3@veth4: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether 06:8f:b5:e8:e9:f6 brd ff:ff:ff:ff:ff:ff
新創建的 veth pair 設備的默認 mtu 是 1500,設備初始狀態是 DOWN。我們同樣可以使用 ip link 命令將這兩塊網卡的狀態設置為UP。

┌──[root@liruilongs.github.io]-[~]
└─$ip link set dev veth3 up
┌──[root@liruilongs.github.io]-[~]
└─$ip link set dev veth4 up
veth pair 設備同樣可以配置IP地址,命令如下:

┌──[root@liruilongs.github.io]-[~]
└─$ifconfig veth3 10.20.30.40/24
┌──[root@liruilongs.github.io]-[~]
└─$ifconfig veth4 10.20.30.41/24
┌──[root@liruilongs.github.io]-[~]
└─$
可以將veth pair設備放到namespace中。把 veth4 放到 網絡命令空間 netns_demo 中

┌──[root@liruilongs.github.io]-[~]
└─$ip link  set veth4 netns netns_demo
在命令空間中查看

┌──[root@liruilongs.github.io]-[~]
└─$ip netns exec netns_demo ip link list
......
5: veth4@if6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether de:3a:5e:42:2d:ff brd ff:ff:ff:ff:ff:ff link-netnsid 0
主機環境中,原來索引為 6 的 veth4 虛擬網卡已經看不到了

┌──[root@liruilongs.github.io]-[~]
└─$ifconfig
...........
veth3: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 10.20.30.40  netmask 255.255.255.0  broadcast 10.20.30.255
        inet6 fe80::48f:b5ff:fee8:e9f6  prefixlen 64  scopeid 0x20<link>
        ether 06:8f:b5:e8:e9:f6  txqueuelen 1000  (Ethernet)
        RX packets 8  bytes 648 (648.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 648 (648.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

┌──[root@liruilongs.github.io]-[~]
└─$
veth pair 設備的原理較簡單,就是向veth pair設備的一端輸入數據,數據通過內核協議棧后從 veth pair 的另一端出來。veth pair 的基本工作原理:

veth pair內核實現
在 veth pair 設備上,任意一端(RX)接收的數據都會在另一端(TX)發送出去,veth pair 在轉發過程中不會篡改數據包的內容

容器與 host veth pair 的關系
經典容器組網模型就是 veth pair+bridge 的模式。容器中的 eth0 實際上和外面 host 上的某個 veth 是成對的(pair)關系.

可以通過下面兩種方式來獲取對應關系

方法一
容器里面看 /sys/class/net/eth0/iflink

┌──[root@liruilongs.github.io]-[/]
└─$ docker exec -it  6471704fd03a sh
/ # cat /sys/class/net/eth0/if
ifalias  ifindex  iflink
/ # cat /sys/class/net/eth0/iflink
95
/ # exit
然后,在主機上遍歷 /sys/claas/net 下面的全部目錄,查看子目錄 ifindex 的值和容器里查出來的 iflink 值相當的 veth 名字,這樣就找到了容器和主機的 veth pair 關系。

┌──[root@liruilongs.github.io]-[/]
└─$ grep -c 95 /sys/class/net/*/ifindex | grep 1
/sys/class/net/veth2e08884/ifindex:1
方法二
從上面的命令輸出可以看到 116:eth0@if117,其中116是eth0接口的 index,117是和它成對的veth的index。當host執行下面的命令時,可以看到對應117的veth網卡是哪一個, 這樣就得到了容器和veth pair的關系(這整句忽略哈,忘記調整了 ^_^)

在目標容器里執行以下命令,獲取網卡索引為 94,其中 94 是 eth0 接口的index,95 是和它成對的veth的index。

┌──[root@liruilongs.github.io]-[/]
└─$ docker exec -it  6471704fd03a sh
/ # ip link show eth0
94: eth0@if95: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
/ # exit
通過 95  index 來定位主機上對應的虛擬網卡

┌──[root@liruilongs.github.io]-[/]
└─$ ip link show | grep 95
95: veth2e08884@if94: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT
┌──[root@liruilongs.github.io]-[/]
└─$
關于 K8s 中 Pod 之間是如何通信的(SDN 使用 Calico)?就和小伙伴們分享到這里,生活加油 ^_^

作者:山河已無恙


歡迎關注微信公眾號 :山河已無恙