一文搞懂 Docker 容器网络:三大网络、自定义网络与容器通信全解析

标签:Docker / 容器网络 / 运维实践

本文导读:本章开始讨论 Docker 网络。我们会首先学习 Docker 提供的几种原生网络,以及如何创建自定义网络;然后探讨容器之间如何通信,以及容器与外界如何交互。Docker 网络从覆盖范围可分为单个 host 上的容器网络和跨多个 host 的网络,本文重点讨论前一种。

目录

一、Docker 网络概览:三个默认网络

Docker 安装时会自动在 host 上创建三个网络,我们可用 docker network ls 命令查看:

[root@docker ~]# docker network ls
NETWORK ID     NAME     DRIVER   SCOPE
a1b809382f0d   bridge   bridge   local
6ad21ed9ea1f   host     host     local
4e31521b27c6   none     null     local

下面我们分别讨论它们。

二、none 网络:封闭隔离的极致选择

none 网络的 driver 类型是 null,IPAM 字段为空。挂在 none 网络上的容器只有 lo,无法与外界通信。

图示说明:容器内只有 lo(本地回环)接口,地址为 127.0.0.1/8,没有其他任何网卡。说明挂在 none 网络上的容器完全封闭,无法与外界通信。

顾名思义,none 网络就是"什么都没有"的网络。挂在这个网络下的容器除了 lo,没有其他任何网卡。容器创建时,可以通过 --network=none 指定使用 none 网络:

[root@docker ~]# docker run -it --network=none busybox
/ # ifconfig
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
/ # hostname
4027da12afaf

我们不禁会问,这样一个封闭的网络有什么用呢?

其实还真有应用场景。封闭意味着隔离,一些对安全性要求高并且不需要联网的应用可以使用 none 网络。比如某个容器的唯一用途是生成随机密码,就可以放到 none 网络中避免密码被窃取。

当然大部分容器是需要网络的,我们接着看 host 网络。

三、host 网络:共享宿主机网络栈

挂在 host 网络上的容器共享宿主机的 network namespace,即容器的网络配置与 host 网络配置完全一样。

图示说明:容器内可见宿主机的所有网卡(如 ens160、docker0 等),且 hostname 与宿主机相同。说明使用 --network=host 时容器直接共享宿主机网络栈,性能最优但失去网络隔离和端口灵活性。

连接到 host 网络的容器共享 Docker host 的网络栈,容器的网络配置与 host 完全一样。可以通过 --network=host 指定使用 host 网络:

[root@docker ~]# docker run -it --network=host busybox    # 容器网络与宿主机相同
/ # ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue
/ # hostname                                              # 容器主机名与宿主机相同
docker

在容器中可以看到 host 的所有网卡,并且连 hostname 也是 host 的。host 网络的使用场景又是什么呢?

直接使用 Docker host 的网络最大的好处就是性能,如果容器对网络传输效率有较高要求,则可以选择 host 网络。当然不便之处就是牺牲一些灵活性,比如要考虑端口冲突问题,Docker host 上已经使用的端口就不能再用了。

Docker host 的另一个用途是让容器可以直接配置 host 网络。比如某些跨 host 的网络解决方案,其本身也是以容器方式运行的,这些方案需要对网络进行配置,比如管理 iptables。

四、bridge 网络:学容器必须懂的默认网络

上一节我们讨论了 none 和 host 类型的容器网络,本节学习应用最广泛、也是默认的 bridge 网络。

Docker 安装时会创建一个命名为 docker0 的 Linux bridge,实际上它是 Linux 的一个 bridge(网桥),可以理解为一个软件交换机,它会在挂载到它的网口之间进行转发。如果不指定 --network,创建的容器默认都会挂到 docker0 上。

当创建一个 Docker 容器的时候,同时会创建一对 veth pair 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包),这对接口:

  • 一端在容器内,即 eth0;
  • 另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 vethAQI2QT)。

通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。

图示说明:默认 bridge 网络结构为——左侧是 Docker Host,中间是 docker0 网桥(Linux bridge),右侧是容器。每个容器通过一对 veth pair 连接到 docker0:容器侧为 eth0,宿主机侧为 vethXXX 接口挂在 docker0 上。veth pair 就像一根虚拟网线,把容器接入网桥,从而实现容器之间以及容器与宿主机之间的通信。

下面演示。先安装 bridge-utils 查看当前网桥:

[root@docker ~]# yum install -y bridge-utils
[root@docker ~]# brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02420be905a5       no

