Docker容器基礎入門認知-網路篇

Blackbinbin發表於2021-11-30

這篇文章中,會從 docker 中的單機中的 netns 到 veth,再到單機多個容器之間的 bridge 網路互動,最後到跨主機容器之間的 nat 和 vxlan 通訊過程,讓大家對 docker 中的網路大概有個初步的瞭解。

 

 單機 netns 和 veth

先從 docker 裡所使用的網路ns說起。在不同的容器中,docker 會為每個容器自動分配 ip 地址。並且在宿主機上是可以互相 ping 通的。比如下面我們起兩個 busybox

$ docker run busybox sh -c "while true;do sleep 3600;done;"

這兩個容器中,網路是互通的,並且在任何其他的容器內去 ping 這兩個容器的 ip 也是聯通的,這就說明在整個 docker 網路中,容器和 ip 分配還有相關的路由轉發都是由 docker 內部來進行維護的。我們檢視一下這兩個容器的 ip

第一個容器中:

第二個容器中:

在除此之外的另一個容器中去 ping 172.17.0.2172.17.0.3

在 docker 中,不同的容器之間網路連通也是使用了 linux 的名稱空間,用一個小實驗來說明這裡所用的原理其實就是使用了 veth 來實現名稱空間的互聯

實驗步驟分為以下幾個步驟:

  1. 建立埠
  2. 產生網線
  3. 分配ip

在 docker 中網路的分配也是根據這三個步驟來生成容器 ip 的,首先我們先產生兩個網路虛擬名稱空間

# 產生名稱空間 test1, test2
$ sudo ip netns add test1
$ sudo ip netns add test2

  $ ip netns list
  test2 (id: 3)
  test1 (id: 2) 

產生一對 veth ,也就是所說的網線

# 產生一對 veth (veth 都是成對出現)
sudo ip link add veth1-test1 type veth peer name veth2-test2

將虛擬名稱空間的網路卡和 veth 進行繫結

# 將兩個虛擬網路卡 veth 分配給 test1 和 test2
$ sudo ip link set veth1-test1 netns test1
$ sudo ip link set veth2-test2 netns test2

開啟網路卡,並且嘗試 ping

# 開啟網路卡(因為新加的狀態都是 DOWN)
$ sudo ip netns exec test1 ip link set veth1-test1 up
$ sudo ip netns exec test2 ip link set veth2-test2 up

# 嘗試 ping
$ sudo ip netns exec test1 ping 192.168.1.2 -c 3
PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data.
64 bytes from 192.168.1.2: icmp_seq=1 ttl=64 time=0.034 ms
64 bytes from 192.168.1.2: icmp_seq=2 ttl=64 time=0.045 ms
64 bytes from 192.168.1.2: icmp_seq=3 ttl=64 time=0.513 ms

--- 192.168.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 0.034/0.197/0.513/0.223 ms

這上面只是帶給大家關於 veth 比較淺顯的初步認知,如果有興趣對虛擬網路中的 veth-pair 有深入的瞭解,建議大家看看這個文章:Linux 虛擬網路裝置 veth-pair 詳解

這篇文章詳細講解了 veth 兩端之間資料的聯通底層原理

 

 bridge 網路

除上面所說之外,我相信你瞭解過 docker 的網路,一定也知道有 bridge 的網路模式,bridge 其實起的就是橋樑的作用,可以理解為路由器,負責中轉,連線,路由所有連線在它上面的容器

安裝 bridge 工具

sudo yum install -y bridge-utils

可以檢視 docker 內網橋與各個容器之間連線的關係

$ brctl show
bridge name    bridge id        STP enabled    interfaces
docker0        8000.0242bfb37b66    no        vethc9f5f33
                                              vethfb9006b
# 發現這裡有兩個 veth 連著,這兩個 veth 另一端連著的就是 docker 容器 test1 和 test2 # 在宿主機中列印所有的網路卡 [vagrant@docker
-node2 ~]$ 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 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 group default qlen 1000 link/ether 52:54:00:4d:77:d3 brd ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0 valid_lft 71714sec preferred_lft 71714sec inet6 fe80::5054:ff:fe4d:77d3/64 scope link valid_lft forever preferred_lft forever 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:bf:b3:7b:66 brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:bfff:feb3:7b66/64 scope link valid_lft forever preferred_lft forever 4: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:33:4f:b9 brd ff:ff:ff:ff:ff:ff inet 192.168.205.11/24 brd 192.168.205.255 scope global noprefixroute eth1 valid_lft forever preferred_lft forever inet6 fe80::a00:27ff:fe33:4fb9/64 scope link valid_lft forever preferred_lft forever 8: vethc9f5f33@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default link/ether 8e:12:0f:a0:7e:48 brd ff:ff:ff:ff:ff:ff link-netnsid 1 inet6 fe80::8c12:fff:fea0:7e48/64 scope link valid_lft forever preferred_lft forever 10: vethfb9006b@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default link/ether 16:4f:bc:53:ae:5d brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet6 fe80::144f:bcff:fe53:ae5d/64 scope link valid_lft forever preferred_lft forever

