理解Docker容器網路之Linux Network Namespace

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

由於2016年年中調換工作的原因,對容器網路的研究中斷過一段時間。隨著當前項目對Kubernetes應用的深入,我感覺之前對於容器網路的粗淺理解已經不夠了,容器網路成了擺在前面的“一道坎”。繼續深入理解K8s網路、容器網路已經勢在必行。而這篇文章就算是一個重新開始,也是對之前淺表理解的一個補充。

我還是先從Docker容器網路入手,雖然Docker與Kubernetes採用了不同的網路模型:K8s是Container Network Interface, CNI模型,而Docker則採用的是Container Network Model, CNM模型。而要瞭解Docker容器網路,理解Linux Network Namespace是不可或缺的。在本文中我們將嘗試理解Linux Network Namespace及相關Linux核心網路裝置的概念,並手工類比Docker容器網路模型的部分實現,包括單機容器網路中的容器與主機連通、容器間連通以及連接埠映射等。

一、Docker的CNM網路模型

Docker通過libnetwork實現了CNM網路模型。libnetwork設計doc中對CNM模型的簡單詮釋如下:

CNM模型有三個組件:

  • Sandbox(沙箱):每個沙箱包含一個容器網路棧(network stack)的配置,配置包括:容器的網口、路由表和DNS設定等。
  • Endpoint(端點):通過Endpoint,沙箱可以被加入到一個Network裡。
  • Network(網路):一組能相互直接通訊的Endpoints。

光看這些,我們還很難將之與現實中的Docker容器聯絡起來,畢竟是抽象的模型不對應到實體,總有種漂浮的趕腳。文檔中又給出了CNM模型在Linux上的參考實現技術,比如:沙箱的實現可以是一個Linux Network Namespace;Endpoint可以是一對VETH;Network則可以用Linux Bridge或Vxlan實現。

這些實現技術反倒是比較接地氣。之前我們在使用Docker容器時,瞭解過Docker是用linux network namespace實現的容器網路隔離的。使用docker時,在物理主機或虛擬機器上會有一個docker0的linux bridge,brctl show時能看到 docker0上“插上了”好多veth網路裝置:

# ip link show... ...3: docker0:  mtu 1500 qdisc noqueue state UP mode DEFAULT group default    link/ether 02:42:30:11:98:ef brd ff:ff:ff:ff:ff:ff19: veth4559467@if18:  mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default    link/ether a6:14:99:52:78:35 brd ff:ff:ff:ff:ff:ff link-netnsid 3... ...$ brctl showbridge name    bridge id        STP enabled    interfaces... ...docker0        8000.0242301198ef    no        veth4559467

模型與現實終於有點接駁了!下面我們將進一步深入對這些術語概念的理解。

二、Linux Bridge、VETH和Network Namespace

Linux Bridge,即Linux橋接器裝置,是Linux提供的一種虛擬網路裝置之一。其工作方式非常類似於物理的網路交換器裝置。Linux Bridge可以工作在二層,也可以工作在三層,預設工作在二層。工作在二層時,可以在同一網路的不同主機間轉寄乙太網路報文;一旦你給一個Linux Bridge分配了IP地址,也就開啟了該Bridge的三層工作模式。在Linux下,你可以用iproute2工具包或brctl命令對Linux bridge進行管理。

VETH(Virtual Ethernet )是Linux提供的另外一種特殊的網路裝置,中文稱為虛擬網卡介面。它總是成對出現,要建立就建立一個pair。一個Pair中的veth就像一個網路線纜的兩個端點,資料從一個端點進入,必然從另外一個端點流出。每個veth都可以被賦予IP地址,並參與三層網路路由過程。

關於Linux Bridge和VETH的具體工作原理,可以參考IBM developerWorks上的這篇文章《Linux 上的基礎網路裝置詳解》。

Network namespace,網路名稱字空間,允許你在Linux建立相互隔離的網路視圖,每個網路名稱字空間都有獨立的網路設定,比如:網路裝置、路由表等。建立的網路名稱字空間與主機預設網路名字空間之間是隔離的。我們平時預設操作的是主機的預設網路名字空間。

