【轉】理解Docker容器網絡之Linux Network Namespace

原文:理解Docker容器網絡之Linux Network Namespacehtml

 

因爲2016年年中調換工做的緣由,對容器網絡的研究中斷過一段時間。隨着當前項目對Kubernetes應用的深刻,我感受以前對於容器網絡的粗淺理解已經不夠了,容器網絡成了擺在前面的「一道坎」。繼續深刻理解K8s網絡、容器網絡已經勢在必行。而這篇文章就算是一個從新開始,也是對以前淺表理解的一個補充。linux

我仍是先從Docker容器網絡入手,雖然Docker與Kubernetes採用了不一樣的網絡模型:K8s是Container Network Interface, CNI模型,而Docker則採用的是Container Network Model, CNM模型。而要了解Docker容器網絡,理解Linux Network Namespace是不可或缺的。在本文中咱們將嘗試理解Linux Network Namespace及相關Linux內核網絡設備的概念,並手工模擬Docker容器網絡模型的部分實現,包括單機容器網絡中的容器與主機連通、容器間連通以及端口映射等。nginx

1、Docker的CNM網絡模型

Docker經過libnetwork實現了CNM網絡模型。libnetwork設計doc中對CNM模型的簡單詮釋以下:git

img{512x368}

CNM模型有三個組件:github

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

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

這些實現技術反卻是比較接地氣。以前咱們在使用Docker容器時,瞭解過Docker是用linux network namespace實現的容器網絡隔離的。使用docker時,在物理主機或虛擬機上會有一個docker0的linux bridge,brctl show時能看到 docker0上「插上了」好多veth網絡設備:docker

# ip link show
... ...
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:30:11:98:ef brd ff:ff:ff:ff:ff:ff
19: veth4559467@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> 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 show
bridge name    bridge id        STP enabled    interfaces
... ...
docker0        8000.0242301198ef    no        veth4559467

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

2、Linux Bridge、VETH和Network Namespace

Linux Bridge,即Linux網橋設備,是Linux提供的一種虛擬網絡設備之一。其工做方式很是相似於物理的網絡交換機設備。Linux Bridge能夠工做在二層,也能夠工做在三層,默認工做在二層。工做在二層時,能夠在同一網絡的不一樣主機間轉發以太網報文;一旦你給一個Linux Bridge分配了IP地址,也就開啓了該Bridge的三層工做模式。在Linux下,你能夠用iproute2工具包或brctl命令對Linux bridge進行管理。app

VETH(Virtual Ethernet )是Linux提供的另一種特殊的網絡設備,中文稱爲虛擬網卡接口。它老是成對出現,要建立就建立一個pair。一個Pair中的veth就像一個網絡線纜的兩個端點,數據從一個端點進入,必然從另一個端點流出。每一個veth均可以被賦予IP地址,並參與三層網絡路由過程。curl

關於Linux Bridge和VETH的具體工做原理,能夠參考IBM developerWorks上的這篇文章《Linux 上的基礎網絡設備詳解》。

Network namespace,網絡名字空間,容許你在Linux建立相互隔離的網絡視圖,每一個網絡名字空間都有獨立的網絡配置,好比:網絡設備、路由表等。新建的網絡名字空間與主機默認網絡名字空間之間是隔離的。咱們平時默認操做的是主機的默認網絡名字空間。

概念老是抽象的,接下來咱們將在一個模擬Docker容器網絡的例子中看到這些Linux網絡概念和網絡設備究竟是起到什麼做用的以及是如何操做的。

3、用Network namespace模擬Docker容器網絡

爲了進一步瞭解network namespace、bridge和veth在docker容器網絡中的角色和做用,咱們來作一個demo:用network namespace模擬Docker容器網絡,實際上Docker容器網絡在linux上也是基於network namespace實現的,咱們只是將其「自動化」的建立過程作成了「分解動做」,便於你們理解。

一、環境

咱們在一臺物理機上進行這個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/amd64

Server:
 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工具。

二、拓撲

咱們來模擬一個擁有兩個容器的容器橋接網絡:

img{512x368}

對應的用手工搭建的模擬版本拓撲以下(因爲在同一臺主機,模擬版本採用172.16.0.0/16網段):

img{512x368}

三、建立步驟

a) 建立Container_ns1和Container_ns2 network namespace

默認狀況下,咱們在Host上看到的都是default network namespace的視圖。爲了模擬容器網絡,咱們新建兩個network namespace:

