Docker 容器網絡的發展歷史
?
在 Dokcer 發布之初,Docker 是將網絡、管理、安全等集成在一起的,其中網絡模塊可以為容器提供橋接網絡、主機網絡等簡單的網絡功能。
?
從 1.7 版本開始,Docker正是把網絡和存儲這兩部分的功能都以插件化形式剝離出來,允許用戶通過指令來選擇不同的后端實現。剝離出來的獨立容器網絡項目叫 libnetwork。
?
在 1.9 版本時,Docker 又引入了一整套 network 子命令和跨主機網絡支持,這允許用戶可以根據他們應用的拓撲結構創建虛擬網絡并將容器接入其所對應的網絡。
?
什么是 Docker?Libnetwork
?
為了標準化網絡的驅動開發步驟和支持多種網絡驅動,Docker 將網絡部分代碼被抽離成為了單獨的網絡庫(Libnetwork),Libnetwork 提供了可以用于開發多種網絡驅動的標準化接口和組件。
?
Docker daemon 通過調用 Libnetwork 對外提供的 API 完成網絡的創建和管理等功能,Libnetwork 內置了5種驅動來提供不同類型的網絡: bridge driver, host?driver,?null?driver,?overlay?driver,?remote?driver
?
?
bridge driver
?
此驅動為Docker的默認設置驅動,使用這個驅動的時候,libnetwork將創建出來的Docker容器連接到Docker網橋上。作為最常規的模式,bridge模式已經可以滿足Docker容器最基本的使用需求了。然而其與外界通信使用NAT,增加了通信的復雜性,在復雜場景下使用會有諸多限制。
?
host?driver
?
使用這種驅動的時候,libnetwork將不為Docker容器創建網絡協議棧,即不會創建獨立的network namespace。Docker容器中的進程處于宿主機的網絡環境中,相當于Docker容器和宿主機共同用一個network namespace,使用宿主機的網卡、IP和端口等信息。
?
但是,容器其他方面,如文件系統、進程列表等還是和宿主機隔離的。host模式很好地解決了容器與外界通信的地址轉換問題,可以直接使用宿主機的IP進行通信,不存在虛擬化網絡帶來的額外性能負擔。但是host驅動也降低了容器與容器之間、容器與宿主機之間網絡層面的隔離性,引起網絡資源的競爭與沖突。
?
因此可以認為host驅動適用于對于容器集群規模不大的場景。
?
null driver
?
使用這種驅動的時候,Docker容器擁有自己的network namespace,但是并不為Docker容器進行任何網絡配置。也就是說,這個Docker容器除了network namespace自帶的loopback網卡名,沒有其他任何網卡、IP、路由等信息,需要用戶為Docker容器添加網卡、配置IP等。
?
這種模式如果不進行特定的配置是無法正常使用的,但是優點也非常明顯,它給了用戶最大的自由度來自定義容器的網絡環境。
?
overlay driver
?
此驅動采用IETE標準的VXLAN方式,并且是VXLAN中被普遍認為最適合大規模的云計算虛擬化環境的SDN controller模式。在使用過程中,需要一個額外的配置存儲服務,例如Consul、etcd和zookeeper。還需要在啟動Docker daemon的時候額外添加參數來指定所使用的配置存儲服務地址。
?
remote Driver
?
這個驅動實際上并未做真正的網絡服務實現,而是調用了用戶自行實現的網絡驅動插件,使libnetwork實現了驅動的可插件化,更好地滿足了用戶的多種需求。用戶只需要根據libnetwork提供的協議標準,實現其所要求的各個接口并向Docker daemon進行注冊。
?
什么是 Docker?CNM
?
Docker?Libnetwork 中使用了 CNM 的容器網絡模式概念,CNM定義了構建容器虛擬化網絡的模型,此后容器網絡模式也被抽象變成了統一接口的驅動。
?
CNM 中主要有?sandbox、endpoint 和 network?3 種核心組件CNM 中核心組件的使用模型如下圖:
?
?
沙盒 (sandbox):一個沙盒包含了一個容器網絡棧的信息。沙盒可以對容器的接口(interface)、路由和 DNS 設置等進行管理,沙盒的實現可以是Linux netns、FreeBSD Jail 或者類似的機制,一個沙盒可以有多個端點和多個網絡。
?
端點 (endpoint):一個端點可以加入一個沙盒和一個網絡。端點的實現可以是 veth pair、ovs 內部端口或者相似的設備,一個端點只屬于一個網絡并且只屬于一個沙盒。
?
網絡 (network):一個網絡是一組可以直接互相聯通的端點。網絡的實現可以是 Linux bridge、VLAN等,一個網絡可以包含多個端點。
?
Libnetwork Remote driver
?
kuryr-libnetwork 是 Libnetwork 框架下的一種 remote driver 實現,現在已經成為Docker 官網推薦的一個 remote driver,kuryr-libnetwork 需要做的就是實現 Libnetwork remote driver 需要實現的接口.?
?
常見的 remote driver 要實現的接口如下,格式:HTTP POST + JSON Body
?
/Plugin.Activate no payload ?????-- Handshake
/NetworkDriver.GetCapabilities ??-- Set capability
/NetworkDriver.DiscoverNew ???-- DiscoverNew Notification
/NetworkDriver.DiscoverDelete ??-- DiscoverDelete Notification
/NetworkDriver.AllocateNetwork ?-- Allocate network specific resources, only called in docker swarm mode
/NetworkDriver.FreeNetwork ????-- Free network specific resources, only called in docker swarm mode
/NetworkDriver.CreateNetwork ??-- Create network
/NetworkDriver.DeleteNetwork ??-- Delete network
/NetworkDriver.CreateEndpoint ??-- Create endpoint
/NetworkDriver.EndpointOperInfo -- Endpoint operational info
/NetworkDriver.DeleteEndpoint ??-- Delete endpoint
/NetworkDriver.Join ???????????-- Join an endpoint to a sandbox
/NetworkDriver.Leave ?????????-- Remove an endpoint from a sandbox
?
IPAM?Driver
?
?在 Libnetwork 中,CNM 模塊通過 IPAM?Driver 管理 IP 地址的分配,Libnetwork 內含有一個默認的IPAM驅動,同時它也允許動態地增加第三方IPAM驅動。
?
在用戶創建網絡時可以指定 Libnetwork 使用的 IPAM 驅動,?Kuryr 項目通過實現了 IPAM 的驅動接口,成為了Docker 的第三方 libnetwork IPAM driver。
?
常見的 IPAM driver 要實現的接口如下,格式:HTTP POST + JSON Body
?
/IpamDriver.GetCapabilities ??-- provides the IPAM driver capabilities. it's called during the registration of the IPAM driver.
/IpamDriver.GetDefaultAddressSpaces ??-- returns the default local and global address space names for this IPAM. it's called after the registration of the IPAM driver
/IpamDriver.RequestPool ????-- registering an address pool with the IPAM driver. multiple identical calls must return the same result.
/IpamDriver.RequestAddress ??-- allocates the IP address
/IpamDriver.ReleaseAddress ??-- deallocates the IP address
/IpamDriver.ReleasePool ?????-- releasing a previously registered address pool
?
Docker 網絡的生命周期
?
Docker 用戶可以通過與 CNM 的 Object 以及 API 的交互來管理對應容器的網絡,下面是一個典型的容器網絡生命周期:
?
1、Driver要向NetworkController注冊。內置的Driver在Libnetwork內注冊,遠程的Driver則通過Plugin mechanism注冊。每一個Driver處理特定的networkType。
?
2、libnetwork.New():NetworkController通過libnetwork.New()創建,用于Network的創建以及通過一些特定的Options配置Driver。
?
3、controller.NewNetwork():Network通過給這個API提供name和networkType來創建,networkType參數用來選擇特定的Driver并且將創建的Network和該Driver相關聯。從此以后,對于Network的任何操作都由Driver處理。controller.NewNetwork() 還有一個可選的options參數,用于提供特定Driver的options和Labels。
?
4、network.CreateEndpoint():可以用于在給定的Network中創建一個新的Endpoint。同時該API還有一個可選的options參數供Driver使用。這個"options"既可以攜帶已知的labels,也可以攜帶和特定Driver相關的labels。之后調用相應的Driver的driver.CreateEndpoint,它可以為在一個Endpoint在Network中被創建時,為它們保留IP地址。Driver會通過driverapi中定義的InterfaceInfo進行這些地址的賦值。IP地址將和endpoint暴露的端口用來完善Endpoint作為Service的定義。事實上,Service endpoint不是其他什么東西,僅僅只是一個網絡地址以及該應用的容器監聽的端口號。
?
5、endpoint.Join():用于將Endpoint與一個容器相連接。Join操作會先創建一個Sandbox如果對應的容器中還沒有的話。Driver可以使用Sandbox Key來識別連接到同一個容器的多個Endpoint。這個API同樣接受可選的options參數供Driver使用。
?
- 雖然這并不是Libnetwork直接的設計要求,但是我們鼓勵像Docker這樣的用戶在執行容器的Start()操作時,即在容器可以操作之前,調用endpoint.Join()。
?
- 另一個關于endpoint.join()這個API經常被提到的問題是,為什么我們需要一個API創建Endpoint和另一個API來join endpoint。事實上Endpoint代表的是一個Service,它可能有,也可能并沒有容器支持。當一個Endpoint被創建的時候,會預留它所需的資源,因此任何容器都能連接該Endpoint并且獲得一個一致的網絡行為。
?
6、endpoint.Leave():會在容器停止的時候被調用。Driver可以清除它在調用Join()時獲取的狀態。Libnetwork會在最后一個Endpoint離開的時候刪除Sandbox。但是只要該Endpoint依舊存在,Libnetwork會依然保有IP地址并且在有新的容器加入的時候進行重用。這保證了容器的資源在停止并重啟的過程中能夠重用。
?
7、endpoint.Delete():用于從一個Network中刪除Endpoint。這將導致Endpoint的刪除以及清空緩存的sandbox.Info。
?
8、network.Delete():用于刪除Network。如果還有Endpoint連接到該網絡,Libnetwork是不允許對它進行刪除的。
?
Docker 網絡命名空間
?
docker 常常使用 linux netns 實現網絡資源隔離,但使用 ip netns 命令卻無法查看,這是因為 docker 默認把創建的網絡命名空間鏈接文件隱藏起來了,導致 ip netns 命令無法讀取,可以通過下面的方法復現 docker 的 ip netns 命名空間。
?
# 創建一個帶有橋接網絡的 docker 容器
$ docker run -it -d --rm --name mytest --network bridge cirros /bin/sh
c093857c756028b4d4f37b16262d017239236bde22a3545f8769fd17366f183a
$ docker ps | grep mytest
c093857c7560 ???cirros ??"/bin/sh" ????6 seconds ago ???Up 2 seconds ??mytest
# 可以通過 inspect 命令查看該容器的 ip 地址和進程號
$ docker inspect mytest |egrep '"IPAddress"|"Pid"'
????????"Pid": 14908,
????????"IPAddress": "172.17.0.2",
?
# 通過進程號參考容器進程 ???????
$ ps -fp 14908
UID ???PID ?PPID ?C STIME ?CMD
root 14889 ?1676 ?0 11:42 ?containerd-shim -namespace moby \
? ? ? ? ? -workdir?
/var/lib/containerd/io.containerd.runtime.v1.linux/moby/c093857c756028b4d4f37b16262d017239236bde22a3545f8769fd17366f183a \
??????????-address /run/containerd/containerd.sock \
??????????-containerd-binary /usr/bin/containerd \
??????????-runtime-root /var/run/docker/runtime-runc
?
# 通過 nsenter 進入容器網絡空間 ?????
$ nsenter --target 14908 --net 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
54: eth0@if55: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
????link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
????inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
???????valid_lft forever preferred_lft forever
?
# 通過軟連接容器命名空間實現在 ip netns 下顯示
$ ls /proc/14908/ns/net
lrwxrwxrwx 1 root root 0 Jul 25 11:42 /proc/14908/ns/net -> net:[4026532445]
$ ln -s /proc/14908/ns/net /var/run/netns/mytest
?
# 最后檢查一下
$ ip netns
mytest (id: 1)
$ ip netns exec mytest 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
54: eth0@if55: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
????link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
????inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
???????valid_lft forever preferred_lft forever
?
Docker 主機名與DNS?
?
一個鏡像可以啟動多個容器,但是它們的主機名和網絡信息并不一樣,也即是說主機名和網絡信息并非是被寫入鏡像中的。實際上容器中/etc/目錄下有三個文件是容器啟動后被虛擬文件覆蓋的,分別是/etc/hostname、/etc/hosts、/etc/resolv.conf。對這三個文件的修改不會被docker commit保存,也就是不會保存在鏡像中,重啟容器也會導致修改失效。
?
?
$ docker exec -it mytest mount | grep etc
/dev/mapper/centos-root on /etc/resolv.conf type xfs (rw,relatime,attr2,inode64,noquota)
/dev/mapper/centos-root on /etc/hostname type xfs (rw,relatime,attr2,inode64,noquota)
/dev/mapper/centos-root on /etc/hosts type xfs (rw,relatime,attr2,inode64,noquota)
?
- 這樣能解決主機名的問題,同時也能讓DNS及時更新(改變resolv.conf)。
?
- 由于這些文件的維護方法隨著Docker版本演進而不斷變化,因此盡量不修改這些文件,而是通過Docker提供的參數進行相關設置。
?
參考資料
https://github.com/docker/libnetwork/blob/master/docs/design.md
http://dockone.io/article/1306
https://www.oreilly.com/library/view/learning-docker-networking/9781785280955/
https://feisky.gitbooks.io/sdn/container/cnm/
https://www.nuagenetworks.net/blog/container-networking-standards/