原理

1. docker0 网桥(Bridge)

通过输入命令

  1. windows:ipconfig
  2. linux:ifconfig

会发现多了一个叫 docker0 的网卡,这个就是 docker0 网桥。

网桥,简而言之,就是早期的两端口二层网络设备,用来连接不同的局域网,对数据包进行存储、转发操作。这里的一个关键点就是两端口,docker0 网桥连接的就是容器网段和宿主机网段
docker0 网桥是在 Docker Daemon 启动的时候自动创建的,从我们上面的结果 (inet 和 netmask) 可以看出来 docker0 的 IP 为 172.17.0.1/16。之后使用 bridge 模式(默认)创建出来的 Docker 容器都将在 docker0 子网的范围内选取一个未被占用的 IP 使用,并连接到 docker0 网桥上。
image.png
docker0 网桥的 IP 地址和子网范围是可以通过参数修改的,使用 CIDR 的格式。

在 Linux 系统中,我们可以通过 brctl 命令来查看网桥的信息(如果提示找不到命令,需要先安装 bridge-utils 软件包)。下面是我的dev机器的一个结果
image.png
我们从 brctl 的结果中可以看到网桥上面连接了很多了 veth 设备,同时 veth 设备总是成对出现的,那么也就意味着 veth 的另一端连接的是容器的 eth0,正如上面那幅图所示。

2. iptables

iptables 可以简单理解为是一个命令行防火墙(firewall)工具,我们可以设置一些 iptables 规则来达到流量控制。Docker 会在宿主机系统上增加一些 iptables 规则,以用来管理 Docker 容器和容器之间以及和外界的通信。

下面我们通过命令 iptables-save 命令来查看一下我的这台虚拟机(运行着多个 Docker 容器)上面的 iptable 规则情况,下面是全部命令输出,我们下面就看看 Docker 的数据转发是怎么做的

  1. # Generated by iptables-save v1.4.21 on Wed Jun 2 16:53:06 2021
  2. *filter
  3. :INPUT ACCEPT [1809099:252349089]
  4. :FORWARD DROP [0:0]
  5. :OUTPUT ACCEPT [1993484:1270123964]
  6. :DOCKER - [0:0]
  7. :DOCKER-ISOLATION-STAGE-1 - [0:0]
  8. :DOCKER-ISOLATION-STAGE-2 - [0:0]
  9. :DOCKER-USER - [0:0]
  10. -A FORWARD -j DOCKER-USER
  11. -A FORWARD -j DOCKER-ISOLATION-STAGE-1
  12. -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
  13. -A FORWARD -o docker0 -j DOCKER
  14. -A FORWARD -i docker0 ! -o docker0 -j ACCEPT
  15. -A FORWARD -i docker0 -o docker0 -j ACCEPT
  16. -A DOCKER -d 172.17.0.3/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 6379 -j ACCEPT
  17. -A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 9000 -j ACCEPT
  18. -A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8081 -j ACCEPT
  19. -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 7101 -j ACCEPT
  20. -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 7100 -j ACCEPT
  21. -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 443 -j ACCEPT
  22. -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
  23. -A DOCKER -d 172.17.0.5/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 15672 -j ACCEPT
  24. -A DOCKER -d 172.17.0.5/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 5672 -j ACCEPT
  25. -A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
  26. -A DOCKER-ISOLATION-STAGE-1 -j RETURN
  27. -A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
  28. -A DOCKER-ISOLATION-STAGE-2 -j RETURN
  29. -A DOCKER-USER -j RETURN
  30. COMMIT
  31. # Completed on Wed Jun 2 16:53:06 2021
  32. # Generated by iptables-save v1.4.21 on Wed Jun 2 16:53:06 2021
  33. *nat
  34. :PREROUTING ACCEPT [6467:1150885]
  35. :INPUT ACCEPT [801:48308]
  36. :OUTPUT ACCEPT [709:52869]
  37. :POSTROUTING ACCEPT [1765:107826]
  38. :DOCKER - [0:0]
  39. -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
  40. -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
  41. -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
  42. -A POSTROUTING -s 172.17.0.3/32 -d 172.17.0.3/32 -p tcp -m tcp --dport 6379 -j MASQUERADE
  43. -A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 9000 -j MASQUERADE
  44. -A POSTROUTING -s 172.17.0.6/32 -d 172.17.0.6/32 -p tcp -m tcp --dport 8081 -j MASQUERADE
  45. -A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 7101 -j MASQUERADE
  46. -A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 7100 -j MASQUERADE
  47. -A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 443 -j MASQUERADE
  48. -A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
  49. -A POSTROUTING -s 172.17.0.5/32 -d 172.17.0.5/32 -p tcp -m tcp --dport 15672 -j MASQUERADE
  50. -A POSTROUTING -s 172.17.0.5/32 -d 172.17.0.5/32 -p tcp -m tcp --dport 5672 -j MASQUERADE
  51. -A DOCKER -i docker0 -j RETURN
  52. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 6379 -j DNAT --to-destination 172.17.0.3:6379
  53. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 9000 -j DNAT --to-destination 172.17.0.4:9000
  54. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 8081 -j DNAT --to-destination 172.17.0.6:8081
  55. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 7101 -j DNAT --to-destination 172.17.0.2:7101
  56. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 7100 -j DNAT --to-destination 172.17.0.2:7100
  57. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 443 -j DNAT --to-destination 172.17.0.2:443
  58. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80
  59. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 15672 -j DNAT --to-destination 172.17.0.5:15672
  60. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 5672 -j DNAT --to-destination 172.17.0.5:5672