当前 docker0 上没有任何其他网络设备,我们创建一个容器看看有什么变化:

[root@docker ~]# docker run -itd --name busybox1 busybox
5225d246f751dbfc4cdf745675d9c79d3de1b91dd4174268c35e671b9f1b0c80
[root@docker ~]# brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02420be905a5       no              vethddb2744

一个新的网络接口 vethddb2744 被挂到了 docker0 上,vethddb2744 就是新创建容器的虚拟网卡。下面看一下容器的网络配置:

[root@docker ~]# docker exec -it busybox1 sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    inet 127.0.0.1/8 scope host lo
# 容器里的网卡是24号网卡名字叫eth0,对面是25号网卡
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0

容器有一个网卡 eth0@if25。大家可能会问了,为什么不是 vethddb2744 呢?

实际上 eth0@if25 和 vethddb2744 是一对 veth pair。veth pair 是一种成对出现的特殊网络设备,可以把它们想象成由一根虚拟网线连接起来的一对网卡:网卡的一头(eth0@if25)在容器中,另一头(vethddb2744)挂在网桥 docker0 上,其效果就是将 eth0@if25 也挂在了 docker0 上。

在宿主机上查看 IP 地址可以证明这一点:宿主机看到有块网卡 25: vethddb2744@if24,含义就是宿主机 25 号网卡对面连接了一块 24 号网卡(即容器中的 eth0),25 号网卡的名字叫 vethddb2744。这就证明了容器 busybox1 里的 eth0 连接到了 docker0 网桥的 vethddb2744。

我们还看到 eth0@if25 已经配置了 IP 172.17.0.2,为什么是这个网段呢?让我们通过 docker network inspect bridge 看一下 bridge 网络的配置信息:

{
    "Name": "bridge",
    "Driver": "bridge",
    "IPAM": {
        "Config": [
            {
                "Subnet": "172.17.0.0/16",      # 这是 bridge 网络分配的网段
                "Gateway": "172.17.0.1"
            }
        ]
    },
    "Containers": {
        "5225d246f751...": {
            "Name": "busybox1",                 # 容器 busybox1
            "IPv4Address": "172.17.0.2/16"      # 分配给 busybox1 的 172.17.0.2
        }
    }
}

原来 bridge 网络配置的 subnet 就是 172.17.0.0/16,并且网关是 172.17.0.1。这个网关在哪儿呢?大概你已经猜出来了,就是 docker0:

[root@docker ~]# ip a | grep docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

图示说明(当前网络拓扑):docker0 网桥 IP 为 172.17.0.1/16,容器 busybox1 的 eth0 IP 为 172.17.0.2/16,两者通过 veth pair(vethddb2744 ↔ eth0@if25)相连。docker0 同时充当该网络的网关,网段为 172.17.0.0/16。

容器创建时,docker 会自动从 172.17.0.0/16 中分配一个 IP,这里 16 位的掩码保证有足够多的 IP 可以供容器使用。

五、自定义网络:创建自己的 bridge 网络

除了 none、host、bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络。Docker 提供三种 user-defined 网络驱动:bridge、overlay 和 macvlan,其中 overlay 和 macvlan 用于创建跨主机的网络。

我们可通过 bridge 驱动创建类似前面默认的 bridge 网络:

[root@docker ~]# docker network create --driver bridge my_net
89f7bc11b602e84452ae01786113ac196a535f7296b9989ad941b3a48a5e6d04
[root@docker ~]# brctl show
bridge name             bridge id               STP enabled     interfaces
br-89f7bc11b602         8000.0242194d039e       no                            # my_net
docker0                 8000.02420be905a5       no              vethddb2744

新增了一个网桥 br-89f7bc11b602,这里 br-89f7bc11b602 正好是新建 bridge 网络 my_net 的短 id。执行 docker network inspect my_net 查看配置,其中 172.18.0.0/16 是 Docker 自动分配的 IP 网段,网关为 172.18.0.1。

我们可以自己指定 IP 网段吗?答案是:可以。只需在创建时指定 --subnet--gateway 参数:

[root@docker ~]# docker network create --driver bridge --subnet 172.22.16.0/24 --gateway 172.22.16.1 my_net2
ec761bc51778f67c2245af72fd969f00cd517ee617a10a70dc01904f9c10279d
[root@docker ~]# brctl show
bridge name     bridge id               STP enabled     interfaces
br-89f7bc11b602 8000.0242194d039e       no                            # my_net
br-ec761bc51778 8000.02427fa54b88       no                            # my_net2
docker0         8000.02420be905a5       no              vethddb2744

