理解Docker跨多主機容器網路
在Docker 1.9 出世前,跨多主機的容器通訊方案大致有如下三種:
1、埠對映
將宿主機A的埠P對映到容器C的網路空間監聽的埠P’上,僅提供四層及以上應用和服務使用。這樣其他主機上的容器通過訪問宿主機A的埠P實 現與容器C的通訊。顯然這個方案的應用場景很有侷限。
2、將物理網絡卡橋接到虛擬網橋,使得容器與宿主機配置在同一網段下
在各個宿主機上都建立一個新虛擬網橋裝置br0,將各自物理網絡卡eth0橋接br0上,eth0的IP地址賦給br0;同時修改Docker daemon的DOCKER_OPTS,設定-b=br0(替代docker0),並限制Container IP地址的分配範圍為同物理段地址(–fixed-cidr)。重啟各個主機的Docker Daemon後,處於與宿主機在同一網段的Docker容器就可以實現跨主機訪問了。這個方案同樣存在侷限和擴充套件性差的問題:比如需將物理網段的地址劃分 成小塊,分佈到各個主機上,防止IP衝突;子網劃分依賴物理交換機設定;Docker容器的主機地址空間大小依賴物理網路劃分等。
關於這些第三方方案的細節大家可以參考O’Reilly的《Docker Cookbook》 一書。
Docker在1.9版本中給大家帶來了一種原生的跨多主機容器網路的解決方案,該方案的實質是採用了基於VXLAN 的覆蓋網技術。方案的使用有一些前提條件:
1、Linux Kernel版本 >= 3.16;
2、需要一個外部Key-value Store(官方例子中使用的是consul);
3、各物理主機上的Docker Daemon需要一些特定的啟動引數;
4、物理主機允許某些特定TCP/UDP埠可用。
本文將帶著大家一起利用Docker 1.9.1建立一個跨多主機容器網路,並分析基於該網路的容器間通訊原理。
一、實驗環境建立
1、升級Linux Kernel
由於實驗環境採用的是Ubuntu 14.04 server amd64,其kernel版本不能滿足建立跨多主機容器網路要求,因此需要對核心版本進行升級。在Ubuntu的核心站點 下載3.16.7 utopic核心 的三個檔案:
linux-headers-3.16.7-031607_3.16.7-031607.201410301735_all.deb linux-image-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb linux-headers-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb
在本地執行下面命令安裝:
sudo dpkg -i linux-headers-3.16.7-*.deb linux-image-3.16.7-*.deb
需要注意的是:kernel mainline上的3.16.7核心沒有帶linux-image-extra,也就沒有了aufs 的驅動,因此Docker Daemon將不支援預設的儲存驅動:–storage-driver=aufs,我們需要將storage driver更換為devicemapper。
核心升級是一個有風險的操作,並且是否能升級成功還要看點“運氣”:我的兩臺刀鋒伺服器,就是一臺升級成功一臺升級失敗(一直報網絡卡問題)。
2、升級Docker到1.9.1版本
從國內下載Docker官方的安裝包比較慢,這裡利用daocloud.io提供的方法 快速安裝Docker最新版本:
$ curl -sSL https://get.daocloud.io/docker | sh
3、拓撲
本次的跨多主機容器網路基於兩臺在不同子網網段內的物理機承載,基於物理機搭建,目的是簡化後續網路通訊原理分析。
拓撲圖如下:
二、跨多主機容器網路搭建
考慮到kv store在本文並非關鍵,僅作跨多主機容器網路建立啟動的前提條件之用,因此僅用包含一個server節點的”cluster”。
參照拓撲圖,我們在10.10.126.101上啟動一個consul,關於consul叢集以及服務註冊、服務發現等細節可以參考我之前的一 篇文章:
$./consul -d agent -server -bootstrap-expect 1 -data-dir ./data -node=master -bind=10.10.126.101 -client=0.0.0.0 &
2、修改Docker Daemon DOCKER_OPTS引數
前面提到過,通過Docker 1.9建立跨多主機容器網路需要重新配置每個主機節點上的Docker Daemon的啟動引數:
ubuntu系統這個配置在/etc/default/docker下:
DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4 -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-advertise eth0:2375 --cluster-store consul://10.10.126.101:8500/network --storage-driver=devicemapper"
這裡多說幾句:
-H(或–host)配置的是Docker client(包括本地和遠端的client)與Docker Daemon的通訊媒介,也是Docker REST api的服務埠。預設是/var/run/docker.sock(僅用於本地),當然也可以通過tcp協議通訊以方便遠端Client訪問,就像上面 配置的那樣。非加密網通訊採用2375埠,而TLS加密連線則用2376埠。這兩個埠已經申請在IANA註冊並獲批,變成了知名埠。-H可以配置多個,就像上面配置的那樣。 unix socket便於本地docker client訪問本地docker daemon;tcp埠則用於遠端client訪問。這樣一來:docker pull ubuntu,走docker.sock;而docker -H 10.10.126.101:2375 pull ubuntu則走tcp socket。
–cluster-advertise 配置的是本Docker Daemon例項在cluster中的地址;
–cluster-store配置的是Cluster的分散式KV store的訪問地址;
如果你之前手工修改過iptables的規則,建議重啟Docker Daemon之前清理一下iptables規則:sudo iptables -t nat -F, sudo iptables -t filter -F等。
3、啟動各節點上的Docker Daemon
以10.10.126.101為例:
$ sudo service docker start
$ ps -ef|grep docker
root 2069 1 0 Feb02 ? 00:01:41 /usr/bin/docker -d --dns 8.8.8.8 --dns 8.8.4.4 --storage-driver=devicemapper -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-advertise eth0:2375 --cluster-store consul://10.10.126.101:8500/network
啟動後iptables的nat, filter規則與單機Docker網路初始情況並無二致。
101節點上初始網路driver型別:
$docker network ls
NETWORK ID NAME DRIVER
47e57d6fdfe8 bridge bridge
7c5715710e34 none null
19cc2d0d76f7 host host
4、建立overlay網路net1和net2
在101節點上,建立net1:
$ sudo docker network create -d overlay net1
在71節點上,建立net2:
$ sudo docker network create -d overlay net2
之後無論在71節點還是101節點,我們檢視當前網路以及驅動型別都是如下結果:
$ docker network ls
NETWORK ID NAME DRIVER
283b96845cbe net2 overlay
da3d1b5fcb8e net1 overlay
00733ecf5065 bridge bridge
71f3634bf562 none null
7ff8b1007c09 host host
此時,iptables規則也並無變化。
5、啟動兩個overlay net下的containers
我們分別在net1和net2下面啟動兩個container,每個節點上各種net1和net2的container各一個:
101:
sudo docker run -itd --name net1c1 --net net1 ubuntu:14.04
sudo docker run -itd --name net2c1 --net net2 ubuntu:14.04
71:
sudo docker run -itd --name net1c2 --net net1 ubuntu:14.04
sudo docker run -itd --name net2c2 --net net2 ubuntu:14.04
啟動後,我們就得到如下網路資訊(容器的ip地址可能與前面拓撲圖中的不一致,每次容器啟動ip地址都可能變化):
net1:
net1c1 - 10.0.0.7
net1c2 - 10.0.0.5
net2:
net2c1 - 10.0.0.4
net2c2 - 10.0.0.6
6、容器連通性
在net1c1中,我們來看看其到net1和net2的連通性:
[email protected]:/# ping net1c2
PING 10.0.0.5 (10.0.0.5) 56(84) bytes of data.
64 bytes from 10.0.0.5: icmp_seq=1 ttl=64 time=0.670 ms
64 bytes from 10.0.0.5: icmp_seq=2 ttl=64 time=0.387 ms
^C
--- 10.0.0.5 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.387/0.528/0.670/0.143 ms
[email protected]:/# ping 10.0.0.4
PING 10.0.0.4 (10.0.0.4) 56(84) bytes of data.
^C
--- 10.0.0.4 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1008ms
可見,net1中的容器是互通的,但net1和net2這兩個overlay net之間是隔離的。
三、跨多主機容器網路通訊原理
在“單機容器網路”一文中,我們說過容器間的通訊以及容器到外部網路的通訊是通過docker0網橋並結合iptables實現的。那麼在上面已經建立的跨多主機容器網路裡,容器的通訊又是如何實現的呢?下面我們一起來理解一下。注意:有了單機容器網路基礎後,這裡很多網路細節就不再贅述了。
我們先來看看,在net1下的容器的網路配置,以101上的net1c1容器為例:
$ sudo docker attach net1c1
[email protected]:/# ip route
default via 172.19.0.1 dev eth1
10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.4
172.19.0.0/16 dev eth1 proto kernel scope link src 172.19.0.2
[email protected]:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
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
8: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 02:42:0a:00:00:04 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.4/24 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe00:4/64 scope link
valid_lft forever preferred_lft forever
10: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.2/16 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe13:2/64 scope link
valid_lft forever preferred_lft forever
可以看出net1c1有兩個網口:eth0(10.0.0.4)和eth1(172.19.0.2);從路由表來看,目的地址在172.19.0.0/16範圍內的,走eth1;目的地址在10.0.0.0/8範圍內的,走eth0。
我們跳出容器,回到主機網路範疇:
在101上:
$ ip a
... ...
5: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:52:35:c9:fc brd ff:ff:ff:ff:ff:ff
inet 172.19.0.1/16 scope global docker_gwbridge
valid_lft forever preferred_lft forever
inet6 fe80::42:52ff:fe35:c9fc/64 scope link
valid_lft forever preferred_lft forever
6: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
link/ether 02:42:4b:70:68:9a brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 scope global docker0
valid_lft forever preferred_lft forever
11: veth26f6db4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP
link/ether b2:32:d7:65:dc:b2 brd ff:ff:ff:ff:ff:ff
inet6 fe80::b032:d7ff:fe65:dcb2/64 scope link
valid_lft forever preferred_lft forever
16: veth54881a0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP
link/ether 9e:45:fa:5f:a0:15 brd ff:ff:ff:ff:ff:ff
inet6 fe80::9c45:faff:fe5f:a015/64 scope link
valid_lft forever preferred_lft forever
我們看到除了我們熟悉的docker0網橋外,還多出了一個docker_gwbridge網橋:
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02424b70689a no
docker_gwbridge 8000.02425235c9fc no veth26f6db4
veth54881a0
並且從brctl的輸出結果來看,兩個veth都橋接在docker_gwbridge上,而不是docker0上;docker0在跨多主機容器網路中並沒有被用到。docker_gwbridge替代了docker0,用來實現101上隸屬於net1網路或net2網路中容器間的通訊以及容器到外部的通訊,其職能就和單機容器網路中docker0一樣。
但位於不同host且隸屬於net1的兩個容器net1c1和net1c2間的通訊顯然並沒有通過docker_gwbridge完成,從net1c1路由表來看,當net1c1 ping net1c2時,訊息是通過eth0,即10.0.0.4這個ip出去的。從host的視角,net1c1的eth0似乎沒有網路裝置與之連線,那網路通訊是如何完成的呢?
這一切是從建立network開始的。前面我們執行docker network create -d overlay net1來建立net1 overlay network,這個命令會建立一個新的network namespace。
我們知道每個容器都有自己的網路namespace,從容器的視角看其網路名字空間,我們能看到網路裝置諸如:lo、eth0。這個eth0與主機網路名字空間中的vethx是一個虛擬網絡卡pair。overlay network也有自己的net ns,而overlay network的net ns與容器的net ns之間也有著一些網路裝置對應關係。
我們先來檢視一下network namespace的id。為了能利用iproute2工具對network ns進行管理,我們需要做如下操作:
$cd /var/run
$sudo ln -s /var/run/docker/netns netns
這是因為iproute2只能操作/var/run/netns下的net ns,而docker預設的net ns卻放在/var/run/docker/netns下。上面的操作成功執行後,我們就可以通過ip命令檢視和管理net ns了:
$ sudo ip netns
29170076ddf6
1-283b96845c
5ae976d9dc6a
1-da3d1b5fcb
我們看到在101主機上,有4個已經建立的net ns。我們大膽猜測一下,這四個net ns分別是兩個container的net ns和兩個overlay network的net ns。從netns的ID格式以及結合下面命令輸出結果中的network id來看:
$ docker network ls
NETWORK ID NAME DRIVER
283b96845cbe net2 overlay
da3d1b5fcb8e net1 overlay
dd84da8e80bf host host
3295c22b22b8 docker_gwbridge bridge
b96e2d8d4068 bridge bridge
23749ee4292f none null
我們大致可以猜測出來:
1-da3d1b5fcb 是 net1的net ns;
1-283b96845c是 net2的net ns;
29170076ddf6和5ae976d9dc6a則分屬於兩個container的net ns。
由於我們以net1為例,因此下面我們就來分析net1的net ns – 1-da3d1b5fcb。通過ip命令我們可以得到如下結果:
$ sudo ip netns exec 1-da3d1b5fcb ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
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: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.1/24 scope global br0
valid_lft forever preferred_lft forever
inet6 fe80::b80a:bfff:fecc:a1e0/64 scope link
valid_lft forever preferred_lft forever
7: vxlan1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UNKNOWN
link/ether ea:0c:e0:bc:19:c5 brd ff:ff:ff:ff:ff:ff
inet6 fe80::e80c:e0ff:febc:19c5/64 scope link
valid_lft forever preferred_lft forever
9: veth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP
link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
inet6 fe80::4b0:c6ff:fe93:25f3/64 scope link
valid_lft forever preferred_lft forever
$ sudo ip netns exec 1-da3d1b5fcb ip route
10.0.0.0/24 dev br0 proto kernel scope link src 10.0.0.1
$ sudo ip netns exec 1-da3d1b5fcb brctl show
bridge name bridge id STP enabled interfaces
br0 8000.06b0c69325f3 no veth2
vxlan1
看到br0、veth2,我們心裡終於有了底兒了。我們猜測net1c1容器中的eth0與veth2是一個veth pair,並橋接在br0上,通過ethtool查詢veth序號的對應關係可以證實這點:
$ sudo docker attach net1c1
[email protected]:/# ethtool -S eth0
NIC statistics:
peer_ifindex: 9
101主機:
$ sudo ip netns exec 1-da3d1b5fcb ip -d link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
bridge
7: vxlan1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UNKNOWN
link/ether ea:0c:e0:bc:19:c5 brd ff:ff:ff:ff:ff:ff
vxlan
9: veth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP
link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
veth
可以看到net1c1的eth0的pair peer index為9,正好與net ns 1-da3d1b5fcb中的veth2的序號一致。
那麼vxlan1呢?注意這個vxlan1並非是veth裝置,在ip -d link輸出的資訊中,它的裝置型別為vxlan。前面說過Docker的跨多主機容器網路是基於vxlan的,這裡的vxlan1就是net1這個overlay network的一個 VTEP,即VXLAN Tunnel End Point – VXLAN隧道端點。它是VXLAN網路的邊緣裝置。VXLAN的相關處理都在VTEP上進行,例如識別乙太網資料幀所屬的VXLAN、基於 VXLAN對資料幀進行二層轉發、封裝/解封裝報文等。
至此,我們可以大致畫出一幅跨多主機網路的原理圖:
如果在net1c1中ping net1c2,資料包的行走路徑是怎樣的呢?
1、net1c1(10.0.0.4)中ping net1c2(10.0.0.5),根據net1c1的路由表,資料包可通過直連網路到達net1c2。於是arp請求獲取net1c2的MAC地址(在vxlan上的arp這裡不詳述了),得到mac地址後,封包,從eth0發出;
2、eth0橋接在net ns 1-da3d1b5fcb中的br0上,這個br0是個網橋(交換機)虛擬裝置,需要將來自eth0的包轉發出去,於是將包轉給了vxlan裝置;這個可以通過arp -a看到一些端倪:
$ sudo ip netns exec 1-da3d1b5fcb arp -a
? (10.0.0.5) at 02:42:0a:00:00:05 [ether] PERM on vxlan1
3、vxlan是個特殊裝置,收到包後,由vxlan裝置建立時註冊的裝置處理程式對包進行處理,即進行VXLAN封包(這期間會查詢consul中儲存的net1資訊),將ICMP包整體作為UDP包的payload封裝起來,並將UDP包通過宿主機的eth0傳送出去。
4、71宿主機收到UDP包後,發現是VXLAN包,根據VXLAN包中的相關資訊(比如Vxlan Network Identifier,VNI=256)找到vxlan裝置,並轉給該vxlan裝置處理。vxlan裝置的處理程式進行解包,並將UDP中的payload取出,整體通過br0轉給veth口,net1c2從eth0收到ICMP資料包,回覆icmp reply。
我們可以通過wireshark抓取相關vxlan包,高版本wireshark內建VXLAN協議分析器,可以直接識別和展示VXLAN包,這裡安裝的是2.0.1版本(注意:一些低版本wireshark不支援VXLAN分析器,比如1.6.7版本):
關於VXLAN協議的細節,過於複雜,在後續的文章中maybe會有進一步理解。
© 2016, bigwhite. 版權所有.