iptables 默认有 4 个表:

  • nat:地址转换表;
  • filter:数据过滤表;
  • raw:状态跟踪表;
  • mangle:包标记表。

外界访问 Docker 容器是通过 iptables 做 DNAT 实现的。DNAT 将 SNAT 中的 Source 换成 Destination,表示目的地址转换。

  1. *nat
  2. ...
  3. -A DOCKER ! -i docker0 -p tcp -m tcp --dport 7101 -j DNAT --to-destination 172.17.0.2:7101
  4. ...
  5. *filter
  6. ...
  7. -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 7101 -j ACCEPT
  8. ...

filter 表中的规则用来对流量做限制,这里的这条规则表示允许所有的外部 IP 访问容器,可以通过在 filter 的 Docker 链上添加规则来对外部的 IP 访问做出限制,这里就不再演示了。
不光是与外界通信,Docker 容器之间通信也受到 iptables 规则限制。我们前面也了解到宿主机上面的所有 Docker 容器都位于 docker0 网桥的子网内。同时我们从 iptables 中的输出看到一条 filter 规则。
-A FORWARD -i docker0 -o docker0 -j ACCEPT

这条规则保证容器之间可以互相通信,如果将 Docker Server 启动参数 —icc 设置为 false,则这条规则会被设置为 DROP,容器之间的相互通信就会被禁止。

3. IP-Forward

在 Docker 容器网络通信的过程中,还涉及到数据包在多个网卡间的转发,这需要将内核参数 ip-forward 打开,参数位于 /proc/sys/net/ipv4/ip_forward。

  1. [root@docker ~]# cat /proc/sys/net/ipv4/ip_forward 1

通常这一步不需要我们手动来设置,Docker server 启动的时候默认会将 ip-forward 设置为 1。

4. DNS 和主机名

容器的主机名以及 DNS 是设置在文件 /etc/hostname、/etc/hosts、/etc/resolv.conf 中的,对于容器来说,在容器启动后会覆盖这些文件从而达到修改属性的目的。下面是我的机器上面的示例。

  1. [root@docker ~]# docker exec -ti 4be4cca01392 sh
  2. / # mount
  3. ...
  4. /dev/vda1 on /etc/hostname type ext4 (rw,relatime,data=ordered)
  5. /dev/vda1 on /etc/hosts type ext4 (rw,relatime,data=ordered)
  6. /dev/vda1 on /etc/resolv.conf type ext4 (rw,relatime,data=ordered)
  7. ...

同时我们也可以通过参数 -h HOSTNAME 和 —dns=IP_ADDRESS… 来对 hostname 和 DNS 进行设置。

Docker网络模式

Docker 默认实现了五种网络模式如下

1. bridge 模式

Docker 的默认网络模式。这种模式会将创建出来的所有 Docker 容器链接到 docker0 网桥或者自定义网桥上,所有的 Docker 容器处于同一个子网。

使用 bridge 模式的 Docker 容器默认使用 docker0 网桥,除此之外,你也可以使用自定义网桥(User-defined bridge network)。自定义网桥和默认 docker0 网桥的区别在于:

  1. 自定义网桥提供容器间的自定义 DNS 解析。默认网桥网络下的 Docker 容器只能通过 IP 地址交互,除非使用 —link 参数将多个 Docker 容器连接起来。
  2. 自定义网桥具有更好的隔离性。默认创建的 Docker 容器如果没有指定 —network 参数,都会连接到默认的 docker0 网桥上,这样相当于将所有不不相干的容器都置于一个同一个网络环境中,可能存在风险。自定义网桥相当于将 docker0 网桥按我们需要分隔成多个自定义网桥,毫无疑问,这样隔离性更好。
  3. 容器可以在运行时和自定义网桥进行绑定或者解绑。这个默认 docker0 网桥是不行的,需要停止容器。
  4. 每个自定义网桥可以自定义自己的配置,比如 MTU 和 iptables 规则等。但是如果使用默认 docker0 网桥,相当于共享配置。
  5. 通过默认网桥 Link 的 Docker 容器可以共享环境变量。所谓 Link 是指 docker run 的时候指定 —link 参数。这个在自定义网桥中是不行的,但是可以通过其他方式来实现,比如:
    1. 将需要共享的数据放到 volume 中,多个 Docker 容器自行 mount。
    2. 使用 docker-compose 启动多个 Docker 容器,将共享变量定义到 compose 文件中。

      2. host 模式

      顾名思义,这种模式下,Docker 容器和宿主机使用同一个网络协议栈,也就是同一个 network namespace,和宿主机共享网卡、IP、端口等信息。好处是性能更好,缺点也很明显,没有做网络隔离。