sudo ip netns add Container_ns1
sudo ip netns add Container_ns2

$ sudo ip netns list
Container_ns2
Container_ns1

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

$ sudo ls /var/run/netns
Container_ns1  Container_ns2

咱們探索一下新建立的ns的網絡空間(經過ip netns exec命令能夠在特定ns的內部執行相關程序,這個exec命令是相當重要的,後續還會發揮更大做用):

$ sudo ip netns exec Container_ns1 ip a
1: lo: <LOOPBACK> 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 a
1: lo: <LOOPBACK> 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 show
bridge name    bridge id        STP enabled    interfaces
MyDocker0        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_ns一、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: <BROADCAST,MULTICAST,M-DOWN> 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 eui64
22: veth1@veth1p: <BROADCAST,MULTICAST,M-DOWN> 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 show
bridge name    bridge id        STP enabled    interfaces
MyDocker0        8000.56cdbbf2103f    no        veth1

將veth1p「放入」Container_ns1中:

$ sudo ip link set veth1p netns Container_ns1

$ sudo ip netns exec Container_ns1 ip a
1: lo: <LOOPBACK> 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
21: veth1p@if22: <BROADCAST,MULTICAST> 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 a
1: lo: <LOOPBACK> 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
21: eth0@if22: <BROADCAST,MULTICAST> 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 route
172.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.254
PING 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 ms
64 bytes from 172.16.1.254: icmp_seq=2 ttl=64 time=0.064 ms
64 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 1998ms
rtt 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.1
connect: 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 route
default via 172.16.1.254 dev eth0
172.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.1
PING 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 ms
64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.076 ms
64 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 1999ms
rtt 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.1
PING 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 ms
64 bytes from 172.16.1.1: icmp_seq=2 ttl=64 time=0.083 ms
64 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 1998ms
rtt 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.2
PING 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 ms
64 bytes from 172.16.1.2: icmp_seq=2 ttl=64 time=0.092 ms
64 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 1999ms
rtt 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 route
default via 172.16.1.254 dev eth0
172.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.2
traceroute 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.2
PING 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 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=63 time=0.101 ms
64 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 1998ms
rtt min/avg/max/mdev = 0.084/0.094/0.101/0.010 ms

如今,你應該大體瞭解docker engine在建立單機容器網絡時都在背後作了哪些手腳了吧(固然,這裏只是簡單模擬,docker實際作的要比這複雜許多)。

4、基於userland proxy的容器端口映射的模擬

端口映射讓位於容器中的service能夠將服務範圍擴展到主機以外,好比:一個運行於container中的nginx能夠經過宿主機的9091端口對外提供http server服務:

$ sudo docker run -d -p 9091:80 nginx:latest
8eef60e3d7b48140c20b11424ee8931be25bc47b5233aa42550efabd5730ac2f

$ curl 10.11.36.15:9091
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    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>

容器的端口映射實際是經過docker engine的docker proxy功能實現的。默認狀況下,docker engine(截至docker 1.12.1版本)採用userland proxy(–userland-proxy=true)爲每一個expose端口的容器啓動一個proxy實例來作端口流量轉發:

$ ps -ef|grep docker-proxy
root     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.go
package main

import "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:8080
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
testfiles 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 8080
0.0.0.0
9090
172.16.1.1
80802017/01/11 17:26:10 listen ok

咱們http get一下宿主機的9090端口:

$curl 10.11.36.15:9090
<pre>
<a href="proxy">proxy</a>
<a href="proxy.go">proxy.go</a>
<a href="testfileserver">testfileserver</a>
<a href="testfileserver.go">testfileserver.go</a>
</pre>

成功得到file list!

proxy的輸出日誌:

2017/01/11 17:26:16 accept conn &{{0xc4200560e0}}
2017/01/11 17:26:16 dial  172.16.1.1:8080  ok
communication over: error:<nil>

因爲每一個作端口映射的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^。

6、參考資料

一、《Docker networking cookbook
二、《Docker cookbook

© 2017, bigwhite. 版權全部.

Related posts:

    1. 理解Docker跨多主機容器網絡
    2. 理解Docker容器端口映射
    3. 理解Docker單機容器網絡
    4. docker容器內服務程序的優雅退出
    5. Kubernetes集羣DNS插件安裝
相關文章
相關標籤/搜索