理解Docker單機容器網絡

jopen 8年前發布 | 63K 次閱讀 Docker 計算機網絡 操作系統

Docker 容器是近兩年最 火的IT技術之一,用“火山爆發式“來形容Docker的成 長也不為過。Docker在產品服務的 devops 運維、云 計算(CaaS)、大數據以及企業內部應用等領域正在被越來越多的接受和廣泛應用。Docker技術的本質在于提升計算密度和提升部署效率,高屋 建瓴的講,它的出現符合人類社會對綠色發展的追求,降低資源消耗,提升資源的單位利用率。不過經歷了兩年多的發展,Docker依舊年輕,尚未成 熟,在集群調度、存儲、網絡、安全等方面,Docker依舊有很長的路要走。

在一年多以前,也就是 Docker發布1.0 后沒幾個月時,我曾經學習過一段時間的Docker,主要學習Docker的概念和基本使用方法。由于當時docker 還相對“稚嫩”,在產品和項目中暫無用武之地,也就沒有深入,但對Docker技術的跟蹤倒是沒有停下來。今年 Docker 1.9發布 ,支持跨主機container netwoking;第三方容器集群調度和服務編織工具蓬勃發展,如 Kubernetesmesosflannel 以及 rancher 等;國內基于Docker的云服 務及產品也 如雨后春筍般發展開來。雖然不到2年,但Docker的演進速度是飛快的,要想跟的上Docker的步伐,僅僅跟蹤技術信息是不夠的,對伴生 Docker發展起來的一些新理念、新技術、新方案需要更深入的理解,這便是這篇文章(以及后續關于這個主題文章)編寫的初衷。

我計劃從容器網絡開始,我們先來看看單機容器網絡。

一、目標

Docker實質上是匯集了linux容器(各種namespaces)、cgroups以及“疊加”類文件系統等多種核心技術的一種復合技術。 其默認容器網絡的建立和控制是一種結合了network namespace、iptables、linux網橋、route table等多種Linux內核技術的綜合方案。理解Docker容器網絡,首先是以對TCP/IP網絡體系的理解為前提的,不過也不需要多深刻,大學本 科學的那套“計算機網絡”足矣^_^,另外還要考慮Linux上對虛擬網絡設備實現的獨特性(區分于硬件網絡設備)。

本篇文章主要針對單機Docker容器網絡,目的是了解Docker容器網絡中容器與容器間通信、容器與宿主機間通信、容器與宿主機所在的物理網 絡中主機通信、容器網絡控制等機制,為后續理解跨主機容器網絡的理解打下基礎。同時稍帶利用工具對Docker容器網絡的網絡性能做初步測量,通 過直觀數據初步評估容器網絡的適用性。

二、試驗環境以及拓撲

本文試驗環境如下:

- 宿主機 Ubuntu 12.04 x86_64 3.13.0-61-generic
- 容器OS:基于Ubuntu 14.04 Server x86_64的自制image
- Docker版本 - v1.9.1 for linux/amd64

為了試驗方便,這里基于官方ubuntu:14.04 image制作了帶有traceroute、brctl以及tcpdump等網絡調試工具的image,簡單起見(考慮到公司內網代理),這里就沒有寫 Dockerfile(即便寫也很簡單),而是直接z在容器內apt-get install后,再通過docker commit基于已經安裝好上述工具的container創建的一個新image:

$sudo docker commit 0580adb079a3 dockernetworking/ubuntu:14.04
a692757cbb7bd7d8b70f393930e954cce625934485e93cf1b28c15efedb5f2d3
$ docker images
REPOSITORY                TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
dockernetworking/ubuntu   14.04               a692757cbb7b        5 seconds ago       302.1 MB

后續的container均是基于dockernetworking/ubuntu創建的。

另外試驗環境的拓撲圖如下:

從拓撲圖中我們可以看到,物理宿主機為10.10.126.101,置于物理局域網10.10.126.0/24中。在宿主機上我們創建了兩 個 Container:Container1和Container2,Container所用網段為172.17.0.0/16。

三、Docker Daemon初始網絡

當你在一個clean環境下,啟動Docker daemon后,比如在Ubuntu下,使用sudo service docker start,Docker Daemon就會初始化后續創建容器時所需的基礎網絡設備和配置。