Host 模式可以通过参数 —network host 指定,比如我们使用 host 模式启动一个 nginx 容器。

  1. [root@docker ~]# docker run --rm -d --network host --name host_nginx nginx:1.19

Host 模式的优缺点都很明显。

  • 缺点:没有和宿主机的 network namespace 进行隔离。可能会存在端口冲突的情况,比如 nginx 镜像的 Docker 容器会使用 80 端口,那么我们就不能以 host 模式启动两个容器,不然会冲突。
  • 优点:共用同一个 network namespace 也就意味没有个多个 network namespace 之间的数据转发,性能更好。

    3. overlay 模式

    这种模式在多个 Docker daemon 主机之间创建一个分布式网络,该网络位于 Docker 主机层次之上,允许容器之间加密通讯,需要处理容器之间和主机之间的网络包。

    4. macvlan 模式

    macvlan 是 Linux 的一个内核模块,算是一个比较新的特性。本质上是一种网卡虚拟化技术,通过 macvlan 可以在同一个物理网卡上虚拟出多个网卡,通过不同的 Mac 地址在数据链路层进行网络数据的转发,一块网卡上配置多个 Mac 地址。Docker 的 macvlan 网络实际上就是使用 Linux 提供的 macvlan 驱动。

    5. none 模式

    这种模式下 Docker 容器拥有自己的 network namespace,但是并不会做任何网络配置。换句话说,这个 Docker 容器除了 network namespace 自带的 lo 网卡(loopback,127.0.0.1)外没有其他任何网卡、IP 等信息。这种模式如果不做额外配置是无法使用的,要使用需要自己添加网卡等,也就是它给了用户最大的自由度。

Dokcer Link

Link 是在 Docker 容器创建的过程中通过 —link 参数将新创建出来的 Docker 容器和已有的容器之间串讲一个安全通道用来做数据交互。
Link 的使用场景还是很常见的,比如我们线上应用有一个 web 应用以 Docker 容器运行,有一个数据库(MySQL)也以 Docker 容器运行,由于 web 应用需要访问数据库的数据,那么我们就可以在这两个容器之间使用 Link 连接起来。

使用

Link 的使用比较简单,我们这里演示一下。首先运行一个 MySQL 的 Docker 容器。

  1. [root@docker1 ~]# docker run -d -e MYSQL_ROOT_PASSWORD=123456 -p 3307:3306 --name mysql mysql:latest

然后我们创建一个 busybox 的 Docker 容器,并通过 telnet 连接 MySQL 的 Docker 容器。

  1. [root@docker1 ~]# docker run -ti --name busybox --link mysql:mysql busybox:latest sh
  2. / # telnet mysql
  3. telnet: can't connect to remote host (172.17.0.2): Connection refused
  4. / # telnet mysql 3306
  5. Connected to mysql
  6. J
  7. �1.1jJXq/%
  8. p@R|Iccaching_sha2_password

其中 busybox 容器的启动参数里面的 —link mysql:mysql 就是将我们新建出来的 busybox 容器和名字叫 mysql 的 Docker 容器建立一个 link 通道。—link 的参数格式为 —link :alias ,第一个参数是目标容器的名字或者 ID,第二个 alias 相当于我们在 busybox Docker 容器中访问 MySQL Docker 容器的 host。

link原理主要就是修改 /etc/hosts 文件。现在已经不推荐了

警告:—link 参数是 Docker 早期的遗留特性,可能最终会被移除掉。除非你一定要使用它,否则我们建议你使用自定义网络的方式来实现多个 container 之间的网络通信。自定义网络相比 —link 的一个弊端是无法共享环境变量,但是你可以通过类似在多个容器中挂载同一个 volume 的方式来实现这个需求。

最佳实践

官方给了一个针对各个网络模式的选择使用建议:

  • User-defined bridge network 适用于同一个宿主机上多个 Docker 容器进行通信。这里的 user-defined 可以理解为自定义网桥,不适用 docker0 网桥,这样可以更灵活地设置子网和 iptables。
  • Host networks 适用于 Docker 容器的网络不需要和宿主机进行隔离的场景,比如对于网络性能比较敏感的场景。
  • Overlay networks 适用于运行在多个宿主机上 Docker 容器之间的通信情况。
  • Macvlan networks 适用于 VM 迁移的场景,这样每个 Docker 容器看起来和物理主机一样。
  • Third-party network plugins 适用于将 Docker 和特定网络协议栈整合的场景。