0x00 概述
在互聯網時代,網絡已經成為絕大多數應用進行數據交換的主要通道,Docker 作為集群部署的利器,在網絡支持上也下了許多功夫。功能豐富和強大,並不代表使用復雜,在 Docker 的封裝下,我們依然可以通過命令和參數輕松的為容器制定不同的網絡方案。在這一節中,我們就來了解 Docker 的網絡部分。
0x01 容器網絡
在之前介紹 Docker 核心組成的時候,我們已經簡單談到了容器網絡的相關知識。容器網絡實質上也是由 Docker 為應用程序所創造的虛擬環境的一部分,它能讓應用從宿主機操作系統的網絡環境中獨立出來,形成容器自有的網絡設備、IP 協議棧、端口套接字、IP 路由表、防火牆等等與網絡相關的模塊。
還是回歸上面這幅之前展示過的關於 Docker 網絡的圖片。在 Docker 網絡中,有三個比較核心的概念,也就是:沙盒 ( Sandbox )、網絡 ( Network )、端點 ( Endpoint )。
- 沙盒提供了容器的虛擬網絡棧,也就是之前所提到的端口套接字、IP 路由表、防火牆等的內容。其實現隔離了容器網絡與宿主機網絡,形成了完全獨立的容器網絡環境。
- 網絡可以理解為 Docker 內部的虛擬子網,網絡內的參與者相互可見並能夠進行通訊。Docker 的這種虛擬網絡也是於宿主機網絡存在隔離關系的,其目的主要是形成容器間的安全通訊環境。
- 端點是位於容器或網絡隔離牆之上的洞,其主要目的是形成一個可以控制的突破封閉的網絡環境的出入口。當容器的端點與網絡的端點形成配對后,就如同在這兩者之間搭建了橋梁,便能夠進行數據傳輸了。
這三者形成了 Docker 網絡的核心模型,也就是容器網絡模型 ( Container Network Model )。
0x02 淺析 Docker 的網絡實現
容器網絡模型為容器引擎提供了一套標准的網絡對接范式,而在 Docker 中,實現這套范式的是 Docker 所封裝的 libnetwork 模塊。
而對於網絡的具體實現,在 Docker 的發展過程中也逐漸抽象,形成了統一的抽象定義。進而通過這些抽象定義,便可以對 Docker 網絡的實現方式進行不同的變化。
目前 Docker 官方為我們提供了五種 Docker 網絡驅動,分別是:Bridge Driver、Host Driver、Overlay Driver、MacLan Driver、None Driver。
其中,Bridge 網絡是 Docker 容器的默認網絡驅動,簡而言之其就是通過網橋來實現網絡通訊 ( 網橋網絡的實現可以基於硬件,也可以基於軟件 )。而 Overlay 網絡是借助 Docker 集群模塊 Docker Swarm 來搭建的跨 Docker Daemon 網絡,我們可以通過它搭建跨物理主機的虛擬網絡,進而讓不同物理機中運行的容器感知不到多個物理機的存在。
Bridge Driver 和 Overlay Driver 在開發中使用頻率較高,之后的小節講解里,關於容器網絡的部分我們都主要圍繞着它們展開。
當然,關於 Docker 的網絡實現還有非常多的細節。對於開發者來說,我們只是 Docker 的使用者而非技術專家,所以這里我們不做更多詳盡的論述。
0x03 容器互聯
由於 Docker 提倡容器與應用共生的輕量級容器理念,所以容器中通常只包含一種應用程序,但我們知道,如今紛繁的系統服務,沒有幾個是可以通過單一的應用程序支撐的。拿最簡單的 Web 應用為例,也至少需要業務應用、數據庫應用、緩存應用等組成。也就是說,在 Docker 里我們需要通過多個容器來組成這樣的系統。
而這些互聯網時代的應用,其間的通訊方式主要以網絡為主,所以打通容器間的網絡,是使它們能夠互相通訊的關鍵所在。
要讓一個容器連接到另外一個容器,我們可以在容器通過 docker create
或 docker run
創建時通過 --link
選項進行配置。
例如,這里我們創建一個 MySQL 容器,將運行我們 Web 應用的容器連接到這個 MySQL 容器上,打通兩個容器間的網絡,實現它們之間的網絡互通。
$ sudo docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes mysql $ sudo docker run -d --name webapp --link mysql webapp:latest
容器間的網絡已經打通,那么我們要如何在 Web 應用中連接到 MySQL 數據庫呢?Docker 為容器間連接提供了一種非常友好的方式,我們只需要將容器的網絡命名填入到連接地址中,就可以訪問需要連接的容器了。
假設我們在 Web 應用中使用的是 JDBC 進行數據庫連接的,我們可以這么填寫連接。
String url = "jdbc:mysql://mysql:3306/webapp";
在這里,連接地址中的 mysql 就好似我們常見的域名解析,Docker 會將其指向 MySQL 容器的 IP 地址。
看到這里,讀者們有沒有發現 Docker 在容器互通中為我們帶來的一項便利,也就是我們不再需要真實的知道另外一個容器的 IP 地址就能進行連接。再具體來對比,在以往的開發中,我們每切換一個環境 ( 例如將程序從開發環境提交到測試環境 ),都需要重新配置程序中的各項連接地址等參數,而在 Docker 里,我們並不需要關心這個,只需要程序中配置被連接容器的別名,映射 IP 的工作就交給 Docker 完成了。
0x04 暴露端口
需要注意的是,雖然容器間的網絡打通了,但並不意味着我們可以任意訪問被連接容器中的任何服務。Docker 為容器網絡增加了一套安全機制,只有容器自身允許的端口,才能被其他容器所訪問。
這個容器自我標記端口可被訪問的過程,我們通常稱為暴露端口。我們在 docker ps
的結果中可以看到容器暴露給其他容器訪問的端口。
$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 95507bc88082 mysql:5.7 "docker-entrypoint.s…" 17 seconds ago Up 16 seconds 3306/tcp, 33060/tcp mysql
這里我們看到,MySQL 這個容器暴露的端口是 3306 和 33060。所以我們連接到 MySQL 容器后,只能對這兩個端口進行訪問。
端口的暴露可以通過 Docker 鏡像進行定義,也可以在容器創建時進行定義。在容器創建時進行定義的方法是借助 --expose
這個選項。
$ sudo docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --expose 13306 --expose 23306 mysql:5.7
這里我們為 MySQL 暴露了 13306 和 23306 這兩個端口,暴露后我們可以在 docker ps
中看到這兩個端口已經成功的打開。
$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3c4e645f21d7 mysql:5.7 "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 3306/tcp, 13306/tcp, 23306/tcp, 33060/tcp mysql
容器暴露了端口只是類似我們打開了容器的防火牆,具體能不能通過這個端口訪問容器中的服務,還需要容器中的應用監聽並處理來自這個端口的請求。
0x05 通過別名連接
純粹的通過容器名來打開容器間的網絡通道缺乏一定的靈活性,在 Docker 里還支持連接時使用別名來使我們擺脫容器名的限制。
$ sudo docker run -d --name webapp --link mysql:database webapp:latest
在這里,我們使用 --link <name>:<alias>
的形式,連接到 MySQL 容器,並設置它的別名為 database。當我們要在 Web 應用中使用 MySQL 連接時,我們就可以使用 database 來代替連接地址了。
String url = "jdbc:mysql://database:3306/webapp";
0x06 管理網絡
容器能夠互相連接的前提是兩者同處於一個網絡中 ( 這里的網絡是指容器網絡模型中的網絡 )。這個限制很好理解,剛才我們說了,網絡這個概念我們可以理解為 Docker 所虛擬的子網,而容器網絡沙盒可以看做是虛擬的主機,只有當多個主機在同一子網里時,才能互相看到並進行網絡數據交換。
當我們啟動 Docker 服務時,它會為我們創建一個默認的 bridge 網絡,而我們創建的容器在不專門指定網絡的情況下都會連接到這個網絡上。所以我們剛才之所以能夠把 webapp 容器連接到 mysql 容器上,其原因是兩者都處於 bridge 這個網絡上。
我們通過 docker inspect
命令查看容器,可以在 Network 部分看到容器網絡相關的信息。
$ sudo docker inspect mysql [ { ## ...... "NetworkSettings": { ## ...... "Networks": { "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "NetworkID": "bc14eb1da66b67c7d155d6c78cb5389d4ffa6c719c8be3280628b7b54617441b", "EndpointID": "1e201db6858341d326be4510971b2f81f0f85ebd09b9b168e1df61bab18a6f22", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:11:00:02", "DriverOpts": null } } ## ...... } ## ...... } ]
這里我們能夠看到 mysql 容器在 bridge 網絡中所分配的 IP 地址,其自身的端點、Mac 地址,bridge 網絡的網關地址等信息。
Docker 默認創建的這個 bridge 網絡是非常重要的,理由自然是在沒有明確指定容器網絡時,容器都會連接到這個網絡中。在之前講解 Docker for Win 和 Docker for Mac 安裝的時候,我們提到過這兩個軟件的配置中都有一塊配置 Docker 中默認網絡的內容,這塊所指的默認網絡就是這個 bridge 網絡。
0x07 創建網絡
在 Docker 里,我們也能夠創建網絡,形成自己定義虛擬子網的目的。
docker CLI 里與網絡相關的命令都以 docker network
開頭,其中創建網絡的命令是 docker network create
。
$ sudo docker network create -d bridge individual
通過 -d
選項我們可以為新的網絡指定驅動的類型,其值可以是剛才我們所提及的 bridge、host、overlay、maclan、none,也可以是其他網絡驅動插件所定義的類型。這里我們使用的是 Bridge Driver ( 當我們不指定網絡驅動時,Docker 也會默認采用 Bridge Driver 作為網絡驅動 )
通過 docker network ls
或是 docker network list
可以查看 Docker 中已經存在的網絡。
$ sudo docker network ls NETWORK ID NAME DRIVER SCOPE bc14eb1da66b bridge bridge local 35c3ef1cc27d individual bridge local
之后在我們創建容器時,可以通過 --network
來指定容器所加入的網絡,一旦這個參數被指定,容器便不會默認加入到 bridge 這個網絡中了 ( 但是仍然可以通過 --network bridge
讓其加入 )。
$ sudo docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --network individual mysql:5.7
我們通過 docker inspect
觀察一下此時的容器網絡。
$ sudo docker inspect mysql [ { ## ...... "NetworkSettings": { ## ...... "Networks": { "individual": { "IPAMConfig": null, "Links": null, "Aliases": [ "2ad678e6d110" ], "NetworkID": "35c3ef1cc27d24e15a2b22bdd606dc28e58f0593ead6a57da34a8ed989b1b15d", "EndpointID": "41a2345b913a45c3c5aae258776fcd1be03b812403e249f96b161e50d66595ab", "Gateway": "172.18.0.1", "IPAddress": "172.18.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:12:00:02", "DriverOpts": null } } ## ...... } ## ...... } ]
可以看到,容器所加入網絡已經變成了 individual 這個網絡了。
這時候我們通過 --link
讓處於另外一個網絡的容器連接到這個容器上,看看會發生什么樣的效果。
$ sudo docker run -d --name webapp --link mysql --network bridge webapp:latest docker: Error response from daemon: Cannot link to /mysql, as it does not belong to the default network. ERRO[0000] error waiting for container: context canceled
可以看到容器並不能正常的啟動,而 Docker 提醒我們兩個容器處於不同的網絡,之間是不能相互連接引用的。
我們來改變一下,讓運行 Web 應用的容器加入到 individual 這個網絡,就可以成功建立容器間的網絡連接了。
$ sudo docker run -d --name webapp --link mysql --network individual webapp:latest
0x08 端口映射
剛才我們提及的都是容器直接通過 Docker 網絡進行的互相訪問,在實際使用中,還有一個非常常見的需求,就是我們需要在容器外通過網絡訪問容器中的應用。最簡單的一個例子,我們提供了 Web 服務,那么我們就需要提供一種方式訪問運行在容器中的 Web 應用。
在 Docker 中,提供了一個端口映射的功能實現這樣的需求。
通過 Docker 端口映射功能,我們可以把容器的端口映射到宿主操作系統的端口上,當我們從外部訪問宿主操作系統的端口時,數據請求就會自動發送給與之關聯的容器端口。
要映射端口,我們可以在創建容器時使用 -p
或者是 --publish
選項。
$ sudo docker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12
使用端口映射選項的格式是 -p <ip>:<host-port>:<container-port>
,其中 ip 是宿主操作系統的監聽 ip,可以用來控制監聽的網卡,默認為 0.0.0.0,也就是監聽所有網卡。host-port 和 container-port 分別表示映射到宿主操作系統的端口和容器的端口,這兩者是可以不一樣的,我們可以將容器的 80 端口映射到宿主操作系統的 8080 端口,傳入 -p 8080:80
即可。
我們可以在容器列表里看到端口映射的配置。
$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES bc79fc5d42a6 nginx:1.12 "nginx -g 'daemon of…" 4 seconds ago Up 2 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp nginx
打印的結果里用 ->
標記了端口的映射關系。
0x09 在 Windows 和 macOS 中使用映射
Docker 的端口映射功能是將容器端口映射到宿主操作系統的端口上,實際來說就是映射到了 Linux 系統的端口上。而我們知道,在 Windows 和 macOS 中運行的 Docker,其 Linux 環境是被虛擬出來的,如果我們僅僅是將端口映射到 Linux 上,由於虛擬環境還有一層隔離,我們依然不能通過 Windows 或 macOS 的端口來訪問容器。
解決這種問題的方法很簡單,只需要再加一次映射,將虛擬 Linux 系統中的端口映射到 Windows 或 macOS 的端口即可。
如果我們使用 Docker for Windows 或 Docker for Mac,這個端口映射的操作程序會自動幫助我們完成,所以我們不需要做任何額外的事情,就能夠直接使用 Windows 或 macOS 的端口訪問容器端口了。
而當我們使用 Docker Toolbox 時,由於其自動化能力比較差,所以需要我們在 VirtualBox 里單獨配置這個操作系統端口到 Linux 端口的映射關系。
在 VirtualBox 配置中的端口轉發一欄里,進行相關的配置即可。