以下是從宿主機的角度看到的:

// 網橋
$ brctl show
bridge name    bridge id        STP enabled    interfaces
docker0        8000.0242f9f8c9ad    no

// 網絡設備
$ ip link show
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: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 2c:59:e5:01:98:28 brd ff:ff:ff:ff:ff:ff
... ...
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:f9:f8:c9:ad brd ff:ff:ff:ff:ff:ff

// 網絡設備ip地址
$ ip addr show
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: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 2c:59:e5:01:98:28 brd ff:ff:ff:ff:ff:ff
    inet 10.10.126.101/24 brd 10.10.126.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::2e59:e5ff:fe01:9828/64 scope link
       valid_lft forever preferred_lft forever
... ...
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:f9:f8:c9:ad brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:f9ff:fef8:c9ad/64 scope link
       valid_lft forever preferred_lft forever

可以看出,與Docker Daemon啟動前相比,宿主物理機中多出來一個虛擬網絡設備:docker0。

docker0是一個標準Linux虛擬網橋設備。在Docker默認的橋接網絡工作模式中,docker0網橋起到了至關重要的作用。物理網橋 是標準的二層網絡設備,一般說,標準物理網橋只有兩個網口,可以將兩個物理網絡(區分以IP為尋址單位的邏輯網絡)連接在一起。但與物理層設備集 線器等相比,網橋具備隔離沖突域的功能。網橋通過MAC地址學習和泛洪的方式實現二層相對高效的通信。在今天,標準網橋設備已經基本被淘汰了,替 代網橋的是是二層交換機。二層交換機也可以看成一個多口網橋。在不劃分vlan的前提下,可以將其當做兩兩端口間都是獨立通道的”hub”使用。

前面說過docker0是一個標準Linux虛擬網橋設備,即一個以軟件實現的網橋,由于其支持多口,實際上它算是一個虛擬交換機設備。與物理網 橋不同的是,它不但可以二層轉發包,還可以將包送到用戶層進行處理。在我們尚未創建container的時候,docker0以一個Linux網 絡設 備的身份存在,并且Linux虛擬網橋可以配置IP,可以作為在三層網絡上的一個Gateway,在主機眼中和物理網口設備eth0區別不大。與 Linux其他網絡設備也可以在三層相互通信,前提是Docker Daemon打開了ip包轉發功能:

$ cat /proc/sys/net/ipv4/ip_forward
1

宿主機的路由表也增加了一條路由(見最后一條):

$ ip route
default via 10.10.126.1 dev eth0  proto static
10.10.126.0/24 dev eth0  proto kernel  scope link  src 10.10.126.101  metric 1
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1

除此之外,Docker Daemon還設置了若干iptables規則以管理containers間的通信以及輔助container訪問外部網絡(NAT轉換):

sudo iptables-save > ./iptables.init.rules

# Generated by iptables-save v1.4.12 on Wed Jan 13 17:25:55 2016
*raw
: PREROUTING ACCEPT [9469:2320376]
:OUTPUT ACCEPT [2990:1335235]
COMMIT
# Completed on Wed Jan 13 17:25:55 2016
# Generated by iptables-save v1.4.12 on Wed Jan 13 17:25:55 2016
*filter
:INPUT ACCEPT [1244:341290]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [483:153047]
: DOCKER - [0:0]
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
COMMIT
# Completed on Wed Jan 13 17:25:55 2016
# Generated by iptables-save v1.4.12 on Wed Jan 13 17:25:55 2016
*nat
: PREROUTING ACCEPT [189:88629]
:INPUT ACCEPT [111:60817]
:OUTPUT ACCEPT [23:1388]
: POSTROUTING ACCEPT [23:1388]
: DOCKER - [0:0]
-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
COMMIT
# Completed on Wed Jan 13 17:25:55 2016

iptables 是Linux內核自帶的包過濾防火墻,支持 NAT 等諸多功能。iptables由表和規則chain概念組成,Docker中所 用的表包括filter表和nat表(參見上述命令輸出結果),這也是iptables中最常用的兩個表。iptables是一個復雜的存在,曾 有一本書《 linux firewalls 》 專門講解iptables,這里先借用本書 中的一幅圖來描述一下ip packets在各個表和chain之間的流轉過程:

網卡收到的數據包進入到iptables后,做路由選擇,本地的包通過INPUT鏈送往user層應用;轉發到其他網口的包通過FORWARD chain;本地產生的數據包在路由選擇后,通過OUTPUT chain;最后POSTROUTING chain多用于source nat轉換。

iptables在容器網絡中最重要的兩個功能:

1、限制container間的通信2、將container到外部網絡包的源地址換成宿主主機地址(MASQUERADE)

后續還會在詳細描述容器通信流程中還會摻雜說明iptables的規則在容器通信中的作用。

四、準備工作:讓iptables輸出log

iptables在Docker單機容器默認網絡工作模式下扮演著重要的角色,并且由于是虛擬設備網絡,數據的流轉是十分復雜的,為了便于跟蹤 iptables在docker容器網絡數據通信過程中起到的作用,這里在默認iptables規則的基礎上,做一些調整,在關鍵位置輸出一些 log,以便調試和理解,這些修改不會影響iptables對數據包的匹配和操作。注意:在操作iptables前,建議通過iptables- save命令備份一份iptables的配置數據。

iptables自身就支持LOG target,日志會輸出到/var/log/syslog或kern.log中。我們的目標就是在關鍵節點輸出iptables的數據日志。考慮到日志 量較大,我們僅攔截icmp包(ping)以及tcp 源端口或目的端口為12580的數據。

考慮到篇幅有限,這里僅給出配置后導出的iptables.final.rules,需要的同學可以通過iptables-restore < iptables.final.rules導入。

# Generated by iptables-save v1.4.12 on Thu Jan 14 09:28:43 2016
*raw
: PREROUTING ACCEPT [788:127290]
:OUTPUT ACCEPT [574:100918]
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
COMMIT
# Completed on Thu Jan 14 09:28:43 2016
# Generated by iptables-save v1.4.12 on Thu Jan 14 09:28:43 2016
*filter
:INPUT ACCEPT [284:49631]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [81:28047]
: DOCKER - [0:0]
:FwdId0Od0 - [0:0]
:FwdId0Ond0 - [0:0]
:FwdOd0 - [0:0]
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j FwdOd0
-A FORWARD -i docker0 ! -o docker0 -j FwdId0Ond0
-A FORWARD -i docker0 -o docker0 -j FwdId0Od0
-A OUTPUT ! -s 127.0.0.1/32 -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A FwdId0Od0 -i docker0 -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Od0:" --log-level 7
-A FwdId0Od0 -i docker0 -o docker0 -j ACCEPT
-A FwdId0Ond0 -i docker0 ! -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Ond0:" --log-level 7
-A FwdId0Ond0 -i docker0 ! -o docker0 -j ACCEPT
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j LOG --log-prefix "[TonyBai]-FwdOd0:" --log-level 7
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
# Completed on Thu Jan 14 09:28:43 2016
# Generated by iptables-save v1.4.12 on Thu Jan 14 09:28:43 2016
*nat
: PREROUTING ACCEPT [37:6070]
:INPUT ACCEPT [20:2585]
:OUTPUT ACCEPT [6:364] :P OSTROUTING ACCEPT [6:364]
: DOCKER - [0:0]
:LogNatPostRouting - [0:0]
-A PREROUTING -p icmp -j LOG --log-prefix "[TonyBai]-Enter iptables:" --log-level 7
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterNatInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-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 LogNatPostRouting
-A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix "[TonyBai]-NatPostRouting:" --log-level 7
-A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
COMMIT
# Completed on Thu Jan 14 09:28:43 2016

一切就緒,只待對docker網絡的分析了。

五、容器網絡

現在我們來啟動容器。根據試驗環境拓撲圖,我們需要創建和啟動兩個容器:container1和container2。

$ docker run -it --name container1 dockernetworking/ubuntu:14.04 /bin/bash
$ docker run -it --name container2 dockernetworking/ubuntu:14.04 /bin/bash

$ docker ps
CONTAINER ID        IMAGE                           COMMAND             CREATED             STATUS              PORTS               NAMES
1104fc63c571        dockernetworking/ubuntu:14.04   "/bin/bash"         7 seconds ago       Up 6 seconds                            container2
8b38131deb28        dockernetworking/ubuntu:14.04   "/bin/bash"         16 seconds ago      Up 15 seconds                           container1

