Docker的本地網絡實現其實就是利用了Linux上的網絡命名空間和虛擬網絡設備(特別是veth pair)。
基本原理
直觀上看,要實現網絡通信,機器需要至少一個網絡接口(物理接口或虛擬接口)與外界相通,並可以收發數據包;此外,如果不同子網之間要進行通信,需要額外的路由機制。
Docker中的網絡接口默認都是虛擬的接口。虛擬接口的最大優勢就是轉發效率極高。這是因為Linux通過在內核中進行數據復制來實現虛擬接口之間的數據轉發,即發送接口的發送緩存中的數據包將被直接復制到接收接口的接收緩存中,而無需通過外部物理網絡設備進行交換。對於本地系統和容器內系統來看,虛擬接口跟一個正常的以太網卡相比並無區別,只是它速度要快得多。
Docker容器網絡就很好地利用了Linux虛擬網絡技術,在本地主機和容器內分別創建一個虛擬接口,並讓它們彼此連通(這樣的一對接口叫做veth pair)。
一般情況下,Docker創建一個容器的時候,會具體執行如下操作:
1.創建一對虛擬接口,分別放到本地主機和新容器的命名空間中;
2.本地主機一端的虛擬接口連接到默認的docker0網橋或指定網橋上,並具有一個以veth開頭的唯一名字,如veth1234;
3.容器一端的虛擬接口將放到新創建的容器中,並修改名字作為eth0。這個接口只在容器的命名空間可見;
4.從網橋可用地址段中獲取一個空閑地址分配給容器的eth0(例如172.17.0.2/16),並配置默認路由網關為docker0網卡的內部接口docker0的IP地址(例如172.17.42.1/16)。
完成這些之后,容器就可以使用它所能看到的eth0虛擬網卡來連接其他容器和訪問外部網絡。用戶也可以通過docker network命令來手動管理網絡。
docker網絡模式
安裝Docker時,它會自動創建三個網絡,bridge(創建容器默認連接到此網絡,也就是在不使用--network參數時)、 none 、host。還有以后一種自定義模式,自定義模式有三種:bridge、overlay、macvlan。
host:容器將不會虛擬出自己的網卡,配置自己的IP等,而是使用宿主機的IP和端口。
Container:創建的容器不會創建自己的網卡,配置自己的IP,而是和一個指定的容器共享IP、端口范圍。
None:該模式關閉了容器的網絡功能。
Bridge:此模式會為每一個容器分配、設置IP等,並將容器連接到一個docker0虛擬網橋,通過docker0網橋以及Iptables nat表配置與宿主機通信。
通過docker network ls可以查看docker網絡:
當容器運行分別以none、host、bridge這三種模式的時候如下:
-
docker none network
-
docker host network
-
docker bridge network
bridge用法也就是容器默認使用,也就是上面基本原理所講到的。
-
自定義bridge模式
創建network
# docker network create -d bridge --ip-range=192.168.1.0/24 --gateway=192.168.1.1 --subnet=192.168.1.0/24 bridge2
# docker network ls
創建兩個容器指定ip並指定network
# docker run -it --network=bridge2 --ip=192.168.1.3 busybox
# docker run -it --network=bridge2 --ip=192.168.1.4 busybox
在使用docker run命令啟動容器的時候,可以通過--net參數來指定容器的網絡配置。
有5個可選值bridge、none、container、host和用戶定義的網絡:
--net=bridge:默認值,在Docker網橋docker0上為容器創建新的網絡棧。
--net=none:讓Docker將新容器放到隔離的網絡棧中,但是不進行網絡配置。之后,用戶可以自行進行配置。
--net=container:NAME_or_ID:讓Docker將新建容器的進程放到一個已存在容器的網絡棧中,新容器進程有自己的文件系統、進程列表和資源限制,但會和已存在的容器共享IP地址和端口等網絡資源,兩者進程可以直接通過lo環回接口通信。
--net=host:告訴Docker不要將容器網絡放到隔離的命名空間中,即不要容器化容器內的網絡。此時容器使用本地主機的網絡,它擁有完全的本地主機接口訪問權限。容器進程可以跟主機其他root進程一樣打開低范圍的端口,可以訪問本地網絡服務,比如D-bus,還可以讓容器做一些影響整個主機系統的事情,比如重啟主機。因此使用這個選項的時候要非常小心。如果進一步的使用--privileged=true參數,容器甚至會被允許直接配置主機的網絡棧。
--net=user_defined_network:用戶自行用network相關命令創建一個網絡,通過這種方式將容器連接到指定的已創建網絡上去。
網絡參數
有些命令選項只有在Docker服務啟動的時候才能配置,而且不能馬上生效:
-b BRIDGE or--bridge=BRIDGE——指定容器掛載的網橋;
--bip=CIDR——定制docker0的掩碼;
-H SOCKET...or--host=SOCKET...——Docker服務端接收命令的通道;
--icc=true|false——是否支持容器之間進行通信;
--ip-forward=true|false——啟用net.ipv4.ip_forward,即打開轉發功能;
--iptables=true|false——禁止Docker添加iptables規則;
--mtu=BYTES——容器網絡中的MTU。
下面2個命令選項既可以在啟動服務時指定,也可以docker run時候指定。在Docker服務啟動的時候指定則會成為默認值,后續執行docker run時可以覆蓋設置的默認值。
--dns=IP_ADDRESS...——使用指定的DNS服務器;
--dns-search=DOMAIN...——指定DNS搜索域。
最后這些選項只能在docker run執行時使用,因為它是針對容器的特性內容:
-h HOSTNAME or--hostname=HOSTNAME——配置容器主機名;
--link=CONTAINER_NAME:ALIAS——添加到另一個容器的連接;
--net=bridge|none|container:NAME_or_ID|host|user_defined_network——配置容器的橋接模式;
-p SPEC or--publish=SPEC——映射容器端口到宿主主機;
-P or--publish-all=true|false——映射容器所有端口到宿主主機。
配置容器DNS和主機名
Docker支持自定義容器的主機名和DNS配置。
1.相關配置文件
容器中主機名和DNS配置信息都是通過三個系統配置文件來維護的:/etc/resolv.conf、/etc/hostname和/etc/hosts。
啟動一個容器,在容器中使用mount命令可以看到這三個文件掛載信息:
/etc/resolv.conf文件在創建容器時候,默認會與宿主機/etc/resolv.conf文件內容保持一致
/etc/hosts文件中默認只記錄了容器自身的一些地址和名稱:
/etc/hostname文件則記錄了容器的主機名。
2.容器內修改配置文件
Docker 1.2.0開始支持在運行中的容器里直接編輯/etc/hosts,/etc/hostname和/etc/resolve.conf文件。但是這些修改是臨時的,只在運行的容器中保留,容器終止或重啟后並不會被保存下來。也不會被docker commit提交。
3.通過參數指定
如果用戶想要自定義容器的配置,可以在創建或啟動容器時利用下面的參數指定:
1)指定主機名-h HOSTNAME或者--hostname=HOSTNAME。設定容器的主機名,它會被寫到容器內的/etc/hostname和/etc/hosts。但這個主機名只有容器內能看到,在容器外部則看不到,既不會在docker ps中顯示,也不會在其他的容器的/etc/hosts看到。
2)記錄其他容器主機名--link=CONTAINER_NAME:ALIAS。選項會在創建容器的時候,添加一個所連接容器的主機名到容器內/etc/hosts文件中。這樣,新創建容器可以直接使用主機名來與所連接容器通信。
3)指定DNS服務器--dns=IP_ADDRESS。添加DNS服務器到容器的/etc/resolv.conf中,容器會用指定的服務器來解析所有不在/etc/hosts中的主機名。
4)指定DNS搜索域--dns-search=DOMAIN。設定容器的搜索域,當設定搜索域為.example.com時,在搜索一個名為host的主機時,DNS不僅搜索host,還會搜索host.example.com。
容器之間的通信
容器之間可通過 IP,Docker DNS Server 和joined 容器三種方式通信。
IP 通信
兩個容器要能通信,必須要有屬於同一個網絡的網卡。滿足這個條件后,容器就可以通過 IP 交互了。具體做法是在容器創建時通過 --network 指定相應的網絡,或者通過 docker network connect 將現有容器加入到指定網絡。
Docker DNS Server
通過 IP 訪問容器雖然滿足了通信的需求,但還是不夠靈活。因為我們在部署應用之前可能無法確定 IP,部署之后再指定要訪問的 IP 會比較麻煩。對於這個問題,可以通過 docker 自帶的 DNS 服務解決。
從 Docker 1.10 版本開始,docker daemon 實現了一個內嵌的 DNS server,使容器可以直接通過"容器名"通信。方法很簡單,只要在啟動時用 --name 為容器命名就可以了。
下面啟動兩個容器 box1 和 box2,並且在上面定義的網絡模式bridge2中:
docker run -it --network=bridge2 --name box1 busybox
docker run -it --network=bridge2 --name box2 busybox
box2和 box1可以互ping通
使用 docker DNS 有個限制:只能在 user-defined 網絡中使用。也就是說,默認的 bridge 網絡是無法使用 DNS 的。
joined 容器
joined 容器是另一種實現容器間通信的方式。它可以使兩個或多個容器共享一個網絡棧,共享網卡和配置信息,joined 容器之間可以通過 127.0.0.1 直接通信。例:
先創建一個http容器,名字為 box1
docker run -it --name box1 http
然后創建 busybox 容器並通過 --network=container:box1 指定 jointed 容器為 box1:
docker run -it --network=container:box1 busybox
box1 的網絡:
busybox 和 box1 的網卡 mac 地址與 IP 完全一樣,它們共享了相同的網絡棧。busybox 可以直接用 127.0.0.1 訪問 box1 的 http 服務。
joined 容器非常適合以下場景:
不同容器中的程序希望通過 loopback 高效快速地通信,比如 web server 與 app server。
希望監控其他容器的網絡流量,比如運行在獨立容器中的網絡監控程序。
容器訪問控制
容器的訪問控制主要通過Linux上的iptables防火牆軟件來進行管理和實現。iptables是Linux系統流行的防火牆軟件,在大部分發行版中都自帶。
1.容器訪問外部網絡
我們知道容器默認指定了網關為docker0網橋上的docker0內部接口。docker0內部接口同時也是宿主機的一個本地接口。因此,容器默認情況下是可以訪問到宿主機本地的。更進一步,容器要想通過宿主機訪問到外部網絡,需要宿主機進行轉發。
如果為0,則沒有開啟轉發,則需要手動打開:
# sysctl -w net.ipv4.ip_forward=1
更簡單的,在啟動Docker服務的時候設定--ip-forward=true,Docker服務會自動打開宿主機系統的轉發服務。
2.外部訪問容器實現
容器允許外部訪問,可以在docker run時候通過-p或-P參數來啟用。
不管用那種辦法,其實也是在本地的iptable的nat表中添加相應的規則,將訪問外部IP地址的網包進行目標地址DNAT,將目標地址修改為容器的IP地址。
以一個開放80端口的Web容器為例,使用-P時,會自動映射本地49000~49900范文內的端口隨機端口到容器的80端口:
可以看到,nat表中涉及兩條鏈,PREROUTING鏈負責包到達網絡接口時,改寫其目的地址。其中規則將所有流量都扔到DOCKER鏈。而DOCKER鏈中將所有不是從docker0進來的網包(意味着不是本地主機產生),將目標端口為49153的,修改目標地址為172.17.0.2,目標端口修改為80。
使用-p 80:80時,與上面類似,只是本地端口也為80:
有兩點需要注意:
這里的規則映射了0.0.0.0,意味着將接受主機來自所有網絡接口上的流量。用戶可以通過-p IP:host_port:container_port或-p IP::port來指定綁定的外部網絡接口,以制定更嚴格的訪問規則;
如果希望映射永久綁定到某個固定的IP地址,可以在Docker配置文件/etc/default/docker中指定DOCKER_OPTS="--ip=IP_ADDRESS",之后重啟Docker服務即可生效。
配置docker網橋
1.默認網橋
Docker服務默認會創建一個名稱為docker0的Linux網橋(其上有一個docker0內部接口),它在內核層連通了其他的物理或虛擬網卡,這就將所有容器和本地主機都放到同一個物理網絡。用戶使用Docker創建多個自定義網絡時可能會出現多個容器網橋。
Docker默認指定了docker0接口的IP地址和子網掩碼,讓主機和容器之間可以通過網橋相互通信,它還給出了MTU(接口允許接收的最大傳輸單元),通常是1500字節,或宿主主機網絡路由上支持的默認值。這些值都可以在服務啟動的時候進行配置:
--bip=CIDR——IP地址加掩碼格式,例如192.168.1.5/24;
--mtu=BYTES——覆蓋默認的Docker mtu配置。
也可以在配置文件中配置OPTIONS,然后重啟服務。
由於目前Docker網橋是Linux網橋,用戶可以使用brctl show來查看網橋和端口連接信息。
brctl命令如果系統中沒有自帶,可以使用 yum install bridge-utils安裝(centos)
每次創建一個新容器的時候,Docker從可用的地址段中選擇一個空閑的IP地址分配給容器的eth0端口。並且使用本地主機上docker0接口的IP作為容器的默認網關。
目前,Docker不支持在啟動容器時候指定IP地址。實際上,Linux網橋自身功能已經十分完備,也可以替換為OpenvSwitch等功能更強大的網橋實現。
2.自定義網橋
在上述docker網絡模式已經講到,使用docker network即可實現。
3.使用OpenvSwitch網橋
Docker默認使用的是Linux自帶的網橋實現,實際上,OpenvSwitch項目作為一個成熟的虛擬交換機實現,具備更豐富的功能。將來會有越來越多的容器支持OpenvSwitch作為底層網橋實現。
1.安裝Docker(centos 7)
安裝並啟動服務。默認Docker服務會創建一個名為docker0的Linux網橋,作為連接容器的本地網橋。
2.安裝OpenvSwitch
# yum install -y openvswitch
測試添加一個網橋br0並查看:
# ovs-vsctl add-br br0
# ovs-vsctl show
3.配置容器連接到OpenvSwitch網橋
目前OpenvSwitch網橋還不能直接支持掛載容器,需要手動在OpenvSwitch網橋上創建虛擬網口並掛載到容器中。
(1)創建無網口容器
啟動一個容器,並指定不創建網絡,后面我們手動添加網絡。較新版本的Docker默認不允許在容器內修改網絡配置,需要在run的時候指定參數--privileged=true:
記住這里容器的id。
(2)手動為容器添加網絡
下載OpenvSwitch項目提供的支持Docker容器的輔助腳本ovs-docker:
# wget https://github.com/openvswitch/ovs/raw/master/utilities/ovs-docker
# chmod a+x ovs-docker
為容器添加網卡,並掛載到br0上,命令為:
#./ovs-docker add-port br0 eth0 00b9028e14aa --ipaddress=10.0.0.2/16
添加成功后,在容器內查看網絡信息,多了一個新添加的網卡eth0,對應添加的IP地址。
在容器外,配置OpenvSwitch的網橋br0內部接口地址為10.0.1.2/16(只要與所掛載容器IP在同一個子網內即可):
# ifconfig br0 10.0.1.2/16
(3)測試連通
經過上面步驟,容器已經連接到了網橋br0上了,拓撲如下所示:
容器(10.0.7.2/16)<-->br0網橋<-->br0內部端口(10.0.1.2/16)
此時,在容器內就可以測試是否連通到網橋br0上了:
在容器內也可以配置默認網關為br0接口地址:
route add default gw GWIP
刪除該接口的命令為:
# ./ovs-docker del-port br0 eth0<container_id>
刪除名為br0的網橋:ovs-vsctl del-br br0
實際上,Docker社區也已經討論對OpenvSwitch進行原生支持了。在Docker原生支持OpenvSwitch之前,用戶可以通過編寫腳本或更高級的工具來讓這一過程自動化。