概念總是抽象的,接下來我們將在一個類比Docker容器網路的例子中看到這些Linux網路概念和網路裝置到底是起到什麼作用的以及是如何操作的。

三、用Network namespace類比Docker容器網路

為了進一步瞭解network namespace、bridge和veth在docker容器網路中的角色和作用,我們來做一個demo:用network namespace類比Docker容器網路,實際上Docker容器網路在linux上也是基於network namespace實現的,我們只是將其“自動化”的建立過程做成了“分解動作”,便於大家理解。

1、環境

我們在一台物理機上進行這個Demo實驗。物理機安裝了Ubuntu 16.04.1,核心版本:4.4.0-57-generic。Docker容器版本:

Client: Version:      1.12.1 API version:  1.24 Go version:   go1.6.3 Git commit:   23cf638 Built:        Thu Aug 18 05:33:38 2016 OS/Arch:      linux/amd64Server: Version:      1.12.1 API version:  1.24 Go version:   go1.6.3 Git commit:   23cf638 Built:        Thu Aug 18 05:33:38 2016 OS/Arch:      linux/amd64

另外,環境中需安裝了iproute2和brctl工具。

2、拓撲

我們來類比一個擁有兩個容器的容器橋接網路:

對應的用手工搭建的類比版本拓撲如下(由於在同一台主機,類比版本採用172.16.0.0/16網段):

3、建立步驟

a) 建立Container_ns1和Container_ns2 network namespace

預設情況下,我們在Host上看到的都是default network namespace的視圖。為了類比容器網路,我們建立兩個network namespace:

sudo ip netns add Container_ns1sudo ip netns add Container_ns2$ sudo ip netns listContainer_ns2Container_ns1

建立的ns也可以在/var/run/netns路徑下看到:

$ sudo ls /var/run/netnsContainer_ns1  Container_ns2

我們探索一下新建立的ns的網路空間(通過ip netns exec命令可以在特定ns的內部執行相關程式,這個exec命令是至關重要的,後續還會發揮更大作用):

$ sudo ip netns exec Container_ns1 ip a1: lo:  mtu 65536 qdisc noop state DOWN group default qlen 1    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00$ sudo ip netns exec Container_ns2 ip a1: lo:  mtu 65536 qdisc noop state DOWN group default qlen 1    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00$ sudo ip netns exec Container_ns2 ip route

可以看到,建立的ns的網路裝置只有一個loopback口,並且路由表為空白。

b) 建立MyDocker0 bridge

我們在default network namespace下建立MyDocker0 linux bridge:

$ sudo brctl addbr MyDocker0$ brctl showbridge name    bridge id        STP enabled    interfacesMyDocker0        8000.000000000000    no

給MyDocker0分配ip地址並生效該裝置,開啟三層,為後續充當Gateway做準備:

$ sudo ip addr add 172.16.1.254/16 dev MyDocker0$ sudo ip link set dev MyDocker0 up

啟用後,我們發現default network namespace的路由配置中增加了一條路由:

$ route -n核心 IP 路由表目標            網關            子網路遮罩        標誌  躍點   引用  使用 介面0.0.0.0         10.11.36.1      0.0.0.0         UG    100    0        0 eno1... ...172.16.0.0      0.0.0.0         255.255.0.0     U     0      0        0 MyDocker0... ...
c) 建立VETH,串連兩對network namespaces

到目前為止,default ns與Container_ns1、Container_ns2之間還沒有任何瓜葛。接下來就是見證奇蹟的時刻了。我們通過veth pair建立起多個ns之間的聯絡:

建立串連default ns與Container_ns1之間的veth pair – veth1和veth1p:

$sudo ip link add veth1 type veth peer name veth1p$sudo ip -d link show... ...21: veth1p@veth1:  mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff promiscuity 0    veth addrgenmode eui6422: veth1@veth1p:  mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 56:cd:bb:f2:10:3f brd ff:ff:ff:ff:ff:ff promiscuity 0    veth addrgenmode eui64... ...

將veth1“插到”MyDocker0這個bridge上:

$ sudo brctl addif MyDocker0 veth1$ sudo ip link set veth1 up$ brctl showbridge name    bridge id        STP enabled    interfacesMyDocker0        8000.56cdbbf2103f    no        veth1

將veth1p“放入”Container_ns1中:

$ sudo ip link set veth1p netns Container_ns1$ sudo ip netns exec Container_ns1 ip a1: lo:  mtu 65536 qdisc noop state DOWN group default qlen 1    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:0021: veth1p@if22:  mtu 1500 qdisc noop state DOWN group default qlen 1000    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0

這時,你在default ns中將看不到veth1p這個虛擬網路裝置了。按照上面拓撲,位於Container_ns1中的veth應該更名為eth0:

$ sudo ip netns exec Container_ns1 ip link set veth1p name eth0$ sudo ip netns exec Container_ns1 ip a1: lo:  mtu 65536 qdisc noop state DOWN group default qlen 1    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:0021: eth0@if22:  mtu 1500 qdisc noop state DOWN group default qlen 1000    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0

將Container_ns1中的eth0生效並配置IP地址:

$ sudo ip netns exec Container_ns1 ip link set eth0 up$ sudo ip netns exec Container_ns1 ip addr add 172.16.1.1/16 dev eth0

賦予IP地址後,自動產生一條直連路由:

sudo ip netns exec Container_ns1 ip route172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1

現在在Container_ns1下可以ping通MyDocker0了,但由於沒有其他路由,包括預設路由,ping其他地址還是不通的(比如:docker0的地址:172.17.0.1):

$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.254PING 172.16.1.254 (172.16.1.254) 56(84) bytes of data.64 bytes from 172.16.1.254: icmp_seq=1 ttl=64 time=0.074 ms64 bytes from 172.16.1.254: icmp_seq=2 ttl=64 time=0.064 ms64 bytes from 172.16.1.254: icmp_seq=3 ttl=64 time=0.068 ms--- 172.16.1.254 ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 1998msrtt min/avg/max/mdev = 0.064/0.068/0.074/0.010 ms$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1connect: Network is unreachable

我們再給Container_ns1添加一條預設路由,讓其能ping通物理主機上的其他網路裝置或其他ns空間中的網路裝置地址:

$ sudo ip netns exec Container_ns1 ip route add default via 172.16.1.254$ sudo ip netns exec Container_ns1 ip routedefault via 172.16.1.254 dev eth0172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.068 ms64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.076 ms64 bytes from 172.17.0.1: icmp_seq=3 ttl=64 time=0.069 ms--- 172.17.0.1 ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 1999msrtt min/avg/max/mdev = 0.068/0.071/0.076/0.003 ms

不過這時候,如果想在Container_ns1中ping通物理主機之外的地址,比如:google.com,那還是不通的。為什麼呢?因為ping的icmp的包的源地址沒有做snat(docker是通過設定iptables規則實現的),導致出去的以172.16.1.1為源地址的包“有去無回”了^0^。

接下來,我們按照上述步驟,再建立串連default ns與Container_ns2之間的veth pair – veth2和veth2p,由於步驟相同,這裡就不列出那麼多資訊了,只列出關鍵操作:

$ sudo ip link add veth2 type veth peer name veth2p$ sudo brctl addif MyDocker0 veth2$ sudo ip link set veth2 up$ sudo ip link set veth2p netns Container_ns2$ sudo ip netns exec Container_ns2 ip link set veth2p name eth0$ sudo ip netns exec Container_ns2 ip link set eth0 up$ sudo ip netns exec Container_ns2 ip addr add 172.16.1.2/16 dev eth0$ sudo ip netns exec Container_ns2 ip route add default via 172.16.1.254

至此,類比建立告一段落!兩個ns之間以及它們與default ns之間連通了!