容器啟動后,從宿主機的視角,可以看到網絡配置有如下變化:

$ brctl show
bridge name    bridge id        STP enabled    interfaces
docker0        8000.0242f9f8c9ad    no        veth00855d7
                            vethee8659f

$ifconfig -a
... ...
veth00855d7 Link encap:以太網  硬件地址 ea:70:65:cf:28:6b
          inet6 地址: fe80::e870:65ff:fecf:286b/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  躍點數:1
          接收數據包:8 錯誤:0 丟棄:0 過載:0 幀數:0
          發送數據包:37 錯誤:0 丟棄:0 過載:0 載波:0
          碰撞:0 發送隊列長度:0
          接收字節:648 (648.0 B)  發送字節:5636 (5.6 KB)

vethee8659f Link encap:以太網  硬件地址 fa:30:bb:0b:1d:eb
          inet6 地址: fe80::f830:bbff:fe0b:1deb/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  躍點數:1
          接收數據包:61 錯誤:0 丟棄:0 過載:0 幀數:0
          發送數據包:82 錯誤:0 丟棄:0 過載:0 載波:0
          碰撞:0 發送隊列長度:0
          接收字節:5686 (5.6 KB)  發送字節:9678 (9.6 KB)
... ...

Docker Daemon創建了兩個veth網絡設備,并將veth掛接到docker0網橋上了。veth是一種虛擬網卡設備,創建時成對(veth pair)出現,從一個veth peer發出的數據包可以到達其pair peer。不過從上面命令輸出來看,我們似乎并沒有看到veth pair,這是因為每個pair的另一peer被放到container的network namespace中了,變成了container中的eth0。veth pair常用于在不同網絡命名空間之間通信。在拓撲圖中,container1中的eth0與veth-x是一個pair;container2中的 eth0與veth-y是另一個pair。veth-x和veth-y掛接在docker0網橋上,這對于container1和 container2來說,就好比用網線將本地網卡(eth0)與網橋設備docker0的網口連接起來一樣。在docker容器網絡默認橋接模式 中,veth只是在二層起作用。

下面是從container1內部看到的網絡配置:

root@8b38131deb28:/# ip addr
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
47: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever

root@8b38131deb28:/# netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         172.17.0.1      0.0.0.0         UG        0 0          0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U         0 0          0 eth0

container網絡配置很簡單,一個eth0網卡,一個loopback口,route表里將網橋作為默認Gateway。

至此,我們拓撲圖中的環境已經全部就緒。接下來我們來探索和理解一下容器網絡的幾種通信流程。

六、Docker0的“雙重身份”

在正式進入每個通信流程前,我們先來點預備性內容 – 如何理解Docker0。下圖中我們給出了Docker0的雙重身份,并對比物理交換機,我們來理解一下Docker0這個軟網橋。

1、從容器視角,網橋(交換機)身份

docker0對于通過veth pair“插在”網橋上的container1和container2來說,首先就是一個二層的交換機的角色:泛洪、維護cam表,在二層轉發數據包;同 時由于docker0自身也具有mac地址(這個與純二層交換機不同),并且綁定了ip(這里是172.17.0.1),因此在 container中還作為container default路由的默認Gateway而存在。

2、從宿主機視角,網卡身份

物理交換機提供了由硬件實現的高效的背板通道,供連接在交換機上的主機高效實現二層通信;對于開啟了三層協議的物理交換機而言,其ip路由的處理 也是由物理交換機管理程序提供的。對于docker0而言,其負責處理二層交換機邏輯以及三層的處理程序其實就是宿主機上的Linux內核 tcp/ip協議棧程序。而從宿主機來看,所有docker0從veth(只是個二層的存在,沒有綁定ipv4地址)接收到的數據包都會被宿主機 看成從docker0這塊網卡(第二個身份,綁定172.17.0.1)接收進來的數據包,尤其是在進入三層時,宿主機上的iptables就會 對docker0進來的數據包按照rules進行相應處理(通過一些內核網絡設置也可以忽略docker0 brigde數據的處理)。