这里我们创建了新的 bridge 网络 my_net2,网段为 172.22.16.0/24,网关为 172.22.16.1。与前面一样,网关在 my_net2 对应的网桥 br-ec761bc51778 上:

[root@docker ~]# ip a | grep br-ec761bc51778
27: br-ec761bc51778: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    inet 172.22.16.1/24 brd 172.22.16.255 scope global br-ec761bc51778

容器要使用新的网络,需要在启动时通过 --network 指定:

[root@docker ~]# docker run -it --network=my_net2 --name busybox2 busybox
/ # ip a
28: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:16:10:02 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.2/24 brd 172.22.16.255 scope global eth0

容器分配到的 IP 为 172.22.16.2。到目前为止,容器的 IP 都是 docker 自动从 subnet 中分配,我们能否指定一个静态 IP 呢?答案是:可以,通过 --ip 指定

[root@docker ~]# docker run -it --network=my_net2 --ip 172.22.16.8 --name busybox3 busybox
/ # ip a
30: eth0@if31: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:16:10:08 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.8/24 brd 172.22.16.255 scope global eth0    # 确实是指定的 IP

注意:只有使用 --subnet 创建的网络才能指定静态 IP。my_net 创建时没有指定 --subnet,如果指定静态 IP 会报错:user specified IP address is supported only when connecting to networks with user configured subnets.

图示说明(当前网络拓扑):docker0(172.17.0.0/16)上挂着 busybox1(172.17.0.2);自定义网桥 br-ec761bc51778(my_net2,172.22.16.0/24)上挂着 busybox2(172.22.16.2)和 busybox3(172.22.16.8)。两个网桥之间没有直连,说明不同网络默认是隔离的。

六、容器之间的连通性

通过前面小节的实践,当前 docker host 上已有 docker0 与 my_net2 两个网络,本节我们将讨论这几个容器之间的连通性。

busybox2、busybox3 容器都挂在 my_net2 上,应该能够互通,我们验证一下:

[root@docker ~]# docker exec -it busybox2 sh
/ # ifconfig eth0
eth0      Link encap:Ethernet  HWaddr 02:42:AC:16:10:02
          inet addr:172.22.16.2  Bcast:172.22.16.255  Mask:255.255.255.0
/ # ping -c 3 172.22.16.8          # ping busybox3
3 packets transmitted, 3 packets received, 0% packet loss
/ # ping -c 3 172.22.16.1          # ping my_net2 网关地址
3 packets transmitted, 3 packets received, 0% packet loss

可见同一网络中的容器、网关之间都是可以通信的

my_net2 与默认 bridge 网络(docker0)能通信吗?从拓扑可知,两个网络属于不同的网桥,应该不能通信,我们通过实验验证一下,让 busybox2 容器 ping busybox1 容器:

/ # ping -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
--- 172.17.0.2 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss

确实 ping 不通,符合预期。

"等等!不同的网络如果加上路由应该就可以通信了吧?"这是一个非常非常好的想法。确实,如果 host 上对每个网络都有一条路由,同时操作系统上打开了 ip forwarding,host 就成了一个路由器,挂接在不同网桥上的网络就能够相互通信。下面我们来看看 docker host 满不满足这些条件:

[root@docker ~]# ip r
default via 192.168.108.2 dev ens160 proto static metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
172.22.16.0/24 dev br-ec761bc51778 proto kernel scope link src 172.22.16.1
192.168.108.0/24 dev ens160 proto kernel scope link src 192.168.108.30 metric 100
[root@docker ~]# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

172.17.0.0/16 和 172.22.16.0/24 两个网络的路由都定义好了,ip forwarding 也已经启用了。条件都满足,为什么不能通行呢?我们还得看看 iptables:

-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-ec761bc51778 ! -o br-ec761bc51778 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-2 -o br-ec761bc51778 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP

原因就在这里了:iptables DROP 掉了网桥 docker0 与 br-ec761bc51778(my_net2)之间双向的流量。从规则的命名 DOCKER-ISOLATION 可知,docker 在设计上就是要隔离不同的 network。

那么接下来的问题是:怎样才能让 busybox1 与 busybox2 通信呢?答案是:为 busybox1 容器添加一块 my_net2 的网卡,这个可以通过 docker network connect 命令实现:

[root@docker ~]# docker network connect my_net2 busybox1
[root@docker ~]# docker exec -it busybox1 sh
/ # ip a
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
32: eth1@if33: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:16:10:03 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.3/24 brd 172.22.16.255 scope global eth1