$ sudo ip netns exec Container_ns2 ping -c 3 172.16.1.1PING 172.16.1.1 (172.16.1.1) 56(84) bytes of data.64 bytes from 172.16.1.1: icmp_seq=1 ttl=64 time=0.101 ms64 bytes from 172.16.1.1: icmp_seq=2 ttl=64 time=0.083 ms64 bytes from 172.16.1.1: icmp_seq=3 ttl=64 time=0.087 ms--- 172.16.1.1 ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 1998msrtt min/avg/max/mdev = 0.083/0.090/0.101/0.010 ms$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.2PING 172.16.1.2 (172.16.1.2) 56(84) bytes of data.64 bytes from 172.16.1.2: icmp_seq=1 ttl=64 time=0.053 ms64 bytes from 172.16.1.2: icmp_seq=2 ttl=64 time=0.092 ms64 bytes from 172.16.1.2: icmp_seq=3 ttl=64 time=0.089 ms--- 172.16.1.2 ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 1999msrtt min/avg/max/mdev = 0.053/0.078/0.092/0.017 ms

當然此時兩個ns之間連通,主要還是通過直連網路,實質上是MyDocker0在二層起到的作用。以在Container_ns1中ping Container_ns2的eth0地址為例:

Container_ns1此時的路由表:

$ sudo ip netns exec Container_ns1 ip routedefault via 172.16.1.254 dev eth0172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1

ping 172.16.1.2執行後,根據路由表,將首先匹配到直連網路(第二條),即無需gateway轉寄便可以直接將資料包送達。arp查詢後(要麼從arp cache中找到,要麼在MyDocker0這個二層交換器中泛洪查詢)獲得172.16.1.2的mac地址。ip包的目的ip填寫172.16.1.2,二層資料幀封包將目的mac填寫為剛剛查到的mac地址,通過eth0(172.16.1.1)發送出去。eth0實際上是一個veth pair,另外一端“插”在MyDocker0這個交換器上,因此這一過程就是一個標準的二層交換器的資料報文交換過程, MyDocker0相當於從交換器上的一個連接埠收到以太幀資料,並將資料從另外一個連接埠發出去。ping應答包亦如此。

而如果是在Container_ns1中ping某個docker container的地址,比如172.17.0.2。當ping執行後,根據Container_ns1下的路由表,沒有匹配到直連網路,只能通過default路由將資料包發給Gateway: 172.16.1.254。雖然都是MyDocker0接收資料,但這次更類似於“資料被直接發到 Bridge 上,而不是Bridge從一個連接埠接收(這塊兒與我之前的文章中的理解稍有差異)”。二層的目的mac地址填寫的是gateway 172.16.1.254自己的mac地址(Bridge的mac地址),此時的MyDocker0更像是一塊普通網卡的角色,工作在三層。MyDocker0收到資料包後,發現並非是發給自己的ip包,通過主機路由表找到直連鏈路路由,MyDocker0將資料包Forward到docker0上(封裝的二層資料包的目的MAC地址為docker0的mac地址)。此時的docker0也是一種“網卡”的角色,由於目的ip依然不是docker0自身,因此docker0也會繼續這一轉寄流程。通過traceroute可以印證這一過程:

$ sudo ip netns exec Container_ns1  traceroute 172.17.0.2traceroute to 172.17.0.2 (172.17.0.2), 30 hops max, 60 byte packets 1  172.16.1.254 (172.16.1.254)  0.082 ms  0.023 ms  0.019 ms 2  172.17.0.2 (172.17.0.2)  0.054 ms  0.034 ms  0.029 ms$ sudo ip netns exec Container_ns1  ping -c 3 172.17.0.2PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.64 bytes from 172.17.0.2: icmp_seq=1 ttl=63 time=0.084 ms64 bytes from 172.17.0.2: icmp_seq=2 ttl=63 time=0.101 ms64 bytes from 172.17.0.2: icmp_seq=3 ttl=63 time=0.098 ms--- 172.17.0.2 ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 1998msrtt min/avg/max/mdev = 0.084/0.094/0.101/0.010 ms

現在,你應該大致瞭解docker engine在建立單機容器網路時都在背後做了哪些手腳了吧(當然,這裡只是簡單類比,docker實際做的要比這複雜許多)。

四、基於userland proxy的容器連接埠映射的類比