在后續的Docker容器網絡通信流程分析中,docker0將在這兩種身份間來回切換。

七、容器網絡通信流程

考慮到大部分tcp/ip實現都是在內核實現的ping服務器,這可能會導致iptables流程走不全,影響我們的理解,因此我這里通過tcp 連接建立的握手過程(sync, ack sync, ack)的通信包來理解container網絡通信。我們可以簡單在服務端啟動一個python httpserver: python -m SimpleHTTPServer 12580或用Go寫個簡單的http server來監聽12580端口;客戶端用telnet ip port的方式與服務端建立連接。

iptables的log我們可以在宿主機(ubuntu 12.04)的/var/log/syslog中查看到。考慮到篇幅,頭兩個例子會作詳細說明,后續將簡要闡述。

1、container to container

場景:我們在container2(172.17.0.3)中啟動監聽12580的服務程序,并在container1(172.17.0.2) 中執行:telnet 172.17.0.3 12580。

分析:

我們首先從container1的視角去看。

在container1中無需考慮iptables過程,可以理解為未開啟。container1的用戶層的數據進入該網絡名字空間 (network namespace)的網絡協議棧處理。在route decision過程中,協議棧處理程序發現目的地址匹配172.17.0.0/16這條網絡路由,該條路由的Flag為U,即該網絡為直連鏈路上的網 絡,即無需使用Gateway,直接可以將數據包發到eth0上并封包發出去即可。

由于可以在直連網路鏈路上找到目的主機,于是二層欲填寫的目的mac地址為172.17.0.3這個ip對應的mac。container1在 arp緩存中查詢172.17.0.3對應的mac地址。如沒有發現172.17.0.3這個ip地址對應的緩存mac地址,則發起一個arp請 求,arp請求的二層目的mac地址填寫為二層廣播地址:bit全1的mac地址(48bit),并通過eth0發出去。

docker0在這個過程中二層交換機的作用。接收到來自veth上的廣播arp請求后,將請求通過二層網絡轉發到其他docker0上的 veth口上。這時container2收到了arp請求,container2上的以太網驅動程序收到arp請求后,將其發給 container2上的arp協議處理程序(不走iptables),arp協議處理程序封裝arp reply后轉出。container1收到reply后,處理二層封包,將container2的mac地址填入以太網數據幀的目的mac地址字段中, 并發出。

上一節提到過,docker0收到container1發來的ip數據包,交由其處理程序,也就是linux內核協議棧處理程序處理,這時 docker0的身份開始轉換了。

我們現在轉換到宿主機視角。

從宿主機視角,docker0是一個mac地址為02:42:f9:f8:c9:ad,ip為172.17.0.1的網卡(網卡身份)。 container1發出的進入到docker0的包,對于host來說,就好比從docker0這塊網卡設備進入到宿主機的數據包。當數據包進 入到三層時,iptables的處理規則就起了作用。我們看到在raw prerouting中的日志:

Jan 14 10:08:12 pc-baim kernel: [830038.910054] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:ac:11:00:03:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.3 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=24284 DF PROTO=TCP SPT=43292 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

這是第一個ip包,承載著tcp sync數據。按照iptables的數據流轉,接下來的route decision發現目的地址是172.17.0.3,不是自身綁定的172.17.0.1,不用送到user層(不走input鏈),在host的路由 表中繼續匹配路由表項,匹配到如下路由表項:172.17.0.0/16 dev docker0,于是走forward鏈:

Jan 14 10:08:12 pc-baim kernel: [830038.910120] [TonyBai]-FwdId0Od0:IN=docker0 OUT=docker0 PHYSIN=vethd9f6465 PHYSOUT=vethfcceafa MAC=02:42:ac:11:00:03:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.3 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=24284 DF PROTO=TCP SPT=43292 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

這又是一個直連網絡,無需Gateway作為下一跳,于是再從docker0將數據送出。

docker0送出時,docker0又回到二層功能范疇。在cam表中查找mac地址02:42:ac:11:00:03對應的網口 vethfcceafa,將數據從vethfcceafa送出去。根據veth pair的描述,container2中的eth0將收到這份數據。container2發現數據包中目的地址是172.17.0.3,就是自身eth0 的地址,于是送到user層處理。

