Docker 网络模式

原生网络模式

Docker 自带了三种常用的网络模式:none、host 和 bridge,用于同一宿主机容器间通信,默认使用 bridge 网络。

None 网络

None 网络表示没有网络,挂载在该网络下的容器除了 lo(回环接口)之外没有网卡。适用于一些对安全性要求较高的单机应用。

Host 网络

使用 host 网络的容器与宿主机共享网络配置,Docker 不会为容器创建单独的网络命名空间。适用于对网络性能要求较高的应用。另外一种用途是允许容器直接配置宿主机网络(--privileged=true),适用于某些跨主机网络解决方案。

由于与宿主机共享网络,需要考虑端口冲突问题。同时,与宿主机位于同一子网的机器也能发现容器的存在。

Bridge 网络

Docker 通过网络命名空间(Network Namespace)为每个容器建立独立的网络,实现与宿主机完全隔离的环境。

默认情况下,Docker 在宿主机上创建一个名为 docker0 的虚拟网桥,用于连接宿主机与容器。容器与 docker0 之间通过虚拟以太网对(Veth Pair)进行连接,所有连接到 docker0 的容器都属于同一个子网。宿主机也通过虚拟网卡连接到 docker0:

[root@server4 ~]$ ifconfig docker0
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:b4ff:fe98:85d5  prefixlen 64  scopeid 0x20<link>
        ether 02:42:b4:98:85:d5  txqueuelen 0  (Ethernet)
        RX packets 794  bytes 2870284 (2.7 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 934  bytes 5709678 (5.4 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
[root@server4 ~]$ brctl show docker0
bridge name     bridge id               STP enabled     interfaces
docker0         8000.0242b49885d5       no              veth5f73575
                                                        veth8125268

跨主机网络

Docker 原生支持跨主机网络方案 overlay 和 macvlan,常用第三方跨主机网络方案有:flannel,weave 和 calico。

Overlay 网络

Overlay 是一种虚拟交换技术,主要是解决不同 IP 地址段之间的网络通信问题。Docker 使用的 Overlay 技术是借助于 libnetwork 实现的 VxLAN。

在 Overlay 网络中,网桥 br0 除了连接所有的 endpoint 还会连接一个 vxlan 设备,用于与其他 host 建立 vxlan tunnel,容器之间通过这个隧道来通信。

Overlay 网络需要一个 K-V 服务器,比如 Consul,zookeeper 或 etcd 来储存相关主机信息。

Macvlan 网络

macvlan 本身是 Linux 内核模块,其功能是允许同一个物理网卡配置多个 MAC 地址,本质上是一种网卡虚拟化技术。其优点是性能好,因为不需要创建网桥,直接通过以太端口连接到物理网络。

在 macvlan 中,容器的网口直接与宿主机的网卡连接,容器无需通过 NAT 和端口映射就能与外网直接通信,在网络上与其他独立主机没有区别,因此一个网卡只能创建一个 macvlan 网络。

可以通过 Vlan 将物理的二层网络划分成最多 4094 个逻辑网络,每个 VLAN 由 ID 区分,互相隔离。也就是 eth0.xxx 的形式,可供 macvlan 连接。

Flannel 网络

flannel 是 CoreOS 开发的容器网络解决方案。它为每个 host 分配一个 subnet,容器从此 subnet 中分配 IP,这些 IP 可以在 host 间路由。每个 subnet 都是从一个更大的 IP 池中划分的,flannel 会在每个主机上运行 flanneld 的 agent,从池子中分配 subnet。

flannel 使用 etcd 存放网络配置信息,数据包转发由基于 vxlan 或 host-gw 的 backend 实现。

flannel 没有 DNS 服务,容器无法通过 hostname 通信。

flannel 网络利用默认 bridge 网络,容器通过 docker0 访问外网。

Weave 网络

weave 创建的虚拟网路可以将多主机的容器连接起来,就像在一个交换机中。

此外,weave 的 DNS 模块可以使容器通过 hostname 访问。

Calico 网络

Calico 是一个纯三层的虚拟网络方案。Calico 为每个容器分配一个 IP,每个 host 都是 router,把不同 host 的容器连接起来。与 VxLAN 不同的是,Calico 不对数据包做额外封装,不需要 NAT,此外还能动态定义 ACL 规则

网络方案比较

从下面几个方面进行比较,根据不同场景选择最适合的方案:

Overlay Macvlan Flannel (v) Flannel (h) Weave Calico
网络模型 VxLAN Underlay VxLAN Underlay 纯三层 VxLAN Underlay 纯三层
数据库 etcd - etcd etcd - etcd
IPAM 单子网 自定义 每主机一子网 每主机一子网 单子网 每主机一子网

网络模型比较

跨主机网络将不同主机上的容器连接到同一个虚拟网络中,这个虚拟网络的拓扑结构和实现技术即为网络模型:

  • Overlay:通过建立主机间的 VxLAN 隧道,原始数据包在发送端被封装成 VxLAN 数据包,在目的地到达后在接收端解包。
  • Macvlan:网络通过二层的 VLAN 连接容器,在三层上依赖外部网关连接不同的 macvlan,不需要封装数据包。
  • Flannel:使用两种 backend,vxlan 与 overlay 类似,host-gw 将主机作为网关依赖三层 IP 转发,不需要封装数据包。
  • Weave:通过 VxLAN 实现。
  • Calico:与 Flannel 的 host-gw 类似,依赖三层 IP 转发。

储存依赖

其中,Overlay、Flannel 和 Calico 都需要额外的 etcd 或 consul 服务支持,Macvlan 是简单的 local 网络,不需要保存和共享网络信息。Weave 自己负责在主机间交换网络配置信息。

IPAM

在驱动管理 IP 地址的分配(IPAM)方面:

  • Overlay:网络中所有主机共享同一个子网,容器启动时会顺序分配 IP,可以通过 --subnet 来设置子网空间。
  • Macvlan:需要用户自己管理子网空间,为容器分配 IP,不同子网通信依赖外部网关。
  • Flannel:为每个主机分配独立子网空间,用户只需要指定一个大的 IP 池。不同子网之间的路由会自动配置。
  • Weave:默认配置下所有容器使用 10.32.0.0/12 子网,可以通过 --ipalloc-range 设置子网空间。
  • Calico:从 IP Pool 中为每个主机分配自己的子网空间,可自定义 IP 池。

连通与隔离

容器的通信和与外网的隔离和通信方面:

  • Overlay:同一网络中容器可通信,不同网络之间可将容器加入多个网络来实现通信。外网通信依赖 Docker 网桥。
  • Macvlan:网络的连通或隔离完全取决于二层 VLAN 和三层路由。
  • Flannel:不同网络中容器可直接通信,没有提供隔离。外网通信依赖 Docker 网桥。
  • Weave:默认所有容器在同一网络,为容器指定不同子网来隔离。外网通信需将主机加入 Weave 网络并作为网关。
  • Calico:默认只允许同一网络中容器通信。通过 Policy 设定可以实现自由控制网络访问。

性能

使用 Underlay 的网络性能优于使用 Overlay(VxLAN)的网络。Overlay 网络利用隧道技术,将数据包封装到 UDP 中进行传输,封装与解包存在额外开销。

不过,Overlay 较 Underlay 可以支持更多的二层网段,能更好地利用已有网络,以及可以避免物理交换机 MAC 表耗尽等优势。

网络设置

在容器内部修改的 /etc/hostname/etc/hosts/etc/resolve.conf 文件内容仅在本次容器运行期间有效,容器退出后修改会丢失。即使使用 docker commit 命令保存成镜像,也不会保留修改。

端口映射

容器与宿主机通过 docker0 网桥进行通信,而容器与外网的访问可以通过 docker0 转发到宿主机的外网网卡上。只要宿主机开启了端口转发功能,容器就能够访问宿主机外部的网络:

[root@server4 ~]$ sysctl net.ipv4.conf.all.forwarding
net.ipv4.conf.all.forwarding = 1
[root@server4 ~]$ iptables -t nat -L -n
Chain DOCKER (2 references)
target     prot opt source               destination              
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0      tcp dpt:5000 to:172.17.0.3:5000
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0      tcp dpt:49156 to:172.17.0.5:8080

然而,默认情况下,外部网络无法访问容器。可以通过端口映射的方式,将容器的端口与宿主机的端口绑定,这样外部网络就能够通过指定的端口访问容器中的应用和服务。

在建立容器时,可以使用 -P 参数进行端口映射,将容器需要暴露的端口随机映射到主机的空闲端口上(默认在 49000~49900 端口范围内):

[root@server4 ~]$ docker run -d -P nginx
555e6a418454ecbb450e519e09ec483645d97d56569627b827e1b54781f6afde
[root@server4 ~]$ docker inspect -f={{.NetworkSettings.Ports}} 555e6a41
map[80/tcp:[{0.0.0.0 49153} {:: 49153}]]

使用 -p 参数可以固定映射到宿主机的端口,并且可以指定端口类型为 UDP:

[root@server4 ~]$ docker run -d -p 192.168.2.241:8001:80/udp nginx
38769e47eebdc96ce3f0eded75e6b8c3de90609e42f01a09148911dc4ea025bb
[root@server4 ~]$ docker inspect -f={{.NetworkSettings.Ports}} 38769e47
map[80/tcp:[] 80/udp:[{192.168.2.241 8001}]]

容器连接

有时,一个容器中运行的应用程序需要通过网络与另一个容器中运行的应用程序交换数据,这时需要通过容器连接来实现。

要设置容器间通信,可以在创建容器时使用 --link 参数指定要连接的容器,这样会打开对被连接容器的网络访问。

例如,创建一个 MySQL 容器,并让一个 Web 服务容器连接到它:

[root@server4 ~]$ docker run -d --name mysql --env='MYSQL_ALLOW_EMPTY_PASSWORD=1' mysql
[root@server4 ~]$ docker run -d -p 80:80 -p 443:443 --name web --link mysql:db nginx

连接容器时,不需要指定或映射被连接容器 MySQL 的端口。被连接容器的端口仅在容器间通信中使用,不会暴露在外网中,其他容器也无法访问。建立连接的唯一条件是连接和被连接的容器都必须处于运行状态。

此外,为了避免连接容器名与连接容器内某些配置重名,Docker 支持使用别名进行容器间连接。例如,上述示例中使用了 db 作为 MySQL 容器的别名。在 Web 容器中,可以使用 db 作为访问时的主机名。可以查看 Web 容器中的 /etc/hosts 文件:

[root@server4 ~]$ docker exec -it web cat /etc/hosts
127.0.0.1       localhost
172.17.0.4      db f2159e1085ed mysql
172.17.0.5      1286ab689c48

在 Web 容器中可以通过 env 命令查看 MySQL 容器的环境变量:

[root@server4 ~]$ docker exec -it web env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=1286ab689c48
TERM=xterm
DB_PORT=tcp://172.17.0.4:3306
DB_PORT_3306_TCP=tcp://172.17.0.4:3306
DB_PORT_3306_TCP_ADDR=172.17.0.4
DB_PORT_3306_TCP_PORT=3306
DB_PORT_3306_TCP_PROTO=tcp
DB_PORT_33060_TCP=tcp://172.17.0.4:33060
DB_PORT_33060_TCP_ADDR=172.17.0.4
DB_PORT_33060_TCP_PORT=33060
DB_PORT_33060_TCP_PROTO=tcp
DB_NAME=/web/db
DB_ENV_MYSQL_ALLOW_EMPTY_PASSWORD=1
DB_ENV_GOSU_VERSION=1.12
DB_ENV_MYSQL_MAJOR=8.0
DB_ENV_MYSQL_VERSION=8.0.27-1debian10
NGINX_VERSION=1.21.3
NJS_VERSION=0.6.2
PKG_RELEASE=1~buster
HOME=/root

域名解析

Docker 服务端自带一个内嵌 DNS 服务器,使容器可以通过容器名进行通信。

但是使用 Docker DNS 有一个限制,只能在用户自定义网络中使用。换句话说,默认的桥接网络 docker0 中无法使用 DNS 解析。

查看网络

可以使用 docker network ls 命令查看 Docker 中定义的网络:

[root@server4 ~]$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
3b3d933326d5   bridge    bridge    local
9c9f9149deef   host      host      local
76306c358978   none      null      local

进一步可以通过 docker network inspect 命令查看网络的详细信息:

[root@server4 ~]$ docker network inspect bridge 
[
    {
        "Name": "bridge",
        "Id": "3b3d933326d5a4dc198970d76c88ae265a09aab3edd12c1eb44d141c769628c8",
        "Created": "2021-10-22T06:08:04.744492282+08:00",
        "Scope": "local",
        "Driver": "bridge",

每次运行一个容器时,都会在全局注册相关的网络信息。

创建网络

可以使用 docker network create 命令自建一个桥接网络,并将容器连接到该网络中,以隔离不相关的应用容器:

[root@server4 ~]$ docker network create --driver bridge mybridge
83a3f95af6b114c139f8887f48ac4d5dca739bfc310b8b209c5336cf211706d1
[root@server4 ~]$ docker run -it --rm --net=mybridge ubuntu:18.04 
root@b476cb94f196:/# 

自建网络时,可以使用 --subnet--gateway 参数自定义网段和网关。

删除网络

删除未被使用的自建网络,可以使用 docker network rm 命令:

[root@server4 ~]$ docker network rm mybridge

也可以使用 docker network prune 命令自动清理未被使用的网络。

加入网络

处于不同 bridge 网络的容器,虽然具有互通的路由表规则,但在容器内部的 iptables 中会隔离访问。可以通过添加一块连接到对方网桥的网卡来实现通信。使用 docker network connect 命令将容器 dshell 连接到名为 mybridge 的网桥上:

[root@server4 ~]$ docker network connect mybridge dshell

同时,也可以使用 docker network disconnect 命令随时断开容器与网络的连接:

[root@server4 ~]$ docker network disconnect mybridge dshell

加入容器

加入容器是一种特殊的容器间通信方式,它可以使两个以上的容器共享一个网络栈、共享网卡和配置信息。

使用加入容器功能,不仅可以让程序通过 lo 接口进行高速通信,还可以用于监控网络程序的容器部署。

例如,将新建的容器加入到容器 dshell 中:

[root@server4 ~]$ docker run -it --network=container:dshell alpine
/ # ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    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
48: eth0@if49: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
    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
       valid_lft forever preferred_lft forever
103: eth1@if104: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
       valid_lft forever preferred_lft forever