連接埠映射讓位於容器中的service可以將服務涵蓋範圍擴充到主機之外,比如:一個運行於container中的nginx可以通過宿主機的9091連接埠對外提供http server服務:

$ sudo docker run -d -p 9091:80 nginx:latest8eef60e3d7b48140c20b11424ee8931be25bc47b5233aa42550efabd5730ac2f$ curl 10.11.36.15:9091Welcome to nginx!

Welcome to nginx!

If you see this page, the nginx web server is successfully installed andworking. Further configuration is required.

For online documentation and support please refer tonginx.org.
Commercial support is available atnginx.com.

Thank you for using nginx.

容器的連接埠映射實際是通過docker engine的docker proxy功能實現的。預設情況下,docker engine(截至docker 1.12.1版本)採用userland proxy(–userland-proxy=true)為每個expose連接埠的容器啟動一個proxy執行個體來做連接埠流量轉寄:

$ ps -ef|grep docker-proxyroot     26246  6228  0 16:18 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9091 -container-ip 172.17.0.2 -container-port 80

docker-proxy實際上就是在default ns和container ns之間轉寄流量而已。我們完全可以類比這一過程。

我們建立一個fileserver demo:

//testfileserver.gopackage mainimport "net/http"func main() {    http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))}

我們在Container_ns1下啟動這個Fileserver service:

$ sudo ip netns exec Container_ns1 ./testfileserver$ sudo ip netns exec Container_ns1 lsof -i tcp:8080COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAMEtestfiles 3605 root    3u  IPv4 297022      0t0  TCP *:http-alt (LISTEN)

可以看到在Container_ns1下面,8080已經被testfileserver監聽,不過在default ns下,8080連接埠依舊是avaiable的。

接下來,我們在default ns下建立一個簡易的proxy:

//proxy.go... ...var (    host          string    port          string    container     string    containerport string)func main() {    flag.StringVar(&host, "host", "0.0.0.0", "host addr")    flag.StringVar(&port, "port", "", "host port")    flag.StringVar(&container, "container", "", "container addr")    flag.StringVar(&containerport, "containerport", "8080", "container port")    flag.Parse()    fmt.Printf("%s\n%s\n%s\n%s", host, port, container, containerport)    ln, err := net.Listen("tcp", host+":"+port)    if err != nil {        // handle error        log.Println("listen error:", err)        return    }    log.Println("listen ok")    for {        conn, err := ln.Accept()        if err != nil {            // handle error            log.Println("accept error:", err)            continue        }        log.Println("accept conn", conn)        go handleConnection(conn)    }}func handleConnection(conn net.Conn) {    cli, err := net.Dial("tcp", container+":"+containerport)    if err != nil {        log.Println("dial error:", err)        return    }    log.Println("dial ", container+":"+containerport, " ok")    go io.Copy(conn, cli)    _, err = io.Copy(cli, conn)    fmt.Println("communication over: error:", err)}

在default ns下執行:

./proxy -host 0.0.0.0 -port 9090 -container 172.16.1.1 -containerport 80800.0.0.09090172.16.1.180802017/01/11 17:26:10 listen ok

我們http get一下宿主機的9090連接埠:

$curl 10.11.36.15:9090
proxyproxy.gotestfileservertestfileserver.go

成功獲得file list!

proxy的輸出日誌:

2017/01/11 17:26:16 accept conn &{{0xc4200560e0}}2017/01/11 17:26:16 dial  172.16.1.1:8080  okcommunication over: error:

由於每個做連接埠映射的Container都要啟動至少一個docker proxy與之配合,一旦啟動並執行container增多,那麼docker proxy對資源的消耗將是大大的。因此docker engine在docker 1.6之後(好像是這個版本)提供了基於iptables的連接埠映射機制,無需再啟動docker proxy process了。我們只需修改一下docker engine的啟動配置即可:

在使用systemd init system的系統中如果為docker engine配置–userland-proxy=false,可以參考《當Docker遇到systemd》這篇文章。

由於這個與network namespace關係不大,後續單獨理解^0^。

六、參考資料

1、《Docker networking cookbook》
2、《Docker cookbook》

2017, bigwhite. 著作權.

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.