接下來是container 3 回復ack sync的過程。與上面類似,container3通過直連網絡將數據包發給docker0。從host視角看,數據包從docker0這個網卡設備進 來:

Jan 14 10:08:12 pc-baim kernel: [830038.910200] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethfcceafa MAC=02:42:ac:11:00:02:02:42:ac:11:00:03:08:00 SRC=172.17.0.3 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43292 WINDOW=28960 RES=0x00 ACK SYN URGP=0

route decision,由于目的地址不是docker0自身的目的地址,匹配路由條目:172.17.0.0/16 dev docker0,于是走forward鏈。這次在iptables forward鏈中匹配到的rules是:FwdOd0

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)

pkts bytes target prot opt in out source destination

6 328 DOCKER all — * docker0 0.0.0.0/0 0.0.0.0/0

5 268 FwdOd0 all — * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED

… …

因為這次是conn established相關的鏈路上回包,日志如下:

Jan 14 10:08:12 pc-baim kernel: [830038.910230] [TonyBai]-FwdOd0:IN=docker0 OUT=docker0 PHYSIN=vethfcceafa PHYSOUT=vethd9f6465 MAC=02:42:ac:11:00:02:02:42:ac:11:00:03:08:00 SRC=172.17.0.3 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43292 WINDOW=28960 RES=0x00 ACK SYN URGP=0

于是ack sync再從docker0送出。docker0送出時封裝包時回到二層功能范疇。在cam表中查找mac地址02:42:ac:11:00:02對應的 網口vethd9f6465,將數據從vethd9f6465送出去。根據veth pair的描述,container1中的eth0將收到這份數據包。container1發現數據包中目的地址是172.17.0.2,就是自身 eth0的地址,于是送到user層處理。

container1接下來的回送ack過程與sync過程類似,這里就不贅述了。

2、container to docker0

場景:我在container1(172.17.0.2)中執行:telnet 172.17.0.1 12580。docker0所在宿主機上并沒有程序在監聽12580端口,因此這個tcp連接是無法建立起來的。sync過去后,對方返回ack rst,而不是ack sync。

分析:

我們首先從container1的視角去看。

container1向172.17.0.1建立連接,在路由decision后,發現目標主機在直連網絡中,于是將對方mac地址封裝到二層協 議幀中后通過eth0將包轉出。docker0收到包后,送到宿主機網絡協議棧,也就是docker0的管理程序去處理。

切換到宿主機視角。宿主機從網卡docker0獲取數據包,宿主機網絡協議棧處理數據包,進入iptables中:

Jan 14 12:53:02 pc-baim kernel: [839935.434253] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=29166 DF PROTO=TCP SPT=41362 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

路由decision后發現目的地址就是docker0自己的地址(172.17.0.1),要送給user層,于是走filter input鏈:

Jan 14 12:53:02 pc-baim kernel: [839935.434309] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=29166 DF PROTO=TCP SPT=41362 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

送到user層后,user層發現沒有程序監聽12580端口,于是向下發出ack rst包。數據包重新路由后,發現是直連網絡,從docker0口出。但出去之前需要先進入iptables的filter output鏈:

Jan 14 12:53:02 pc-baim kernel: [839935.434344] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.2 LEN=40 TOS=0x10 PREC=0x00 TTL=64 ID=781 DF PROTO=TCP SPT=12580 DPT=41362 WINDOW=0 RES=0x00 ACK RST URGP=0

數據包從docker0進入后,docker0承擔網橋角色,在二層轉發給container1,結束處理。

3、container to host

場景:我在container1(172.17.0.2)中執行:telnet 10.10.126.101 12580。docker0所在宿主機上啟動服務程序在監聽12580端口,因此這是個標準tcp連接建立過程(sync, ack sync, ack)。

分析:

我們首先從container1的視角去看。

container1在經過路由判斷后,匹配到default路由,需要走gateway(flags = UG),于是將目的mac填寫為Gateway 172.0.0.1的mac地址,將包通過eth0轉給Gateway,即docker0。

切換到宿主機視角。

宿主機從網卡docker0收到一個數據包,進入iptables:

Jan 14 14:11:28 pc-baim kernel: [844644.563436] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=55780 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