可以看到上面的網路卡 8 和 10 正是連著 docker0 的網橋的 veth,因為 veth 都成對出現的,所以這兩個 veth,名字為 vethc9f5f33 和 vethfb9006b 的 veth,另外一端是連著上面所建立的兩個容器

在上面建立的第一個 name=test1 容器中,可以看到 id=9 的網路卡裝置:

 

這裡連線的就是宿主機中 id=10 的 vethfb9006b。

 

 跨主機容器之間的 nat 和 vxlan 

在講完容器和容器之間的網路連通的底層後,我們再來看看外部訪問和 docker 容器之間是怎麼進行資料交換的?在容器中請求外部網址,他是怎麼做到的呢?當在容器內 ping www.baidu.com 的時候,資料是怎麼互動的呢?

還是在上面的容器內,宿主機 host 的 ip 為 172.31.243.112,如下面所視

[root@izm5e37rlunev9ij58ixy9z ~]# ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:2c:a8:7c:9d  txqueuelen 0  (Ethernet)
        RX packets 22  bytes 1362 (1.3 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 22  bytes 2099 (2.0 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.31.243.112  netmask 255.255.240.0  broadcast 172.31.255.255
        ether 00:16:3e:05:96:e1  txqueuelen 1000  (Ethernet)
        RX packets 2247026  bytes 271820827 (259.2 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5270453  bytes 378089658 (360.5 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1  (Local Loopback)
        RX packets 2098  bytes 104900 (102.4 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2098  bytes 104900 (102.4 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth05c8be8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        ether c6:8c:35:49:68:69  txqueuelen 0  (Ethernet)
        RX packets 14  bytes 1048 (1.0 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 14  bytes 1334 (1.3 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

建立一個容器 busybox ,此容器位於 docker0 這個私有 bridge 網路中(172.17.0.0/16),當 busybox 從容器向外 ping 時,資料包是怎樣到達 bing.com 的呢?這裡的關鍵就是 NAT。我們檢視一下 docker host 上的 iptables 規則:

docker run busybox sh -c "while true;do sleep 3600;done;"

檢視此主機上的 iptables,因為所有容器和外部的網路互動,都是通過 NAT 來實現的

[root@izm5e37rlunev9ij58ixy9z ~]# iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN

注意一下這裡

-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

其含義是:如果網橋 docker0 收到來自 172.17.0.0/16 網段的外出包,把它交給 MASQUERADE 處理,而 MASQUERADE 的處理方式是將包的源地址替換成 host 的地址傳送出去,即做了一次網路地址轉換(NAT);

下面我們通過 tcpdump 檢視地址是如何轉換的。先檢視 docker host 的路由表:

[root@izm5e37rlunev9ij58ixy9z ~]# ip r
default via 172.31.255.253 dev eth0
169.254.0.0/16 dev eth0  scope link  metric 1002
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1
172.31.240.0/20 dev eth0  proto kernel  scope link  src 172.31.243.112

預設路由通過 enp0s3 發出去,所以我們要同時監控 eth0 和 docker0 上的 icmp(ping)資料包。

busybox ping baidu.com 時,

/ # ping -c 3 www.baidu.com
PING www.baidu.com (110.242.68.4): 56 data bytes
64 bytes from 110.242.68.4: seq=0 ttl=50 time=17.394 ms
64 bytes from 110.242.68.4: seq=1 ttl=50 time=17.433 ms
64 bytes from 110.242.68.4: seq=2 ttl=50 time=17.453 ms

這個時候在分別在宿主機對 docker0 和 eth0 抓包:

[root@izm5e37rlunev9ij58ixy9z ~]# sudo tcpdump -i docker0 -n icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 65535 bytes
14:26:05.241364 IP 172.17.0.2 > 110.242.68.4: ICMP echo request, id 14, seq 0, length 64
14:26:05.258772 IP 110.242.68.4 > 172.17.0.2: ICMP echo reply, id 14, seq 0, length 64
14:26:06.241458 IP 172.17.0.2 > 110.242.68.4: ICMP echo request, id 14, seq 1, length 64
14:26:06.258835 IP 110.242.68.4 > 172.17.0.2: ICMP echo reply, id 14, seq 1, length 64
14:26:07.241578 IP 172.17.0.2 > 110.242.68.4: ICMP echo request, id 14, seq 2, length 64
14:26:07.258940 IP 110.242.68.4 > 172.17.0.2: ICMP echo reply, id 14, seq 2, length 64
[root@izm5e37rlunev9ij58ixy9z ~]# sudo tcpdump -i eth0 -n icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
14:33:00.015219 IP 172.31.243.112 > 110.242.68.4: ICMP echo request, id 15, seq 0, length 64
14:33:00.032516 IP 110.242.68.4 > 172.31.243.112: ICMP echo reply, id 15, seq 0, length 64
14:33:01.015332 IP 172.31.243.112 > 110.242.68.4: ICMP echo request, id 15, seq 1, length 64
14:33:01.032650 IP 110.242.68.4 > 172.31.243.112: ICMP echo reply, id 15, seq 1, length 64
14:33:02.015433 IP 172.31.243.112 > 110.242.68.4: ICMP echo request, id 15, seq 2, length 64
14:33:02.032787 IP 110.242.68.4 > 172.31.243.112: ICMP echo reply, id 15, seq 2, length 64

docker0 收到 busybox 的 ping 包,源地址為容器 IP 172.17.0.2,然後交給 MASQUERADE 處理。這個規則就是上面我們通過 iptables 查到的 -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

ping 包的源地址變成了 eth0 的 IP 172.31.243.112,也就是宿主機 host ip,這就是 iptable NAT 規則處理的結果,從而保證資料包能夠到達外網。

來總結一下整個的過程:

  • busybox 傳送 ping 包:172.17.0.2 >  www.baidu.com;
  • docker0 收到包,發現是傳送到外網的,交給 NAT 處理;
  • NAT 將源地址換成 enp0s3 的 IP:172.31.243.112 > www.baidu.com;
  • ping 包從 enp0s3 傳送出去,到達 www.baidu.com;

即通過 NAT,docker 實現了容器對外網的訪問;

那麼外部網路如何訪問到容器?答案是:埠對映

docker 可將容器對外提供服務的埠對映到 host 的某個埠,外網通過該埠訪問容器。容器啟動時通過-p引數對映埠:

[root@izm5e37rlunev9ij58ixy9z ~]# docker run --name nginx-test -p 8080:80 -d nginx

檢視對映:

[root@izm5e37rlunev9ij58ixy9z ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
9a7edd9e4133        nginx               "/docker-entrypoint.…"   4 seconds ago       Up 3 seconds        0.0.0.0:8080->80/tcp   nginx-test

容器啟動後,可通過 docker ps 或者 docker port 檢視到 host 對映的埠。可以看到,httpd 容器的 80 埠被對映到 host 8080 上,這樣就可以通過 <host ip>:<8080 > 訪問容器的 web 服務了;

[root@izm5e37rlunev9ij58ixy9z ~]# curl 172.31.243.112:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

每一個對映的埠,host 都會啟動一個 docker-proxy 程式來處理訪問容器的流量:

[root@izm5e37rlunev9ij58ixy9z ~]# ps -ef | grep docker-proxy
root     29833  6912  0 15:15 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.3 -container-port 80

具體的流程為

  • docker-proxy 監聽 host 的 8080 埠

  • 當 curl 訪問 172.31.243.112:8080 時,docker-proxy 轉發給容器 172.31.243.112:80。

  • httpd 容器響應請求並返回結果

如圖是 nat 和外部網路互動的示意圖:

至於在多機通訊過程中,docker 是怎麼組織跨主機的統一網路的呢?這裡就簡單介紹一下 overlay 網路,底層使用的是 vxlan 協議。

VXLAN(Virtual Extensible Local Area Network,虛擬可擴充套件區域網),通過將物理伺服器或虛擬機器發出的資料包封裝到UDP中,並使用物理網路的IP/MAC作為外層報文頭進行封裝,然後在IP網路上傳輸,到達目的地後由隧道端點解封裝並將資料傳送給目標物理伺服器或虛擬機器,擴充套件了大規模虛擬機器網路通訊。由於VLAN Header頭部限制長度是12bit,導致只能分配4095個VLAN,也就是4095個網段,在大規模虛擬網路。VXLAN標準定義Header限制長度24bit,可以支援1600萬個VLAN,滿足大規模虛擬機器網路需求。

在不同的跨主機網路中,想讓分佈在不同主機的容器能夠在統一的內網中互相訪問互通,那麼首先第一是叢集中所有主機必須是可以互聯並且是可以感知的,第二是所有容器的內網ip必定是和當前的宿主ip所繫結。這些資訊是維護在 etcd 儲存中的。當某一個容器內需要跨機器去訪問另一機器上的容器時,會第一時間在本地宿主機內查詢對應的宿主機器,這一部分是 iptables 進行維護,查詢到對應的 host 後直接進行 vxlan 的協議封裝,讓其變成普通的網路包可以在外部網路中進行傳輸。最後在目標宿主機上接受資料包後,發現是 vxlan 協議,會交由 docker 進行解包的操作,當把 vxlan 協議剝離後,內部的真正的容器資料包會被髮送給目標容器。

關於docker中跨主機通訊怎麼維護一個統一的二層和三層網路,我建議讀一下這一篇文章,會比較好理解:二層網路三層網路理解

 

在實際工程中,跨主機跨叢集的網路是非常複雜的,具體看 k8s 中層出不窮的網路外掛就可以瞭解到,建議看看相關的網路外掛實現原理,比如 weave,calico,flannel 這些,具體可以看看這一篇官網文件:https://feisky.gitbooks.io/kubernetes/content/network/network.html 

相關文章