容器中增加了一个网卡 eth1,分配了 my_net2 的 IP 172.22.16.3;同时网桥 br-ec761bc51778 上新增了接口 veth6642cc9 连接 busybox1。现在 busybox2 应该能够访问 busybox1 了,验证一下:

[root@docker ~]# docker exec -it busybox2 sh
/ # ping -c 3 172.22.16.3
3 packets transmitted, 3 packets received, 0% packet loss

busybox2 能够 ping 到 busybox1。

图示说明(connect 后的网络结构):执行 docker network connect my_net2 busybox1 后,busybox1 新增 eth1 网卡(172.22.16.3/24),通过新的 veth pair(veth6642cc9)接入 my_net2 网桥。此时 busybox1 同时属于 docker0 和 my_net2 两个网络,因此可以与 busybox2、busybox3 互通。

七、容器通信的三种方式

容器之间可通过 IP、Docker DNS Server 或 joined 容器三种方式通信。

1. IP 通信

从上一节的例子可以得出这样一个结论:两个容器要能通信,必须要有属于同一个网络的网卡。满足这个条件后,容器就可以通过 IP 交互了。具体做法是在容器创建时通过 --network 指定相应的网络,或者通过 docker network connect 将现有容器加入到指定网络。可参考上一节 busybox 的例子,这里不再赘述。

2. Docker DNS Server

通过 IP 访问容器虽然满足了通信的需求,但还是不够灵活。因为我们在部署应用之前可能无法确定 IP,部署之后再指定要访问的 IP 会比较麻烦。对于这个问题,可以通过 docker 自带的 DNS 服务解决。

从 Docker 1.10 版本开始,docker daemon 实现了一个内嵌的 DNS server,使容器可以直接通过"容器名"通信。方法很简单,只要在启动时用 --name 为容器命名就可以了:

[root@docker ~]# docker run -it --network my_net2 --name bbox1 busybox
[root@docker ~]# docker run -it --network my_net2 --name bbox2 busybox
[root@docker ~]# docker exec -it bbox2 sh
/ # ping -c 3 bbox1
PING bbox1 (172.22.16.2): 56 data bytes
64 bytes from 172.22.16.2: seq=0 ttl=64 time=0.177 ms
--- bbox1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss

使用 docker DNS 有个限制:只能在 user-defined 网络中使用。也就是说,默认的 bridge 网络是无法使用 DNS 的。下面验证一下,创建 bbox3 和 bbox4,均连接到 bridge 网络:

[root@docker ~]# docker run -it --name bbox3 busybox
[root@docker ~]# docker run -it --name bbox4 busybox
[root@docker ~]# docker exec -it bbox4 sh
/ # ping -c 3 bbox3
ping: bad address 'bbox3'

bbox4 无法通过容器名 ping 到 bbox3。

3. joined 容器

joined 容器是另一种实现容器间通信的方式。joined 容器非常特别,它可以使两个或多个容器共享一个网络栈,共享网卡和配置信息,joined 容器之间可以通过 127.0.0.1 直接通信。请看下面的例子:

先创建一个 httpd 容器,名字为 web1:

[root@docker ~]# docker run -d -it --name web1 httpd
f7641c43eb7021724e869f77bd4541e909ebf0968fc35e6a43e8e674d046ef3f
[root@docker ~]# docker exec -it web1 bash
root@1d5181ce7a85:/usr/local/apache2# hostname -I
172.17.0.4

然后创建 busybox 容器并通过 --network=container:web1 指定 joined 容器为 web1,请注意 busybox 容器中的网络配置信息:

[root@docker ~]# docker run -it --network container:web1 busybox
/ # ip a
18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0

看!busybox 和 web1 的网卡 MAC 地址与 IP 完全一样,它们共享了相同的网络栈。busybox 可以直接用 127.0.0.1 访问 web1 的 http 服务:

/ # wget 127.0.0.1
Connecting to 127.0.0.1 (127.0.0.1:80)
saving to 'index.html'
'index.html' saved
/ # cat index.html
<html><body><h1>It works!</h1></body></html>

joined 容器非常适合以下场景:

  1. 不同容器中的程序希望通过 loopback 高效快速地通信,比如 web server 与 app server。
  2. 希望监控其他容器的网络流量,比如运行在独立容器中的网络监控程序。

容器之间的通信我们已经搞清楚了,接下来要考虑的是容器如何与外部世界通信。