路由decision,由于目的地址是10.10.126.101,docker0的管理程序,也就是host的linux網絡棧處理程序發現這 不是我自己么(雖然是從 docker0收到的,但網絡棧程序知道172.0.0.1和10.10.126.101都是自己),于是user層收下了這個包。因此在路由 后,數據包走到filter input:

Jan 14 14:11:28 pc-baim kernel: [844644.563476] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=55780 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

user層監聽12580的服務程序收到包后,回復ack syn到172.17.0.2,路由Decision后,發現在直連網絡中,通過docker0轉出,于是走iptable filter output。

Jan 14 14:11:28 pc-baim kernel: [844644.563519] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=59373 WINDOW=28960 RES=0x00 ACK SYN URGP=0

container1收到ack syn后再回復ack,路徑與sync一致,日志如下:

Jan 14 14:11:28 pc-baim kernel: [844644.563566] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=55781 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 14 14:11:28 pc-baim kernel: [844644.563584] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=55781 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0

4、host to container

場景:我在宿主機(10.10.126.101)中執行:telnet 172.17.0.2 12580。container1上啟動服務程序在監聽12580端口,因此這是個標準tcp連接建立過程(sync, ack sync, ack)。

分析:

這次我們首先從宿主機角度出發。

host的telnet程序在用戶層產生數據包,經路由decision,匹配直連網絡路由,出口docker0,然后進入iptables的 filter output鏈:

Jan 14 14:19:25 pc-baim kernel: [845121.897441] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.2 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=51756 DF PROTO=TCP SPT=44120 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

你會發現在這個log中,數據包的src ip地址為172.17.0.1,這是協議棧處理程序的選擇,沒有選擇10.10.126.101,這些地址都標識host自己。

container1在收到sync后,回復ack sync,這就相當于container to host。host這次從docker0收到目的為172.17.0.1的ack sync包 , 走的是filer input,這里不贅述。

Jan 14 14:19:25 pc-baim kernel: [845121.897552] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=44120 WINDOW=28960 RES=0x00 ACK SYN URGP=0

host再回復ack,與sync相同,走filter output鏈,不贅述。

Jan 14 14:19:25 pc-baim kernel: [845121.897588] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.2 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=51757 DF PROTO=TCP SPT=44120 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0

5、container to 10.10.126.187

場景:我們在container1中向與宿主機直接網絡的主機10.10.126.187建立連接。我在container1中執 行:telnet 10.10.126.187 12580。187上啟動服務程序在監聽12580端口,因此這是個標準tcp連接建立過程(sync, ack sync, ack)。

分析:

container1視角:將sync包發個目的地址10.10.126.187,根據路由選擇,從默認路由走,下一跳為Gateway,即 172.17.0.1。消息發到docker0。

切換到host視角:host從docker0網卡收到一個sync包,目的地址是10.10.126.187,進入到iptables:

Jan 14 14:47:17 pc-baim kernel: [846795.243863] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=34160 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

路由選擇后,匹配到host的直連網絡路由(10.10.126.0/24 via eth0),包將從eth0出去,于是docker0轉發到eth0,走foward chain:

Jan 14 14:47:17 pc-baim kernel: [846795.243931] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=34160 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

出forward chain后,匹配到nat表的postrouting鏈,做Masquerade(SNAT)。將源地址從172.0.0.2換為 10.10.126.101再發出去。

Jan 14 14:47:17 pc-baim kernel: [846795.243940] [TonyBai]-NatPostRouting:IN= OUT=eth0 PHYSIN=vethd9f6465 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=34160 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

10.10.126.187收到后,回復ack sync。由于10.10.126.187上增加了172.17.0.0/16的路由,gateway為10.10.126.101,因此ack sync被回送給宿主機,host會從187收到ack sync包。

Jan 14 14:47:17 pc-baim kernel: [846795.244155] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=55148 WINDOW=5792 RES=0x00 ACK SYN URGP=0

進入iptables時,目的地址還是10.10.126.101,進入路由選擇前iptables會將10.10.126.101換成 172.17.0.2(由于之間在natpostrouting做了masquerade)。這樣后續路由的目的地址為docker0,需要由 eth0轉到docker0,走 forward鏈。由于是RELATED, ESTABLISHED 連接,因此匹配到FwdOd0:

Jan 14 14:47:17 pc-baim kernel: [846795.244182] [TonyBai]-FwdOd0:IN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=55148 WINDOW=5792 RES=0x00 ACK SYN URGP=0

切換到container1視角。收到ack sync后,回復ack,同sync流程,不贅述:

Jan 14 14:47:17 pc-baim kernel: [846795.244249] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=34161 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 14 14:47:17 pc-baim kernel: [846795.244266] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=34161 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0

不用再走一遍natpostrouting,屬于一個流的包只會 經過這個表一次。如果第一個包被允許做NAT或Masqueraded,那么余下的包都會自 動地被做 相同的操作。也就是說,余下的包不會再通過這個表一個一個的被NAT,而是自動地完成。

6、10.10.126.187 to container

場景:我們在10.10.126.187向container1建立連接。我在187中執行:telnet 172.17.0.2 12580。container1上啟動服務程序在監聽12580端口,因此這是個標準tcp連接建立過程(sync, ack sync, ack)。

分析:

由于187上增加了container1的路由,187將sync包發到gateway 10.10.126.101。

宿主機視角:從eth0收到目的地址為172.17.0.2的sync包,到達iptables:

Jan 14 15:06:08 pc-baim kernel: [847926.218791] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=48735 DF PROTO=TCP SPT=53225 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0

路由后應該通過docker0發到直連網絡。應該走Forward鏈,但由于上面的log沒有覆蓋到,只是匹配到DOCKER chain,沒有匹配到可以log的rules,沒有打印出來log。

docker0將sync發給container1,container1回復ack sync。消息報目的地址187,走gateway,即docker0。

再回到主機視角,host從docker0網卡收到ack sync包,目的187,因此路由后,走直連網絡轉發口eth0。iptables中走forward chain:FwdId0Ond0:

Jan 14 15:06:08 pc-baim kernel: [847926.219010] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=53225 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 14 15:06:08 pc-baim kernel: [847926.219103] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=53225 WINDOW=28960 RES=0x00 ACK SYN URGP=0

注意這塊是已經建立的連接,雙方都知道對方的地址了(187上配置了172.17.0.2的路由),因此并沒有走nat postroutiing chain,沒有SNAT轉換地址。

187收到后,回復ack。這個過程重復sync過程,但forward鏈可以匹配到FwdOd0:

Jan 14 15:06:08 pc-baim kernel: [847926.219417] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=48736 DF PROTO=TCP SPT=53225 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0
Jan 14 15:06:08 pc-baim kernel: [847926.219477] [TonyBai]-FwdOd0:IN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=48736 DF PROTO=TCP SPT=53225 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0

八、容器網絡性能測量

這里順便對容器網絡性能做一個初步的測量,測量可以考慮使用傳統工具: netperf ,其服務端為netserver,會同netperf一并安裝到主機中。但前些時候發現了一款顯示結果更直觀的用go實現的工具: sparkyfish 。這里我打算用這個新工具來粗粗的測量一下容器網絡的性能。

由于sparkyfish會執行upload和download場景,因此server放在哪個位置均可。

我們執行兩個場景,對比host和container的網絡性能:

1、與同局域網的一個主機通信

我們在一臺與host在同一局域網的主機(105.71)上啟動sparkyfish-server,然后分別在host和container上執行sparkyfish-cli 10.10.105.71,結果截圖如下:

host to 105.71

container to 105.71

對比發現:container、host到外部網絡的度量值差不多,avg值幾乎相同。

2、container to host and container

我們在host和另一個container2上分別啟動一個sparkyfish-server,然后在container1上執行分別執行sparkyfish-cli 10.10.126.101和sparkyfish-cli 172.17.0.3,結果截圖如下:

container to host

container to container

對比可以看出:container to container的出入網絡性能均僅為container to host的網絡性能的三分之一不到。

九、小結

以上粗略理解了docker單機容器網絡,有些地方理解難免有偏頗,甚至是錯誤,歡迎指正。Docker技術雖然成長迅猛,前景廣闊,但Docker也非銀彈,深入之處必然有坑。填坑之路雖然痛苦,但能有所收獲也算是很好了。

? 2016,bigwhite. 版權所有.

來自: http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!