八、容器访问外部世界:NAT 的魔法

前面我们已经解决了容器间通信的问题,接下来讨论容器如何与外部世界通信。这里涉及两个方向:

  1. 容器访问外部世界
  2. 外部世界访问容器

在我们当前的实验环境下,docker host 是可以访问外网的。我们看一下容器是否也能访问外网:

[root@docker ~]# docker run -it --name test1 busybox
/ # ping -c 3 www.baidu.com
PING www.baidu.com (223.109.82.6): 56 data bytes
64 bytes from 223.109.82.6: seq=0 ttl=127 time=21.240 ms
--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss

可见,容器默认就能访问外网。请注意:这里外网指的是容器网络以外的网络环境,并非特指 internet。

现象很简单,但更重要的:我们应该理解现象下的本质。在上面的例子中,busybox 位于 docker0 这个私有 bridge 网络中(172.17.0.0/16),当 busybox 从容器向外 ping 时,数据包是怎样到达 www.baidu.com 的呢?这里的关键就是 NAT。我们查看一下 docker host 上的 iptables 规则:

[root@docker ~]# iptables -t nat -S
-P PREROUTING ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i docker0 -j RETURN

在 NAT 表中,有这么一条规则:

-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

其含义是:如果网桥 docker0 收到来自 172.17.0.0/16 网段的外出包,把它交给 MASQUERADE 处理。而 MASQUERADE 的处理方式是将包的源地址替换成 host 的地址发送出去,即做了一次网络地址转换(NAT)。

下面我们通过 tcpdump 查看地址是如何转换的。先查看 docker host 的路由表,默认路由通过 ens160 发出去,所以我们要同时监控 ens160 和 docker0 上的 icmp(ping)数据包:

[root@docker ~]# yum install -y tcpdump
# 窗口1
[root@docker ~]# tcpdump -i docker0 -n icmp
# 窗口2
[root@docker ~]# tcpdump -i ens160 -n icmp
# 窗口3:busybox ping www.baidu.com
[root@docker ~]# docker run -it busybox
/ # ping -c 3 www.baidu.com

tcpdump 输出如下:

# docker0 抓包窗口
15:23:42.854951 IP 172.17.0.2 > 223.109.82.41: ICMP echo request, id 7, seq 0, length 64
15:23:42.870473 IP 223.109.82.41 > 172.17.0.2: ICMP echo reply, id 7, seq 0, length 64

# ens160 抓包窗口
15:25:41.886472 IP 192.168.108.30 > 223.109.82.41: ICMP echo request, id 8, seq 0, length 64
15:25:41.906738 IP 223.109.82.41 > 192.168.108.30: ICMP echo reply, id 8, seq 0, length 64

docker0 收到 busybox 的 ping 包,源地址为容器 IP 172.17.0.2,这没问题,交给 MASQUERADE 处理。这时,在 ens160 上我们看到了变化:ping 包的源地址变成了 ens160 的 IP 192.168.108.30。这就是 iptables NAT 规则处理的结果,从而保证数据包能够到达外网。整个过程的四个步骤如下:

  1. busybox 发送 ping 包:172.17.0.2 > www.baidu.com。
  2. docker0 收到包,发现是发送到外网的,交给 NAT 处理。
  3. NAT 将源地址换成 ens160 的 IP:192.168.108.30 > www.baidu.com。
  4. ping 包从 ens160 发送出去,到达 www.baidu.com。

通过 NAT,docker 实现了容器对外网的访问。下一节我们讨论另一个方向的流量:外部世界如何访问容器。

九、外部世界访问容器:端口映射

上节我们学习了容器如何访问外部网络,本节讨论另一个方向:外部网络如何访问到容器?答案是:端口映射

docker 可将容器对外提供服务的端口映射到 host 的某个端口,外网通过该端口访问容器。容器启动时通过 -p 参数映射端口。

图示说明(端口映射原理):httpd 容器内部监听 80 端口,通过 -p 参数映射到宿主机的一个随机高端口(如 32768)。外部请求访问 <host_ip>:32768 时,由 docker-proxy 转发到容器的 80 端口。

容器启动后,可通过 docker ps 或者 docker port 查看到 host 映射的端口。在上面的例子中,httpd 容器的 80 端口被映射到 host 32768 上,docker ps 的 PORTS 列显示 0.0.0.0:32768->80/tcp,表示该端口已映射到宿主机所有网卡的 32768 端口。这样就可以通过 <host ip>:<32768> 访问容器的 web 服务了:

[root@docker ~]# curl 192.168.108.30:32768
<html><body><h1>It works!</h1></body></html>

除了映射动态端口,也可在 -p 中指定映射到 host 某个特定端口,例如可将 80 端口映射到 host 的 8080 端口:

[root@docker ~]# docker run -d -p 8080:80 httpd
25b04cb7f6d6c012c609251a425475fcf7da03c93feb20c1d951eb45cb1ef79b
[root@docker ~]# curl 192.168.108.30:8080
<html><body><h1>It works!</h1></body></html>

每一个映射的端口,host 都会启动一个 docker-proxy 进程来处理访问容器的流量。以 0.0.0.0:8080->80/tcp 为例分析整个过程:

  1. docker-proxy 监听 host 的 8080 端口。
  2. 当 curl 访问 192.168.108.30:8080 时,docker-proxy 转发给容器 172.17.0.3:80。
  3. httpd 容器响应请求并返回结果。

可见每个 -p 映射都会启动一个独立的 docker-proxy 进程来处理流量。

实战:安装 tomcat

在 Docker Hub 上面查找 tomcat 镜像并拉取到本地:

[root@docker ~]# docker search tomcat
NAME       DESCRIPTION                                       STARS   OFFICIAL
tomcat     Apache Tomcat is an open source implementati…   3751    [OK]
[root@docker ~]# docker pull tomcat
[root@docker ~]# docker images
REPOSITORY   TAG      IMAGE ID       CREATED       SIZE
httpd        latest   90f191b9781e   2 weeks ago   148MB
tomcat       latest   9ca267cc83c7   3 weeks ago   468MB

使用 tomcat 镜像创建容器实例(也叫运行镜像):

[root@docker ~]# docker run -itd -p 8080:8080 tomcat
92d78922526ba2a54ef63c48d2fec80b10997027de93457a4e2d56d7ea7448e2

此时用浏览器访问 localhost:8080,看到的却是 HTTP Status 404 – Not Found 错误页面,提示 “The origin server did not find a current representation for the target resource”。原因是官方 tomcat 镜像的 webapps 目录为空,示例应用都放在 webapps.dist 里尚未部署。

把 webapps.dist 目录换成 webapps 即可解决。先查看刚才创建的 tomcat 容器 ID,再进入容器:

[root@docker ~]# docker ps
CONTAINER ID   IMAGE    COMMAND           CREATED       STATUS       PORTS                                     NAMES
92d78922526b   tomcat   "catalina.sh run" 2 minutes ago Up 2 minutes 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp fervent_brahmagupta
[root@docker ~]# docker exec -it 92d bash
# 查看 webapps 文件夹为空
root@92d78922526b:/usr/local/tomcat# ls webapps
# 文件在 webapps.dist
root@92d78922526b:/usr/local/tomcat# ls webapps.dist/
docs  examples  host-manager  manager  ROOT
# 用 webapps.dist 替换 webapps
root@92d78922526b:/usr/local/tomcat# mv webapps.dist/* webapps

再次访问 tomcat,首页正常显示:页面中央是 Tomcat 猫形 Logo,下方有 “If you’re seeing this, you’ve successfully installed Tomcat. Congratulations!” 字样,以及 Server Status、Manager App 等管理链接。说明将 webapps.dist 内容移入 webapps 后服务恢复正常。

思考:我想要基于 tomcat 镜像做出一个能够直接访问的 tomcat 镜像,该如何做?

十、本章小结

在这一章我们首先学习了 Docker 的三种网络:none、host 和 bridge,并讨论了它们的不同使用场景;然后我们实践了创建自定义网络;最后详细讨论了如何实现容器与容器之间、容器与外部网络之间的通信。

本章重点关注的是单个主机内的容器网络。整理成一张速查表方便回顾:

网络 / 方式特点典型场景
none只有 lo,完全封闭高安全、无需联网的应用(如生成随机密码)
host共享宿主机网络栈对网络性能要求高、需配置 host 网络的容器
bridge(默认)docker0 网桥 + veth pair,自动分配 172.17.0.0/16绝大多数常规容器
user-defined bridge可指定 subnet / gateway / 静态 IP,支持容器名 DNS业务网络隔离、容器间按名称互访
joined 容器共享网络栈,127.0.0.1 互访web server 与 app server 高效通信、流量监控
容器出网iptables MASQUERADE(NAT)容器访问外网
外部入网-p 端口映射 + docker-proxy外网访问容器服务
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