Docker-网络秘籍(全)
Docker 网络秘籍(全)
原文:
zh.annas-archive.org/md5/15C8E8C8C0D58C74AF1054F5CB887C66译者:飞龙
前言
本书的目的是为您提供关于 Docker 如何实现容器网络的深入知识。无论您是每天都在使用 Docker,还是刚刚开始接触,本书都将向您介绍 Docker 如何使用 Linux 网络原语来为容器进行网络连接。通过大量示例,我们将涵盖从 Linux 网络基础知识到最新的 Docker 网络驱动程序的所有内容。在此过程中,我们还将探讨如何将现有的网络结构和第三方插件集成到 Docker 中。最终目标是让您对 Docker 提供容器网络功能的过程感到舒适。
像许多开源项目一样,Docker 是一个快速发展的软件。在出版时,最新版本的 Docker 是 1.12。我已尽力确保本书中的内容反映了基于这个版本的最新功能和配置。无论版本如何,这些功能中的许多在 Docker 的早期版本中以某种形式存在。因此,尽管在过去几年中 Docker 的网络功能发生了重大变化,但大部分网络功能仍然以相同的方式实现。正是因为这个原因,我相信本书中的大部分内容在未来很长一段时间内仍然具有相关性。
本书涵盖的内容
第一章 Linux 网络结构,将重点介绍 Linux 网络原语。诸如接口创建、寻址和一般连接性等主题将被详细讨论。您还将了解与 Linux 主机网络配置相关的常见 Linux 命令行语法和工具。了解这些基本结构将极大地增加您理解 Docker 如何处理容器网络的能力。
第二章 配置和监控 Docker 网络,解释了 Docker 处理容器网络的默认方式。这包括 Docker 网络操作的桥接、主机和映射容器模式。我们还将开始探讨 Docker 如何将基于容器的服务映射到外部或外部网络。还将讨论 Docker 网络的 Linux 主机要求以及一些可能被修改的 Docker 服务级参数。
第三章,“用户定义的网络”,开始了我们关于 Docker 用户定义网络的讨论。用户定义网络的出现极大地增加了 Docker 网络的灵活性,为最终用户提供了更多关于容器连接的可能性。我们将讨论创建用户定义网络所需的语法,并展示如何创建用户定义的桥接和覆盖网络的示例。最后,我们将介绍一些在 Docker 中隔离网络段的选项。
第四章,“构建 Docker 网络”,首先深入探讨了 Docker 如何提供容器连接。从一个没有网络接口的容器开始,我们将介绍在网络上使容器通信所需的所有步骤。然后,我们将讨论使用自定义桥接与 Docker 以及与 Docker 一起使用 OVS 的多个用例。
第五章,“容器链接和 Docker DNS”,讨论了容器名称解析的可用选项。这包括默认的名称解析行为以及存在于用户定义网络中的新嵌入式 DNS 服务器功能。您将熟悉确定每种情况下名称服务器分配的过程。
第六章,“保护容器网络”,展示了与容器安全相关的各种功能和策略。您将了解到几种限制容器暴露和连接范围的选项。我们还将讨论实现利用用户定义的覆盖网络的基于容器的负载均衡器的选项。
第七章,“使用 Weave Net”,将是我们首次接触与 Docker 集成的第三方网络解决方案。Weave 提供了多种与 Docker 集成的方法,包括其自己的 CLI 工具以及一个完整的 Docker 驱动程序。还将演示使用 Weave 提供网络隔离的示例。
第八章《使用 Flannel》,使用 Flannel,检查了由 CoreOS 团队构建的第三方网络插件。Flannel 是一个有趣的例子,说明了网络插件如何通过更改 Docker 服务级参数来集成到 Docker 中。除了提供覆盖类型的网络外,Flannel 还提供了主机网关后端,允许主机在满足某些要求的情况下直接路由到彼此。
第九章《探索网络功能》,探索网络功能,侧重于新的网络功能如何集成到 Docker 中。我们将研究如何通过评估不同版本的 Docker 引擎来获得对这些新功能的访问和测试。在本章的过程中,我们还将研究现在集成的 MacVLAN 网络驱动程序以及仍在测试中的 IPVLAN 网络驱动程序。
第十章《利用 IPv6》,利用 IPv6,涵盖了 IPv6 及 Docker 对其的支持。IPv6 是一个重要的话题,考虑到 IPv4 的当前状态,它值得引起大量关注。在本章中,我们将回顾在 Linux 系统上使用 IPv6 的一些基础知识。然后,我们将花一些时间审查 Docker 如何支持 IPv6,并讨论您在部署周围的一些选项。
第十一章《故障排除 Docker 网络》,故障排除 Docker 网络,探讨了在故障排除 Docker 网络时可能采取的一些常见步骤。重点将放在验证配置上,但您还将学习一些可以证明配置是否按预期工作的步骤。
您需要为本书做些什么
本书中显示的所有实验都是在运行版本 16.04 和 Docker 引擎版本 1.12 的 Ubuntu Linux 主机上执行的。
注意
您会注意到,本书中主机上使用的网络接口名称使用熟悉的 eth(eth0、eth1 等)命名约定。虽然这在许多 Linux 版本上仍然是标准,但运行 systemd 的新版本(如 Ubuntu 16.04)现在使用称为可预测网络接口名称(PNIN)的东西。使用 PNIN 时,网络接口使用基于接口本身信息的更可预测的名称。在这些情况下,接口名称将以不同的名称显示,例如 ens1 或 ens32。为了使本书中的内容更容易理解,我选择在所有主机上禁用了 PNIN。如果您有兴趣执行相同的操作,可以通过网络搜索“Ubuntu 禁用可预测接口名称”找到说明。如果您选择不这样做,只需知道您的接口名称将显示为与我的示例不同的方式。
本书中显示的实验室要求包括在每个配方的开头。后续的配方可能会基于早期配方中显示的配置。
这本书是为谁准备的
本书适用于那些对了解 Docker 如何实现容器网络感兴趣的人。虽然这些配方涵盖了许多基础知识,但假定您具有对 Linux 和 Docker 的工作知识,并且具有对网络的基本理解。
约定
在本书中,您会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的例子以及它们的含义解释。
文本中的代码词、文件路径和可执行文件显示如下:
“可以使用ip link show命令在主机上查看接口”。
任何命令行输入或输出都将按如下方式编写:
user@net1:~$ sudo ifdown eth1 && sudo ifup eth1
在可能的情况下,任何多行命令行输入将使用 Linux 行继续方法编写,即在要继续的行的末尾包括一个尾随的\:
user@net1:~$ sudo ip netns exec ns_1 ip link set \
dev edge_veth1 master edge_bridge1
在某些情况下,命令行输出也可能是多行的。在这种情况下,格式化是为了使输出易于阅读。
当我们希望引起您对命令行输出的特别关注时,相关行或项目将以粗体显示:
user@net2:~$ ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:59:ca:ca brd ff:ff:ff:ff:ff:ff
inet **172.16.10.2/26** brd 172.16.10.63 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe59:caca/64 scope link
valid_lft forever preferred_lft forever
user@net2:~$
注意
警告或重要说明显示在这样的框中。
第一章:Linux 网络构造
在本章中,我们将涵盖以下配方:
-
使用接口和地址
-
配置 Linux 主机路由
-
探索桥接
-
建立连接
-
探索网络命名空间
介绍
Linux 是一个功能强大的操作系统,具有许多强大的网络构造。就像任何网络技术一样,它们单独使用时很强大,但在创造性地组合在一起时变得更加强大。Docker 是一个很好的例子,它将许多 Linux 网络堆栈的单独组件组合成一个完整的解决方案。虽然 Docker 大部分时间都在为您管理这些内容,但当查看 Docker 使用的 Linux 网络组件时,了解一些基本知识仍然是有帮助的。
在本章中,我们将花一些时间单独查看这些构造,而不是在 Docker 之外。我们将学习如何在 Linux 主机上进行网络配置更改,并验证网络配置的当前状态。虽然本章并不专门针对 Docker 本身,但重要的是要了解原语,以便在以后的章节中讨论 Docker 如何使用这些构造来连接容器。
使用接口和地址
了解 Linux 如何处理网络是理解 Docker 处理网络的一个重要部分。在这个配方中,我们将专注于 Linux 网络基础知识,学习如何在 Linux 主机上定义和操作接口和 IP 地址。为了演示配置,我们将在本配方中开始构建一个实验室拓扑,并在本章的其他配方中继续进行。
准备工作
为了查看和操作网络设置,您需要确保已安装iproute2工具集。如果系统上没有安装它,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要 root 级别的访问权限。
为了在本章中进行演示,我们将使用一个简单的实验室拓扑。主机的初始网络布局如下:

在这种情况下,我们有三台主机,每台主机已经定义了一个eth0接口:
-
net1:10.10.10.110/24,默认网关为10.10.10.1 -
net2:172.16.10.2/26 -
net3:172.16.10.66/26
操作步骤
大多数终端主机的网络配置通常仅限于单个接口的 IP 地址、子网掩码和默认网关。这是因为大多数主机都是网络端点,在单个 IP 接口上提供一组离散的服务。但是如果我们想要定义更多的接口或操作现有的接口会发生什么呢?为了回答这个问题,让我们首先看一下像前面例子中的net2或net3这样的简单单宿主服务器。
在 Ubuntu 主机上,所有的接口配置都是在/etc/network/interfaces文件中完成的。让我们检查一下net2主机上的文件:
# The loopback network interface
auto lo
iface lo inet loopback
# The primary network interface
auto eth0
iface eth0 inet static
address 172.16.10.2
netmask 255.255.255.192
我们可以看到这个文件定义了两个接口——本地的loopback接口和接口eth0。eth0接口定义了以下信息:
-
address:主机接口的 IP 地址 -
netmask:与 IP 接口相关的子网掩码
该文件中的信息将在每次接口尝试进入上行或操作状态时进行处理。我们可以通过使用ip addr show <interface name>命令验证该配置文件在系统启动时是否被处理:
user@net2:~$ ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:59:ca:ca brd ff:ff:ff:ff:ff:ff
inet 172.16.10.2/26 brd 172.16.10.63 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe59:caca/64 scope link
valid_lft forever preferred_lft forever
user@net2:~$
现在我们已经审查了单宿主配置,让我们来看看在单个主机上配置多个接口需要做些什么。目前为止,net1主机是唯一一个在本地子网之外具有可达性的主机。这是因为它有一个定义好的默认网关指向网络的其他部分。为了使net2和net3可达,我们需要找到一种方法将它们连接回网络的其他部分。为了做到这一点,让我们假设主机net1有两个额外的网络接口,我们可以直接连接到主机net2和net3:

让我们一起来看看如何在net1上配置额外的接口和 IP 地址,以完成拓扑结构。
我们要做的第一件事是验证我们在net1上有可用的额外接口可以使用。为了做到这一点,我们将使用ip link show命令:
user@net1:~$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: **eth0**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:2d:dd:79 brd ff:ff:ff:ff:ff:ff
3: **eth1**: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 00:0c:29:2d:dd:83 brd ff:ff:ff:ff:ff:ff
4: **eth2**: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 00:0c:29:2d:dd:8d brd ff:ff:ff:ff:ff:ff
user@net1:~$
从输出中我们可以看到,除了eth0接口,我们还有eth1和eth2接口可供使用。要查看哪些接口有与之关联的 IP 地址,我们可以使用ip address show命令:
user@net1:~$ ip address show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:2d:dd:79 brd ff:ff:ff:ff:ff:ff
inet **10.10.10.110/24** brd 10.10.10.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe2d:dd79/64 scope link
valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 00:0c:29:2d:dd:83 brd ff:ff:ff:ff:ff:ff
4: eth2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 00:0c:29:2d:dd:8d brd ff:ff:ff:ff:ff:ff
user@net1:~$
前面的输出证明我们目前只在接口eth0上分配了一个 IP 地址。这意味着我们可以使用接口eth1连接到服务器net2,并使用接口eth2连接到服务器net3。
我们可以有两种方法来配置这些新接口。第一种是在net1上更新网络配置文件,包括相关的 IP 地址信息。让我们为面向主机net2的链接进行配置。要配置这种连接,只需编辑文件/etc/network/interfaces,并为两个接口添加相关的配置。完成的配置应该是这样的:
# The primary network interface
auto eth0
iface eth0 inet static
address 10.10.10.110
netmask 255.255.255.0
gateway 10.10.10.1
auto eth1
iface eth1 inet static
address 172.16.10.1
netmask 255.255.255.192
保存文件后,您需要找到一种方法告诉系统重新加载配置文件。做到这一点的一种方法是重新加载系统。一个更简单的方法是重新加载接口。例如,我们可以执行以下命令来重新加载接口eth1:
user@net1:~$ **sudo ifdown eth1 && sudo ifup eth1
ifdown: interface eth1 not configured
user@net1:~$
注意
在这种情况下并不需要,但同时关闭和打开接口是一个好习惯。这样可以确保如果关闭了你正在管理主机的接口,你不会被切断。
在某些情况下,您可能会发现更新接口配置的这种方法不像预期的那样工作。根据您使用的 Linux 版本,您可能会遇到一个情况,即之前的 IP 地址没有从接口中删除,导致接口具有多个 IP 地址。为了解决这个问题,您可以手动删除旧的 IP 地址,或者重新启动主机,这将防止旧的配置持续存在。
执行完命令后,我们应该能够看到接口eth1现在被正确地寻址了。
user@net1:~$ ip addr show dev eth1
3: **eth1**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:2d:dd:83 brd ff:ff:ff:ff:ff:ff
inet **172.16.10.1/26** brd 172.16.10.63 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe2d:dd83/64 scope link
valid_lft forever preferred_lft forever
user@net1:~$
要在主机net1上配置接口eth2,我们将采用不同的方法。我们将使用iproute2命令行来更新接口的配置,而不是依赖配置文件。为此,我们只需执行以下命令:
user@net1:~$ sudo ip address add **172.16.10.65/26** dev **eth2
user@net1:~$ sudo ip link set eth2 up
这里需要注意的是,这种配置是不持久的。也就是说,由于它不是在系统初始化时加载的配置文件的一部分,这个配置在重新启动后将会丢失。对于使用iproute2或其他命令行工具集手动完成的任何与网络相关的配置都是一样的情况。
注意
在网络配置文件中配置接口信息和地址是最佳实践。在这些教程中,修改配置文件之外的接口配置仅用于举例。
到目前为止,我们只是通过向现有接口添加 IP 信息来修改现有接口。我们实际上还没有向任何系统添加新接口。添加接口是一个相当常见的任务,正如后面的教程将展示的那样,有各种类型的接口可以添加。现在,让我们专注于添加 Linux 所谓的虚拟接口。虚拟接口在网络中的作用类似于环回接口,并描述了一种始终处于开启和在线状态的接口类型。接口是通过使用ip link add语法来定义或创建的。然后,您指定一个名称,并定义您正在定义的接口类型。例如,让我们在主机net2和net3上定义一个虚拟接口:
user@net2:~$ sudo ip link add dummy0 type dummy
user@net2:~$ sudo ip address add 172.16.10.129/26 dev dummy0
user@net2:~$ sudo ip link set dummy0 up
user@net3:~$ sudo ip link add dummy0 type dummy
user@net3:~$ sudo ip address add 172.16.10.193/26 dev dummy0
user@net3:~$ sudo ip link set dummy0 up
在定义接口之后,每个主机都应该能够 ping 通自己的dummy0接口:
user@net2:~$ ping **172.16.10.129** -c 2
PING 172.16.10.129 (172.16.10.129) 56(84) bytes of data.
64 bytes from 172.16.10.129: icmp_seq=1 ttl=64 time=0.030 ms
64 bytes from 172.16.10.129: icmp_seq=2 ttl=64 time=0.031 ms
--- 172.16.10.129 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.030/0.030/0.031/0.005 ms
user@net2:~$
user@net3:~$ ping **172.16.10.193** -c 2
PING 172.16.10.193 (172.16.10.193) 56(84) bytes of data.
64 bytes from 172.16.10.193: icmp_seq=1 ttl=64 time=0.035 ms
64 bytes from 172.16.10.193: icmp_seq=2 ttl=64 time=0.032 ms
--- 172.16.10.193 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.032/0.033/0.035/0.006 ms
user@net3:~$
注意
您可能会想知道为什么我们必须启动dummy0接口,如果它们被认为是一直开启的。实际上,接口是可以在不启动接口的情况下到达的。但是,如果不启动接口,接口的本地路由将不会出现在系统的路由表中。
配置 Linux 主机路由
一旦您定义了新的 IP 接口,下一步就是配置路由。在大多数情况下,Linux 主机路由配置仅限于指定主机的默认网关。虽然这通常是大多数人需要做的,但 Linux 主机有能力成为一个完整的路由器。在这个教程中,我们将学习如何查询 Linux 主机的路由表,以及手动配置路由。
准备工作
为了查看和操作网络设置,您需要确保已安装iproute2工具集。如果系统上没有安装,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要 root 级别的访问权限。本教程将继续上一个教程中的实验拓扑。我们在上一个教程之后留下的拓扑如下所示:

操作步骤
尽管 Linux 主机有路由的能力,但默认情况下不会这样做。为了进行路由,我们需要修改内核级参数以启用 IP 转发。我们可以通过几种不同的方式来检查设置的当前状态:
- 通过使用
sysctl命令:
sysctl net.ipv4.ip_forward
- 通过直接查询
/proc/文件系统:
more /proc/sys/net/ipv4/ip_forward
无论哪种情况,如果返回值为1,则启用了 IP 转发。如果没有收到1,则需要启用 IP 转发,以便 Linux 主机通过系统路由数据包。您可以使用sysctl命令手动启用 IP 转发,或者再次直接与/proc/文件系统交互:
sudo sysctl -w net.ipv4.ip_forward=1
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
虽然这可以在运行时启用 IP 转发,但此设置不会在重新启动后保持。要使设置持久,您需要修改/etc/sysctl.conf,取消注释 IP 转发的行,并确保将其设置为1:
…<Additional output removed for brevity>…
# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward=1
…<Additional output removed for brevity>…
注意
您可能会注意到,我们目前只修改了与 IPv4 相关的设置。不用担心;我们稍后会在第十章 利用 IPv6中介绍 IPv6 和 Docker 网络。
一旦我们验证了转发配置,让我们使用ip route show命令查看所有三个实验室主机的路由表:
user@**net1**:~$ ip route show
default via 10.10.10.1 dev eth0
10.10.10.0/24 dev eth0 proto kernel scope link src 10.10.10.110
172.16.10.0/26 dev eth1 proto kernel scope link src 172.16.10.1
172.16.10.64/26 dev eth2 proto kernel scope link src 172.16.10.65
user@**net2**:~$ ip route show
172.16.10.0/26 dev eth0 proto kernel scope link src 172.16.10.2
172.16.10.128/26 dev dummy0 proto kernel scope link src 172.16.10.129
user@**net3**:~$ ip route show
172.16.10.64/26 dev eth0 proto kernel scope link src 172.16.10.66
172.16.10.192/26 dev dummy0 proto kernel scope link src 172.16.10.193
这里有几个有趣的地方需要注意。首先,我们注意到主机列出了与其每个 IP 接口相关联的路由。根据与接口相关联的子网掩码,主机可以确定接口所关联的网络。这条路由是固有的,并且可以说是直接连接的。直接连接的路由是系统知道哪些 IP 目的地是直接连接的,而哪些需要转发到下一跳以到达远程目的地。
其次,在上一篇文章中,我们向主机net1添加了两个额外的接口,以便与主机net2和net3进行连接。但是,仅凭这一点,只允许net1与net2和net3通信。如果我们希望通过网络的其余部分到达net2和net3,它们将需要指向net1上各自接口的默认路由。让我们再次以两种不同的方式进行。在net2上,我们将更新网络配置文件并重新加载接口,在net3上,我们将通过命令行直接添加默认路由。
在主机net2上,更新文件/etc/network/interfaces,并在eth0接口上添加一个指向主机net1连接接口的网关:
# The primary network interface
auto eth0
iface eth0 inet static
address 172.16.10.2
netmask 255.255.255.192
gateway 172.16.10.1
要激活新配置,我们将重新加载接口:
user@net2:~$ sudo ifdown eth0 && sudo ifup eth0
现在我们应该能够在net2主机的路由表中看到默认路由,指向net1主机直接连接的接口(172.16.10.1):
user@net2:~$ ip route show
default via 172.16.10.1 dev eth0
172.16.10.0/26 dev eth0 proto kernel scope link src 172.16.10.2
172.16.10.128/26 dev dummy0 proto kernel scope link src 172.16.10.129
user@net2:~$
在主机net3上,我们将使用iproute2工具集动态修改主机的路由表。为此,我们将执行以下命令:
user@net3:~$ sudo ip route add default via 172.16.10.65
注意
请注意,我们使用关键字default。这代表了无类域间路由(CIDR)表示法中的默认网关或目的地0.0.0.0/0。我们也可以使用0.0.0.0/0语法执行该命令。
执行命令后,我们将检查路由表,以确保我们现在有一个默认路由指向net1(172.16.10.65):
user@net3:~$ ip route show
default via 172.16.10.65 dev eth0
172.16.10.64/26 dev eth0 proto kernel scope link src 172.16.10.66
172.16.10.192/26 dev dummy0 proto kernel scope link src 172.16.10.193
user@net3:~$
此时,主机和网络的其余部分应该能够完全访问其所有物理接口。然而,在上一个步骤中创建的虚拟接口对于除了它们所定义的主机之外的任何其他主机都是不可达的。为了使它们可达,我们需要添加一些静态路由。
虚拟接口网络是172.16.10.128/26和172.16.10.192/26。因为这些网络是较大的172.16.10.0/24汇总的一部分,网络的其余部分已经知道要路由到net1主机的10.10.10.110接口以到达这些前缀。然而,net1目前不知道这些前缀位于何处,因此会将流量原路返回到它来自的地方,遵循其默认路由。为了解决这个问题,我们需要在net1上添加两个静态路由:

我们可以通过iproute2命令行工具临时添加这些路由,也可以将它们作为主机网络脚本的一部分以更持久的方式添加。让我们各做一次:
要添加指向net2的172.16.10.128/26路由,我们将使用命令行工具:
user@net1:~$ sudo ip route add 172.16.10.128/26 via 172.16.10.2
如您所见,通过ip route add命令语法添加手动路由。需要到达的子网以及相关的下一跳地址都会被指定。该命令立即生效,因为主机会立即填充路由表以反映更改:
user@net1:~$ ip route
default via 10.10.10.1 dev eth0
10.10.10.0/24 dev eth0 proto kernel scope link src 10.10.10.110
172.16.10.0/26 dev eth1 proto kernel scope link src 172.16.10.1
172.16.10.64/26 dev eth2 proto kernel scope link src 172.16.10.65
172.16.10.128/26 via 172.16.10.2 dev eth1
user@net1:~$
如果我们希望使路由持久化,我们可以将其分配为post-up接口配置。post-up接口配置在接口加载后直接进行。如果我们希望在eth2上线时立即将路由172.16.10.192/26添加到主机的路由表中,我们可以编辑/etc/network/interfaces配置脚本如下:
auto eth2
iface eth2 inet static
address 172.16.10.65
netmask 255.255.255.192
post-up ip route add 172.16.10.192/26 via 172.16.10.66
添加配置后,我们可以重新加载接口以强制配置文件重新处理:
user@net1:~$ sudo ifdown eth2 && sudo ifup eth2
注意
在某些情况下,主机可能不会处理post-up命令,因为我们在早期的配置中手动定义了接口上的地址。在重新加载接口之前删除 IP 地址将解决此问题;然而,在这些情况下,重新启动主机是最简单(也是最干净)的操作方式。
我们的路由表现在将显示两条路由:
user@net1:~$ ip route
default via 10.10.10.1 dev eth0
10.10.10.0/24 dev eth0 proto kernel scope link src 10.10.10.110
172.16.10.0/26 dev eth1 proto kernel scope link src 172.16.10.1
172.16.10.64/26 dev eth2 proto kernel scope link src 172.16.10.65
172.16.10.128/26 via 172.16.10.2 dev eth1
172.16.10.192/26 via 172.16.10.66 dev eth2
user@net1:~$
为了验证这是否按预期工作,让我们从尝试 ping 主机net2(172.16.10.129)上的虚拟接口的远程工作站进行一些测试。假设工作站连接到的接口不在外部网络上,流程可能如下:

-
具有 IP 地址
192.168.127.55的工作站正在尝试到达连接到net2的虚拟接口,其 IP 地址为172.16.10.129。由于工作站寻找的目的地不是直接连接的,它将流量发送到其默认网关。 -
网络中有一个指向
net1的eth0接口(10.10.10.110)的172.16.10.0/24路由。目标 IP 地址(172.16.10.129)是该较大前缀的成员,因此网络将工作站的流量转发到主机net1。 -
主机
net1检查流量,查询其路由表,并确定它有一个指向net2的该前缀的路由,下一跳是172.16.10.2。 -
net2收到请求,意识到虚拟接口直接连接,并尝试将回复发送回工作站。由于没有目的地为192.168.127.55的特定路由,主机net2将其回复发送到其默认网关,即net1(172.16.10.1)。 -
同样,
net1没有目的地为192.168.127.55的特定路由,因此它将流量通过其默认网关转发回网络。假设网络具有返回流量到工作站的可达性。
如果我们想要删除静态定义的路由,可以使用ip route delete子命令来实现。例如,这是一个添加路由然后删除它的示例:
user@net1:~$ sudo ip route add 172.16.10.128/26 via 172.16.10.2
user@net1:~$ sudo ip route delete 172.16.10.128/26
请注意,我们在删除路由时只需要指定目标前缀,而不需要指定下一跳。
探索桥接
Linux 中的桥是网络连接的关键构建块。Docker 在许多自己的网络驱动程序中广泛使用它们,这些驱动程序包含在docker-engine中。桥已经存在很长时间,在大多数情况下,非常类似于物理网络交换机。Linux 中的桥可以像二层桥一样工作,也可以像三层桥一样工作。
注意
二层与三层
命名法是指 OSI 网络模型的不同层。二层代表数据链路层,与在主机之间进行帧交换相关联。三层代表网络层,与在网络中路由数据包相关联。两者之间的主要区别在于交换与路由。二层交换机能够在同一网络上的主机之间发送帧,但不能根据 IP 信息进行路由。如果您希望在不同网络或子网上的两台主机之间进行路由,您将需要一台能够在两个子网之间进行路由的三层设备。另一种看待这个问题的方式是,二层交换机只能处理 MAC 地址,而三层设备可以处理 IP 地址。
默认情况下,Linux 桥是二层结构。因此,它们通常被称为协议无关。也就是说,任意数量的更高级别(三层)协议可以在同一个桥实现上运行。但是,您也可以为桥分配一个 IP 地址,将其转换为三层可用的网络结构。在本教程中,我们将通过几个示例来向您展示如何创建、管理和检查 Linux 桥。
准备工作
为了查看和操作网络设置,您需要确保已安装iproute2工具集。如果系统上没有安装,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要具有根级别访问权限。本教程将继续上一个教程中的实验室拓扑。之前提到的所有先决条件仍然适用。
操作步骤
为了演示桥接工作原理,让我们考虑对我们一直在使用的实验室拓扑进行轻微更改:

与其让服务器通过物理接口直接连接到彼此,我们将利用主机net1上的桥接来连接到下游主机。以前,我们依赖于net1和任何其他主机之间的一对一映射连接。这意味着我们需要为每个物理接口配置唯一的子网和 IP 地址。虽然这是可行的,但并不是很实际。与标准接口相比,利用桥接接口为我们提供了一些在早期配置中没有的灵活性。我们可以为桥接接口分配一个单独的 IP 地址,然后将许多物理连接连接到同一个桥接上。例如,net4主机可以添加到拓扑结构中,其在net1上的接口可以简单地添加到host_bridge2上。这将允许它使用与net3相同的网关(172.16.10.65)。因此,虽然添加主机的物理布线要求不会改变,但这确实使我们不必为每个主机定义一对一的 IP 地址映射。
注意
从net2和net3主机的角度来看,当我们重新配置以使用桥接时,什么都不会改变。
由于我们正在更改如何定义net1主机的eth1和eth2接口,因此我们将首先清除它们的配置:
user@net1:~$ sudo ip address flush dev eth1
user@net1:~$ sudo ip address flush dev eth2
清除接口只是清除接口上的任何与 IP 相关的配置。我们接下来要做的是创建桥接本身。我们使用的语法与我们在上一个示例中创建虚拟接口时看到的非常相似。我们使用ip link add命令并指定桥接类型:
user@net1:~$ sudo ip link add host_bridge1 type bridge
user@net1:~$ sudo ip link add host_bridge2 type bridge
创建桥接之后,我们可以通过使用ip link show <interface>命令来验证它们的存在,检查可用的接口:
user@net1:~$ ip link show host_bridge1
5: **host_bridge1**: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default
link/ether f6:f1:57:72:28:a7 brd ff:ff:ff:ff:ff:ff
user@net1:~$ ip link show host_bridge2
6: **host_bridge2**: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default
link/ether be:5e:0b:ea:4c:52 brd ff:ff:ff:ff:ff:ff
user@net1:~$
接下来,我们希望使它们具有第 3 层意识,因此我们为桥接接口分配一个 IP 地址。这与我们在以前的示例中为物理接口分配 IP 地址非常相似:
user@net1:~$ sudo ip address add **172.16.10.1/26** dev **host_bridge1
user@net1:~$ sudo ip address add **172.16.10.65/26** dev **host_bridge2
我们可以通过使用ip addr show dev <interface>命令来验证 IP 地址的分配情况:
user@net1:~$ ip addr show dev host_bridge1
5: **host_bridge1**: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default
link/ether f6:f1:57:72:28:a7 brd ff:ff:ff:ff:ff:ff
inet **172.16.10.1/26** scope global **host_bridge1
valid_lft forever preferred_lft forever
user@net1:~$ ip addr show dev host_bridge2
6: host_bridge2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default
link/ether be:5e:0b:ea:4c:52 brd ff:ff:ff:ff:ff:ff
inet **172.16.10.65/26** scope global **host_bridge2
valid_lft forever preferred_lft forever
user@net1:~$
下一步是将与每个下游主机关联的物理接口绑定到正确的桥上。在我们的情况下,我们希望连接到net1的eth1接口的主机net2成为桥host_bridge1的一部分。同样,我们希望连接到net1的eth2接口的主机net3成为桥host_bridge2的一部分。使用ip link set子命令,我们可以将桥定义为物理接口的主设备:
user@net1:~$ sudo ip link set dev eth1 master host_bridge1
user@net1:~$ sudo ip link set dev eth2 master host_bridge2
我们可以使用bridge link show命令验证接口是否成功绑定到桥上。
注意
bridge命令是iproute2软件包的一部分,用于验证桥接配置。
user@net1:~$ bridge link show
3: **eth1** state UP : <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 **master host_bridge1** state forwarding priority 32 cost 4
4: **eth2** state UP : <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 **master host_bridge2** state forwarding priority 32 cost 4
user@net1:~$
最后,我们需要将桥接接口打开,因为它们默认处于关闭状态:
user@net1:~$ sudo ip link set host_bridge1 up
user@net1:~$ sudo ip link set host_bridge2 up
再次,我们现在可以检查桥接的链路状态,以验证它们是否成功启动:
user@net1:~$ ip link show host_bridge1
5: **host_bridge1**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state **UP** mode DEFAULT group default
link/ether 00:0c:29:2d:dd:83 brd ff:ff:ff:ff:ff:ff
user@net1:~$ ip link show host_bridge2
6: **host_bridge2**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state **UP** mode DEFAULT group default
link/ether 00:0c:29:2d:dd:8d brd ff:ff:ff:ff:ff:ff
user@net1:~$
此时,您应该再次能够到达主机net2和net3。但是,虚拟接口现在无法访问。这是因为在我们清除接口eth1和eth2之后,虚拟接口的路由被自动撤销。从这些接口中删除 IP 地址使得用于到达虚拟接口的下一跳不可达。当下一跳变得不可达时,设备通常会从其路由表中撤销路由。我们可以很容易地再次添加它们:
user@net1:~$ sudo ip route add 172.16.10.128/26 via 172.16.10.2
user@net1:~$ sudo ip route add 172.16.10.192/26 via 172.16.10.66
现在一切都恢复正常了,我们可以执行一些额外的步骤来验证配置。Linux 桥,就像真正的第二层交换机一样,也可以跟踪它们接收到的 MAC 地址。我们可以使用bridge fdb show命令查看系统知道的 MAC 地址:
user@net1:~$ bridge fdb show
…<Additional output removed for brevity>…
00:0c:29:59:ca:ca dev eth1
00:0c:29:17:f4:03 dev eth2
user@net1:~$
我们在前面的输出中看到的两个 MAC 地址是指net1直接连接的接口,以便到达主机net2和net3,以及其关联的dummy0接口上定义的子网。我们可以通过查看主机 ARP 表来验证这一点:
user@net1:~$ arp -a
? (**10.10.10.1**) at **00:21:d7:c5:f2:46** [ether] on **eth0
? (**172.16.10.2**) at **00:0c:29:59:ca:ca** [ether] on **host_bridge1
? (**172.16.10.66**) at **00:0c:29:17:f4:03** [ether] on **host_bridge2
user@net1:~$
注意
在旧工具更好的情况并不多见,但在bridge命令行工具的情况下,一些人可能会认为旧的brctl工具有一些优势。首先,输出更容易阅读。在学习 MAC 地址的情况下,它将通过brctl showmacs <bridge name>命令为您提供更好的映射视图。如果您想使用旧工具,可以安装bridge-utils软件包。
通过ip link set子命令可以从桥中移除接口。例如,如果我们想要从桥host_bridge1中移除eth1,我们将运行以下命令:
sudo ip link set dev eth1 nomaster
这将删除eth1与桥host_bridge1之间的主从绑定。接口也可以重新分配给新的桥(主机),而无需将它们从当前关联的桥中移除。如果我们想要完全删除桥,可以使用以下命令:
sudo ip link delete dev host_bridge2
需要注意的是,在删除桥之前,您不需要将所有接口从桥中移除。删除桥将自动删除所有主绑定。
建立连接
到目前为止,我们一直专注于使用物理电缆在接口之间建立连接。但是,如果两个接口没有物理接口,我们该如何连接它们?为此,Linux 网络具有一种称为虚拟以太网(VETH)对的内部接口类型。VETH 接口总是成对创建,使其表现得像一种虚拟补丁电缆。VETH 接口也可以分配 IP 地址,这使它们能够参与第 3 层路由路径。在本教程中,我们将通过构建之前教程中使用的实验拓扑来研究如何定义和实现 VETH 对。
准备工作
为了查看和操作网络设置,您需要确保已安装了iproute2工具集。如果系统上没有安装,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要具有根级访问权限。本教程将继续上一个教程中的实验拓扑。之前提到的所有先决条件仍然适用。
操作步骤
让我们再次修改实验拓扑,以便使用 VETH 对:

再次强调,主机net2和net3上的配置将保持不变。在主机net1上,我们将以两种不同的方式实现 VETH 对。
在net1和net2之间的连接上,我们将使用两个不同的桥接,并使用 VETH 对将它们连接在一起。桥接host_bridge1将保留在net1上,并保持其 IP 地址为172.16.10.1。我们还将添加一个名为edge_bridge1的新桥接。该桥接将不分配 IP 地址,但将具有net1的接口面向net2(eth1)作为其成员。在那时,我们将使用 VETH 对连接这两个桥接,允许流量从net1通过两个桥接流向net2。在这种情况下,VETH 对将被用作第 2 层构造。
在net1和net3之间的连接上,我们将以稍微不同的方式使用 VETH 对。我们将添加一个名为edge_bridge2的新桥,并将net1主机的接口面向主机net3(eth2)放在该桥上。然后,我们将配置一个 VETH 对,并将一端放在桥edge_bridge2上。然后,我们将分配之前分配给host_bridge2的 IP 地址给 VETH 对的主机端。在这种情况下,VETH 对将被用作第 3 层构造。
让我们从在net1和net2之间的连接上添加新的边缘桥开始:
user@net1:~$ sudo ip link add edge_bridge1 type bridge
然后,我们将把面向net2的接口添加到edge_bridge1上:
user@net1:~$ sudo ip link set dev eth1 master edge_bridge1
接下来,我们将配置用于连接host_bridge1和edge_bridge1的 VETH 对。VETH 对始终成对定义。创建接口将产生两个新对象,但它们是相互依赖的。也就是说,如果删除 VETH 对的一端,另一端也将被删除。为了定义 VETH 对,我们使用ip link add子命令:
user@net1:~$ sudo ip link add **host_veth1** type veth peer name **edge_veth1
注意
请注意,该命令定义了 VETH 连接的两侧的名称。
我们可以使用ip link show子命令查看它们的配置:
user@net1:~$ ip link show
…<Additional output removed for brevity>…
13: **edge_veth1@host_veth1**: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 0a:27:83:6e:9a:c3 brd ff:ff:ff:ff:ff:ff
14: **host_veth1@edge_veth1**: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether c2:35:9c:f9:49:3e brd ff:ff:ff:ff:ff:ff
user@net1:~$
请注意,我们有两个条目显示了定义的 VETH 对的每一侧的接口。下一步是将 VETH 对的端点放在正确的位置。在net1和net2之间的连接中,我们希望一个端点在host_bridge1上,另一个端点在edge_bridge1上。为此,我们使用了分配接口给桥接的相同语法:
user@net1:~$ sudo ip link set **host_veth1** master **host_bridge1
user@net1:~$ sudo ip link set **edge_veth1** master **edge_bridge1
我们可以使用ip link show命令验证映射:
user@net1:~$ ip link show
…<Additional output removed for brevity>…
9: **edge_veth1@host_veth1**: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop **master edge_bridge1** state DOWN mode DEFAULT group default qlen 1000
link/ether f2:90:99:7d:7b:e6 brd ff:ff:ff:ff:ff:ff
10: **host_veth1@edge_veth1**: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop **master host_bridge1** state DOWN mode DEFAULT group default qlen 1000
link/ether da:f4:b7:b3:8d:dd brd ff:ff:ff:ff:ff:ff
我们需要做的最后一件事是启动与连接相关的接口:
user@net1:~$ sudo ip link set host_bridge1 up
user@net1:~$ sudo ip link set edge_bridge1 up
user@net1:~$ sudo ip link set host_veth1 up
user@net1:~$ sudo ip link set edge_veth1 up
要到达net2上的虚拟接口,您需要添加路由,因为在重新配置期间它再次丢失了:
user@net1:~$ sudo ip route add 172.16.10.128/26 via 172.16.10.2
此时,我们应该可以完全到达net2及其通过net1到达dummy0接口。
在主机net1和net3之间的连接上,我们需要做的第一件事是清理任何未使用的接口。在这种情况下,那将是host_bridge2:
user@net1:~$ sudo ip link delete dev host_bridge2
然后,我们需要添加新的边缘桥接(edge_bridge2)并将net1面向net3的接口与桥接关联起来:
user@net1:~$ sudo ip link add edge_bridge2 type bridge
user@net1:~$ sudo ip link set dev eth2 master edge_bridge2
然后,我们将为此连接定义 VETH 对:
user@net1:~$ sudo ip link add **host_veth2** type veth peer name **edge_veth2
在这种情况下,我们将使主机端的 VETH 对与桥接不相关,而是直接为其分配一个 IP 地址:
user@net1:~$ sudo ip address add 172.16.10.65/25 dev host_veth2
就像任何其他接口一样,我们可以使用ip address show dev命令来查看分配的 IP 地址:
user@net1:~$ ip addr show dev **host_veth2
12: host_veth2@edge_veth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 56:92:14:83:98:e0 brd ff:ff:ff:ff:ff:ff
inet **172.16.10.65/25** scope global host_veth2
valid_lft forever preferred_lft forever
inet6 fe80::5492:14ff:fe83:98e0/64 scope link
valid_lft forever preferred_lft forever
user@net1:~$
然后,我们将另一端的 VETH 对放入edge_bridge2连接net1到边缘桥接:
user@net1:~$ sudo ip link set edge_veth2 master edge_bridge2
然后,我们再次启动所有相关接口:
user@net1:~$ sudo ip link set edge_bridge2 up
user@net1:~$ sudo ip link set host_veth2 up
user@net1:~$ sudo ip link set edge_veth2 up
最后,我们读取我们到达net3的虚拟接口的路由:
user@net1:~$ sudo ip route add 172.16.10.192/26 via 172.16.10.66
配置完成后,我们应该再次完全进入环境和所有接口的可达性。如果配置有任何问题,您应该能够通过使用ip link show和ip addr show命令来诊断它们。
如果您曾经怀疑 VETH 对的另一端是什么,您可以使用ethtool命令行工具返回对的另一端。例如,假设我们创建一个非命名的 VETH 对如下所示:
user@docker1:/$ sudo ip link add type veth
user@docker1:/$ ip link show
…<output removed for brevity>,,,
16: **veth1@veth2**: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 12:3f:7b:8d:33:90 brd ff:ff:ff:ff:ff:ff
17: **veth2@veth1**: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 9e:9f:34:bc:49:73 brd ff:ff:ff:ff:ff:ff
在这个例子中很明显,我们可以使用ethtool来确定这个 VETH 对的接口索引或 ID 的一端或另一端:
user@docker1:/$ ethtool -S **veth1
NIC statistics:
peer_ifindex: **17
user@docker1:/$ ethtool -S **veth2
NIC statistics:
peer_ifindex: **16
user@docker1:/$
在确定 VETH 对的端点不像在这些示例中那样明显时,这可能是一个方便的故障排除工具。
探索网络命名空间
网络命名空间允许您创建网络的隔离视图。命名空间具有唯一的路由表,可以与主机上的默认路由表完全不同。此外,您可以将物理主机的接口映射到命名空间中,以在命名空间内使用。网络命名空间的行为与大多数现代网络硬件中可用的虚拟路由和转发(VRF)实例的行为非常相似。在本教程中,我们将学习网络命名空间的基础知识。我们将逐步介绍创建命名空间的过程,并讨论如何在网络命名空间中使用不同类型的接口。最后,我们将展示如何连接多个命名空间。
准备工作
为了查看和操作网络设置,您需要确保已安装了iproute2工具集。如果系统上没有安装,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要具有根级别的访问权限。这个示例将继续上一个示例中的实验室拓扑。之前提到的所有先决条件仍然适用。
如何做…
网络命名空间的概念最好通过一个例子来进行演示,所以让我们直接回到上一个示例中的实验室拓扑:

这个图表和上一个示例中使用的拓扑是一样的,但有一个重要的区别。我们增加了两个命名空间NS_1和NS_2。每个命名空间包含主机net1上的特定接口:
-
NS_1:
-
edge_bridge1 -
eth1 -
edge_veth1 -
NS_2:
-
edge_bridge2 -
eth2 -
edge_veth2
请注意命名空间的边界在哪里。在任何情况下,边界都位于物理接口(net1主机的eth1和eth2)上,或者直接位于 VETH 对的中间。正如我们将很快看到的,VETH 对可以在命名空间之间桥接,使它们成为连接网络命名空间的理想工具。
要开始重新配置,让我们从定义命名空间开始,然后将接口添加到命名空间中。定义命名空间相当简单。我们使用ip netns add子命令:
user@net1:~$ sudo ip netns add ns_1
user@net1:~$ sudo ip netns add ns_2
然后可以使用ip netns list命令来查看命名空间:
user@net1:~$ ip netns list
ns_2
ns_1
user@net1:~$
命名空间创建后,我们可以分配特定的接口给我们确定为每个命名空间的一部分的接口。在大多数情况下,这意味着告诉一个现有的接口它属于哪个命名空间。然而,并非所有接口都可以移动到网络命名空间中。例如,桥接可以存在于网络命名空间中,但需要在命名空间内实例化。为此,我们可以使用ip netns exec子命令来在命名空间内运行命令。例如,要在每个命名空间中创建边缘桥接,我们将运行这两个命令:
user@net1:~$ sudo ip netns exec ns_1 **ip link add \
edge_bridge1 type bridge
user@net1:~$ sudo ip netns exec ns_2 **ip link add \
edge_bridge2 type bridge
让我们把这个命令分成两部分:
-
sudo ip nent exec ns_1:这告诉主机你想在特定的命名空间内运行一个命令,在这种情况下是ns_1 -
ip link add edge_bridge1 type bridge:正如我们在之前的示例中看到的,我们执行这个命令来构建一个桥接并给它起一个名字,在这种情况下是edge_bridge1。
使用相同的语法,我们现在可以检查特定命名空间的网络配置。例如,我们可以使用sudo ip netns exec ns_1 ip link show查看接口:
user@net1:~$ sudo ip netns exec ns_1 **ip link show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: **edge_bridge1**: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default
link/ether 26:43:4e:a6:30:91 brd ff:ff:ff:ff:ff:ff
user@net1:~$
正如我们预期的那样,我们在命名空间中看到了我们实例化的桥接器。图表中显示在命名空间中的另外两种接口类型是可以动态分配到命名空间中的类型。为此,我们使用ip link set命令:
user@net1:~$ sudo ip link set dev **eth1** netns **ns_1
user@net1:~$ sudo ip link set dev **edge_veth1** netns **ns_1
user@net1:~$ sudo ip link set dev **eth2** netns **ns_2
user@net1:~$ sudo ip link set dev **edge_veth2** netns **ns_2
现在,如果我们查看可用的主机接口,我们应该注意到我们移动的接口不再存在于默认命名空间中:
user@net1:~$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:2d:dd:79 brd ff:ff:ff:ff:ff:ff
5: host_bridge1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 56:cc:26:4c:76:f6 brd ff:ff:ff:ff:ff:ff
7: **edge_bridge1**: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
8: **edge_bridge2**: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
10: host_veth1@if9: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast master host_bridge1 state LOWERLAYERDOWN mode DEFAULT group default qlen 1000
link/ether 56:cc:26:4c:76:f6 brd ff:ff:ff:ff:ff:ff
12: host_veth2@if11: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state LOWERLAYERDOWN mode DEFAULT group default qlen 1000
link/ether 2a:8b:54:81:36:31 brd ff:ff:ff:ff:ff:ff
user@net1:~$
注意
您可能已经注意到,edge_bridge1和edge_bridge2仍然存在于此输出中,因为我们从未删除它们。这很有趣,因为它们现在也存在于命名空间ns_1和ns_2中。重要的是要指出,由于命名空间是完全隔离的,甚至接口名称也可以重叠。
现在所有接口都在正确的命名空间中,剩下的就是应用标准的桥接映射并启动接口。由于我们需要在每个命名空间中重新创建桥接接口,我们需要重新将接口附加到每个桥接器上。这就像通常做的那样;我们只需在命名空间内运行命令:
user@net1:~$ sudo ip netns exec ns_1 **ip link set \
dev edge_veth1 master edge_bridge1
user@net1:~$ sudo ip netns exec ns_1 **ip link set \
dev eth1 master edge_bridge1
user@net1:~$ sudo ip netns exec ns_2 **ip link set \
dev edge_veth2 master edge_bridge2
user@net1:~$ sudo ip netns exec ns_2 **ip link set \
dev eth2 master edge_bridge2
一旦我们将所有接口放入正确的命名空间并连接到正确的桥接器,剩下的就是将它们全部启动:
user@net1:~$ sudo ip netns exec ns_1 **ip link set edge_bridge1 up
user@net1:~$ sudo ip netns exec ns_1 **ip link set edge_veth1 up
user@net1:~$ sudo ip netns exec ns_1 **ip link set eth1 up
user@net1:~$ sudo ip netns exec ns_2 **ip link set edge_bridge2 up
user@net1:~$ sudo ip netns exec ns_2 **ip link set edge_veth2 up
user@net1:~$ sudo ip netns exec ns_2 **ip link set eth2 up
接口启动后,我们应该再次可以连接到所有三个主机连接的网络。
虽然命名空间的这个示例只是将第 2 层类型的结构移入了一个命名空间,但它们还支持每个命名空间具有唯一路由表实例的第 3 层路由。例如,如果我们查看其中一个命名空间的路由表,我们会发现它是完全空的:
user@net1:~$ sudo ip netns exec ns_1 ip route
user@net1:~$
这是因为在命名空间中没有定义 IP 地址的接口。这表明命名空间内部隔离了第 2 层和第 3 层结构。这是网络命名空间和 VRF 实例之间的一个主要区别。VRF 实例只考虑第 3 层配置,而网络命名空间隔离了第 2 层和第 3 层结构。在第三章中,当我们讨论 Docker 用于容器网络的过程时,我们将在用户定义的网络中看到网络命名空间中的第 3 层隔离的示例。
第二章:配置和监控 Docker 网络
在本章中,我们将涵盖以下内容:
-
验证影响 Docker 网络的主机级设置
-
在桥接模式下连接容器
-
暴露和发布端口
-
连接容器到现有容器
-
在主机模式下连接容器
-
配置服务级设置
介绍
Docker 使得使用容器技术比以往任何时候都更容易。Docker 以其易用性而闻名,提供了许多高级功能,但安装时使用了一组合理的默认设置,使得快速开始构建容器变得容易。虽然网络配置通常是需要在使用之前额外关注的一个领域,但 Docker 使得让容器上线并连接到网络变得容易。
验证影响 Docker 网络的主机级设置
Docker 依赖于主机能够执行某些功能来使 Docker 网络工作。换句话说,您的 Linux 主机必须配置为允许 IP 转发。此外,自 Docker 1.7 发布以来,您现在可以选择使用 hairpin Network Address Translation(NAT)而不是默认的 Docker 用户空间代理。在本教程中,我们将回顾主机必须启用 IP 转发的要求。我们还将讨论 NAT hairpin,并讨论该选项的主机级要求。在这两种情况下,我们将展示 Docker 对其设置的默认行为,以及您如何更改它们。
准备工作
您需要访问运行 Docker 的 Linux 主机,并能够停止和重新启动服务。由于我们将修改系统级内核参数,您还需要对系统具有根级访问权限。
如何做…
正如我们在第一章中所看到的,Linux 主机必须启用 IP 转发才能够在接口之间路由流量。由于 Docker 正是这样做的,因此 Docker 网络需要启用 IP 转发才能正常工作。如果 Docker 检测到 IP 转发被禁用,当您尝试运行容器时,它将警告您存在问题:
user@docker1:~$ docker run --name web1 -it \
jonlangemak/web_server_1 /bin/bash
WARNING: **IPv4 forwarding is disabled. Networking will not work.
root@071d673821b8:/#
大多数 Linux 发行版将 IP 转发值默认为disabled或0。幸运的是,在默认配置中,Docker 会在 Docker 服务启动时负责更新此设置为正确的值。例如,让我们看一个刚刚重启过并且没有在启动时启用 Docker 服务的主机。如果我们在启动 Docker 之前检查设置的值,我们会发现它是禁用的。启动 Docker 引擎会自动为我们启用该设置:
user@docker1:~$ more /proc/sys/net/ipv4/ip_forward
0
user@docker1:~$
user@docker1:~$ sudo systemctl start docker
user@docker1:~$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = **1
user@docker1:~$
Docker 中的这种默认行为可以通过在运行时选项中传递--ip-forward=false来更改为否。
注意
Docker 特定参数的配置根据使用的init 系统而有很大不同。在撰写本文时,许多较新的 Linux 操作系统使用systemd作为其 init 系统。始终请查阅 Docker 文档,以了解其针对您使用的操作系统的服务配置建议。Docker 服务配置和选项将在本章的即将推出的食谱中更详细地讨论。在本食谱中,只需关注更改这些设置对 Docker 和主机本身的影响。
有关内核 IP 转发参数的进一步讨论可以在第一章的配置 Linux 主机路由食谱中找到,Linux 网络构造。在那里,您将找到如何自己更新参数以及如何通过重新启动使设置持久化。
Docker 的另一个最近的功能依赖于内核级参数,即 hairpin NAT 功能。较早版本的 Docker 实现并依赖于所谓的 Docker 用户态代理来促进容器间和发布端口的通信。默认情况下,任何暴露端口的容器都是通过用户态代理进程来实现的。例如,如果我们启动一个示例容器,我们会发现除了 Docker 进程本身外,我们还有一个docker-proxy进程:
user@docker1:~$ docker run --name web1 -d -P jonlangemak/web_server_1
bf3cb30e826ce53e6e7db4e72af71f15b2b8f83bd6892e4838ec0a59b17ac33f
user@docker1:~$
user@docker1:~$ ps aux | grep docker
root 771 0.0 0.1 509676 41656 ? Ssl 19:30 0:00 /usr/bin/docker daemon
root 1861 0.2 0.0 117532 28024 ? Sl 19:41 0:00 **docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 32769 -container-ip 172.17.0.2 -container-port 80
…<Additional output removed for brevity>…
user@docker1:~$
每个发布的端口都会在 Docker 主机上启动一个新的docker-proxy进程。作为用户态代理的替代方案,您可以选择让 Docker 使用 hairpin NAT 而不是用户态代理。Hairpin NAT 依赖于主机系统配置为在主机的本地环回接口上启用路由。同样,当 Docker 服务启动时,Docker 服务会负责更新正确的主机参数以启用此功能,如果被告知这样做的话。
Hairpin NAT 依赖于内核参数net.ipv4.conf.docker0.route_localnet被启用(设置为1),以便主机可以通过主机的环回接口访问容器服务。这可以通过与我们描述 IP 转发参数的方式实现:
使用sysctl命令:
sysctl net.ipv4.conf.docker0.route_localnet
通过直接查询/proc/文件系统:
more /proc/sys/net/ipv4/conf/docker0/route_localnet
如果返回的值是0,那么 Docker 很可能处于其默认配置,并依赖于用户态代理。由于您可以选择在两种模式下运行 Docker,我们需要做的不仅仅是更改内核参数,以便对 hairpin NAT 进行更改。我们还需要告诉 Docker 通过将选项--userland-proxy=false作为运行时选项传递给 Docker 服务来更改其发布端口的方式。这样做将启用 hairpin NAT,并告诉 Docker 更新内核参数以使 hairpin NAT 正常工作。让我们启用 hairpin NAT 以验证 Docker 是否正在执行其应该执行的操作。
首先,让我们检查内核参数的值:
user@docker1:~$ sysctl net.ipv4.conf.docker0.route_localnet
net.ipv4.conf.docker0.route_localnet = 0
user@docker1:~$
它目前被禁用。现在我们可以告诉 Docker 通过将--userland-proxy=false作为参数传递给 Docker 服务来禁用用户态代理。一旦 Docker 服务被告知禁用用户态代理,并且服务被重新启动,我们应该看到参数在主机上被启用:
user@docker1:~$ sysctl net.ipv4.conf.docker0.route_localnet
net.ipv4.conf.docker0.route_localnet = **1
user@docker1:~$
此时运行具有映射端口的容器将不会创建额外的docker-proxy进程实例:
user@docker1:~$ docker run --name web1 -d -P jonlangemak/web_server_1
5743fac364fadb3d86f66cb65532691fe926af545639da18f82a94fd35683c54
user@docker1:~$ ps aux | grep docker
root 2159 0.1 0.1 310696 34880 ? Ssl 14:26 0:00 /usr/bin/docker daemon --userland-proxy=false
user@docker1:~$
此外,我们仍然可以通过主机的本地接口访问容器:
user@docker1:~$ **curl 127.0.0.1:32768
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
再次禁用参数会导致此连接失败:
user@docker1:~$ **sudo sysctl -w net.ipv4.conf.docker0.route_localnet=0
net.ipv4.conf.docker0.route_localnet = 0
user@docker1:~$ curl 127.0.0.1:32768
curl: (7) Failed to connect to 127.0.0.1 port 32768: Connection timed out
user@docker1:~$
在桥接模式下连接容器
正如我们之前提到的,Docker 带有一组合理的默认值,可以使您的容器在网络上进行通信。从网络的角度来看,Docker 的默认设置是将任何生成的容器连接到docker0桥接器上。在本教程中,我们将展示如何在默认桥接模式下连接容器,并解释离开容器和目的地容器的网络流量是如何处理的。
做好准备
您需要访问 Docker 主机,并了解您的 Docker 主机如何连接到网络。在我们的示例中,我们将使用一个具有两个物理网络接口的 Docker 主机,就像下图所示的那样:

您需要确保可以查看iptables规则以验证netfilter策略。如果您希望下载和运行示例容器,您的 Docker 主机还需要访问互联网。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何做…
安装并启动 Docker 后,您应该注意到添加了一个名为docker0的新 Linux 桥。默认情况下,docker0桥的 IP 地址为172.17.0.1/16:
user@docker1:~$ **ip addr show docker0
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:54:87:8b:ea brd ff:ff:ff:ff:ff:ff
inet **172.17.0.1/16** scope global docker0
valid_lft forever preferred_lft forever
user@docker1:~$
Docker 将在未指定网络的情况下启动的任何容器放置在docker0桥上。现在,让我们看一个在此主机上运行的示例容器:
user@docker1:~$ **docker run -it jonlangemak/web_server_1 /bin/bash
root@abe6eae2e0b3:/# **ip addr
1: **lo**: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
6: **eth0**@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
root@abe6eae2e0b3:/#
通过以交互模式运行容器,我们可以查看容器认为自己的网络配置是什么。在这种情况下,我们可以看到容器有一个非回环网络适配器(eth0),IP 地址为172.17.0.2/16。
此外,我们可以看到容器认为其默认网关是 Docker 主机上的docker0桥接口:
root@abe6eae2e0b3:/# **ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2
root@abe6eae2e0b3:/#
通过运行一些基本测试,我们可以看到容器可以访问 Docker 主机的物理接口以及基于互联网的资源。
注意
基于互联网的访问容器本身的前提是 Docker 主机可以访问互联网。
root@abe6eae2e0b3:/# **ping 10.10.10.101 -c 2
PING 10.10.10.101 (10.10.10.101): 48 data bytes
56 bytes from 10.10.10.101: icmp_seq=0 ttl=64 time=0.084 ms
56 bytes from 10.10.10.101: icmp_seq=1 ttl=64 time=0.072 ms
--- 10.10.10.101 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.072/0.078/0.084/0.000 ms
root@abe6eae2e0b3:/#
root@abe6eae2e0b3:/# **ping 4.2.2.2 -c 2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=29.388 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=26.766 ms
--- 4.2.2.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 26.766/28.077/29.388/1.311 ms
root@abe6eae2e0b3:/#
考虑到容器所在的网络是由 Docker 创建的,我们可以安全地假设网络的其余部分不知道它。也就是说,外部网络不知道172.17.0.0/16网络,因为它是本地的 Docker 主机。也就是说,容器能够访问docker0桥之外的资源似乎有些奇怪。Docker 通过将容器的 IP 地址隐藏在 Docker 主机的 IP 接口后使其工作。流量流向如下图所示:

由于容器的流量在物理网络上被视为 Docker 主机的 IP 地址,其他网络资源知道如何将流量返回到容器。为了执行这种出站 NAT,Docker 使用 Linux netfilter 框架。我们可以使用 netfilter 命令行工具iptables来查看这些规则:
user@docker1:~$ sudo iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 anywhere
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- anywhere anywhere
user@docker1:~$
正如你所看到的,我们在POSTROUTING链中有一个规则,它将来自我们的docker0桥(172.17.0.0/16)的任何东西伪装或隐藏在主机接口的背后。
尽管出站连接是默认配置和允许的,但 Docker 默认情况下不提供一种从 Docker 主机外部访问容器中的服务的方法。为了做到这一点,我们必须在容器运行时传递额外的标志给 Docker。具体来说,当我们运行容器时,我们可以传递-P标志。为了检查这种行为,让我们看一个暴露端口的容器镜像:
docker run --name web1 -d -P jonlangemak/web_server_1
这告诉 Docker 将一个随机端口映射到容器镜像暴露的任何端口。在这个演示容器的情况下,镜像暴露端口80。运行容器后,我们可以看到主机端口映射到容器:
user@docker1:~$ docker run --name web1 **-P** -d jonlangemak/web_server_1
556dc8cefd79ed1d9957cc52827bb23b7d80c4b887ee173c2e3b8478340de948
user@docker1:~$
user@docker1:~$ docker port web1
80/tcp -> 0.0.0.0:32768
user@docker1:~$
正如我们所看到的,容器端口80已经映射到主机端口32768。这意味着我们可以通过主机的接口在端口32768上访问容器上运行的端口80的服务。与出站容器访问类似,入站连接也使用 netfilter 来创建端口映射。我们可以通过检查 NAT 和过滤表来看到这一点:
user@docker1:~$ sudo iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 anywhere
MASQUERADE tcp -- 172.17.0.2 172.17.0.2 tcp dpt:http
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- anywhere anywhere
DNAT tcp -- anywhere anywhere tcp dpt:32768 to:172.17.0.2:80
user@docker1:~$ sudo iptables -t filter -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
DOCKER-ISOLATION all -- anywhere anywhere
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Chain DOCKER (1 references)
target prot opt source destination
ACCEPT tcp -- anywhere 172.17.0.2 tcp dpt:http
Chain DOCKER-ISOLATION (1 references)
target prot opt source destination
RETURN all -- anywhere anywhere
user@docker1:~$
由于连接在所有接口(0.0.0.0)上暴露,我们的入站图将如下所示:

如果没有另行定义,生活在同一主机上的容器,因此是相同的docker0桥,可以通过它们分配的 IP 地址在任何端口上固有地相互通信,这些端口绑定到服务。允许这种通信是默认行为,并且可以在后面的章节中更改,当我们讨论容器间通信(ICC)配置时会看到。
注意
应该注意的是,这是在没有指定任何额外网络参数的情况下运行的容器的默认行为,也就是说,使用 Docker 默认桥接网络的容器。后面的章节将介绍其他选项,允许您将生活在同一主机上的容器放置在不同的网络上。
生活在不同主机上的容器之间的通信需要使用先前讨论的流程的组合。为了测试这一点,让我们通过添加一个名为docker2的第二个主机来扩展我们的实验。假设主机docker2上的容器web2希望访问主机docker1上的容器web1,后者在端口80上托管服务。流程将如下所示:

让我们在每个步骤中走一遍流程,并展示数据包在每个步骤中传输时的样子。在这种情况下,容器web1正在暴露端口80,该端口已发布到主机docker1的端口32771。
-
流量离开容器
web2,目的地是主机docker1的10.10.10.101接口上的暴露端口(32771):![操作步骤…]()
-
流量到达容器的默认网关,即
docker0桥接的 IP 接口(172.17.0.1)。主机进行路由查找,并确定目的地位于其10.10.10.102接口之外,因此它将容器的真实源 IP 隐藏在该接口的 IP 地址后面:![操作步骤…]()
-
流量到达
docker1主机,并由 netfilter 规则检查。docker1有一个规则,将容器 1 的服务端口(80)暴露在主机的端口32271上:![操作步骤…]()
-
目标端口从
32771更改为80,并传递到web1容器,该容器在正确的端口80上接收流量:![操作步骤…]()
为了自己尝试一下,让我们首先运行web1容器并检查服务暴露在哪个端口上:
user@docker1:~/apache$ docker run --name web1 -P \
-d jonlangemak/web_server_1
974e6eba1948ce5e4c9ada393b1196482d81f510de 12337868ad8ef65b8bf723
user@docker1:~/apache$
user@docker1:~/apache$ docker port web1
80/tcp -> **0.0.0.0:32771
user@docker1:~/apache$
现在让我们在主机 docker2 上运行一个名为 web2 的第二个容器,并尝试访问端口 32771 上的 web1 服务…
user@docker2:~$ docker run --name web2 -it \
jonlangemak/web_server_2 /bin/bash
root@a97fea6fb0c9:/#
root@a97fea6fb0c9:/# curl http://**10.10.10.101:32771
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
暴露和发布端口
正如我们在之前的例子中看到的,将容器中的服务暴露给外部世界是 Docker 的一个关键组成部分。到目前为止,我们已经让镜像和 Docker 引擎在实际端口映射方面为我们做了大部分工作。为了做到这一点,Docker 使用了容器镜像的元数据以及用于跟踪端口分配的内置系统的组合。在这个示例中,我们将介绍定义要暴露的端口以及发布端口的选项的过程。
准备工作
您需要访问一个 Docker 主机,并了解您的 Docker 主机如何连接到网络。在这个示例中,我们将使用之前示例中使用的docker1主机。您需要确保可以查看iptables规则以验证 netfilter 策略。如果您希望下载和运行示例容器,您的 Docker 主机还需要访问互联网。在某些情况下,我们所做的更改可能需要您具有系统的 root 级别访问权限。
操作步骤…
虽然经常混淆,但暴露端口和发布端口是两个完全不同的操作。暴露端口实际上只是一种记录容器可能提供服务的端口的方式。这些定义存储在容器元数据中作为镜像的一部分,并可以被 Docker 引擎读取。发布端口是将容器端口映射到主机端口的实际过程。这可以通过使用暴露的端口定义自动完成,也可以在不使用暴露端口的情况下手动完成。
让我们首先讨论端口是如何暴露的。暴露端口的最常见机制是在镜像的Dockerfile中定义它们。当您构建一个容器镜像时,您有机会定义要暴露的端口。考虑一下我用来构建本书一些演示容器的 Dockerfile 定义:
FROM ubuntu:12.04
MAINTAINER Jon Langemak jon@interubernet.com
RUN apt-get update && apt-get install -y apache2 net-tools inetutils-ping curl
ADD index.html /var/www/index.html
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2
EXPOSE 80
CMD ["/usr/sbin/apache2", "-D", "FOREGROUND"]
作为 Dockerfile 的一部分,我可以定义我希望暴露的端口。在这种情况下,我知道 Apache 默认会在端口80上提供其 Web 服务器,所以这是我希望暴露的端口。
注意
请注意,默认情况下,Docker 始终假定您所指的端口是 TCP。如果您希望暴露 UDP 端口,可以在端口定义的末尾包括/udp标志来实现。例如,EXPOSE 80/udp。
现在,让我们运行一个使用这个 Dockerfile 构建的容器,看看会发生什么:
user@docker1:~$ docker run --name web1 -d jonlangemak/web_server_1
b0177ed2d38afe4f4d8c26531d00407efc0fee6517ba5a0f49955910a5dbd426
user@docker1:~$
user@docker1:~$ docker port web1
user@docker1:~$
正如我们所看到的,尽管有一个定义的要暴露的端口,Docker 实际上并没有在主机和容器之间映射任何端口。如果您回忆一下之前的示例,其中容器提供了一个服务,我们在docker run命令语法中包含了-P标志。-P标志告诉 Docker 发布所有暴露的端口。让我们尝试使用设置了-P标志的容器运行此容器:
user@docker1:~$ docker run --name web1 -d -P jonlangemak/web_server_1
d87d36d7cbcfb5040f78ff730d079d353ee81fde36ecbb5ff932ff9b9bef5502
user@docker1:~$
user@docker1:~$ docker port web1
80/tcp -> 0.0.0.0:32775
user@docker1:~$
在这里,我们可以看到 Docker 现在已经自动将暴露的端口映射到主机上的一个随机高端口。端口80现在将被视为已发布。
除了通过镜像 Dockerfile 暴露端口,我们还可以在容器运行时暴露它们。以这种方式暴露的任何端口都将与 Dockerfile 中暴露的端口合并。例如,让我们再次运行相同的容器,并在docker run命令中暴露端口80 UDP:
user@docker1:~$ docker run --name web1 **--expose=80/udp \
-d -P jonlangemak/web_server_1
f756deafed26f9635a3b9c738089495efeae86a393f94f17b2c4fece9f71a704
user@docker1:~$
user@docker1:~$ docker port web1
80/udp -> 0.0.0.0:32768
80/tcp -> 0.0.0.0:32776
user@docker1:~$
如您所见,我们不仅发布了来自 Dockerfile 的端口(80/tcp),还发布了来自docker run命令的端口(80/udp)。
注意
在容器运行时暴露端口允许您有一些额外的灵活性,因为您可以定义要暴露的端口范围。这在 Dockerfile 的expose语法中目前是不可能的。当暴露一系列端口时,您可以通过在命令的末尾添加您要查找的容器端口来过滤docker port命令的输出。
虽然暴露方法确实很方便,但它并不能满足我们所有的需求。对于您想要更多控制使用的端口和接口的情况,您可以在启动容器时绕过expose并直接发布端口。通过传递-P标志发布所有暴露的端口,通过传递-p标志允许您指定映射端口时要使用的特定端口和接口。-p标志可以采用几种不同的形式,语法看起来像这样:
–p <host IP interface>:<host port>:<container port>
任何选项都可以省略,唯一需要的字段是容器端口。例如,以下是您可以使用此语法的几种不同方式:
- 指定主机端口和容器端口:
–p <host port>:<container port>
- 指定主机接口、主机端口和容器端口:
–p <host IP interface>:<host port>:<container port>
- 指定主机接口,让 Docker 选择一个随机的主机端口,并指定容器端口:
–p <host IP interface>::<container port>
- 只指定一个容器端口,让 Docker 使用一个随机的主机端口:
–p <container port>
到目前为止,我们看到的所有发布的端口都使用了目标 IP 地址(0.0.0.0),这意味着它们绑定到 Docker 主机的所有 IP 接口。默认情况下,Docker 服务始终将发布的端口绑定到所有主机接口。然而,正如我们将在本章的下一个示例中看到的那样,我们可以告诉 Docker 通过传递--ip参数来使用特定的接口。
鉴于我们还可以在docker run命令中定义要绑定的发布端口的接口,我们需要知道哪个选项优先级更高。一般规则是,在容器运行时定义的任何选项都会获胜。例如,让我们看一个例子,我们告诉 Docker 服务通过向服务传递以下选项来绑定到docker1主机的192.168.10.101 IP 地址:
--ip=10.10.10.101
现在,让我们以几种不同的方式运行一个容器,并查看结果:
user@docker1:~$ docker run --name web1 -P -d jonlangemak/web_server_1
629129ccaebaa15720399c1ac31c1f2631fb4caedc7b3b114a92c5a8f797221d
user@docker1:~$ docker port web1
80/tcp -> 10.10.10.101:32768
user@docker1:~$
在前面的例子中,我们看到了预期的行为。发布的端口绑定到服务级别--ip选项(10.10.10.101)中指定的 IP 地址。然而,如果我们在容器运行时指定 IP 地址,我们可以覆盖服务级别的设置:
user@docker1:~$ docker run --name web2 **-p 0.0.0.0::80 \
-d jonlangemak/web_server_2
7feb252d7bd9541fe7110b2aabcd6a50522531f8d6ac5422f1486205fad1f666
user@docker1:~$ docker port web2
80/tcp -> 0.0.0.0:32769
user@docker1:~$
We can see that we specified a host IP address of 0.0.0.0, which will match all the IP addresses on the Docker host. When we check the port mapping, we see that the 0.0.0.0 specified in the command overrode the service-level default.
您可能不会发现暴露端口的用途,而是完全依赖手动发布它们。EXPOSE命令不是创建镜像的 Dockerfile 的要求。不定义暴露端口的容器镜像可以直接发布,如以下命令所示:
user@docker1:~$ docker run --name noexpose **-p 0.0.0.0:80:80 \
-d jonlangemak/web_server_noexpose
2bf21219b45ba05ef7169fc30d5eac73674857573e54fd1a0499b73557fdfd45
user@docker1:~$ docker port noexpose
80/tcp -> 0.0.0.0:80
user@docker1:~$
在上面的示例中,容器镜像jonlangemak/web_server_noexpose是一个不在其定义中暴露任何端口的容器。
连接容器到现有容器
到目前为止,Docker 网络连接依赖于将托管在容器中的单个服务暴露给物理网络。但是,如果您想将一个容器中的服务暴露给另一个容器而不暴露给 Docker 主机怎么办?在本教程中,我们将介绍如何在同一 Docker 主机上运行的两个容器之间映射服务。
准备工作
您将需要访问 Docker 主机,并了解您的 Docker 主机如何连接到网络。在本教程中,我们将使用之前教程中使用过的docker1主机。您需要确保可以访问iptables规则以验证 netfilter 策略。如果您希望下载和运行示例容器,您的 Docker 主机还需要访问互联网。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
操作步骤...
有时将一个容器中的服务映射到另一个容器中被称为映射容器模式。映射容器模式允许您启动一个利用现有或主要容器网络配置的容器。也就是说,映射容器将使用与主容器相同的 IP 和端口配置。举个例子,让我们考虑运行以下容器:
user@docker1:~$ docker run --name web4 -d -P \
jonlangemak/web_server_4_redirect
运行此容器将以桥接模式启动容器,并将其附加到docker0桥接,正如我们所期望的那样。
此时,拓扑看起来非常标准,类似于以下拓扑所示:

现在在同一主机上运行第二个容器,但这次指定网络应该是主容器web4的网络:
user@docker1:~$ docker run --name web3 -d **--net=container:web4 \
jonlangemak/web_server_3_8080
我们的拓扑现在如下所示:

请注意,容器web3现在被描述为直接连接到web4,而不是docker0桥接。通过查看每个容器的网络配置,我们可以验证这是否属实:
user@docker1:~$ **docker exec web4 ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
16: **eth0**@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
user@docker1:~$ **docker exec web3 ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
16: **eth0**@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
正如我们所看到的,接口在 IP 配置和 MAC 地址方面都是相同的。在docker run命令中使用--net:container<container name/ID>的语法将新容器加入到与所引用容器相同的网络结构中。这意味着映射的容器具有与主容器相同的网络配置。
这种配置有一个值得注意的限制。加入另一个容器网络的容器无法发布自己的任何端口。因此,这意味着我们无法将映射容器的端口发布到主机,但我们可以在本地使用它们。回到我们的例子,这意味着我们无法将容器web3的端口8080发布到主机。但是,容器web4可以在本地使用容器web3的未发布服务。例如,这个例子中的每个容器都托管一个 Web 服务:
-
web3托管在端口8080上运行的 Web 服务器 -
web4托管在端口80上运行的 Web 服务器
从外部主机的角度来看,无法访问容器web3的 Web 服务。但是,我们可以通过容器web4访问这些服务。容器web4托管一个名为test.php的 PHP 脚本,该脚本提取其自己的 Web 服务器以及在端口8080上运行的 Web 服务器的索引页面。脚本如下:
<?
$page = file_get_contents('**http://localhost:80/**');
echo $page;
$page1 = file_get_contents('**http://localhost:8080/**');
echo $page1;
?>
脚本位于 Web 服务器的根托管目录(/var/www/)中,因此我们可以通过浏览web4容器的发布端口,然后跟上test.php来访问端口:
user@docker1:~$ docker port web4
80/tcp -> 0.0.0.0:32768
user@docker1:~$
user@docker1:~$ curl **http://localhost:32768/test.php
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #4 - Running on port 80**</span>
</h1>
</body>
</html>
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #3 - Running on port 8080**</span>
</h1>
</body>
</html>
user@docker1:~$
如您所见,脚本能够从两个容器中提取索引页面。让我们停止容器web3,然后再次运行此测试,以证明它确实是提供此索引页面响应的容器:
user@docker1:~$ docker stop web3
web3
user@docker1:~$ curl **http://localhost:32768/test.php
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #4 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
如您所见,我们不再从映射的容器中获得响应。映射容器模式对于需要向现有容器提供服务但不需要直接将映射容器的任何端口发布到 Docker 主机或外部网络的情况非常有用。尽管映射容器无法发布自己的任何端口,但这并不意味着我们不能提前发布它们。
例如,当我们运行主容器时,我们可以暴露端口8080:
user@docker1:~$ docker run --name web4 -d **--expose 8080 \
-P** jonlangemak/web_server_4_redirect
user@docker1:~$ docker run --name web3 -d **--net=container:web4 \
jonlangemak/web_server_3_8080
因为我们在运行主容器(web4)时发布了映射容器的端口,所以在运行映射容器(web3)时就不需要再次发布它。现在我们应该能够通过其发布的端口直接访问每个服务:
user@docker1:~$ docker port web4
80/tcp -> 0.0.0.0:32771
8080/tcp -> 0.0.0.0:32770
user@docker1:~$
user@docker1:~$ curl **localhost:32771
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #4 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$ curl **localhost:32770
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #3 - Running on port 8080**</span>
</h1>
</body>
</html>
user@docker1:~$
在映射容器模式下,应注意不要尝试在不同的容器上公开或发布相同的端口。由于映射容器与主容器共享相同的网络结构,这将导致端口冲突。
在主机模式下连接容器
到目前为止,我们所做的所有配置都依赖于使用docker0桥来促进容器之间的连接。我们不得不考虑端口映射、NAT 和容器连接点。由于我们连接和寻址容器的方式的性质以及确保灵活的部署模型,必须考虑这些因素。主机模式采用了一种不同的方法,直接将容器绑定到 Docker 主机的接口上。这不仅消除了入站和出站 NAT 的需要,还限制了我们可以部署容器的方式。由于容器将位于与物理主机相同的网络结构中,我们不能重叠服务端口,因为这将导致冲突。在本教程中,我们将介绍在主机模式下部署容器,并描述这种方法的优缺点。
准备工作
您需要访问一个 Docker 主机,并了解您的 Docker 主机如何连接到网络。在本教程中,我们将使用之前教程中使用过的docker1和docker2主机。您需要确保可以查看iptables规则以验证 netfilter 策略。如果您希望下载和运行示例容器,您的 Docker 主机还需要访问互联网。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何做…
从 Docker 的角度来看,在这种模式下部署容器相当容易。就像映射容器模式一样,我们将一个容器放入另一个容器的网络结构中;主机模式直接将一个容器放入 Docker 主机的网络结构中。不再需要发布和暴露端口,因为你将容器直接映射到主机的网络接口上。这意味着容器进程可以执行某些特权操作,比如在主机上打开较低级别的端口。因此,这个选项应该谨慎使用,因为在这种配置下容器将对系统有更多的访问权限。
这也意味着 Docker 不知道你的容器在使用什么端口,并且无法阻止你部署具有重叠端口的容器。让我们在主机模式下部署一个测试容器,这样你就能明白我的意思了:
user@docker1:~$ docker run --name web1 -d **--net=host \
jonlangemak/web_server_1
64dc47af71fade3cde02f7fed8edf7477e3cc4c8fc7f0f3df53afd129331e736
user@docker1:~$
user@docker1:~$ curl **localhost
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
为了使用主机模式,我们在容器运行时传递--net=host标志。在这种情况下,你可以看到没有任何端口映射,我们仍然可以访问容器中的服务。Docker 只是将容器绑定到 Docker 主机,这意味着容器提供的任何服务都会自动映射到 Docker 主机的接口上。
如果我们尝试在端口80上运行另一个提供服务的容器,我们会发现 Docker 并不会阻止我们:
user@docker1:~$ docker run --name web2 -d **--net=host \
jonlangemak/web_server_2
c1c00aa387111e1bb09e3daacc2a2820c92f6a91ce73694c1e88691c3955d815
user@docker1:~$
虽然从 Docker 的角度来看,这看起来像是一个成功的容器启动,但实际上容器在被生成后立即死掉了。如果我们检查容器web2的日志,我们会发现它遇到了冲突,无法启动:
user@docker1:~$ docker logs **web2
apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName
(98)**Address already in use: make_sock: could not bind to address 0.0.0.0:80
no listening sockets available, shutting down
Unable to open logs
user@docker1:~$
在主机模式下部署容器会限制你可以运行的服务数量,除非你的容器被构建为在不同端口上提供相同的服务。
由于服务的配置和它所使用的端口是容器的责任,我们可以通过一种方式部署多个使用相同服务端口的容器。举个例子,我们之前提到的两个 Docker 主机,每个主机有两个网络接口:

在一个场景中,你的 Docker 主机有多个网络接口,你可以让容器绑定到不同接口上的相同端口。同样,由于这是容器的责任,只要你不尝试将相同的端口绑定到多个接口上,Docker 就不会知道你是如何实现这一点的。
解决方案是更改服务绑定到接口的方式。大多数服务在启动时绑定到所有接口(0.0.0.0)。例如,我们可以看到我们的容器web1绑定到 Docker 主机上的0.0.0.0:80:
user@docker1:~$ sudo netstat -plnt
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 3724/apache2
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1056/sshd
tcp6 0 0 :::22 :::* LISTEN 1056/sshd
user@docker1:~$
我们可以限制服务的范围,而不是让服务绑定到所有接口。如果我们可以将容器服务绑定到一个接口,我们就可以将相同的端口绑定到不同的接口而不会引起冲突。在这个例子中,我创建了两个容器镜像,允许您向它们传递一个环境变量($APACHE_IPADDRESS)。该变量在 Apache 配置中被引用,并指定服务应该绑定到哪个接口。我们可以通过在主机模式下部署两个容器来测试这一点:
user@docker1:~$ docker run --name web6 -d --net=host \
-e APACHE_IPADDRESS=10.10.10.101** jonlangemak/web_server_6_pickip
user@docker1:~$ docker run --name web7 -d --net=host \
-e APACHE_IPADDRESS=192.168.10.101** jonlangemak/web_server_7_pickip
请注意,在每种情况下,我都会向容器传递一个不同的 IP 地址,以便它绑定到。快速查看主机上的端口绑定应该可以确认容器不再绑定到所有接口:
user@docker1:~$ sudo netstat -plnt
[sudo] password for user:
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 192.168.10.101:80 0.0.0.0:* LISTEN 1518/apache2
tcp 0 0 10.10.10.101:80 0.0.0.0:* LISTEN 1482/apache2
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1096/sshd
tcp6 0 0 :::22 :::* LISTEN 1096/sshd
user@docker1:~$
请注意,Apache 不再绑定到所有接口,我们有两个 Apache 进程,一个绑定到 Docker 主机的每个接口。来自另一个 Docker 主机的测试将证明每个容器在其各自的接口上提供 Apache:
user@docker2:~$ curl **http://10.10.10.101
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #6 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker2:~$
user@docker2:~$ curl **http://192.168.10.101
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #7 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker2:~$
虽然主机模式有一些限制,但它也更简单,可能因为缺乏 NAT 和使用docker0桥而提供更高的性能。
注意
请记住,由于 Docker 不涉及主机模式,如果您有一个基于主机的防火墙来执行策略,您可能需要手动打开防火墙端口,以便容器可以被访问。
配置服务级设置
虽然许多设置可以在容器运行时配置,但有一些设置必须作为启动 Docker 服务的一部分进行配置。也就是说,它们需要在服务配置中定义为 Docker 选项。在之前的示例中,我们接触到了一些这些服务级选项,比如--ip-forward、--userland-proxy和--ip。在这个示例中,我们将介绍如何将服务级参数传递给 Docker 服务,以及讨论一些关键参数的功能。
准备工作
您需要访问 Docker 主机,并了解您的 Docker 主机如何连接到网络。在本教程中,我们将使用之前教程中使用的docker1和docker2主机。您需要确保可以访问iptables规则以验证 netfilter 策略。如果您希望下载和运行示例容器,您的 Docker 主机还需要访问互联网。
操作步骤…
为了传递运行时选项或参数给 Docker,我们需要修改服务配置。在我们的情况下,我们使用的是 Ubuntu 16.04 版本,它使用systemd来管理在 Linux 主机上运行的服务。向 Docker 传递参数的推荐方法是使用systemd的附加文件。要创建附加文件,我们可以按照以下步骤创建一个服务目录和一个 Docker 配置文件:
sudo mkdir /etc/systemd/system/docker.service.d
sudo vi /etc/systemd/system/docker.service.d/docker.conf
将以下行插入docker.conf配置文件中:
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
如果您希望向 Docker 服务传递任何参数,可以通过将它们附加到第三行来实现。例如,如果我想在服务启动时禁用 Docker 自动启用主机上的 IP 转发,我的文件将如下所示:
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --ip-forward=false
在对系统相关文件进行更改后,您需要要求systemd重新加载配置。使用以下命令完成:
sudo systemctl daemon-reload
最后,您可以重新启动服务以使设置生效:
systemctl restart docker
每次更改配置后,您需要重新加载systemd配置以及重新启动服务。
docker0 桥接地址
正如我们之前所看到的,docker0桥的 IP 地址默认为172.17.0.1/16。但是,如果您希望,可以使用--bip配置标志更改此 IP 地址。例如,您可能希望将docker0桥的子网更改为192.168.127.1/24。这可以通过将以下选项传递给 Docker 服务来完成:
ExecStart=/usr/bin/dockerd **--bip=192.168.127.1/24
更改此设置时,请确保配置 IP 地址(192.168.127.1/24)而不是您希望定义的子网(192.168.127.0/24)。以前的 Docker 版本需要重新启动主机或手动删除现有的桥接才能分配新的桥接 IP。在较新的版本中,您只需重新加载systemd配置并重新启动服务,新的桥接 IP 就会被分配:
user@docker1:~$ **sudo systemctl daemon-reload
user@docker1:~$ **sudo systemctl restart docker
user@docker1:~$
user@docker1:~$ **ip addr show docker0
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:a6:d1:b3:37 brd ff:ff:ff:ff:ff:ff
inet **192.168.127.1/24** scope global docker0
valid_lft forever preferred_lft forever
user@docker1:~$
除了更改docker0桥的 IP 地址,您还可以定义 Docker 可以分配给容器的 IP 地址。这是通过使用--fixed-cidr配置标志来完成的。例如,假设以下配置:
ExecStart=/usr/bin/dockerd --bip=192.168.127.1/24
--fixed-cidr=192.168.127.128/25
在这种情况下,docker0桥接口本身位于192.168.127.0/24子网中,但我们告诉 Docker 只从子网192.168.127.128/25中分配容器 IP 地址。如果我们添加这个配置,再次重新加载systemd并重新启动服务,我们可以看到 Docker 将为第一个容器分配 IP 地址192.168.127.128:
user@docker1:~$ docker run --name web1 -it \
jonlangemak/web_server_1 /bin/bash
root@ff8872212cb4:/# **ip addr show eth0
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:c0:a8:7f:80 brd ff:ff:ff:ff:ff:ff
inet 192.168.127.128/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:c0ff:fea8:7f80/64 scope link
valid_lft forever preferred_lft forever
root@ff8872212cb4:/#
由于容器使用定义的docker0桥接 IP 地址作为它们的默认网关,固定的 CIDR 范围必须是docker0桥本身上定义的子网的较小子网。
发布端口的 Docker 接口绑定
在某些情况下,您可能有一个 Docker 主机,它有多个位于不同网络段的网络接口。例如,考虑这样一个例子,您有两个主机,它们都有两个网络接口:

考虑这样一种情况,我们在主机docker1上启动一个提供 Web 服务的容器,使用以下语法:
docker run -d --name web1 -P jonlangemak/web_server_1
如您所见,我们传递了-P标志,告诉 Docker 将图像中存在的任何暴露端口发布到 Docker 主机上的随机端口。如果我们检查端口映射,我们注意到虽然有动态端口分配,但没有主机 IP 地址分配:
user@docker1:~$ docker run -d --name web1 -P jonlangemak/web_server_1
d96b4dd005edb2218257a7701b674f51f4318b92baf4be686400d77912c75e58
user@docker1:~$ docker port web1
80/tcp -> **0.0.0.0:32768
user@docker1:~$
Docker 不是指定特定的 IP 地址,而是用0.0.0.0指定所有接口。这意味着容器中的服务可以在 Docker 主机的任何 IP 接口上的端口32768上访问。我们可以通过从docker2主机进行测试来证明这一点:
user@docker2:~$ curl http://**10.10.10.101:32768
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker2:~$ curl http://**192.168.10.101:32768
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker2:~$
如果我们希望限制 Docker 默认发布端口的接口,我们可以将--ip选项传递给 Docker 服务。继续上面的例子,我的选项现在可能是这样的:
ExecStart=/usr/bin/dockerd --bip=192.168.127.1/24
--fixed-cidr=192.168.127.128/25 **--ip=192.168.10.101
将这些选项传递给 Docker 服务,并重新运行我们的容器,将导致端口只映射到定义的 IP 地址:
user@docker1:~$ docker port web1
80/tcp -> **192.168.10.101:32768
user@docker1:~$
如果我们从docker2主机再次运行我们的测试,我们应该看到服务只暴露在192.168.10.101接口上,而不是10.10.10.101接口上:
user@docker2:~$ curl http://**10.10.10.101:32768
curl: (7) Failed to connect to 10.10.10.101 port 32768: **Connection refused
user@docker2:~$
user@docker2:~$ curl http://**192.168.10.101:32768
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker2:~$
请记住,此设置仅适用于已发布的端口。 这不会影响容器可能用于出站连接的接口。 这由主机的路由表决定。
容器接口 MTU
在某些情况下,可能需要更改容器的网络接口的 MTU。 这可以通过向 Docker 服务传递--mtu选项来完成。 例如,我们可能希望将容器的接口 MTU 降低到1450以适应某种封装。 要做到这一点,您可以传递以下标志:
ExecStart=/usr/bin/dockerd **--mtu=1450
添加此选项后,您可能会检查docker0桥 MTU 并发现它保持不变,如下面的代码所示:
user@docker1:~$ **ip addr show docker0
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> **mtu 1500** qdisc noqueue state DOWN group default
link/ether 02:42:a6:d1:b3:37 brd ff:ff:ff:ff:ff:ff
inet 192.168.127.1/24 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:a6ff:fed1:b337/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
这实际上是预期行为。 Linux 桥默认情况下会自动使用与其关联的任何从属接口中的最低 MTU。 当我们告诉 Docker 使用 MTU 为1450时,我们实际上是在告诉它以 MTU 为1450启动任何容器。 由于此时没有运行任何容器,桥的 MTU 保持不变。 让我们启动一个容器来验证这一点:
user@docker1:~$ docker run --name web1 -d jonlangemak/web_server_1
18f4c038eadba924a23bd0d2841ac52d90b5df6dd2d07e0433eb5315124ce427
user@docker1:~$
user@docker1:~$ **docker exec web1 ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
10: **eth0**@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> **mtu 1450** qdisc noqueue state UP
link/ether 02:42:c0:a8:7f:02 brd ff:ff:ff:ff:ff:ff
inet 192.168.127.2/24 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:c0ff:fea8:7f02/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
我们可以看到容器的 MTU 正确为1450。 检查 Docker 主机,我们应该看到桥的 MTU 现在也较低:
user@docker1:~$ **ip addr show docker0
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> **mtu 1450** qdisc noqueue state UP group default
link/ether 02:42:a6:d1:b3:37 brd ff:ff:ff:ff:ff:ff
inet 192.168.127.1/24 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:a6ff:fed1:b337/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
以较低的 MTU 启动容器自动影响了桥的 MTU,正如我们所预期的那样。
容器默认网关
默认情况下,Docker 将任何容器的默认网关设置为docker0桥的 IP 地址。 这是有道理的,因为容器需要通过docker0桥进行路由才能到达外部网络。 但是,可以覆盖此设置,并让 Docker 将默认网关设置为docker0桥网络上的另一个 IP 地址。
例如,我们可以通过传递这些配置选项给 Docker 服务来将默认网关更改为192.168.127.50。
ExecStart=/usr/bin/dockerd --bip=192.168.127.1/24 --fixed-cidr=192.168.127.128/25 **--default-gateway=192.168.127.50
如果我们添加这些设置,重新启动服务并生成一个容器,我们可以看到新容器的默认网关已配置为192.168.127.50:
user@docker1:~$ docker run --name web1 -it \
jonlangemak/web_server_1 /bin/bash
root@b36baa4d0950:/# ip addr show eth0
12: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:c0:a8:7f:80 brd ff:ff:ff:ff:ff:ff
inet 192.168.127.128/24 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:c0ff:fea8:7f80/64 scope link
valid_lft forever preferred_lft forever
root@b36baa4d0950:/#
root@b36baa4d0950:/# **ip route show
default via 192.168.127.50 dev eth0
192.168.127.0/24 dev eth0 proto kernel scope link src 192.168.127.128
root@b36baa4d0950:/#
请记住,此时此容器在其当前子网之外没有连接性,因为该网关目前不存在。 为了使容器在其本地子网之外具有连接性,需要从容器中访问192.168.127.50并具有连接到外部网络的能力。
注意
服务级别还有其他配置选项,例如--iptables和--icc。 这些将在后面的章节中讨论它们的相关用例。
第三章:用户定义的网络
在这一章中,我们将涵盖以下配方:
-
查看 Docker 网络配置
-
创建用户定义的网络
-
将容器连接到网络
-
定义用户定义的桥接网络
-
创建用户定义的覆盖网络
-
隔离网络
介绍
早期版本的 Docker 依赖于一个基本静态的网络模型,这对大多数容器网络需求来说工作得相当好。然而,如果您想做一些不同的事情,您就没有太多选择了。例如,您可以告诉 Docker 将容器部署到不同的桥接,但 Docker 和该网络之间没有一个强大的集成点。随着 Docker 1.9 中用户定义的网络的引入,游戏已经改变了。您现在可以直接通过 Docker 引擎创建和管理桥接和多主机网络。此外,第三方网络插件也可以通过 libnetwork 及其容器网络模型(CNM)模型与 Docker 集成。
注意
CNM 是 Docker 用于定义容器网络模型的模型。在第七章中,使用 Weave Net,我们将研究一个第三方插件(Weave),它可以作为 Docker 驱动程序集成。本章的重点将放在 Docker 引擎中默认包含的网络驱动程序上。
转向基于驱动程序的模型象征着 Docker 网络的巨大变化。除了定义新网络,您现在还可以动态连接和断开容器接口。这种固有的灵活性为连接容器打开了许多新的可能性。
查看 Docker 网络配置
如前所述,现在可以通过添加network子命令直接通过 Docker 定义和管理网络。network命令为您提供了构建网络并将容器连接到网络所需的所有选项:
user@docker1:~$ docker network --help
docker network --help
Usage: docker network COMMAND
Manage Docker networks
Options:
--help Print usage
Commands:
connect Connect a container to a network
create Create a network
disconnect Disconnect a container from a network
inspect Display detailed information on one or more networks
ls List networks
rm Remove one or more networks
Run 'docker network COMMAND --help' for more information on a command.
user@docker1:~$
在这个配方中,我们将学习如何查看定义的 Docker 网络以及检查它们的具体细节。
做好准备
docker network子命令是在 Docker 1.9 中引入的,因此您需要运行至少该版本的 Docker 主机。在我们的示例中,我们将使用 Docker 版本 1.12。您还需要对当前网络布局有很好的了解,这样您就可以跟着我们检查当前的配置。假设每个 Docker 主机都处于其本机配置中。
如何做…
我们要做的第一件事是弄清楚 Docker 认为已经定义了哪些网络。这可以使用network ls子命令来完成:
user@docker1:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
200d5292d5db **bridge** **bridge** **local
12e399864b79 **host** **host** **local
cb6922b8b84f **none** **null** **local
user@docker1:~$
正如我们所看到的,Docker 显示我们已经定义了三种不同的网络。要查看有关网络的更多信息,我们可以使用network inspect子命令检索有关网络定义及其当前状态的详细信息。让我们仔细查看每个定义的网络。
Bridge
桥接网络代表 Docker 引擎默认创建的docker0桥:
user@docker1:~$ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "62fcda0787f2be01e65992e2a5a636f095970ea83c59fdf0980da3f3f555c24e",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16"
}
]
},
"Internal": false,
"Containers": {},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
user@docker1:~$
inspect命令的输出向我们展示了关于定义网络的大量信息:
-
Driver:在这种情况下,我们可以看到网络桥实现了Driver桥。尽管这似乎是显而易见的,但重要的是要指出,所有网络功能,包括本机功能,都是通过驱动程序实现的。 -
子网:在这种情况下,子网是我们从docker0桥预期的默认值,172.17.0.1/16。 -
bridge.default_bridge:值为true意味着 Docker 将为所有容器提供此桥,除非另有规定。也就是说,如果您启动一个没有指定网络(--net)的容器,该容器将最终出现在此桥上。 -
bridge.host_binding_ipv4:默认情况下,这将设置为0.0.0.0或所有接口。正如我们在第二章中所看到的,配置和监控 Docker 网络,我们可以通过将--ip标志作为 Docker 选项传递给服务,告诉 Docker 在服务级别限制这一点。 -
bridge.name:正如我们所怀疑的,这个网络代表docker0桥。 -
driver.mtu:默认情况下,这将设置为1500。正如我们在第二章中所看到的,配置和监控 Docker 网络,我们可以通过将--mtu标志作为 Docker 选项传递给服务,告诉 Docker 在服务级别更改MTU(最大传输单元)。
无
none网络表示的就是它所说的,什么也没有。当您希望定义一个绝对没有网络定义的容器时,可以使用none模式。检查网络后,我们可以看到就网络定义而言,没有太多内容:
user@docker1:~$ docker network inspect none
[
{
"Name": "none",
"Id": "a191c26b7dad643ca77fe6548c2480b1644a86dcc95cde0c09c6033d4eaff7f2",
"Scope": "local",
"Driver": "null",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": []
},
"Internal": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
user@docker1:~$
如您所见,Driver由null表示,这意味着这根本不是这个网络的Driver。none网络模式有一些用例,我们将在稍后讨论连接和断开连接到定义网络的容器时进行介绍。
主机
host网络代表我们在第二章中看到的主机模式,配置和监视 Docker 网络,在那里容器直接绑定到 Docker 主机自己的网络接口。通过仔细观察,我们可以看到,与none网络一样,对于这个网络并没有太多定义。
user@docker1:~$ docker network inspect host
[
{
"Name": "host",
"Id": "4b94353d158cef25b9c9244ca9b03b148406a608b4fd85f3421c93af3be6fe4b",
"Scope": "local",
"Driver": "host",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": []
},
"Internal": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
user@docker1:~$
尽管主机网络肯定比none模式做得更多,但从检查其定义来看,似乎并非如此。这里的关键区别在于这个网络使用主机Driver。由于这种网络类型使用现有主机的网络接口,我们不需要将其作为网络的一部分进行定义。
使用network ls命令时,可以传递附加参数以进一步过滤或更改输出:
-
--quiet(-q):仅显示数字网络 ID -
--no-trunc:这可以防止命令自动截断输出中的网络 ID,从而使您可以看到完整的网络 ID -
--filter(-f):根据网络 ID、网络名称或网络定义(内置或用户定义)对输出进行过滤
例如,我们可以使用以下过滤器显示所有用户定义的网络:
user@docker1:~$ docker network ls -f type=custom
NETWORK ID NAME DRIVER SCOPE
a09b7617c550 mynetwork bridge local
user@docker1:~$
或者我们可以显示所有包含158的网络 ID 的网络:
user@docker1:~$ docker network ls -f id=158
NETWORK ID NAME DRIVER SCOPE
4b94353d158c host host local
user@docker1:~$
创建用户定义的网络
到目前为止,我们已经看到,每个 Docker 安装都有至少两种不同的网络驱动程序,即桥接和主机。除了这两种之外,由于先决条件而没有最初定义,还有另一个Driver叠加,也可以立即使用。本章的后续内容将涵盖有关桥接和叠加驱动程序的具体信息。
因为使用主机Driver创建另一个主机网络的迭代没有意义,所以内置的用户定义网络仅限于桥接和叠加驱动程序。在本教程中,我们将向您展示创建用户定义网络的基础知识,以及与network create和network rm Docker 子命令相关的选项。
准备工作
docker network子命令是在 Docker 1.9 中引入的,因此您需要运行至少该版本的 Docker 主机。在我们的示例中,我们将使用 Docker 版本 1.12。您还需要对当前网络布局有很好的了解,以便在我们检查当前配置时能够跟随。假定每个 Docker 主机都处于其本机配置中。
注意
警告:在 Linux 主机上创建网络接口必须谨慎进行。Docker 将尽力防止您自找麻烦,但在定义 Docker 主机上的新网络之前,您必须对网络拓扑有一个很好的了解。要避免的一个常见错误是定义与网络中其他子网重叠的新网络。在远程管理的情况下,这可能会导致主机和容器之间的连接问题。
如何做到这一点…
网络是通过使用network create子命令来定义的,该命令具有以下选项:
user@docker1:~$ docker network create --help
Usage: docker network create [OPTIONS] NETWORK
Create a network
Options:
--aux-address value** Auxiliary IPv4 or IPv6 addresses used by Network driver (default map[])
-d, --driver string** Driver to manage the Network (default "bridge")
--gateway value** IPv4 or IPv6 Gateway for the master subnet (default [])
--help Print usage
--internal** Restrict external access to the network
--ip-range value** Allocate container ip from a sub-range (default [])
--ipam-driver string** IP Address Management Driver (default "default")
--ipam-opt value** Set IPAM driver specific options (default map[])
--ipv6** Enable IPv6 networking
--label value** Set metadata on a network (default [])
-o, --opt value** Set driver specific options (default map[])
--subnet value** Subnet in CIDR format that represents a network segment (default [])
user@docker1:~$
让我们花点时间讨论每个选项的含义:
-
aux-address:这允许您定义 Docker 在生成容器时不应分配的 IP 地址。这相当于 DHCP 范围中的 IP 保留。 -
Driver:网络实现的Driver。内置选项包括 bridge 和 overlay,但您也可以使用第三方驱动程序。 -
gateway:网络的网关。如果未指定,Docker 将假定它是子网中的第一个可用 IP 地址。 -
internal:此选项允许您隔离网络,并将在本章后面更详细地介绍。 -
ip-range:这允许您指定用于容器寻址的已定义网络子网的较小子网。 -
ipam-driver:除了使用第三方网络驱动程序外,您还可以利用第三方 IPAM 驱动程序。对于本书的目的,我们将主要关注默认或内置的 IPAMDriver。 -
ipv6:这在网络上启用 IPv6 网络。 -
label:这允许您指定有关网络的其他信息,这些信息将被存储为元数据。 -
ipam-opt:这允许您指定要传递给 IPAMDriver的选项。 -
opt:这允许您指定可以传递给网络Driver的选项。将在相关的配方中讨论每个内置Driver的特定选项。 -
subnet:这定义了与您正在创建的网络类型相关联的子网。
您可能会注意到这里一些重叠,即 Docker 网络的服务级别可以定义的一些设置与前面列出的用户定义选项之间。检查这些选项时,您可能会想要比较以下配置标志:

尽管这些设置在很大程度上是等效的,但它们并不完全相同。唯一完全相同的是--fixed-cidr和ip-range。这两个选项都定义了一个较大主网络的较小子网络,用于容器 IP 寻址。另外两个选项是相似的,但并不相同。
在服务选项的情况下,--bip适用于docker0桥接口,--default-gateway适用于容器本身。在用户定义方面,--subnet和--gateway选项直接适用于正在定义的网络构造(在此比较中是一个桥接)。请记住,--bip选项期望接收一个网络中的 IP 地址,而不是网络本身。以这种方式定义桥接 IP 地址既覆盖了子网,也覆盖了网关,这在定义用户定义网络时是分开定义的。也就是说,服务定义在这方面更加灵活,因为它允许您定义桥接的接口以及分配给容器的网关。
保持合理的默认设置主题,实际上并不需要这些选项来创建用户定义网络。您可以通过只给它一个名称来创建您的第一个用户定义网络:
user@docker1:~$ docker network create mynetwork
3fea20c313e8880538ab50fd591398bdfdac2378abac29aacb1be131cbfab40f
user@docker1:~$
经过检查,我们可以看到 Docker 使用的默认设置:
user@docker1:~$ docker network inspect mynetwork
[
{
"Name": "mynetwork",
"Id": "a09b7617c5504d4afd80c26b82587000c64046f1483de604c51fa4ba53463b50",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1/16"
}
]
},
"Internal": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
user@docker1:~$
Docker 假设如果您没有指定Driver,那么您想要使用桥接Driver创建网络。如果您在创建网络时没有定义子网,它还会自动选择并分配一个子网给这个桥接。
注意
在创建网络时,建议您为网络指定子网。正如我们将在后面看到的,不是所有的网络拓扑都依赖于将容器网络隐藏在主机接口后面。在这些情况下,定义一个可路由的不重叠子网将是必要的。
它还会自动选择子网的第一个可用 IP 地址作为网关。因为我们没有为Driver定义任何选项,所以网络没有,但在这种情况下会使用默认值。这些将在与每个特定Driver相关的配方中讨论。
空的网络,即没有活动端点的网络,可以使用 network rm 命令删除:
user@docker1:~$ docker network rm mynetwork
user@docker1:~$
这里值得注意的另一项是,Docker 使用户定义的网络持久化。在大多数情况下,手动定义的任何 Linux 网络结构在系统重新启动时都会丢失。Docker 记录网络配置并在 Docker 服务重新启动时负责回放。这对于通过 Docker 而不是自己构建网络来说是一个巨大的优势。
连接容器到网络
虽然拥有创建自己网络的能力是一个巨大的进步,但如果没有一种方法将容器连接到网络,这就毫无意义。在以前的 Docker 版本中,这通常是在容器运行时通过传递 --net 标志来完成的,指定容器应该使用哪个网络。虽然这仍然是这种情况,但 docker network 子命令也允许您将正在运行的容器连接到现有网络或从现有网络断开连接。
准备工作
docker network 子命令是在 Docker 1.9 中引入的,因此您需要运行至少该版本的 Docker 主机。在我们的示例中,我们将使用 Docker 版本 1.12。您还需要对当前网络布局有很好的了解,这样您就可以在我们检查当前配置时跟上。假设每个 Docker 主机都处于其本机配置中。
如何做…
通过 network connect 和 network disconnect 子命令来连接和断开连接容器:
user@docker1:~$ docker network connect --help
Usage: docker network connect [OPTIONS] NETWORK CONTAINER
Connects a container to a network
--alias=[] Add network-scoped alias for the container
--help Print usage
--ip IP Address
--ip6 IPv6 Address
--link=[] Add link to another container
user@docker1:~$
让我们回顾一下连接容器到网络的选项:
-
别名:这允许您在连接容器的网络中为容器名称解析定义别名。我们将在第五章中更多地讨论这一点,容器链接和 Docker DNS,在那里我们将讨论 DNS 和链接。
-
IP:这定义了要用于容器的 IP 地址。只要 IP 地址当前未被使用,它就可以工作。一旦分配,只要容器正在运行或暂停,它就会保留。停止容器将删除保留。
-
IP6:这定义了要用于容器的 IPv6 地址。适用于 IPv4 地址的相同分配和保留要求也适用于 IPv6 地址。
-
Link:这允许您指定与另一个容器的链接。我们将在第五章中更多地讨论这个问题,容器链接和 Docker DNS,在那里我们将讨论 DNS 和链接。
一旦发送了network connect请求,Docker 会处理所有所需的配置,以便容器开始使用新的接口。让我们来看一个快速的例子:
user@docker1:~$ **docker run --name web1 -d jonlangemak/web_server_1
e112a2ab8197ec70c5ee49161613f2244f4353359b27643f28a18be47698bf59
user@docker1:~$
user@docker1:~$ **docker exec web1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
8: **eth0**@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
在上面的输出中,我们启动了一个简单的容器,没有指定任何与网络相关的配置。结果是容器被映射到了docker0桥接。现在让我们尝试将这个容器连接到我们在上一个示例中创建的网络mynetwork:
user@docker1:~$ **docker network connect mynetwork web1
user@docker1:~$
user@docker1:~$ docker exec web1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
8: **eth0**@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
10: **eth1**@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe12:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
如您所见,容器现在在mynetwork网络上有一个 IP 接口。如果我们现在再次检查网络,我们应该看到一个容器关联:
user@docker1:~$ docker network inspect mynetwork
[
{
"Name": "mynetwork",
"Id": "a09b7617c5504d4afd80c26b82587000c64046f1483de604c51fa4ba53463b50",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1/16"
}
]
},
"Internal": false,
"Containers": { **"e112a2ab8197ec70c5ee49161613f2244f4353359b27643f28a18be47698bf59": {
"Name": "web1",
"EndpointID": "678b07162dc958599bf7d463da81a4c031229028ebcecb1af37ee7d448b54e3d",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
user@docker1:~$
网络也可以很容易地断开连接。例如,我们现在可以通过将容器从桥接网络中移除来从docker0桥接中移除容器:
user@docker1:~$ **docker network disconnect bridge web1
user@docker1:~$
user@docker1:~$ docker exec web1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
10: **eth1**@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe12:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
有趣的是,Docker 还负责确保在连接和断开容器与网络时容器的连通性。例如,在将容器从桥接网络断开连接之前,容器的默认网关仍然在docker0桥接之外:
user@docker1:~$ docker exec web1 ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth2 proto kernel scope link src 172.17.0.2
172.18.0.0/16 dev eth1 proto kernel scope link src 172.18.0.2
user@docker1:~$
这是有道理的,因为我们不希望在将容器连接到新网络时中断容器的连接。然而,一旦我们通过断开与桥接网络的接口来移除托管默认网关的网络,我们会发现 Docker 会将默认网关更新为mynetwork桥接的剩余接口:
user@docker1:~$ docker exec web1 ip route
default via 172.18.0.1 dev eth1
172.18.0.0/16 dev eth1 proto kernel scope link src 172.18.0.2
user@docker1:~$
这确保了无论连接到哪个网络,容器都具有连通性。
最后,我想指出连接和断开容器与网络时none网络类型的一个有趣方面。正如我之前提到的,none网络类型告诉 Docker 不要将容器分配给任何网络。然而,这并不仅仅意味着最初,它是一个配置状态,告诉 Docker 容器不应该与任何网络关联。例如,假设我们使用none网络启动以下容器:
user@docker1:~$ docker run --net=none --name web1 -d jonlangemak/web_server_1
9f5d73c55ee859335cd2449b058b68354f5b71cf37e57b72f5c984afcafb4b21
user@docker1:~$ docker exec web1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
user@docker1:~$
如您所见,除了回环接口之外,容器没有任何网络接口。现在,让我们尝试将这个容器连接到一个新的网络:
user@docker1:~$ docker network connect mynetwork web1
Error response from daemon: Container cannot be connected to multiple networks with one of the networks in private (none) mode
user@docker1:~$
Docker 告诉我们,这个容器被定义为没有网络,并且阻止我们将容器连接到任何网络。如果我们检查none网络,我们可以看到这个容器实际上附加到它上面:
user@docker1:~$ docker network inspect none
[
{
"Name": "none",
"Id": "a191c26b7dad643ca77fe6548c2480b1644a86dcc95cde0c09c6033d4eaff7f2",
"Scope": "local",
"Driver": "null",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": []
},
"Internal": false,
"Containers": { **"931a0d7ad9244c135a19de6e23de314698112ccd00bc3856f4fab9b8cb241e60": {
"Name": "web1",
"EndpointID": "6a046449576e0e0a1e8fd828daa7028bacba8de335954bff2c6b21e01c78baf8",
"MacAddress": "",
"IPv4Address": "",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
user@docker1:~$
为了将这个容器连接到一个新的网络,我们首先必须将其与none网络断开连接:
user@docker1:~$ **docker network disconnect none web1
user@docker1:~$ **docker network connect mynetwork web1
user@docker1:~$ docker exec web1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
18: **eth0**@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe12:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
一旦您将其与none网络断开连接,您就可以自由地将其连接到任何其他定义的网络。
定义用户定义的桥接网络
通过使用桥接Driver,用户可以提供自定义桥接以连接到容器。您可以创建尽可能多的桥接,唯一的限制是您必须在每个桥接上使用唯一的 IP 地址。也就是说,您不能与已在其他网络接口上定义的现有子网重叠。
在这个教程中,我们将学习如何定义用户定义的桥接,以及在创建过程中可用的一些独特选项。
准备工作
docker network子命令是在 Docker 1.9 中引入的,因此您需要运行至少该版本的 Docker 主机。在我们的示例中,我们将使用 Docker 版本 1.12。您还需要对当前网络布局有很好的了解,这样您就可以跟着我们检查当前的配置。假设每个 Docker 主机都处于其本机配置中。
如何做…
在上一个教程中,我们讨论了定义用户定义网络的过程。虽然那里讨论的选项适用于所有网络类型,但我们可以通过传递--opt标志将其他选项传递给我们的网络实现的Driver。让我们快速回顾一下与桥接Driver可用的选项:
-
com.docker.network.bridge.name:这是您希望给桥接的名称。 -
com.docker.network.bridge.enable_ip_masquerade:这指示 Docker 主机在容器尝试路由离开本地主机时,隐藏或伪装该网络中所有容器在 Docker 主机接口后面。 -
com.docker.network.bridge.enable_icc:这为桥接打开或关闭容器间连接(ICC)模式。这个功能在第六章 保护容器网络中有更详细的介绍。 -
com.docker.network.bridge.host_binding_ipv4:这定义了应该用于端口绑定的主机接口。 -
com.docker.network.driver.mtu:这为连接到这个桥接的容器设置 MTU。
这些选项可以直接与我们在 Docker 服务下定义的选项进行比较,以更改默认的docker0桥。
如何做到这一点...
上表比较了影响docker0桥的服务级设置与您在定义用户定义的桥接网络时可用的设置。它还列出了在任一情况下如果未指定设置,则使用的默认设置。
在定义容器网络时,通过驱动程序特定选项和network create子命令的通用选项,我们在定义容器网络时具有相当大的灵活性。让我们通过一些快速示例来构建用户定义的桥接。
示例 1
docker network create --driver bridge \
--subnet=10.15.20.0/24 \
--gateway=10.15.20.1 \
--aux-address 1=10.15.20.2 --aux-address 2=10.15.20.3 \
--opt com.docker.network.bridge.host_binding_ipv4=10.10.10.101 \
--opt com.docker.network.bridge.name=linuxbridge1 \
testbridge1
前面的network create语句定义了具有以下特征的网络:
-
一个类型为
bridge的用户定义网络 -
一个
子网为10.15.20.0/24 -
一个
网关或桥接 IP 接口为10.15.20.1 -
两个保留地址:
10.15.20.2和10.15.20.3 -
主机上的端口绑定接口为
10.10.10.101 -
一个名为
linuxbridge1的 Linux 接口名称 -
一个名为
testbridge1的 Docker 网络
请记住,其中一些选项仅用于示例目的。实际上,在前面的示例中,我们不需要为网络驱动程序定义“网关”,因为默认设置将覆盖我们。
如果我们在检查后创建了前面提到的网络,我们应该看到我们定义的属性:
user@docker1:~$ docker network inspect testbridge1
[
{
"Name": "testbridge1",
"Id": "97e38457e68b9311113bc327e042445d49ff26f85ac7854106172c8884d08a9f",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "10.15.20.0/24",
"Gateway": "10.15.20.1",
"AuxiliaryAddresses": {
"1": "10.15.20.2",
"2": "10.15.20.3"
}
}
]
},
"Internal": false,
"Containers": {},
"Options": {
"com.docker.network.bridge.host_binding_ipv4": "10.10.10.101",
"com.docker.network.bridge.name": "linuxbridge1"
},
"Labels": {}
}
]
user@docker1:~$
注意
您传递给网络的选项不会得到验证。也就是说,如果您将host_binding拼错为host_bniding,Docker 仍然会让您创建网络,并且该选项将被定义;但是,它不会起作用。
示例 2
docker network create \
--subnet=192.168.50.0/24 \
--ip-range=192.168.50.128/25 \
--opt com.docker.network.bridge.enable_ip_masquearde=false \
testbridge2
前面的network create语句定义了具有以下特征的网络:
-
一个类型为
bridge的用户定义网络 -
一个
子网为192.168.50.0/24 -
一个
网关或桥接 IP 接口为192.168.50.1 -
一个容器网络范围为
192.168.50.128/25 -
主机上的 IP 伪装关闭
-
一个名为
testbridge2的 Docker 网络
如示例 1 所述,如果我们创建桥接网络,则无需定义驱动程序类型。此外,如果我们可以接受网关是容器定义子网中的第一个可用 IP,我们也可以将其从定义中排除。创建后检查此网络应该显示类似于这样的结果:
user@docker1:~$ docker network inspect testbridge2
[
{
"Name": "testbridge2",
"Id": "2c8270425b14dab74300d8769f84813363a9ff15e6ed700fa55d7d2c3b3c1504",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "192.168.50.0/24",
"IPRange": "192.168.50.128/25"
}
]
},
"Internal": false,
"Containers": {},
"Options": {
"com.docker.network.bridge.enable_ip_masquearde": "false"
},
"Labels": {}
}
]
user@docker1:~$
创建用户定义的覆盖网络
虽然创建自己的桥接能力确实很吸引人,但你的范围仍然局限于单个 Docker 主机。覆盖网络Driver旨在通过允许您使用覆盖网络在多个 Docker 主机上扩展一个或多个子网来解决这个问题。覆盖网络是在现有网络之上构建隔离网络的一种手段。在这种情况下,现有网络为覆盖提供传输,并且通常被称为底层网络。覆盖Driver实现了 Docker 所谓的多主机网络。
在这个示例中,我们将学习如何配置覆盖Driver的先决条件,以及部署和验证基于覆盖的网络。
准备就绪
在接下来的示例中,我们将使用这个实验室拓扑:

拓扑结构由总共四个 Docker 主机组成,其中两个位于10.10.10.0/24子网,另外两个位于192.168.50.0/24子网。当我们按照这个示例进行操作时,图中显示的主机将扮演以下角色:
-
docker1:作为 Consul键值存储提供服务的 Docker 主机 -
docker2:参与覆盖网络的 Docker 主机 -
docker3:参与覆盖网络的 Docker 主机 -
docker4:参与覆盖网络的 Docker 主机
如前所述,覆盖Driver不是默认实例化的。这是因为覆盖Driver需要满足一些先决条件才能工作。
一个键值存储
由于我们现在处理的是一个分布式系统,Docker 需要一个地方来存储关于覆盖网络的信息。为此,Docker 使用一个键值存储,并支持 Consul、etcd 和 ZooKeeper。它将存储需要在所有节点之间保持一致性的信息,如 IP 地址分配、网络 ID 和容器端点。在我们的示例中,我们将部署 Consul。
侥幸的是,Consul 本身可以作为一个 Docker 容器部署:
user@docker1:~$ docker run -d -p 8500:8500 -h consul \
--name consul progrium/consul -server -bootstrap
运行这个镜像将启动一个 Consul 键值存储的单个实例。一个单个实例就足够用于基本的实验测试。在我们的情况下,我们将在主机docker1上启动这个镜像。所有参与覆盖的 Docker 主机必须能够通过网络访问键值存储。
注意
只有在演示目的下才应该使用单个集群成员运行 Consul。您至少需要三个集群成员才能具有任何故障容忍性。确保您研究并了解您决定部署的键值存储的配置和故障容忍性。
Linux 内核版本为 3.16
您的 Linux 内核版本需要是 3.16 或更高。您可以使用以下命令检查当前的内核版本:
user@docker1:~$ uname -r
4.2.0-34-generic
user@docker1:~$
打开端口
Docker 主机必须能够使用以下端口相互通信:
-
TCP 和 UDP
7946(Serf) -
UDP
4789(VXLAN) -
TCP
8500(Consul 键值存储)
Docker 服务配置选项
参与覆盖的所有主机都需要访问键值存储。为了告诉它们在哪里,我们定义了一些服务级选项:
ExecStart=/usr/bin/dockerd --cluster-store=consul://10.10.10.101:8500/network --cluster-advertise=eth0:0
cluster-store 变量定义了键值存储的位置。在我们的情况下,它是在主机docker1(10.10.10.101)上运行的容器。我们还需要启用cluster-advertise功能并传递一个接口和端口。这个配置更多地涉及使用 Swarm 集群,但该标志也作为启用多主机网络的一部分。也就是说,您需要传递一个有效的接口和端口。在这种情况下,我们使用主机物理接口和端口0。在我们的示例中,我们将这些选项添加到主机docker2,docker3和docker4上,因为这些是参与覆盖网络的主机。
添加选项后,重新加载systemd配置并重新启动 Docker 服务。您可以通过检查docker info命令的输出来验证 Docker 是否接受了该命令:
user@docker2:~$ docker info
…<Additional output removed for brevity>…
Cluster store: **consul://10.10.10.101:8500/network
Cluster advertise: **10.10.10.102:0
…<Additional output removed for brevity>…
如何做…
现在我们已经满足了使用覆盖Driver的先决条件,我们可以部署我们的第一个用户定义的覆盖网络。定义用户定义的覆盖网络遵循与定义用户定义的桥网络相同的过程。例如,让我们使用以下命令配置我们的第一个覆盖网络:
user@docker2:~$ docker network create -d overlay myoverlay
e4bdaa0d6f3afe1ae007a07fe6a1f49f1f963a5ddc8247e716b2bd218352b90e
user@docker2:~$
就像用户定义的桥一样,我们不必输入太多信息来创建我们的第一个覆盖网络。事实上,唯一的区别在于我们必须将Driver指定为覆盖类型,因为默认的Driver类型是桥接。一旦我们输入命令,我们应该能够在参与覆盖网络的任何节点上看到定义的网络。
user@docker3:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
55f86ddf18d5 bridge bridge local
8faef9d2a7cc host host local
3ad850433ed9 myoverlay overlay global
453ad78e11fe none null local
user@docker3:~$
user@docker4:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
3afd680b6ce1 bridge bridge local
a92fe912af1d host host local
3ad850433ed9 myoverlay overlay global
7dbc77e5f782 none null local
user@docker4:~$
当主机docker2创建网络时,它将网络配置推送到存储中。现在所有主机都可以看到新的网络,因为它们都在读写来自同一个键值存储的数据。一旦网络创建完成,任何参与覆盖的节点(配置了正确的服务级选项)都可以查看、连接容器到并删除覆盖网络。
例如,如果我们去到主机docker4,我们可以删除最初在主机docker2上创建的网络:
user@docker4:~$ **docker network rm myoverlay
myoverlay
user@docker4:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
3afd680b6ce1 bridge bridge local
a92fe912af1d host host local
7dbc77e5f782 none null local
user@docker4:~$
现在让我们用更多的配置来定义一个新的覆盖。与用户定义的桥接不同,覆盖Driver目前不支持在创建时使用--opt标志传递任何附加选项。也就是说,我们可以在覆盖类型网络上配置的唯一选项是network create子命令的一部分。
-
aux-address:与用户定义的桥接一样,这个命令允许您定义 Docker 在生成容器时不应分配的 IP 地址。 -
gateway:虽然您可以为网络定义一个网关,如果您不这样做,Docker 会为您做这个,但实际上在覆盖网络中并不使用这个。也就是说,没有接口会被分配这个 IP 地址。 -
internal:此选项允许您隔离网络,并在本章后面更详细地介绍。 -
ip-range:允许您指定一个较小的子网,用于容器寻址。 -
ipam-driver:除了使用第三方网络驱动程序,您还可以利用第三方 IPAM 驱动程序。在本书中,我们将主要关注默认或内置的 IPAM 驱动程序。 -
ipam-opt:这允许您指定要传递给 IPAM 驱动程序的选项。 -
subnet:这定义了与您创建的网络类型相关联的子网。
让我们在主机docker4上重新定义网络myoverlay:
user@docker4:~$ docker network create -d overlay \
--subnet 172.16.16.0/24 --aux-address ip2=172.16.16.2 \
--ip-range=172.16.16.128/25 myoverlay
在这个例子中,我们使用以下属性定义网络:
-
一个
subnet为172.16.16.0/24 -
一个保留或辅助地址为
172.16.16.2(请记住,Docker 将分配一个网关 IP 作为子网中的第一个 IP,尽管实际上并没有使用。在这种情况下,这意味着.1和.2在这一点上在技术上是保留的。) -
一个可分配的容器 IP 范围为
172.16.16.128/25 -
一个名为
myoverlay的网络
与以前一样,这个网络现在可以在参与覆盖配置的三个主机上使用。现在让我们从主机docker2上的覆盖网络中定义我们的第一个容器:
user@docker2:~$ docker run --net=myoverlay --name web1 \
-d -P jonlangemak/web_server_1
3d767d2d2bda91300827f444aa6c4a0762a95ce36a26537aac7770395b5ff673
user@docker2:~$
在这里,我们要求主机启动一个名为web1的容器,并将其连接到网络myoverlay。现在让我们检查容器的 IP 接口配置:
user@docker2:~$ docker exec web1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
7: **eth0@if8**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether 02:42:ac:10:10:81 brd ff:ff:ff:ff:ff:ff
inet **172.16.16.129/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe10:1081/64 scope link
valid_lft forever preferred_lft forever
10: **eth1**@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe12:2/64 scope link
valid_lft forever preferred_lft forever
user@docker2:~$
令人惊讶的是,容器有两个接口。eth0接口连接到与覆盖网络myoverlay相关联的网络,但eth1与一个新网络172.18.0.0/16相关联。
注意
到目前为止,您可能已经注意到容器中的接口名称使用 VETH 对命名语法。Docker 使用 VETH 对将容器连接到桥接,并直接在容器侧接口上配置容器 IP 地址。这将在第四章中进行详细介绍,构建 Docker 网络,在这里我们将详细介绍 Docker 如何将容器连接到网络。
为了找出它连接到哪里,让我们试着找到容器的eth1接口连接到的 VETH 对的另一端。如第一章所示,Linux 网络构造,我们可以使用ethtool来查找 VETH 对的对等接口 ID。然而,当查看用户定义的网络时,有一种更简单的方法可以做到这一点。请注意,在前面的输出中,VETH 对的名称具有以下语法:
<interface name>@if<peers interface ID>
幸运的是,if后面显示的数字是 VETH 对的另一端的接口 ID。因此,在前面的输出中,我们看到eth1接口的匹配接口具有接口 ID为11。查看本地 Docker 主机,我们可以看到我们定义了一个接口11,它的对等接口 ID是10,与容器中的接口 ID匹配。
user@docker2:~$ ip addr show
…<Additional output removed for brevity>…
9: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:af:5e:26:cc brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 scope global docker_gwbridge
valid_lft forever preferred_lft forever
inet6 fe80::42:afff:fe5e:26cc/64 scope link
valid_lft forever preferred_lft forever
11: veth02e6ea5@if10:** <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue **master docker_gwbridge** state UP group default
link/ether ba:c7:df:7c:f4:48 brd ff:ff:ff:ff:ff:ff
inet6 fe80::b8c7:dfff:fe7c:f448/64 scope link
valid_lft forever preferred_lft forever
user@docker2:~$
注意,VETH 对的这一端(接口 ID 11)有一个名为docker_gwbridge的主机。也就是说,VETH 对的这一端是桥接docker_gwbridge的一部分。让我们再次查看 Docker 主机上定义的网络:
user@docker2:~$ docker network ls
NETWORK ID NAME DRIVER
9c91f85550b3 **myoverlay** **overlay
b3143542e9ed none null
323e5e3be7e4 host host
6f60ea0df1ba bridge bridge
e637f106f633 **docker_gwbridge** **bridge
user@docker2:~$
除了我们的覆盖网络,还有一个同名的新用户定义桥接。如果我们检查这个桥接,我们会看到我们的容器按预期连接到它,并且网络定义了一些选项:
user@docker2:~$ docker network inspect docker_gwbridge
[
{
"Name": "docker_gwbridge",
"Id": "10a75e3638b999d7180e1c8310bf3a26b7d3ec7b4e0a7657d9f69d3b5d515389",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1/16"
}
]
},
"Internal": false,
"Containers": {
"e3ae95368057f24fefe1a0358b570848d8798ddfd1c98472ca7ea250087df452": {
"Name": "gateway_e3ae95368057",
"EndpointID": "4cdfc1fb130de499eefe350b78f4f2f92797df9fe7392aeadb94d136abc7f7cd",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.enable_icc": "false",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.name": "docker_gwbridge"
},
"Labels": {}
}
]
user@docker2:~$
正如我们所看到的,此桥的 ICC 模式已禁用。ICC 防止同一网络桥上的容器直接通信。但是这个桥的目的是什么,为什么生成在myoverlay网络上的容器被连接到它上面呢?
docker_gwbridge网络是用于覆盖连接的容器的外部容器连接的解决方案。覆盖网络可以被视为第 2 层网络段。您可以将多个容器连接到它们,并且该网络上的任何内容都可以跨越本地网络段进行通信。但是,这并不允许容器与网络外的资源通信。这限制了 Docker 通过发布端口访问容器资源的能力,以及容器与外部网络通信的能力。如果我们检查容器的路由配置,我们可以看到它的默认网关指向docker_gwbridge的接口:
user@docker2:~$ docker exec web1 ip route
default via 172.18.0.1 dev eth1
172.16.16.0/24 dev eth0 proto kernel scope link src 172.16.16.129
172.18.0.0/16 dev eth1 proto kernel scope link src 172.18.0.2
user@docker2:~$
再加上docker_gwbridge启用了 IP 伪装的事实,这意味着容器仍然可以与外部网络通信:
user@docker2:~$ docker exec -it web1 ping **4.2.2.2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=27.473 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=37.736 ms
--- 4.2.2.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 27.473/32.605/37.736/5.132 ms
user@docker2:~$
与默认桥网络一样,如果容器尝试通过路由到达外部网络,它们将隐藏在其 Docker 主机 IP 接口后面。
这也意味着,由于我使用-P标志在此容器上发布了端口,Docker 已经使用docker_gwbridge发布了这些端口。我们可以使用docker port子命令来验证端口是否已发布:
user@docker2:~$ docker port web1
80/tcp -> 0.0.0.0:32768
user@docker2:~$
通过使用iptables检查 netfilter 规则来验证端口是否在docker_gwbridge上发布:
user@docker2:~$ sudo iptables -t nat -L
…<Additional output removed for brevity>…
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- anywhere anywhere
RETURN all -- anywhere anywhere
DNAT tcp -- anywhere anywhere tcp dpt:32768 to:172.18.0.2:80
user@docker2:~$
正如您在前面的输出中所看到的,Docker 正在使用docker_gwbridge上的容器接口来为 Docker 主机的接口提供端口发布。
此时,我们的容器拓扑如下:

将容器添加到覆盖网络会自动创建桥docker_gwbridge,用于容器连接到主机以及离开主机。myoverlay覆盖网络仅用于与定义的subnet,172.16.16.0/24相关的连接。
现在让我们启动另外两个容器,一个在主机docker3上,另一个在主机docker4上:
user@docker3:~$ **docker run --net=myoverlay --name web2 -d jonlangemak/web_server_2
da14844598d5a6623de089674367d31c8e721c05d3454119ca8b4e8984b91957
user@docker3:~$
user@docker4:~$ **docker run --net=myoverlay --name web2 -d jonlangemak/web_server_2
be67548994d7865ea69151f4797e9f2abc28a39a737eef48337f1db9f72e380c
docker: Error response from daemon: service endpoint with name web2 already exists.
user@docker4:~$
请注意,当我尝试在两个主机上运行相同的容器时,Docker 告诉我容器web2已经存在。Docker 不允许您在同一覆盖网络上以相同的名称运行容器。请回想一下,Docker 正在将与覆盖中的每个容器相关的信息存储在键值存储中。当我们开始讨论 Docker 名称解析时,使用唯一名称变得很重要。
注意
此时您可能会注意到容器可以通过名称解析彼此。这是与用户定义的网络一起提供的非常强大的功能之一。我们将在第五章中更详细地讨论这一点,容器链接和 Docker DNS,在那里我们将讨论 DNS 和链接。
使用唯一名称在docker4上重新启动容器:
user@docker4:~$ docker run --net=myoverlay --name **web2-2** -d jonlangemak/web_server_2
e64d00093da3f20c52fca52be2c7393f541935da0a9c86752a2f517254496e26
user@docker4:~$
现在我们有三个容器在运行,每个主机上都有一个参与覆盖。让我们花点时间来想象这里发生了什么:

我已经在图表上删除了主机和底层网络,以便更容易阅读。如描述的那样,每个容器都有两个 IP 网络接口。一个 IP 地址位于共享的覆盖网络上,位于172.16.16.128/25网络中。另一个位于桥接docker_gwbridge上,每个主机上都是相同的。由于docker_gwbridge独立存在于每个主机上,因此不需要为此接口设置唯一的地址。该桥上的容器接口仅用作容器与外部网络通信的手段。也就是说,位于相同主机上的每个容器,在覆盖类型网络上都会在同一桥上接收一个 IP 地址。
您可能会想知道这是否会引起安全问题,因为所有连接到覆盖网络的容器,无论连接到哪个网络,都会在共享桥上(docker_gwbridge)上有一个接口。请回想一下之前我指出过docker_gwbridge已禁用了 ICC 模式。这意味着,虽然可以将许多容器部署到桥上,但它们都无法通过桥上的 IP 接口直接与彼此通信。我们将在第六章中更详细地讨论这一点,容器网络安全,在那里我们将讨论容器安全,但现在知道 ICC 可以防止在共享桥上发生 ICC。
容器在覆盖网络上相信它们在同一网络段上,或者彼此相邻的第 2 层。让我们通过从容器web1连接到容器web2上的 web 服务来证明这一点。回想一下,当我们配置容器web2时,我们没有要求它发布任何端口。
与其他 Docker 网络构造一样,连接到同一覆盖网络的容器可以直接在它们绑定服务的任何端口上相互通信,而无需发布端口:
注意
重要的是要记住,Docker 主机没有直接连接到覆盖连接的容器的手段。对于桥接网络类型,这是可行的,因为主机在桥接上有一个接口,在覆盖类型网络的情况下,这个接口是不存在的。
user@docker2:~$ docker exec web1 curl -s http://172.16.16.130
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span></h1>
</body>
</html>
user@docker2:~$
正如你所看到的,我们可以成功地从容器web1访问运行在容器web2中的 web 服务器。这些容器不仅位于完全不同的主机上,而且主机本身位于完全不同的子网上。这种类型的通信以前只有在两个容器坐在同一主机上,并连接到同一个桥接时才可用。我们可以通过检查每个相应容器上的 ARP 和 MAC 条目来证明容器相信自己是第 2 层相邻的:
user@**docker2**:~$ docker exec web1 arp -n
Address HWtype HWaddress Flags Mask Iface
172.16.16.130 ether 02:42:ac:10:10:82 C eth0
172.18.0.1 ether 02:42:07:3d:f3:2c C eth1
user@docker2:~$
user@docker3:~$ docker exec web2 ip link show dev eth0
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether **02:42:ac:10:10:82** brd ff:ff:ff:ff:ff:ff
user@docker3:~$
我们可以看到容器有一个 ARP 条目,来自远程容器,指定其 IP 地址以及 MAC 地址。如果容器不在同一网络上,容器web1将不会有web2的 ARP 条目。
我们可以验证我们从docker4主机上的web2-2容器对所有三个容器之间的本地连接性:
user@docker4:~$ docker exec -it web2-2 ping **172.16.16.129** -c 2
PING 172.16.16.129 (172.16.16.129): 48 data bytes
56 bytes from 172.16.16.129: icmp_seq=0 ttl=64 time=0.642 ms
56 bytes from 172.16.16.129: icmp_seq=1 ttl=64 time=0.777 ms
--- 172.16.16.129 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.642/0.710/0.777/0.068 ms
user@docker4:~$ docker exec -it web2-2 ping **172.16.16.130** -c 2
PING 172.16.16.130 (172.16.16.130): 48 data bytes
56 bytes from 172.16.16.130: icmp_seq=0 ttl=64 time=0.477 ms
56 bytes from 172.16.16.130: icmp_seq=1 ttl=64 time=0.605 ms
--- 172.16.16.130 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.477/0.541/0.605/0.064 ms
user@docker4:~$ docker exec -it web2-2 arp -n
Address HWtype HWaddress Flags Mask Iface
172.16.16.129 ether 02:42:ac:10:10:81 C eth0
172.16.16.130 ether 02:42:ac:10:10:82 C eth0
user@docker4:~$
现在我们知道覆盖网络是如何工作的,让我们谈谈它是如何实现的。覆盖传输所使用的机制是 VXLAN。我们可以通过查看在物理网络上进行的数据包捕获来看到容器生成的数据包是如何穿越底层网络的。

在从捕获中获取的数据包的前面的屏幕截图中,我想指出一些项目:
-
外部 IP 数据包源自
docker2主机(10.10.10.102),目的地是docker3主机(192.168.50.101)。 -
我们可以看到外部 IP 数据包是 UDP,并且被检测为 VXLAN 封装。
-
VNI(VXLAN 网络标识符)或段 ID 为
260。VNI 在每个子网中是唯一的。 -
内部帧具有第 2 层和第 3 层标头。第 2 层标头具有容器
web2的目标 MAC 地址,如前所示。IP 数据包显示了容器web1的源和容器web2的目标。
Docker 主机使用自己的 IP 接口封装覆盖流量,并通过底层网络将其发送到目标 Docker 主机。来自键值存储的信息用于确定给定容器所在的主机,以便 VXLAN 封装将流量发送到正确的主机。
您现在可能想知道 VXLAN 覆盖的所有配置在哪里。到目前为止,我们还没有看到任何实际涉及 VXLAN 或隧道的配置。为了提供 VXLAN 封装,Docker 为每个用户定义的覆盖网络创建了我所说的 覆盖命名空间。正如我们在第一章中看到的 Linux 网络构造,您可以使用 ip netns 工具与网络命名空间进行交互。然而,由于 Docker 将它们的网络命名空间存储在非默认位置,我们将无法使用 ip netns 工具查看任何由 Docker 创建的命名空间。默认情况下,命名空间存储在 /var/run/netns 中。问题在于 Docker 将其网络命名空间存储在 /var/run/docker/netns 中,这意味着 ip netns 工具正在错误的位置查看由 Docker 创建的网络命名空间。为了解决这个问题,我们可以创建一个 symlink,将 /var/run/docker/netns/ 链接到 /var/run/nents,如下所示:
user@docker4:~$ cd /var/run
user@docker4:/var/run$ sudo ln -s /var/run/docker/netns netns
user@docker4:/var/run$ sudo ip netns list
eb40d6527d17 (id: 2)
2-4695c5484e (id: 1)
user@docker4:/var/run$
请注意,定义了两个网络命名空间。覆盖命名空间将使用以下语法进行标识 x-<id>,其中 x 是一个随机数。
注意
我们在输出中看到的另一个命名空间与主机上运行的容器相关联。在下一章中,我们将深入探讨 Docker 如何创建和使用这些命名空间。
因此,在我们的情况下,覆盖命名空间是 2-4695c5484e,但它是从哪里来的呢?如果我们检查这个命名空间的网络配置,我们会看到它定义了一些不寻常的接口:
user@docker4:/var/run$ **sudo ip netns exec 2-4695c5484e ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: **br0**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default
link/ether a6:1e:2a:c4:cb:14 brd ff:ff:ff:ff:ff:ff
11: **vxlan1**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue **master br0** state UNKNOWN mode DEFAULT group default
link/ether a6:1e:2a:c4:cb:14 brd ff:ff:ff:ff:ff:ff link-netnsid 0
13: **veth2@if12**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP mode DEFAULT group default
link/ether b2:fa:2d:cc:8b:51 brd ff:ff:ff:ff:ff:ff link-netnsid 1
user@docker4:/var/run$
这些接口定义了我之前提到的叠加网络命名空间。之前我们看到web2-2容器有两个接口。eth1接口是 VETH 对的一端,另一端放在docker_gwbridge上。在前面的叠加网络命名空间中显示的 VETH 对代表了容器eth0接口的一侧。我们可以通过匹配 VETH 对的一侧来证明这一点。请注意,VETH 对的这一端显示另一端的接口 ID为12。如果我们查看容器web2-2,我们会看到它的eth0接口的 ID 为12。反过来,容器的接口显示了一个 ID 为13的对 ID,这与我们在叠加命名空间中看到的输出相匹配:
user@docker4:/var/run$ **docker exec web2-2 ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
12: eth0@if13:** <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether 02:42:ac:10:10:83 brd ff:ff:ff:ff:ff:ff
14: eth1@if15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
user@docker4:/var/run$
现在我们知道容器的叠加接口(eth0)是如何连接的,我们需要知道进入叠加命名空间的流量是如何封装并发送到其他 Docker 主机的。这是通过叠加命名空间的vxlan1接口完成的。该接口具有特定的转发条目,描述了叠加中的所有其他端点:
user@docker4:/var/run$ sudo ip netns exec 2-4695c5484e \
bridge fdb show dev vxlan1
a6:1e:2a:c4:cb:14 master br0 permanent
a6:1e:2a:c4:cb:14 vlan 1 master br0 permanent
02:42:ac:10:10:82 dst 192.168.50.101 link-netnsid 0 self permanent
02:42:ac:10:10:81 dst 10.10.10.102 link-netnsid 0 self permanent
user@docker4:/var/run$
请注意,我们有两个条目引用 MAC 地址和目的地。MAC 地址表示叠加中另一个容器的 MAC 地址,IP 地址是容器所在的 Docker 主机。我们可以通过检查其他主机来验证:
user@docker2:~$ ip addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether f2:e8:00:24:e2:de brd ff:ff:ff:ff:ff:ff
inet **10.10.10.102/24** brd 10.10.10.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::f0e8:ff:fe24:e2de/64 scope link
valid_lft forever preferred_lft forever
user@docker2:~$
user@docker2:~$ **docker exec web1 ip link show dev eth0
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether **02:42:ac:10:10:81** brd ff:ff:ff:ff:ff:ff
user@docker2:~$
有了这些信息,叠加命名空间就知道为了到达目的地 MAC 地址,它需要在 VXLAN 中封装流量并将其发送到10.10.10.102(docker2)。
隔离网络
用户定义的网络可以支持所谓的内部模式。我们在早期关于创建用户定义网络的示例中看到了这个选项,但并没有花太多时间讨论它。在创建网络时使用--internal标志可以防止连接到网络的容器与任何外部网络通信。
准备工作
docker network子命令是在 Docker 1.9 中引入的,因此您需要运行至少该版本的 Docker 主机。在我们的示例中,我们将使用 Docker 版本 1.12。您还需要对当前网络布局有很好的了解,以便在我们检查当前配置时能够跟上。假设每个 Docker 主机都处于其本机配置中。
如何做…
将用户定义的网络设置为内部网络非常简单,只需在network create子命令中添加--internal选项。由于用户定义的网络可以是桥接类型或覆盖类型,我们应该了解 Docker 如何在任何情况下实现隔离。
创建内部用户定义的桥接网络
定义一个用户定义的桥接并传递internal标志,以及在主机上为桥接指定自定义名称的标志。我们可以使用以下命令来实现这一点:
user@docker2:~$ **docker network create --internal \
-o com.docker.network.bridge.name=mybridge1 myinternalbridge
aa990a5436fb2b01f92ffc4d47c5f76c94f3c239f6e9005081ff5c5ecdc4059a
user@docker2:~$
现在,让我们看一下 Docker 分配给桥接的 IP 信息:
user@docker2:~$ ip addr show dev mybridge1
13: mybridge1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:b5:c7:0e:63 brd ff:ff:ff:ff:ff:ff
inet **172.19.0.1/16** scope global mybridge1
valid_lft forever preferred_lft forever
user@docker2:~$
有了这些信息,我们现在来检查一下 Docker 为这个桥接在 netfilter 中编程了什么。让我们检查过滤表并查看:
注意
在这种情况下,我正在使用iptables-save语法来查询当前的规则。有时,这比查看单个表更易读。
user@docker2:~$ sudo iptables-save
# Generated by iptables-save v1.4.21
…<Additional output removed for brevity>…
-A DOCKER-ISOLATION ! -s 172.19.0.0/16 -o mybridge1 -j DROP
-A DOCKER-ISOLATION ! -d 172.19.0.0/16 -i mybridge1 -j DROP
-A DOCKER-ISOLATION -j RETURN
COMMIT
# Completed on Tue Oct 4 23:45:24 2016
user@docker2:~$
在这里,我们可以看到 Docker 添加了两条规则。第一条规定,任何不是源自桥接子网并且正在离开桥接接口的流量应该被丢弃。这可能很难理解,所以最容易的方法是以一个例子来思考。假设您网络上的主机192.168.127.57正在尝试访问这个桥接上的某些内容。该流量的源 IP 地址不会在桥接子网中,这满足了规则的第一部分。它还将尝试离开(或进入)mybridge1,满足了规则的第二部分。这条规则有效地阻止了所有入站通信。
第二条规则寻找没有在桥接子网中具有目的地,并且具有桥接mybridge1的入口接口的流量。在这种情况下,容器可能具有 IP 地址 172.19.0.5/16。如果它试图离开本地网络进行通信,目的地将不在172.19.0.0/16中,这将匹配规则的第一部分。当它试图离开桥接朝向外部网络时,它将匹配规则的第二部分,因为它进入mybridge1接口。这条规则有效地阻止了所有出站通信。
在这两条规则之间,桥接内部不允许任何流量进出。但是,这并不妨碍在同一桥接上的容器之间的容器之间的连接。
应该注意的是,Docker 允许您在针对内部桥接运行容器时指定发布(-P)标志。但是,端口将永远不会被映射:
user@docker2:~$ docker run --net=myinternalbridge --name web1 -d -P jonlangemak/web_server_1
b5f069a40a527813184c7156633c1e28342e0b3f1d1dbb567f94072bc27a5934
user@docker2:~$ docker port web1
user@docker2:~$
创建内部用户定义的覆盖网络
创建内部覆盖遵循相同的过程。我们只需向network create子命令传递--internal标志。然而,在覆盖网络的情况下,隔离模型要简单得多。我们可以按以下方式创建内部覆盖网络:
user@docker2:~$ **docker network create -d overlay \
--subnet 192.10.10.0/24 --internal myinternaloverlay
1677f2c313f21e58de256d9686fd2d872699601898fd5f2a3391b94c5c4cd2ec
user@docker2:~$
创建后,它与非内部覆盖没有什么不同。区别在于当我们在内部覆盖上运行容器时:
user@docker2:~$ docker run --net=myinternaloverlay --name web1 -d -P jonlangemak/web_server_1
c5b05a3c829dfc04ecc91dd7091ad7145cbce96fc7aa0e5ad1f1cf3fd34bb02b
user@docker2:~$
检查容器接口配置,我们可以看到容器只有一个接口,它是覆盖网络(192.10.10.0/24)的成员。通常连接容器到docker_gwbridge(172.18.0.0/16)网络以进行外部连接的接口缺失:
user@docker2:~$ docker exec -it web1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
11: **eth0**@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether 02:42:c0:0a:0a:02 brd ff:ff:ff:ff:ff:ff
inet **192.10.10.2/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:c0ff:fe0a:a02/64 scope link
valid_lft forever preferred_lft forever
user@docker2:~$
覆盖网络本质上是隔离的,因此需要docker_gwbridge。不将容器接口映射到docker_gwbridge意味着没有办法在覆盖网络内部或外部进行通信。
第四章:构建 Docker 网络
在本章中,我们将涵盖以下教程:
-
手动网络容器
-
指定自己的桥
-
使用 OVS 桥
-
使用 OVS 桥连接 Docker 主机
-
OVS 和 Docker 一起
介绍
正如我们在前几章中看到的,Docker 在处理许多容器网络需求方面做得很好。然而,这并不限制您只能使用 Docker 提供的网络元素来连接容器。因此,虽然 Docker 可以为您提供网络,但您也可以手动连接容器。这种方法的缺点是 Docker 对容器的网络状态不知情,因为它没有参与网络配置。正如我们将在第七章 使用 Weave Net中看到的,Docker 现在也支持自定义或第三方网络驱动程序,帮助弥合本机 Docker 和第三方或自定义容器网络配置之间的差距。
手动网络容器
在第一章 Linux 网络构造和第二章 配置和监视 Docker 网络中,我们回顾了常见的 Linux 网络构造,以及涵盖了 Docker 容器网络的本机选项。在这个教程中,我们将演示如何手动网络连接容器,就像 Docker 在默认桥接网络模式下所做的那样。了解 Docker 如何处理容器的网络配置是理解容器网络的非本机选项的关键构建块。
准备工作
在这个教程中,我们将演示在单个 Docker 主机上的配置。假设这个主机已经安装了 Docker,并且 Docker 处于默认配置。为了查看和操作网络设置,您需要确保已安装iproute2工具集。如果系统上没有安装,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要 root 级别的访问权限。
如何做…
为了手动配置容器的网络,我们需要明确告诉 Docker 在运行时不要配置容器的网络。为此,我们使用none网络模式来运行容器。例如,我们可以使用以下语法启动一个没有任何网络配置的 web 服务器容器:
user@docker1:~$ docker run --name web1 **--net=none** -d \
jonlangemak/web_server_1
c108ca80db8a02089cb7ab95936eaa52ef03d26a82b1e95ce91ddf6eef942938
user@docker1:~$
容器启动后,我们可以使用docker exec子命令来检查其网络配置:
user@docker1:~$ docker exec web1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
user@docker1:~$
正如你所看到的,除了本地环回接口之外,容器没有定义任何接口。此时,没有办法连接到容器。我们所做的实质上是在一个气泡中创建了一个容器:

因为我们的目标是模拟默认的网络配置,现在我们需要找到一种方法将容器web1连接到docker0桥,并从桥的 IP 分配(172.17.0.0/16)中分配一个 IP 地址给它。
话虽如此,我们需要做的第一件事是创建我们将用来连接容器到docker0桥的接口。正如我们在第一章中看到的,Linux 网络构造,Linux 有一个名为虚拟以太网(VETH)对的网络组件,这对于此目的非常有效。接口的一端将连接到docker0桥,另一端将连接到容器。
让我们从创建 VETH 对开始:
user@docker1:~$ **sudo ip link add bridge_end type veth \
peer name container_end
user@docker1:~$ ip link show
…<Additional output removed for brevity>…
5: **container_end@bridge_end**: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether ce:43:d8:59:ac:c1 brd ff:ff:ff:ff:ff:ff
6: **bridge_end@container_end**: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 72:8b:e7:f8:66:45 brd ff:ff:ff:ff:ff:ff
user@docker1:~$
正如预期的那样,现在我们有两个直接关联的接口。现在让我们将其中一个端口绑定到docker0桥上并启用该接口:
user@docker1:~$ sudo ip link set dev **bridge_end** master docker0
user@docker1:~$ sudo ip link set **bridge_end** up
user@docker1:~$ ip link show bridge_end
6: **bridge_end@container_end**: <NO-CARRIER,BROADCAST,MULTICAST,UP,M-DOWN> mtu 1500 qdisc pfifo_fast **master docker0** state LOWERLAYERDOWN mode DEFAULT group default qlen 1000
link/ether 72:8b:e7:f8:66:45 brd ff:ff:ff:ff:ff:ff
user@docker1:~$
注意
此时接口的状态将显示为LOWERLAYERDOWN。这是因为接口的另一端未绑定,仍处于关闭状态。
下一步是将 VETH 对的另一端连接到容器。这就是事情变得有趣的地方。Docker 会在自己的网络命名空间中创建每个容器。这意味着 VETH 对的另一端需要落入容器的网络命名空间。关键是确定容器的网络命名空间是什么。可以通过两种不同的方式找到给定容器的命名空间。
第一种方法依赖于将容器的进程 ID(PID)与已定义的网络命名空间相关联。它比第二种方法更复杂,但可以让您了解一些网络命名空间的内部情况。您可能还记得第三章中所述,默认情况下,我们无法使用命令行工具ip netns查看 Docker 创建的命名空间。为了查看它们,我们需要创建一个符号链接,将 Docker 存储其网络命名空间的位置(/var/run/docker/netns)与ip netns正在查找的位置(/var/run/netns)联系起来。
user@docker1:~$ cd /var/run
user@docker1:/var/run$ sudo ln -s /var/run/docker/netns netns
现在,如果我们尝试列出命名空间,我们应该至少看到一个列在返回中:
user@docker1:/var/run$ sudo ip netns list
712f8a477cce
default
user@docker1:/var/run$
但是我们怎么知道这是与此容器关联的命名空间呢?要做出这一决定,我们首先需要找到相关容器的 PID。我们可以通过检查容器来检索这些信息:
user@docker1:~$ docker inspect web1
…<Additional output removed for brevity>…
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 3156**,
"ExitCode": 0,
"Error": "",
"StartedAt": "2016-10-05T21:32:00.163445345Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
…<Additional output removed for brevity>…
user@docker1:~$
现在我们有了 PID,我们可以使用ip netns identify子命令从 PID 中找到网络命名空间的名称:
user@docker1:/var/run$ sudo ip netns identify **3156
712f8a477cce
user@docker1:/var/run$
注意
即使您选择使用第二种方法,请确保创建符号链接,以便ip netns在后续步骤中起作用。
找到容器网络命名空间的第二种方法要简单得多。我们可以简单地检查和引用容器的网络配置:
user@docker1:~$ docker inspect web1
…<Additional output removed for brevity>…
"NetworkSettings": {
"Bridge": "",
"SandboxID": "712f8a477cceefc7121b2400a22261ec70d6a2d9ab2726cdbd3279f1e87dae22",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {},
"SandboxKey": "/var/run/docker/netns/712f8a477cce",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "",
…<Additional output removed for brevity>…
user@docker1:~$
注意名为SandboxKey的字段。您会注意到文件路径引用了我们说过 Docker 存储其网络命名空间的位置。此路径中引用的文件名是容器的网络命名空间的名称。Docker 将网络命名空间称为沙盒,因此使用了这种命名约定。
现在我们有了网络命名空间名称,我们可以在容器和docker0桥之间建立连接。回想一下,VETH 对可以用来连接网络命名空间。在这个例子中,我们将把 VETH 对的容器端放在容器的网络命名空间中。这将把容器桥接到docker0桥上的默认网络命名空间中。为此,我们首先将 VETH 对的容器端移入我们之前发现的命名空间中:
user@docker1:~$ sudo ip link set container_end netns **712f8a477cce
我们可以使用docker exec子命令验证 VETH 对是否在命名空间中:
user@docker1:~$ docker exec web1 ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: **container_end@if6**: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
link/ether 86:15:2a:f7:0e:f9 brd ff:ff:ff:ff:ff:ff
user@docker1:~$
到目前为止,我们已成功地使用 VETH 对将容器和默认命名空间连接在一起,因此我们的连接现在看起来像这样:

然而,容器 web1 仍然缺乏任何类型的 IP 连通性,因为它尚未被分配可路由的 IP 地址。回想一下,在第一章中,Linux 网络构造,我们看到 VETH 对接口可以直接分配 IP 地址。为了给容器分配一个可路由的 IP 地址,Docker 简单地从 docker0 桥的子网中分配一个未使用的 IP 地址给 VETH 对的容器端。
注意
IPAM 是允许 Docker 为您管理容器网络的巨大优势。没有 IPAM,你将需要自己跟踪分配,并确保你不分配任何重叠的 IP 地址。
user@docker1:~$ sudo ip netns exec 712f8a477cce ip \
addr add 172.17.0.99/16 dev container_end
在这一点上,我们可以启用接口,我们应该可以从主机到容器的可达性。但在这样做之前,让我们通过将 container_end VETH 对重命名为 eth0 来使事情变得更清晰一些:
user@docker1:~$ sudo ip netns exec 712f8a477cce ip link \
set dev container_end name eth0
现在我们可以启用新命名的 eth0 接口,这是 VETH 对的容器端:
user@docker1:~$ sudo ip netns exec 712f8a477cce ip link \
set eth0 up
user@docker1:~$ ip link show bridge_end
6: **bridge_end**@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master docker0 state UP mode DEFAULT group default qlen 1000
link/ether 86:04:ed:1b:2a:04 brd ff:ff:ff:ff:ff:ff
user@docker1:~$ sudo ip netns exec **4093b3b4e672 ip link show eth0
5: **eth0**@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 86:15:2a:f7:0e:f9 brd ff:ff:ff:ff:ff:ff
user@docker1:~$ sudo ip netns exec **4093b3b4e672 ip addr show eth0
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 86:15:2a:f7:0e:f9 brd ff:ff:ff:ff:ff:ff
inet **172.17.0.99/16** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::8415:2aff:fef7:ef9/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
如果我们从主机检查,现在我们应该可以到达容器:
user@docker1:~$ ping **172.17.0.99** -c 2
PING 172.17.0.99 (172.17.0.99) 56(84) bytes of data.
64 bytes from 172.17.0.99: icmp_seq=1 ttl=64 time=0.104 ms
64 bytes from 172.17.0.99: icmp_seq=2 ttl=64 time=0.045 ms
--- 172.17.0.99 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.045/0.074/0.104/0.030 ms
user@docker1:~$
user@docker1:~$ curl **http://172.17.0.99
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span></h1>
</body>
</html>
user@docker1:~$
连接已经建立,我们的拓扑现在看起来是这样的:

因此,虽然我们有 IP 连通性,但只限于相同子网上的主机。最后剩下的问题是解决主机级别的容器连通性。对于出站连通性,主机将容器的 IP 地址隐藏在主机接口 IP 地址的后面。对于入站连通性,在默认网络模式下,Docker 使用端口映射将 Docker 主机的 NIC 上的随机高端口映射到容器的暴露端口。
在这种情况下解决出站问题就像给容器指定一个指向 docker0 桥的默认路由,并确保你有一个 netfilter masquerade 规则来覆盖这个一样简单:
user@docker1:~$ sudo ip netns exec 712f8a477cce ip route \
add default via **172.17.0.1
user@docker1:~$ docker exec -it **web1** ping 4.2.2.2 -c 2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=39.764 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=40.210 ms
--- 4.2.2.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 39.764/39.987/40.210/0.223 ms
user@docker1:~$
如果你像我们在这个例子中使用 docker0 桥,你就不需要添加自定义 netfilter masquerade 规则。这是因为默认的伪装规则已经覆盖了整个 docker0 桥的子网:
user@docker1:~$ sudo iptables -t nat -L
…<Additional output removed for brevity>…
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 anywhere
…<Additional output removed for brevity>…
user@docker1:~$
对于入站服务,我们需要创建一个自定义规则,使用网络地址转换(NAT)将主机上的随机高端口映射到容器中暴露的服务端口。我们可以使用以下规则来实现:
user@docker1:~$ sudo iptables -t nat -A DOCKER ! -i docker0 -p tcp -m tcp \
--dport 32799 -j DNAT --to-destination 172.17.0.99:80
在这种情况下,我们将主机接口上的端口32799进行 NAT 转发到容器上的端口80。这将允许外部网络上的系统通过 Docker 主机的接口访问在web1上运行的 Web 服务器,端口为32799。
最后,我们成功地复制了 Docker 在默认网络模式下提供的内容:

这应该让您对 Docker 在幕后所做的事情有所了解。跟踪容器 IP 地址、发布端口的端口分配以及iptables规则集是 Docker 代表您跟踪的三个主要事项。鉴于容器的短暂性质,手动完成这些工作几乎是不可能的。
指定您自己的桥接
在大多数网络场景中,Docker 严重依赖于docker0桥。docker0桥是在启动 Docker 引擎服务时自动创建的,并且是 Docker 服务生成的任何容器的默认连接点。我们在之前的配方中也看到,可以在服务级别修改这个桥的一些属性。在这个配方中,我们将向您展示如何告诉 Docker 使用完全不同的桥接。
准备工作
在这个配方中,我们将演示在单个 Docker 主机上的配置。假设这个主机已经安装了 Docker,并且 Docker 处于默认配置状态。为了查看和操作网络设置,您需要确保安装了iproute2工具集。如果系统上没有安装,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要 root 级别的访问权限。
操作步骤
与其他服务级参数一样,指定 Docker 使用不同的桥接是通过更新我们在第二章中向您展示如何创建的 systemd drop-in 文件来完成的,配置和监控 Docker 网络。
在指定新桥之前,让我们首先确保没有正在运行的容器,停止 Docker 服务,并删除docker0桥:
user@docker1:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
user@docker1:~$
user@docker1:~$ sudo systemctl stop docker
user@docker1:~$
user@docker1:~$ sudo ip link delete dev docker0
user@docker1:~$
user@docker1:~$ ip link show dev docker0
Device "docker0" does not exist.
user@docker1:~$
在这一点上,默认的docker0桥已被删除。现在,让我们为 Docker 创建一个新的桥接。
注意
如果您不熟悉iproute2命令行工具,请参考第一章中的示例,Linux 网络构造。
user@docker1:~$ sudo ip link add mybridge1 type bridge
user@docker1:~$ sudo ip address add 10.11.12.1/24 dev mybridge1
user@docker1:~$ sudo ip link set dev mybridge1 up
user@docker1:~$ ip addr show dev mybridge1
7: mybridge1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
link/ether 9e:87:b4:7b:a3:c0 brd ff:ff:ff:ff:ff:ff
inet **10.11.12.1/24** scope global mybridge1
valid_lft forever preferred_lft forever
inet6 fe80::9c87:b4ff:fe7b:a3c0/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
我们首先创建了一个名为mybridge1的桥接,然后给它分配了 IP 地址10.11.12.1/24,最后启动了接口。此时,接口已经启动并可达。现在我们可以告诉 Docker 使用这个桥接作为其默认桥接。要做到这一点,编辑 Docker 的 systemd drop-in 文件,并确保最后一行如下所示:
ExecStart=/usr/bin/dockerd --bridge=mybridge1
现在保存文件,重新加载 systemd 配置,并启动 Docker 服务:
user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl start docker
现在,如果我们启动一个容器,我们应该看到它被分配到桥接mybridge1上:
user@docker1:~$ **docker run --name web1 -d -P jonlangemak/web_server_1
e8a05afba6235c6d8012639aa79e1732ed5ff741753a8c6b8d9c35a171f6211e
user@docker1:~$ **ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 62:31:35:63:65:63 brd ff:ff:ff:ff:ff:ff
3: eth1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 36:b3:5c:94:c0:a6 brd ff:ff:ff:ff:ff:ff
17: **mybridge1**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 7a:1b:30:e6:94:b7 brd ff:ff:ff:ff:ff:ff
22: veth68fb58a@if21**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue **master mybridge1** state UP mode DEFAULT group default
link/ether 7a:1b:30:e6:94:b7 brd ff:ff:ff:ff:ff:ff link-netnsid 0
user@docker1:~$
请注意,在服务启动时并未创建docker0桥接。还要注意,我们在默认命名空间中看到了一个 VETH 对的一端,其主接口为mybridge1。
利用我们从本章第一个配方中学到的知识,我们还可以确认 VETH 对的另一端在容器的网络命名空间中:
user@docker1:~$ docker inspect web1 | grep SandboxKey
"SandboxKey": "/var/run/docker/netns/926ddab911ae",
user@docker1:~$
user@docker1:~$ sudo ip netns exec **926ddab911ae ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
21: eth0@if22**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:0a:0b:0c:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
user@docker1:~$
我们可以看出这是一个 VETH 对,因为它使用<interface>@<interface>的命名语法。如果我们比较 VETH 对接口的编号,我们可以看到这两个与 VETH 对的主机端匹配,索引为22连接到 VETH 对的容器端,索引为21。
注意
您可能会注意到我在使用ip netns exec和docker exec命令在容器内执行命令时来回切换。这样做的目的不是为了混淆,而是为了展示 Docker 代表您在做什么。需要注意的是,为了使用ip netns exec语法,您需要在我们在早期配方中演示的位置放置符号链接。只有在手动配置命名空间时才需要使用ip netns exec。
如果我们查看容器的网络配置,我们可以看到 Docker 已经为其分配了mybridge1子网范围内的 IP 地址:
user@docker1:~$ docker exec web1 ip addr show dev **eth0
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:0a:0b:0c:02 brd ff:ff:ff:ff:ff:ff
inet **10.11.12.2/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe0b:c02/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
现在 Docker 也在为桥接分配 IP 地址时跟踪 IP 分配。IP 地址管理是 Docker 在容器网络空间提供的一个重要价值。将 IP 地址映射到容器并自行管理将是一项重大工作。
最后一部分将是处理容器的 NAT 配置。由于10.11.12.0/24空间不可路由,我们需要隐藏或伪装容器的 IP 地址在 Docker 主机上的物理接口后面。幸运的是,只要 Docker 为您管理桥,Docker 仍然会负责制定适当的 netfilter 规则。我们可以通过检查 netfilter 规则集来确保这一点:
user@docker1:~$ sudo iptables -t nat -L -n
…<Additional output removed for brevity>…
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 10.11.12.0/24 0.0.0.0/0
…<Additional output removed for brevity>…
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:32768 to:10.11.12.2:80
此外,由于我们使用-P标志在容器上暴露端口,入站 NAT 也已分配。我们还可以在相同的输出中看到这个 NAT 转换。总之,只要您使用的是 Linux 桥,Docker 将像使用docker0桥一样为您处理整个配置。
使用 OVS 桥
对于寻找额外功能的用户来说,OpenVSwitch(OVS)正在成为本地 Linux 桥的流行替代品。OVS 在略微更高的复杂性的代价下,为 Linux 桥提供了显著的增强。例如,OVS 桥不能直接由我们到目前为止一直在使用的iproute2工具集进行管理,而是需要自己的命令行管理工具。然而,如果您正在寻找在 Linux 桥上不存在的功能,OVS 很可能是您的最佳选择。Docker 不能本地管理 OVS 桥,因此使用 OVS 桥需要手动建立桥和容器之间的连接。也就是说,我们不能只是告诉 Docker 服务使用 OVS 桥而不是默认的docker0桥。在本教程中,我们将展示如何安装、配置和直接连接容器到 OVS 桥,以取代标准的docker0桥。
准备工作
在本教程中,我们将演示在单个 Docker 主机上的配置。假设该主机已安装了 Docker,并且 Docker 处于默认配置。为了查看和操作网络设置,您需要确保已安装iproute2工具集。如果系统上没有安装,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要 root 级别的访问权限。
如何做…
我们需要执行的第一步是在我们的 Docker 主机上安装 OVS。为了做到这一点,我们可以直接拉取 OVS 包:
user@docker1:~$ sudo apt-get install openvswitch-switch
如前所述,OVS 有自己的命令行工具集,其中一个工具被命名为ovs-vsctl,用于直接管理 OVS 桥。更具体地说,ovs-vsctl用于查看和操作 OVS 配置数据库。为了确保 OVS 正确安装,我们可以运行以下命令:
user@docker1:~$ sudo ovs-vsctl -V
ovs-vsctl (Open vSwitch) 2.5.0
Compiled Mar 10 2016 14:16:49
DB Schema 7.12.1
user@docker1:~$
这将返回 OVS 版本号,并验证我们与 OVS 的通信。我们接下来要做的是创建一个 OVS 桥。为了做到这一点,我们将再次使用ovs-vsctl命令行工具:
user@docker1:~$ sudo ovs-vsctl add-br ovs_bridge
这个命令将添加一个名为ovs_bridge的 OVS 桥。创建后,我们可以像查看任何其他网络接口一样查看桥接口:
user@docker1:~$ ip link show dev ovs_bridge
6: ovs_bridge: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1
link/ether b6:45:81:aa:7c:47 brd ff:ff:ff:ff:ff:ff
user@docker1:~$
但是,要查看任何特定于桥的信息,我们将再次需要依赖ocs-vsctl命令行工具。我们可以使用show子命令查看有关桥的信息:
user@docker1:~$ sudo ovs-vsctl show
0f2ced94-aca2-4e61-a844-fd6da6b2ce38
Bridge ovs_bridge
Port ovs_bridge
Interface ovs_bridge
type: internal
ovs_version: "2.5.0"
user@docker1:~$
为 OVS 桥分配 IP 地址并更改其状态可以再次使用更熟悉的iproute2工具完成:
user@docker1:~$ sudo ip addr add dev ovs_bridge 10.11.12.1/24
user@docker1:~$ sudo ip link set dev ovs_bridge up
一旦启动,接口就像任何其他桥接口一样。我们可以看到 IP 接口已经启动,本地主机可以直接访问它:
user@docker1:~$ ip addr show dev ovs_bridge
6: ovs_bridge: <BROADCAST,MULTICAST**,UP,LOWER_UP**> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1
link/ether b6:45:81:aa:7c:47 brd ff:ff:ff:ff:ff:ff
inet **10.11.12.1/24** scope global ovs_bridge
valid_lft forever preferred_lft forever
inet6 fe80::b445:81ff:feaa:7c47/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
user@docker1:~$ ping 10.11.12.1 -c 2
PING 10.11.12.1 (10.11.12.1) 56(84) bytes of data.
64 bytes from 10.11.12.1: icmp_seq=1 ttl=64 time=0.036 ms
64 bytes from 10.11.12.1: icmp_seq=2 ttl=64 time=0.025 ms
--- 10.11.12.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.025/0.030/0.036/0.007 ms
user@docker1:~$
我们接下来要做的是创建我们将用来连接容器到 OVS 桥的 VETH 对:
user@docker1:~$ sudo ip link add ovs_end1 type veth \
peer name container_end1
创建后,我们需要将 VETH 对的 OVS 端添加到 OVS 桥上。这是 OVS 与标准 Linux 桥有很大区别的地方之一。每个连接到 OVS 的都是以端口的形式。这比 Linux 桥提供的更像是物理交换机。再次强调,因为我们直接与 OVS 桥交互,我们需要使用ovs-vsctl命令行工具:
user@docker1:~$ sudo ovs-vsctl add-port ovs_bridge ovs_end1
添加后,我们可以查询 OVS 以查看所有桥接口的端口:
user@docker1:~$ sudo ovs-vsctl list-ports ovs_bridge
ovs_end1
user@docker1:~$
如果您检查定义的接口,您会看到 VETH 对的 OVS 端将ovs-system列为其主机:
user@docker1:~$ **ip link show dev ovs_end1
8: **ovs_end1@container_end1**: <BROADCAST,MULTICAST> mtu 1500 qdisc noop **master ovs-system** state DOWN mode DEFAULT group default qlen 1000
link/ether 56:e0:12:94:c5:43 brd ff:ff:ff:ff:ff:ff
user@docker1:~$
不要深入细节,这是预期的。ovs-system接口代表 OVS 数据路径。现在,只需知道这是预期的行为即可。
现在 OVS 端已经完成,我们需要专注于容器端。这里的第一步将是启动一个没有任何网络配置的容器。接下来,我们将按照之前的步骤手动连接容器命名空间到 VETH 对的另一端:
- 启动容器:
docker run --name web1 --net=none -d jonlangemak/web_server_1
- 查找容器的网络命名空间:
docker inspect web1 | grep SandboxKey
"SandboxKey": "/var/run/docker/netns/54b7dfc2e422"
- 将 VETH 对的容器端移入该命名空间:
sudo ip link set container_end1 netns 54b7dfc2e422
- 将 VETH 接口重命名为
eth0:
sudo ip netns exec 54b7dfc2e422 ip link set dev \
container_end1 name eth0
- 将
eth0接口的 IP 地址设置为该子网中的有效 IP:
sudo ip netns exec 54b7dfc2e422 ip addr add \
10.11.12.99/24 dev eth0
- 启动容器端的接口
sudo ip netns exec 54b7dfc2e422 ip link set dev eth0 up
- 启动 VETH 对的 OVS 端:
sudo ip link set dev ovs_end1 up
此时,容器已成功连接到 OVS,并可以通过主机访问:
user@docker1:~$ ping 10.11.12.99 -c 2
PING 10.11.12.99 (10.11.12.99) 56(84) bytes of data.
64 bytes from 10.11.12.99: icmp_seq=1 ttl=64 time=0.469 ms
64 bytes from 10.11.12.99: icmp_seq=2 ttl=64 time=0.028 ms
--- 10.11.12.99 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.028/0.248/0.469/0.221 ms
user@docker1:~$
如果我们想更深入地了解 OVS,可以使用以下命令查看交换机的 MAC 地址表:
user@docker1:~$ sudo ovs-appctl fdb/show ovs_bridge
port VLAN MAC Age
LOCAL 0 b6:45:81:aa:7c:47 7
1 0 b2:7e:e8:42:58:39 7
user@docker1:~$
注意它在port 1上学到的 MAC 地址。但port 1是什么?要查看给定 OVS 的所有端口,可以使用以下命令:
user@docker1:~$ sudo ovs-dpctl show
system@ovs-system:
lookups: hit:13 missed:11 lost:0
flows: 0
masks: hit:49 total:1 hit/pkt:2.04
port 0: ovs-system (internal)
port 1: ovs_bridge (internal)
port 2: ovs_end1
user@docker1:~$
在这里,我们可以看到port 1是我们预配的 OVS 桥,我们将 VETH 对的 OVS 端连接到了这里。
正如我们所看到的,连接到 OVS 所需的工作量可能很大。幸运的是,有一些很棒的工具可以帮助我们简化这个过程。其中一个比较显著的工具是由 Jérôme Petazzoni 开发的,名为Pipework。它可以在 GitHub 上找到,网址如下:
如果我们使用 Pipework 来连接到 OVS,并假设桥已经创建,我们可以将连接容器到桥所需的步骤从6减少到1。
要使用 Pipework,必须先从 GitHub 上下载它。可以使用 Git 客户端完成这一步:
user@docker1:~$ git clone https://github.com/jpetazzo/pipework
…<Additional output removed for brevity>…
user@docker1:~$ cd pipework/
user@docker1:~/pipework$ ls
docker-compose.yml doctoc LICENSE pipework pipework**.spec README.md
user@docker1:~/pipework$
为了演示使用 Pipework,让我们启动一个名为web2的新容器,没有任何网络配置:
user@docker1:~$ docker run --name web2 --net=none -d \
jonlangemak/web_server_2
985384d0b0cd1a48cb04de1a31b84f402197b2faade87d073e6acdc62cf29151
user@docker1:~$
现在,我们要做的就是将这个容器连接到我们现有的 OVS 桥上,只需运行以下命令,指定 OVS 桥的名称、容器名称和我们希望分配给容器的 IP 地址:
user@docker1:~/pipework$ sudo ./pipework **ovs_bridge \
web2 10.11.12.100/24
Warning: arping not found; interface may not be immediately reachable
user@docker1:~/pipework$
Pipework 会为我们处理所有的工作,包括将容器名称解析为网络命名空间,创建一个唯一的 VETH 对,正确地将 VETH 对的端点放在容器和指定的桥上,并为容器分配一个 IP 地址。
Pipework 还可以帮助我们在运行时为容器添加额外的接口。考虑到我们以none网络模式启动了这个容器,容器目前只有根据第一个 Pipework 配置连接到 OVS。然而,我们也可以使用 Pipework 将连接添加回docker0桥:
user@docker1:~/pipework$ sudo ./pipework docker0 -i eth0 web2 \
172.17.0.100/16@172.17.0.1
语法类似,但在这种情况下,我们指定了要使用的接口名称(eth0),并为172.17.0.1的接口添加了一个网关。这将允许容器使用docker0桥作为默认网关,并且允许它使用默认的 Docker 伪装规则进行出站访问。我们可以使用一些docker exec命令验证配置是否存在于容器中:
user@docker1:~/pipework$ **docker exec web2 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
9: **eth1**@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
link/ether da:40:35:ec:c2:45 brd ff:ff:ff:ff:ff:ff
inet **10.11.12.100/24** scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::d840:35ff:feec:c245/64 scope link
valid_lft forever preferred_lft forever
11: **eth0**@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
link/ether 2a:d0:32:ef:e1:07 brd ff:ff:ff:ff:ff:ff
inet **172.17.0.100/16** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::28d0:32ff:feef:e107/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~/pipework$ **docker exec web2 ip route
default via 172.17.0.1 dev eth0
10.11.12.0/24 dev eth1 proto kernel scope link src 10.11.12.100
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.100
user@docker1:~/pipework$
因此,虽然 Pipework 可以使许多这些手动工作变得更容易,但您应该始终查看 Docker 是否有本机手段来提供您正在寻找的网络连接。让 Docker 管理您的容器网络连接具有许多好处,包括自动 IPAM 分配和用于入站和出站连接的 netfilter 配置。许多这些非本机配置已经有第三方 Docker 网络插件在进行中,这将允许您无缝地利用它们从 Docker 中。
使用 OVS 桥连接 Docker 主机
上一个教程展示了我们如何可以使用 OVS 来代替标准的 Linux 桥。这本身并不是很有趣,因为它并没有比标准的 Linux 桥做更多的事情。可能有趣的是,与您的 Docker 容器一起使用 OVS 的一些更高级的功能。例如,一旦创建了 OVS 桥,就可以相当轻松地在两个不同的 Docker 主机之间配置 GRE 隧道。这将允许连接到任一 Docker 主机的任何容器直接彼此通信。在这个教程中,我们将讨论使用 OVS 提供的 GRE 隧道连接两个 Docker 主机所需的配置。
注意
再次强调,这个教程仅用于举例说明。这种行为已经得到 Docker 的用户定义的覆盖网络类型的支持。如果出于某种原因,您需要使用 GRE 而不是 VXLAN,这可能是一个合适的替代方案。与往常一样,在开始自定义之前,请确保您使用任何 Docker 本机网络构造。这将为您节省很多麻烦!
准备工作
在这个教程中,我们将演示在两个 Docker 主机上的配置。这些主机需要能够在网络上相互通信。假设主机已经安装了 Docker,并且 Docker 处于默认配置。为了查看和操作网络设置,您需要确保已安装了iproute2工具集。如果系统上没有安装,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要 root 级别的访问权限。
如何做…
为了本教程的目的,我们将假设在本例中使用的两台主机上有一个基本配置。也就是说,每台主机只安装了 Docker,并且其配置与默认配置相同。
我们将使用的拓扑将如下图所示。两个不同子网上的两个 Docker 主机:

此配置的目标是在每台主机上配置 OVS,将容器连接到 OVS,然后将两个 OVS 交换机连接在一起,以允许通过 GRE 进行 OVS 之间的直接通信。我们将在每台主机上按照以下步骤来实现这一目标:
-
安装 OVS。
-
添加一个名为
ovs_bridge的 OVS 桥。 -
为该桥分配一个 IP 地址。
-
运行一个网络模式设置为
none的容器。 -
使用 Pipework 将该容器连接到 OVS 桥(假设每台主机上都安装了 Pipework。如果没有,请参考之前的安装步骤)。
-
使用 OVS 在另一台主机上建立一个 GRE 隧道。
让我们从第一台主机docker1开始配置:
user@docker1:~$ sudo apt-get install openvswitch-switch
…<Additional output removed for brevity>…
Setting up openvswitch-switch (2.0.2-0ubuntu0.14.04.3) ...
openvswitch-switch start/running
user@docker1:~$
user@docker1:~$ sudo ovs-vsctl add-br ovs_bridge
user@docker1:~$ sudo ip addr add dev ovs_bridge 10.11.12.1/24
user@docker1:~$ sudo ip link set dev ovs_bridge up
user@docker1:~$
user@docker1:~$ docker run --name web1 --net=none -dP \
jonlangemak/web_server_1
5e6b335b12638a7efecae650bc8e001233842bb97ab07b32a9e45d99bdffe468
user@docker1:~$
user@docker1:~$ cd pipework
user@docker1:~/pipework$ sudo ./pipework ovs_bridge \
web1 10.11.12.100/24
Warning: arping not found; interface may not be immediately reachable
user@docker1:~/pipework$
此时,您应该有一个正在运行的容器。您应该能够从本地 Docker 主机访问该容器:
user@docker1:~$ curl http://**10.11.12.100
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
现在,让我们在第二台主机docker3上执行类似的配置:
user@docker3:~$ sudo apt-get install openvswitch-switch
…<Additional output removed for brevity>…
Setting up openvswitch-switch (2.0.2-0ubuntu0.14.04.3) ...
openvswitch-switch start/running
user@docker3:~$
user@docker3:~$ sudo ovs-vsctl add-br ovs_bridge
user@docker3:~$ sudo ip addr add dev ovs_bridge 10.11.12.2/24
user@docker3:~$ sudo ip link set dev ovs_bridge up
user@docker3:~$
user@docker3:~$ docker run --name web2 --net=none -dP \
jonlangemak/web_server_2
155aff2847e27c534203b1ae01894b0b159d09573baf9844cc6f5c5820803278
user@docker3:~$
user@docker3:~$ cd pipework
user@docker3:~/pipework$ sudo ./pipework ovs_bridge web2 10.11.12.200/24
Warning: arping not found; interface may not be immediately reachable
user@docker3:~/pipework$
这样就完成了对第二台主机的配置。确保您可以连接到本地主机上运行的web2容器:
user@docker3:~$ curl http://**10.11.12.200
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker3:~$
此时,我们的拓扑看起来是这样的:

如果我们的目标是允许容器web1直接与容器web2通信,我们将有两个选项。首先,由于 Docker 不知道 OVS 交换机,它不会尝试根据连接到它的容器应用 netfilter 规则。也就是说,通过正确的路由配置,这两个容器可以直接路由到彼此。然而,即使在这个简单的例子中,这也需要大量的配置。由于我们在两台主机之间共享一个公共子网(就像 Docker 在默认模式下一样),配置变得不那么简单。为了使其工作,您需要做以下几件事:
-
在每个容器中添加路由,告诉它们另一个容器的特定
/32路由位于子网之外。这是因为每个容器都认为整个10.11.12.0/24网络是本地的,因为它们都在该网络上有一个接口。您需要一个比/24更具体(更小)的前缀来强制容器路由以到达目的地。 -
在每个 Docker 主机上添加路由,告诉它们另一个容器的特定
/32路由位于子网之外。同样,这是因为每个主机都认为整个10.11.12.0/24网络是本地的,因为它们都在该网络上有一个接口。您需要一个比/24更具体(更小)的前缀来强制主机路由以到达目的地。 -
在多层交换机上添加路由,以便它知道
10.11.12.100可以通过10.10.10.101(docker1)到达,10.11.12.200可以通过192.168.50.101(docker3)到达。
现在想象一下,如果你正在处理一个真实的网络,并且必须在路径上的每个设备上添加这些路由。第二个,也是更好的选择是在两个 OVS 桥之间创建隧道。这将阻止网络看到10.11.12.0/24的流量,这意味着它不需要知道如何路由它:

幸运的是,对于我们来说,这个配置在 OVS 上很容易实现。我们只需添加另一个类型为 GRE 的 OVS 端口,并指定另一个 Docker 主机作为远程隧道目的地。
在主机docker1上,按以下方式构建 GRE 隧道:
user@docker1:~$ sudo ovs-vsctl add-port ovs_bridge ovs_gre \
-- set interface ovs_gre type=gre options:remote_ip=192.168.50.101
在主机docker3上,按以下方式构建 GRE 隧道:
user@docker3:~$ sudo ovs-vsctl add-port ovs_bridge ovs_gre \
-- set interface ovs_gre type=gre options:remote_ip=10.10.10.101
此时,两个容器应该能够直接相互通信:
user@**docker1**:~$ docker exec -it **web1** curl http://**10.11.12.200
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
user@**docker3**:~$ docker exec -it **web2** curl http://**10.11.12.100
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker3:~$
作为最终证明这是通过 GRE 隧道传输的,我们可以在主机的一个物理接口上运行tcpdump,同时在容器之间进行 ping 测试:

OVS 和 Docker 一起
到目前为止,这些配方展示了手动配置 Docker 网络时可能出现的几种可能性。尽管这些都是可能的解决方案,但它们都需要大量的手动干预和配置,并且在它们当前的形式下不容易消化。如果我们以前的配方作为例子,有一些显著的缺点:
-
您负责跟踪容器上的 IP 分配,增加了将不同容器分配冲突的风险
-
没有动态端口映射或固有的出站伪装来促进容器与网络的通信。
-
虽然我们使用了 Pipework 来减轻配置负担,但仍然需要进行相当多的手动配置才能将容器连接到 OVS 桥接器。
-
大多数配置默认情况下不会在主机重启后持久化。
话虽如此,根据我们迄今所学到的知识,我们可以利用 OVS 的 GRE 功能的另一种方式,同时仍然使用 Docker 来管理容器网络。在这个示例中,我们将回顾这个解决方案,并描述如何使其成为一个更持久的解决方案,可以在主机重启后仍然存在。
注意
再次强调,这个示例仅用于举例说明。这种行为已经得到 Docker 的用户定义的覆盖网络类型的支持。如果出于某种原因,您需要使用 GRE 而不是 VXLAN,这可能是一个合适的替代方案。与以往一样,在开始自定义之前,请确保使用任何 Docker 原生网络构造。这将为您节省很多麻烦!
准备工作
在这个示例中,我们将演示在两个 Docker 主机上的配置。这些主机需要能够通过网络相互通信。假设主机已安装了 Docker,并且 Docker 处于默认配置状态。为了查看和操作网络设置,您需要确保已安装了iproute2工具集。如果系统上没有安装,可以使用以下命令进行安装:
sudo apt-get install iproute2
为了对主机进行网络更改,您还需要具有根级别的访问权限。
如何做…
受到上一个示例的启发,我们的新拓扑将看起来类似,但有一个重要的区别:

您会注意到每个主机现在都有一个名为newbridge的 Linux 桥。我们将告诉 Docker 使用这个桥而不是docker0桥来进行默认容器连接。这意味着我们只是使用 OVS 的 GRE 功能,将其变成newbridge的从属。使用 Linux 桥进行容器连接意味着 Docker 能够为我们进行 IPAM,并处理入站和出站 netfilter 规则。使用除docker0之外的桥接器更多是与配置有关,而不是可用性,我们很快就会看到。
我们将再次从头开始配置,假设每个主机只安装了 Docker 的默认配置。我们要做的第一件事是配置每个主机上将要使用的两个桥接。我们将从主机docker1开始:
user@docker1:~$ sudo apt-get install openvswitch-switch
…<Additional output removed for brevity>…
Setting up openvswitch-switch (2.0.2-0ubuntu0.14.04.3) ...
openvswitch-switch start/running
user@docker1:~$
user@docker1:~$ sudo ovs-vsctl add-br ovs_bridge
user@docker1:~$ sudo ip link set dev ovs_bridge up
user@docker1:~$
user@docker1:~$ sudo ip link add newbridge type bridge
user@docker1:~$ sudo ip link set newbridge up
user@docker1:~$ sudo ip address add 10.11.12.1/24 dev newbridge
user@docker1:~$ sudo ip link set newbridge up
此时,我们在主机上配置了 OVS 桥和标准 Linux 桥。为了完成桥接配置,我们需要在 OVS 桥上创建 GRE 接口,然后将 OVS 桥绑定到 Linux 桥上。
user@docker1:~$ sudo ovs-vsctl add-port ovs_bridge ovs_gre \
-- set interface ovs_gre type=gre options:remote_ip=192.168.50.101
user@docker1:~$
user@docker1:~$ sudo ip link set ovs_bridge master newbridge
现在桥接配置已经完成,我们可以告诉 Docker 使用newbridge作为其默认桥接。我们通过编辑 systemd drop-in 文件并添加以下选项来实现这一点:
ExecStart=/usr/bin/dockerd --bridge=newbridge --fixed-cidr=10.11.12.128/26
请注意,除了告诉 Docker 使用不同的桥接之外,我还告诉 Docker 只从10.11.12.128/26分配容器 IP 地址。当我们配置第二个 Docker 主机(docker3)时,我们将告诉 Docker 只从10.11.12.192/26分配容器 IP 地址。这是一个技巧,但它可以防止两个 Docker 主机在不知道对方已经分配了什么 IP 地址的情况下出现重叠的 IP 地址问题。
注意
第三章,“用户定义网络”表明,本地覆盖网络通过跟踪参与覆盖网络的所有主机之间的 IP 分配来解决了这个问题。
为了让 Docker 使用新的选项,我们需要重新加载系统配置并重新启动 Docker 服务:
user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl restart docker
最后,启动一个容器而不指定网络模式:
user@docker1:~$ **docker run --name web1 -d -P jonlangemak/web_server_1
82c75625f8e5436164e40cf4c453ed787eab102d3d12cf23c86d46be48673f66
user@docker1:~$
user@docker1:~$ docker exec **web1 ip addr
…<Additional output removed for brevity>…
8: **eth0**@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:0a:0b:0c:80 brd ff:ff:ff:ff:ff:ff
inet **10.11.12.128/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe0b:c80/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
正如预期的那样,我们运行的第一个容器获得了10.11.12.128/26网络中的第一个可用 IP 地址。现在,让我们继续配置第二个主机docker3:
user@docker3:~$ sudo apt-get install openvswitch-switch
…<Additional output removed for brevity>…
Setting up openvswitch-switch (2.0.2-0ubuntu0.14.04.3) ...
openvswitch-switch start/running
user@docker3:~$
user@docker3:~$ sudo ovs-vsctl add-br ovs_bridge
user@docker3:~$ sudo ip link set dev ovs_bridge up
user@docker3:~$
user@docker3:~$ sudo ip link add newbridge type bridge
user@docker3:~$ sudo ip link set newbridge up
user@docker3:~$ sudo ip address add 10.11.12.2/24 dev newbridge
user@docker3:~$ sudo ip link set newbridge up
user@docker3:~$
user@docker3:~$ sudo ip link set ovs_bridge master newbridge
user@docker3:~$ sudo ovs-vsctl add-port ovs_bridge ovs_gre \
-- set interface ovs_gre type=gre options:remote_ip=10.10.10.101
user@docker3:~$
在这个主机上,通过编辑 systemd drop-in 文件,告诉 Docker 使用以下选项:
ExecStart=/usr/bin/dockerd --bridge=newbridge --fixed-cidr=10.11.12.192/26
重新加载系统配置并重新启动 Docker 服务:
user@docker3:~$ sudo systemctl daemon-reload
user@docker3:~$ sudo systemctl restart docker
现在在这个主机上启动一个容器:
user@docker3:~$ **docker run --name web2 -d -P jonlangemak/web_server_2
eb2b26ee95580a42568051505d4706556f6c230240a9c6108ddb29b6faed9949
user@docker3:~$
user@docker3:~$ docker exec **web2 ip addr
…<Additional output removed for brevity>…
9: **eth0**@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:0a:0b:0c:c0 brd ff:ff:ff:ff:ff:ff
inet **10.11.12.192/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe0b:cc0/64 scope link
valid_lft forever preferred_lft forever
user@docker3:~$
此时,每个容器应该能够通过 GRE 隧道相互通信:
user@docker3:~$ docker exec -it **web2** curl http://**10.11.12.128
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker3:~$
此外,每个主机仍然可以通过 IPAM、发布端口和容器伪装来获得 Docker 提供的所有好处,以便进行出站访问。
我们可以验证端口发布:
user@docker1:~$ docker port **web1
80/tcp -> 0.0.0.0:**32768
user@docker1:~$ curl http://**localhost:32768
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
我们可以通过默认的 Docker 伪装规则验证出站访问:
user@docker1:~$ docker exec -it web1 ping **4.2.2.2** -c 2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=30.797 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=31.399 ms
--- 4.2.2.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 30.797/31.098/31.399/0.301 ms
user@docker1:~$
这种设置的最后一个优点是我们可以很容易地使其在主机重启后保持。唯一需要重新创建的配置将是 Linux 桥接newbridge和newbridge与 OVS 桥接之间的连接的配置。为了使其持久化,我们可以在每个主机的网络配置文件(/etc/network/interfaces)中添加以下配置。
注意
除非在主机上安装了桥接实用程序包,否则 Ubuntu 不会处理与桥接相关的接口文件中的配置。
sudo apt-get install bridge-utils
- 主机
docker1:
auto newbridge
iface newbridge inet static
address 10.11.12.1
netmask 255.255.255.0
bridge_ports ovs_bridge
- 主机
docker3:
auto newbridge
iface newbridge inet static
address 10.11.12.2
netmask 255.255.255.0
bridge_ports ovs_bridge
将newbridge配置信息放入网络启动脚本中,我们完成了两项任务。首先,在实际 Docker 服务启动之前,我们创建了 Docker 期望使用的桥接。如果没有这个,Docker 服务将无法启动,因为它找不到这个桥接。其次,这个配置允许我们通过指定桥接的bridge_ports在创建桥接的同时将 OVS 绑定到newbridge上。因为这个配置之前是通过ip link命令手动完成的,所以绑定不会在系统重启后保持。
第五章:容器链接和 Docker DNS
在本章中,我们将涵盖以下内容:
-
验证容器内的基于主机的 DNS 配置
-
覆盖默认名称解析设置
-
配置名称和服务解析的链接
-
利用 Docker DNS
-
创建 Docker DNS 别名
介绍
在前几章中,我已经指出 Docker 在网络空间为您做了很多事情。正如我们已经看到的,通过 IPAM 管理 IP 分配是使用 Docker 时并不明显的巨大好处。Docker 为您提供的另一项服务是 DNS 解析。正如我们将在本章中看到的,Docker 可以提供多个级别的名称和服务解析。随着 Docker 的成熟,提供这些类型的服务的选项也在不断增加。在本章中,我们将开始审查基本的名称解析以及容器如何知道使用哪个 DNS 服务器。然后,我们将涵盖容器链接,并了解 Docker 如何告诉容器有关其他容器和它们托管的服务。最后,我们将介绍随着用户定义网络的增加而带来的一些 DNS 增强功能。
验证容器内的基于主机的 DNS 配置
您可能没有意识到,但默认情况下,Docker 为您的容器提供了基本的名称解析手段。Docker 将名称解析选项从 Docker 主机直接传递到容器中。结果是,生成的容器可以本地解析 Docker 主机本身可以解析的任何内容。Docker 用于在容器中实现名称解析的机制非常简单。在本教程中,我们将介绍如何完成这项工作以及如何验证它是否按预期工作。
准备就绪
在本教程中,我们将演示单个 Docker 主机上的配置。假设该主机已安装 Docker,并且 Docker 处于默认配置状态。我们将在主机上更改名称解析设置,因此您需要 root 级别的访问权限。
操作步骤:
让我们在我们的主机docker1上启动一个新的容器,并检查容器如何处理名称解析:
user@docker1:~$ docker run -d -P --name=web8 \
jonlangemak/web_server_8_dns
d65baf205669c871d1216dc091edd1452a318b6522388e045c211344815c280a
user@docker1:~$
user@docker1:~$ docker exec web8 host **www.google.com
www.google.com has address **216.58.216.196
www.google.com has IPv6 address 2607:f8b0:4009:80e::2004
user@docker1:~ $
看起来容器有能力解析 DNS 名称。如果我们查看我们的本地 Docker 主机并运行相同的测试,我们应该会得到类似的结果:
user@docker1:~$ host www.google.com
www.google.com has address **216.58.216.196
www.google.com has IPv6 address 2607:f8b0:4009:80e::2004
user@docker1:~$
此外,就像我们的 Docker 主机一样,容器也可以解析与本地域lab.lab相关的本地 DNS 记录:
user@docker1:~$ docker exec web8 host **docker4
docker4.lab.lab** has address **192.168.50.102
user@docker1:~$
您会注意到,我不需要指定一个完全合格的域名来解析域lab.lab中的主机名docker4。此时,可以安全地假设容器正在从 Docker 主机接收某种智能更新,为其提供有关本地 DNS 配置的相关信息。
注意
请注意,resolv.conf文件通常是您定义 Linux 系统名称解析参数的地方。在许多情况下,它会被其他地方的配置信息自动更改。但是,无论如何更改,它都应该始终是系统处理名称解析的真相来源。
要查看容器正在接收的内容,让我们检查容器的resolv.conf文件:
user@docker1:~$ docker exec -t web8 more **/etc/resolv.conf
::::::::::::::
/etc/resolv.conf
::::::::::::::
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab
user@docker1:~$
正如您所看到的,容器已经学会了本地 DNS 服务器是10.20.30.13,本地 DNS 搜索域是lab.lab。它是从哪里获取这些信息的?答案相当简单。当容器启动时,Docker 为每个生成的容器实例生成以下三个文件的实例,并将其保存在容器配置中:
-
/etc/hostname -
/etc/hosts -
/etc/resolv.conf
这些文件作为容器配置的一部分存储,然后挂载到容器中。我们可以使用容器内的findmnt工具来检查挂载的来源:
root@docker1:~# docker exec web8 findmnt -o SOURCE
…<Additional output removed for brevity>…
/dev/mapper/docker1--vg-root[**/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/resolv.conf
/dev/mapper/docker1--vg-root[**/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/hostname]
/dev/mapper/docker1--vg-root[**/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/hosts]
root@docker1:~#
因此,虽然容器认为它在其/etc/目录中有hostname、hosts和resolv.conf文件的本地副本,但实际文件实际上位于 Docker 主机上的容器配置目录(/var/lib/docker/containers/)中。
当您告诉 Docker 运行一个容器时,它会执行以下三件事:
-
它检查 Docker 主机的
/etc/resolv.conf文件,并将其副本放在容器目录中 -
它在容器的目录中创建一个
hostname文件,并为容器分配一个唯一的hostname -
它在容器的目录中创建一个
hosts文件,并添加相关记录,包括 localhost 和引用主机本身的记录
每次容器重新启动时,容器的resolv.conf文件都会根据 Docker 主机resolv.conf文件中找到的值进行更新。这意味着每次容器重新启动时,对resolv.conf文件所做的任何更改都会丢失。hostname和hosts配置文件也会在每次容器重新启动时被重写,丢失在上一次运行期间所做的任何更改。
为了验证给定容器正在使用的配置文件,我们可以检查这些变量的容器配置:
user@docker1:~$ docker inspect web8 | grep **HostsPath
“HostsPath”: “/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/hosts”,
user@docker1:~$ docker inspect web8 | grep **HostnamePath
“HostnamePath”: “/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/hostname”,
user@docker1:~$ docker inspect web8 | grep **ResolvConfPath
“ResolvConfPath”: “/var/lib/docker/containers/c803f130b7a2450609672c23762bce3499dec9abcfdc540a43a7eb560adaf62a/resolv.conf”,
user@docker1:~$
正如预期的那样,这些是我们在容器内部运行findmnt命令时看到的相同挂载路径。这些代表了每个文件的确切挂载路径到容器的/etc/目录中的每个相应文件。
覆盖默认的名称解析设置
Docker 用于为容器提供名称解析的方法在大多数情况下都运行良好。然而,可能会有一些情况,您希望 Docker 为容器提供与 Docker 主机配置的 DNS 服务器不同的 DNS 服务器。在这些情况下,Docker 为您提供了一些选项。您可以告诉 Docker 服务为所有服务生成的容器提供不同的 DNS 服务器。您还可以通过在docker run子命令中提供 DNS 服务器作为选项,手动覆盖此设置。在本教程中,我们将向您展示更改默认名称解析行为的选项以及如何验证设置是否有效。
准备工作
在本教程中,我们将演示单个 Docker 主机上的配置。假设这个主机已经安装了 Docker,并且 Docker 处于默认配置。我们将在主机上更改名称解析设置,因此您需要 root 级别的访问权限。
操作步骤
正如我们在本章的第一个教程中看到的,默认情况下,Docker 为容器提供 Docker 主机本身使用的 DNS 服务器。这是通过复制主机的resolv.conf文件并提供给每个生成的容器。除了名称服务器设置,该文件还包括 DNS 搜索域的定义。这两个选项都可以在服务级别进行配置,以覆盖任何生成的容器,也可以在个体级别进行配置。
为了进行比较,让我们首先检查 Docker 主机的 DNS 配置:
root@docker1:~# more /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab
root@docker1:~#
通过这个配置,我们期望在这个主机上生成的任何容器都会收到相同的名称服务器和 DNS 搜索域。让我们生成一个名为web8的容器,以验证这是否按预期工作:
root@docker1:~# docker run -d -P --name=**web8** \
jonlangemak/web_server_8_dns
156bc29d28a98e2fbccffc1352ec390bdc8b9b40b84e4c5f58cbebed6fb63474
root@docker1:~#
root@docker1:~# docker exec -t web8 more /etc/resolv.conf
::::::::::::::
/etc/resolv.conf
::::::::::::::
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab
正如预期的那样,容器接收相同的配置。现在让我们检查容器,看看是否有任何与 DNS 相关的选项被定义:
user@docker1:~$ docker inspect web8 | grep Dns
“Dns”: [],
“DnsOptions”: [],
“DnsSearch”: [],
user@docker1:~$
因为我们使用默认配置,所以在容器内部针对 DNS 服务器或搜索域没有必要配置任何特定的内容。每次容器启动时,Docker 都会将主机的resolv.conf文件的设置应用到容器的 DNS 配置文件中。
如果我们希望 Docker 为容器提供不同的 DNS 服务器或 DNS 搜索域,我们可以通过 Docker 选项来实现。在这种情况下,我们感兴趣的两个选项是:
-
--dns=<DNS 服务器>:指定 Docker 应该为容器提供的 DNS 服务器地址 -
--dns-search=<DNS 搜索域>:指定 Docker 应该为容器提供的 DNS 搜索域
让我们配置 Docker 以为容器提供一个公共 DNS 服务器(4.2.2.2)和一个搜索域lab.external。我们可以通过将以下选项传递给 Docker systemd drop-in 文件来实现:
ExecStart=/usr/bin/dockerd --dns=4.2.2.2 --dns-search=lab.external
一旦配置了选项,重新加载 systemd 配置,重新启动服务以加载新选项,并重新启动我们的容器web8:
user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl restart docker
user@docker1:~$ docker start web8
web8
user@docker1:~$ docker exec -t web8 more /etc/resolv.conf
search lab.external
nameserver 4.2.2.2
user@docker1:~$
您会注意到,尽管此容器最初具有主机的 DNS 服务器(10.20.30.13)和搜索域(lab.lab),但现在它具有我们刚刚指定的服务级 DNS 选项。如果您回想一下之前,我们看到,当我们检查这个容器时,它没有定义特定的 DNS 服务器或搜索域。由于没有指定,Docker 现在使用优先级较高的 Docker 选项的设置。尽管这提供了一定程度的灵活性,但它还不够灵活。在这一点上,此服务器上生成的任何和所有容器都将提供相同的 DNS 服务器和搜索域。为了真正灵活,我们应该能够让 Docker 在每个容器级别上改变名称解析配置。幸运的是,这些选项也可以直接在容器运行时提供。

前面的图表定义了 Docker 在启动容器时决定应用哪些名称解析设置时使用的优先级。正如我们在前几章中看到的那样,容器运行时定义的设置始终优先。如果那里没有定义设置,Docker 然后会查看它们是否在服务级别上配置。如果那里没有设置,它将退回到依赖 Docker 主机的 DNS 设置的默认方法。
例如,我们可以启动一个名为web2的容器并提供不同的选项:
root@docker1:~# docker run -d **--dns=8.8.8.8 --dns-search=lab.dmz \
-P --name=web8-2 jonlangemak/web_server_8_dns
1e46d66a47b89d541fa6b022a84d702974414925f5e2dd56eeb840c2aed4880f
root@docker1:~#
如果我们检查容器,我们会看到dns和dns-search字段现在作为容器配置的一部分被定义:
root@docker1:~# docker inspect web8-2
...<Additional output removed for brevity>...
“Dns”: [
“8.8.8.8”
],
“DnsOptions”: [],
“DnsSearch”: [
“lab.dmz”
],
...<Additional output removed for brevity>...
root@docker1:~#
这确保了如果容器重新启动,它仍将具有最初在第一次运行容器时提供的相同的 DNS 设置。让我们对 Docker 服务进行一些微小的更改,以验证优先级是否按预期工作。让我们将我们的 Docker 选项更改为如下所示:
ExecStart=/usr/bin/dockerd --dns-search=lab.external
现在重新启动服务并运行以下容器:
user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl restart docker
root@docker1:~#
root@docker1:~# docker run -d -P --name=web8-3 \
jonlangemak/web_server_8_dns
5e380f8da17a410eaf41b772fde4e955d113d10e2794512cd20aa5e551d9b24c
root@docker1:~#
因为我们在容器运行时没有提供任何与 DNS 相关的选项,所以我们需要检查的下一个地方将是服务级选项。我们的 Docker 服务级选项包括一个 DNS 搜索域lab.external。我们期望容器会收到该搜索域。然而,由于我们没有定义 DNS 服务器,我们需要回退到 Docker 主机本身上配置的 DNS 服务器。
现在检查它的resolv.conf文件,确保一切按预期工作:
user@docker1:~$ docker exec -t web8-3 more /etc/resolv.conf
search lab.external
nameserver 10.20.30.13
user@docker1:~$
为名称和服务解析配置链接
容器链接提供了一种容器之间在同一主机上轻松通信的方式。正如我们在之前的例子中看到的,大多数容器之间的通信是通过 IP 地址进行的。容器链接通过允许链接的容器通过名称进行通信来改进了这一点。除了提供基本的名称解析外,它还提供了一种查看链接容器提供的服务的方法。在本教程中,我们将回顾如何创建容器链接,并讨论它们的一些局限性。
准备工作
在本教程中,我们将演示在单个 Docker 主机上的配置。假设该主机已安装 Docker,并且 Docker 处于默认配置。我们将在主机上更改名称解析设置,因此您需要 root 级别的访问权限。
如何做…
短语“容器链接”可能暗示着涉及某种网络配置或修改。实际上,容器链接与容器网络几乎没有关系。在默认模式下,容器链接提供了一种容器解析另一个容器名称的方法。例如,让我们在我们的实验主机docker1上启动两个容器:
root@docker1:~# docker run -d -P --name=**web1** jonlangemak/web_server_1
88f9c862966874247c8e2ba90c18ac673828b5faac93ff08090adc070f6d2922 root@docker1:~# docker run -d -P --name=**web2 --link=web1 \
jonlangemak/web_server_2
00066ea46367c07fc73f73bdcdff043bd4c2ac1d898f4354020cbcfefd408449
root@docker1:~#
请注意,当我启动第二个容器时,我使用了一个名为 --link 的新标志,并引用了容器 web1。我们现在会说 web2 现在链接到 web1。但是,它们实际上并没有以任何方式链接。更好的描述可能是说 web2 现在知道了 web1。让我们连接到容器 web2,以便向您展示我的意思:
root@docker1:~# docker exec -it web2 /bin/bash
root@00066ea46367:/# ping **web1** -c 2
PING **web1 (172.17.0.2)**: 48 data bytes
56 bytes from 172.17.0.2: icmp_seq=0 ttl=64 time=0.163 ms
56 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.092 ms
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.092/0.128/0.163/0.036 ms
root@00066ea46367:/#
看起来 web2 容器现在能够通过名称解析容器 web1。这是因为链接过程将记录插入到 web2 容器的 hosts 文件中:
root@00066ea46367:/# more /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 web1 88f9c8629668
172.17.0.3 00066ea46367
root@00066ea46367:/#
有了这个配置,web2 容器可以通过我们在运行时给容器的名称 (web1) 或 Docker 为容器生成的唯一 hostname 来到达 web1 容器 (88f9c8629668)。
除了更新 hosts 文件之外,web2 还生成了一些新的环境变量:
root@00066ea46367:/# printenv
WEB1_ENV_APACHE_LOG_DIR=/var/log/apache2
HOSTNAME=00066ea46367
APACHE_RUN_USER=www-data
WEB1_PORT_80_TCP=tcp://172.17.0.2:80
WEB1_PORT_80_TCP_PORT=80
LS_COLORS=
WEB1_PORT=tcp://172.17.0.2:80
WEB1_ENV_APACHE_RUN_GROUP=www-data
APACHE_LOG_DIR=/var/log/apache2
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
WEB1_PORT_80_TCP_PROTO=tcp
APACHE_RUN_GROUP=www-data
SHLVL=1
HOME=/root
WEB1_PORT_80_TCP_ADDR=172.17.0.2
WEB1_ENV_APACHE_RUN_USER=www-data
WEB1_NAME=/web2/web1
_=/usr/bin/printenv
root@00066ea46367:/#
您会注意到许多新的环境变量。Docker 将复制来自链接容器的任何环境变量,这些环境变量是作为容器的一部分定义的。这包括:
-
Docker 镜像中描述的环境变量。更具体地说,来自镜像 Dockerfile 的任何
ENV变量 -
通过
--env或-e标志在运行时传递给容器的环境变量
在这种情况下,这三个变量在镜像的 Dockerfile 中被定义为 ENV 变量:
APACHE_RUN_USER=www-data
APACHE_RUN_GROUP=www-data
APACHE_LOG_DIR=/var/log/apache2
因为两个容器镜像都定义了相同的 ENV 变量,我们将看到本地变量以及以 WEB1_ENV_ 为前缀的来自容器 web1 的相同环境变量:
WEB1_ENV_APACHE_RUN_USER=www-data
WEB1_ENV_APACHE_RUN_GROUP=www-data
WEB1_ENV_APACHE_LOG_DIR=/var/log/apache2
此外,Docker 还创建了描述 web1 容器以及其任何暴露端口的其他六个环境变量:
WEB1_PORT=tcp://172.17.0.2:80
WEB1_PORT_80_TCP=tcp://172.17.0.2:80
WEB1_PORT_80_TCP_ADDR=172.17.0.2
WEB1_PORT_80_TCP_PORT=80
WEB1_PORT_80_TCP_PROTO=tcp
WEB1_NAME=/web2/web1
链接还允许您指定别名。例如,让我们使用稍微不同的链接语法停止、删除和重新生成容器 web2:
user@docker1:~$ docker stop web2
web2
user@docker1:~$ docker rm web2
web2
user@docker1:~$ docker run -d -P --name=web2 **--link=web1:webserver \
jonlangemak/web_server_2
e102fe52f8a08a02b01329605dcada3005208d9d63acea257b8d99b3ef78e71b
user@docker1:~$
请注意,在链接定义之后,我们插入了 a :webserver. 冒号后面的名称表示链接的别名。在这种情况下,我指定了容器 web1 的别名为 webserver。
如果我们检查 web2 容器,我们会看到别名现在也列在 hosts 文件中:
root@c258c7a0884d:/# more /etc/hosts
…<Additional output removed for brevity>…
172.17.0.2 **webserver** 88f9c8629668 web1
172.17.0.3 c258c7a0884d
root@c258c7a0884d:/#
别名还会影响链接期间创建的环境变量。它们不会使用容器名称,而是使用别名:
user@docker1:~$ docker exec web2 printenv
…<Additional output removed for brevity>…
WEBSERVER**_PORT_80_TCP_ADDR=172.17.0.2
WEBSERVER**_PORT_80_TCP_PORT=80
WEBSERVER**_PORT_80_TCP_PROTO=tcp
…<Additional output removed for brevity>…
user@docker1:~$
此时,您可能想知道这有多动态。毕竟,Docker 通过更新每个容器中的静态文件来提供这个功能。如果容器的 IP 地址发生变化会发生什么?例如,让我们停止容器web1,然后使用相同的镜像启动一个名为web3的新容器:
user@docker1:~$ docker stop web1
web1
user@docker1:~$ docker run -d -P --name=web3 jonlangemak/web_server_1
69fa80be8b113a079e19ca05c8be9e18eec97b7bbb871b700da4482770482715
user@docker1:~$
如果您还记得之前,容器web1的 IP 地址是172.17.0.2。由于我停止了容器,Docker 将释放该 IP 地址的保留,使其可以重新分配给我们启动的下一个容器。让我们检查分配给容器web3的 IP 地址:
user@docker1:~$ docker exec **web3** ip addr show dev eth0
79: eth0@if80: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
正如预期的那样,web3获取了先前属于web1容器的现在开放的 IP 地址172.17.0.2。我们还可以验证容器web2仍然认为这个 IP 地址属于web1容器:
user@docker1:~$ docker exec –t web2 more /etc/hosts | grep 172.17.0.2
172.17.0.2 webserver 88f9c8629668 web1
user@docker1:~$
如果我们再次启动容器web1,我们应该看到它将获得一个新的分配给它的 IP 地址:
user@docker1:~$ docker start **web1
web1
user@docker1:~$ docker exec **web1** ip addr show dev **eth0
81: eth0@if82: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff
inet **172.17.0.4/16** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:4/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
如果我们再次检查容器web2,我们应该看到 Docker 已经更新它以引用web1容器的新 IP 地址:
user@docker1:~$ docker exec **web2** more /etc/hosts | grep **web1
172.17.0.4 webserver 88f9c8629668 web1
user@docker1:~$
然而,虽然 Docker 负责更新hosts文件中的新 IP 地址,但它不会负责更新任何环境变量以反映新的 IP 地址:
user@docker1:~$ docker exec web2 printenv
…<Additional output removed for brevity>…
WEBSERVER**_PORT=tcp://**172.17.0.2:80
WEBSERVER**_PORT_80_TCP=tcp://**172.17.0.2:80
WEBSERVER**_PORT_80_TCP_ADDR=**172.17.0.2
…<Additional output removed for brevity>…
user@docker1:~$
此外,应该指出,这个链接只是单向的。也就是说,这个链接不会使容器web1意识到web2容器。Web1不会接收主机记录或引用web2容器的环境变量:
user@docker1:~$ docker exec -it **web1 ping web2
ping: unknown host
user@docker1:~$
另一个配置链接的原因是当您将 Docker 容器间连接(ICC)模式设置为false时。正如我们之前讨论过的,ICC 阻止同一网桥上的任何容器直接交流。这迫使它们只能通过发布的端口进行交流。链接提供了一个机制来覆盖默认的 ICC 规则。为了演示,让我们停止并删除主机docker1上的所有容器,然后将以下 Docker 选项添加到 systemd drop-in 文件中:
ExecStart=/usr/bin/dockerd --icc=false
现在重新加载 systemd 配置,重新启动服务,并启动以下容器:
docker run -d -P --name=web1 jonlangemak/web_server_1
docker run -d -P --name=web2 jonlangemak/web_server_2
在 ICC 模式下,您会注意到容器无法直接交流:
user@docker1:~$ docker exec **web1** ip addr show dev eth0
87: eth0@if88: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$ docker exec -it **web2** curl http://**172.17.0.2
user@docker1:~$
在上面的例子中,web2无法访问web1上的 Web 服务器。现在,让我们删除并重新创建web2容器,这次将其链接到web1:
user@docker1:~$ docker stop web2
web2
user@docker1:~$ docker rm web2
web2
user@docker1:~$ docker run -d -P --name=web2 **--link=web1 \
jonlangemak/web_server_2
4c77916bb08dfc586105cee7ae328c30828e25fcec1df55f8adba8545cbb2d30
user@docker1:~$ docker exec -it **web2** curl http://**172.17.0.2
<body>
<html>
<h1><span style=”color:#FF0000;font-size:72px;”>**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
我们可以看到,链接建立后,通信按预期允许。再次强调,就像链接一样,这种访问是单向允许的。
应该注意的是,在使用用户定义网络时,链接的工作方式不同。在本教程中,我们涵盖了现在被称为传统链接的内容。连接到用户定义网络将在接下来的两个教程中介绍。
利用 Docker DNS
用户定义网络的引入标志着 Docker 网络的重大变化。虽然提供自定义网络的能力是重大新闻,但名称解析也有了重大改进。用户定义网络可以受益于被称为嵌入式 DNS的功能。Docker 引擎本身现在具有为所有容器提供名称解析的能力。这是与传统解决方案相比的显著改进,传统解决方案中名称解析的唯一手段是外部 DNS 或依赖hosts文件的链接。在本教程中,我们将介绍如何使用和配置嵌入式 DNS。
准备工作
在本教程中,我们将演示在单个 Docker 主机上的配置。假设该主机已安装了 Docker,并且 Docker 处于默认配置状态。我们将在主机上更改名称解析设置,因此您需要 root 级别的访问权限。
操作步骤…
如前所述,嵌入式 DNS 系统仅在用户定义的 Docker 网络上运行。也就是说,让我们提供一个用户定义的网络,然后在其上启动一个简单的容器:
user@docker1:~$ docker network create -d bridge **mybridge1
0d75f46594eb2df57304cf3a2b55890fbf4b47058c8e43a0a99f64e4ede98f5f
user@docker1:~$ docker run -d -P --name=web1 **--net=mybridge1 \
jonlangemak/web_server_1
3a65d84a16331a5a84dbed4ec29d9b6042dde5649c37bc160bfe0b5662ad7d65
user@docker1:~$
正如我们在之前的教程中看到的,默认情况下,Docker 从 Docker 主机获取名称解析配置,并将其提供给容器。可以通过在服务级别或容器运行时提供不同的 DNS 服务器或搜索域来更改此行为。对于连接到用户定义网络的容器,提供给容器的 DNS 设置略有不同。例如,让我们看看刚刚连接到用户定义桥接mybridge1的容器的resolv.conf文件:
user@docker1:~$ docker exec -t web1 more /etc/resolv.conf
search lab.lab
nameserver 127.0.0.11
options ndots:0
user@docker1:~$
注意这个容器的名称服务器现在是127.0.0.11。这个 IP 地址代表 Docker 的嵌入式 DNS 服务器,并将用于任何连接到用户定义网络的容器。任何连接到用户定义网络的容器都应该使用嵌入式 DNS 服务器。
最初未在用户定义的网络上启动的容器将在连接到用户定义的网络时进行更新。例如,让我们启动另一个名为web2的容器,但让它使用默认的docker0桥接:
user@docker1:~$ docker run -dP --name=web2 jonlangemak/web_server_2
d0c414477881f03efac26392ffbdfb6f32914597a0a7ba578474606d5825df3f
user@docker1:~$ docker exec -t web2 more /etc/resolv.conf
::::::::::::::
/etc/resolv.conf
::::::::::::::
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab
user@docker1:~$
如果我们现在将web2容器连接到我们自定义的网络,Docker 将更新名称服务器以反映嵌入式 DNS 服务器:
user@docker1:~$ docker network connect mybridge1 web2
user@docker1:~$ docker exec -t web2 more /etc/resolv.conf
search lab.lab
nameserver 127.0.0.11
options ndots:0
user@docker1:~$
由于我们的两个容器现在都连接到同一个用户定义的网络,它们现在可以通过名称相互访问:
user@docker1:~$ docker exec -t **web1** ping **web2** -c 2
PING web2 (172.18.0.3): 48 data bytes
56 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.107 ms
56 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.087 ms
--- web2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.087/0.097/0.107/0.000 ms
user@docker1:~$ docker exec -t **web2** ping **web1** -c 2
PING web1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.060 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.119 ms
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.060/0.089/0.119/0.030 ms
user@docker1:~$
您会注意到名称解析是双向的,并且它在没有任何链接的情况下固有地工作。也就是说,使用用户定义的网络,我们仍然可以定义链接,以便创建本地别名。例如,让我们停止并删除web1和web2两个容器,然后重新配置它们如下:
user@docker1:~$ docker run -d -P --name=**web1** --net=mybridge1 \
--link=web2:thesecondserver** jonlangemak/web_server_1
fd21c53def0c2255fc20991fef25766db9e072c2bd503c7adf21a1bd9e0c8a0a
user@docker1:~$ docker run -d -P --name=**web2** --net=mybridge1 \
--link=web1:thefirstserver** jonlangemak/web_server_2
6e8f6ab4dec7110774029abbd69df40c84f67bcb6a38a633e0a9faffb5bf625e
user@docker1:~$
要指出的第一件有趣的事情是,Docker 允许我们链接到尚不存在的容器。当我们运行容器web1时,我们要求 Docker 将其链接到容器web2。那时,web2并不存在。这是链接与嵌入式 DNS 服务器工作方式的一个显着差异。在传统的链接中,Docker 需要在进行链接之前知道目标容器的信息。这是因为它必须手动更新源容器的主机文件和环境变量。第二个有趣的事情是,别名不再列在容器的hosts文件中。如果我们查看每个容器的hosts文件,我们会发现链接不再生成条目:
user@docker1:~$ docker exec -t web1 more /etc/resolv.conf
search lab.lab
nameserver 127.0.0.11
options ndots:0
user@docker1:~$ docker exec -t web1 more /etc/hosts
…<Additional output removed for brevity>…
172.18.0.2 9cee9ce88cc3
user@docker1:~$
user@docker1:~$ docker exec -t web2 more /etc/hosts
…<Additional output removed for brevity>…
172.18.0.3 2d4b63452c8a
user@docker1:~$
现在所有的解析都是在嵌入式 DNS 服务器中进行的。这包括跟踪定义的别名及其范围。因此,即使没有主机记录,每个容器也能够通过嵌入式 DNS 服务器解析其他容器的别名:
user@docker1:~$ docker exec -t web1 ping **thesecondserver** -c2
PING thesecondserver (172.18.0.3): 48 data bytes
56 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.067 ms
56 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.067 ms
--- thesecondserver ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.067/0.067/0.067/0.000 ms
user@docker1:~$ docker exec -t web2 ping **thefirstserver** -c 2
PING thefirstserver (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.062 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.042 ms
--- thefirstserver ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.042/0.052/0.062/0.000 ms
user@docker1:~$
创建的别名的范围仅限于容器本身。例如,同一用户定义的网络上的第三个容器无法解析链接的一部分创建的别名:
user@docker1:~$ docker run -d -P --name=web3 --net=**mybridge1** \
jonlangemak/web_server_1
d039722a155b5d0a702818ce4292270f30061b928e05740d80bb0c9cb50dd64f
user@docker1:~$ docker exec -it web3 ping **thefirstserver** -c 2
ping: unknown host
user@docker1:~$ docker exec -it web3 ping **thesecondserver** -c 2
ping: unknown host
user@docker1:~$
您会记得,传统的链接还会自动在源容器上创建一组环境变量。这些环境变量引用了目标容器和它可能正在暴露的任何端口。在用户定义的网络中进行链接不会创建这些环境变量:
user@docker1:~$ docker exec web1 printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=4eba77b66d60
APACHE_RUN_USER=www-data
APACHE_RUN_GROUP=www-data
APACHE_LOG_DIR=/var/log/apache2
HOME=/root
user@docker1:~$
正如我们在上一个示例中看到的,即使使用传统的链接,也无法保持这些变量的最新状态。也就是说,当处理用户定义的网络时,功能不存在并不是完全令人惊讶。
除了提供本地容器解析外,嵌入式 DNS 服务器还处理任何外部请求。正如我们在前面的示例中看到的,来自 Docker 主机(在我的情况下是lab.lab)的搜索域仍然被传递给容器,并在它们的resolv.conf文件中配置。从主机学习的名称服务器成为嵌入式 DNS 服务器的转发器。这允许嵌入式 DNS 服务器处理任何容器名称解析请求,并将外部请求移交给 Docker 主机使用的名称服务器。这种行为可以在服务级别或在运行时通过传递--dns或--dns-search标志来覆盖。例如,我们可以启动web1容器的另外两个实例,并在任何情况下指定特定的 DNS 服务器:
user@docker1:~$ docker run -dP --net=mybridge1 --name=web4 \
--dns=10.20.30.13** jonlangemak/web_server_1
19e157b46373d24ca5bbd3684107a41f22dea53c91e91e2b0d8404e4f2ccfd68
user@docker1:~$ docker run -dP --net=mybridge1 --name=web5 \
--dns=8.8.8.8** jonlangemak/web_server_1
700f8ac4e7a20204100c8f0f48710e0aab8ac0f05b86f057b04b1bbfe8141c26
user@docker1:~$
注意
请注意,即使我们没有明确指定,web4也会接收10.20.30.13作为 DNS 转发器。这是因为这也是 Docker 主机使用的 DNS 服务器,当未指定时,容器会继承自主机。这里为了示例而指定。
现在,如果我们尝试在任何一个容器上解析本地 DNS 记录,我们可以看到,在web1的情况下它可以工作,因为它定义了本地 DNS 服务器,而在web2上的查找失败,因为8.8.8.8不知道lab.lab域:
user@docker1:~$ docker exec -it **web4 ping docker1.lab.lab** -c 2
PING docker1.lab.lab (10.10.10.101): 48 data bytes
56 bytes from 10.10.10.101: icmp_seq=0 ttl=64 time=0.080 ms
56 bytes from 10.10.10.101: icmp_seq=1 ttl=64 time=0.078 ms
--- docker1.lab.lab ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.078/0.079/0.080/0.000 ms
user@docker1:~$ docker exec -it **web5 ping docker1.lab.lab** -c 2
ping: unknown host
user@docker1:~$
创建 Docker DNS 别名
在嵌入式 DNS 之前,将容器别名为不同名称的唯一方法是使用链接。正如我们在之前的示例中看到的,这仍然是用于创建本地化或特定于容器的别名的方法。但是,如果您想要具有更大范围的别名,任何连接到给定网络的容器都可以解析的别名呢?嵌入式 DNS 服务器提供了所谓的网络范围别名,这些别名可以在给定的用户定义网络中解析。在本示例中,我们将向您展示如何在用户定义的网络中创建网络范围的别名。
准备工作
在本示例中,我们将演示在单个 Docker 主机上的配置。假设该主机已安装 Docker,并且 Docker 处于默认配置状态。我们将更改主机上的名称解析设置,因此您需要 root 级别的访问权限。
如何做…
网络别名可以以几种不同的方式定义。它们可以在容器运行时定义,也可以在将容器连接到网络时定义。再次强调,网络别名是仅在容器实现用户定义网络时提供的功能。您不能在不同时指定用户定义网络的情况下创建网络别名。Docker 将阻止您在容器运行时指定它们:
user@docker1:~$ docker run -dP --name=web1 --net-alias=webserver1 \
jonlangemak/web_server_1
460f587d0fb3e70842b37736639c150b6d333fd0b647345aa7ed9e0505ebfd2d
docker: Error response from daemon: Network-scoped alias is supported only for containers in user defined networks.
user@docker1:~$
如果我们创建一个用户定义的网络并将其指定为容器配置的一部分,该命令将成功执行:
user@docker1:~$ docker network create -d bridge **mybridge1
663f9fe0b4a0dbf7a0be3c4eaf8da262f7e2b3235de252ed5a5b481b68416ca2
user@docker1:~$ docker run -dP --name=web1 --**net=mybridge1 \
--net-alias=webserver1** jonlangemak/web_server_1
05025adf381c7933f427e647a512f60198b29a3cd07a1d6126bc9a6d4de0a279
user@docker1:~$
一旦别名被创建,我们可以将其视为特定容器配置的一部分。例如,如果我们现在检查容器web1,我们将在其网络配置下看到一个定义的别名:
user@docker1:~$ docker inspect **web1
…<Additional output removed for brevity>…
“mybridge1”: {
“IPAMConfig”: null,
“Links”: null,
“Aliases”: [
“**webserver1**”,
“6916ac68c459”
],
“NetworkID”: “a75b46cc785b88ddfbc83ad7b6ab7ced88bbafef3f64e3e4314904fb95aa9e5c”,
“EndpointID”: “620bc4bf9962b7c6a1e59a3dad8d3ebf25831ea00fea4874a9a5fcc750db5534”,
“Gateway”: “172.18.0.1”,
“IPAddress”: “172.18.0.2”,
…<Additional output removed for brevity>…
user@docker1:~$
现在,让我们启动另一个名为web2的容器,并看看我们是否可以解析别名:
user@docker1:~$ docker run -dP --name=web2 **--net=mybridge1 \
jonlangemak/web_server_2
9b6d23ce868bf62999030a8c1eb29c3ca7b3836e8e3cbb7247d4d8e12955f117
user@docker1:~$ docker exec -it **web2** ping **webserver1** -c 2
PING webserver1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.104 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.091 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.091/0.098/0.104/0.000 ms
user@docker1:~$
这里有几件有趣的事情要指出。首先,定义别名的方法与链接方法有很大不同,不仅仅是范围。通过链接,源容器指定了它希望将目标容器别名为的内容。在网络别名的情况下,源容器设置了自己的别名。
其次,这只能工作是因为容器web2在与web1相同的用户定义网络上。因为别名的范围是整个用户定义的网络,这意味着同一个容器在不同的用户定义网络上可以使用不同的别名。例如,让我们创建另一个用户定义的网络:
user@docker1:~$ docker network create -d bridge **mybridge2
d867d7ad3a1f639cde8926405acd3a36e99352f0e2a45871db5263caf3b59c44
user@docker1:~$
现在,让我们将容器web1连接到它:
user@docker1:~$ docker network connect --**alias=fooserver** mybridge2 web1
回想一下,我们说过您可以在network connect子命令的一部分中定义网络范围的别名:
user@docker1:~$ docker inspect **web1
…<Additional output removed for brevity>…
“**mybridge1**”: {
“IPAMConfig”: null,
“Links”: null,
“**Aliases**”: [
“**webserver1**”,
“6916ac68c459”
],
“NetworkID”: “a75b46cc785b88ddfbc83ad7b6ab7ced88bbafef3f64e3e4314904fb95aa9e5c”,
“EndpointID”: “620bc4bf9962b7c6a1e59a3dad8d3ebf25831ea00fea4874a9a5fcc750db5534”,
“Gateway”: “172.18.0.1”,
“IPAddress”: “172.18.0.2”,
“IPPrefixLen”: 16,
“IPv6Gateway”: “”,
“GlobalIPv6Address”: “”,
“GlobalIPv6PrefixLen”: 0,
“MacAddress”: “02:42:ac:12:00:02”
},
“**mybridge2**”: {
“IPAMConfig”: {},
“Links”: null,
“**Aliases**”: [
“**fooserver**”,
“6916ac68c459”
],
“NetworkID”: “daf24590cc8f9c9bf859eb31dab42554c6c14c1c1e4396b3511524fe89789a58”,
“EndpointID”: “a36572ec71077377cebfe750f4e533e0316669352894b93df101dcdabebf9fa7”,
“Gateway”: “172.19.0.1”,
“IPAddress”: “172.19.0.2”,
user@docker1:~$
请注意,容器web1现在有两个别名,一个在每个网络上。因为容器web2只连接到一个网络,所以它仍然只能解析与mybridge1网络关联的别名:
user@docker1:~$ docker exec -it **web2 ping webserver1 -c 2
PING webserver1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.079 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.123 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.079/0.101/0.123/0.000 ms
user@docker1:~$ docker exec -it **web2 ping fooserver -c 2
ping: unknown host
user@docker1:~$
然而,一旦我们将web2连接到mybridge2网络,它现在可以解析两个别名:
user@docker1:~$ docker network connect **mybridge2 web2
user@docker1:~$ docker exec -it **web2 ping webserver1 -c 2
PING webserver1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.064 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.097 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.064/0.081/0.097/0.000 ms
user@docker1:~$ docker exec -**it web2 ping fooserver -c 2
PING fooserver (172.19.0.2): 48 data bytes
56 bytes from 172.19.0.2: icmp_seq=0 ttl=64 time=0.080 ms
56 bytes from 172.19.0.2: icmp_seq=1 ttl=64 time=0.087 ms
--- fooserver ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.080/0.083/0.087/0.000 ms
user@docker1:~$
有趣的是,Docker 还允许您将相同的别名定义为多个容器。例如,现在让我们启动一个名为web3的第三个容器,并使用与web1(webserver1)相同的别名将其连接到mybridge1:
user@docker1:~$ docker run -dP **--name=web3 --net=mybridge1 \
--net-alias=webserver1** jonlangemak/web_server_1
cdf22ba64231553dd7e876b5718e155b1312cca68a621049e04265f5326e063c
user@docker1:~$
别名现在已经为容器web1和web2定义。但是,尝试从web2解析别名仍然指向web1:
user@docker1:~$ docker exec -**it web2 ping webserver1 -c 2
PING webserver1 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.066 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.088 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.066/0.077/0.088/0.000 ms
user@docker1:~$
如果我们断开或停止容器web1,我们应该会看到分辨率现在改变为web3,因为它仍然在网络上活动,并且具有相同的别名:
user@docker1:~$ **docker stop web1
web1
user@docker1:~$ docker exec -it **web2 ping webserver1 -c 2
PING webserver1 (**172.18.0.4**): 48 data bytes
56 bytes from 172.18.0.4: icmp_seq=0 ttl=64 time=0.085 ms
56 bytes from 172.18.0.4: icmp_seq=1 ttl=64 time=0.091 ms
--- webserver1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.085/0.088/0.091/0.000 ms
user@docker1:~$
这个功能可以为您提供一些有趣的选择,特别是在与叠加网络类型配合使用时,可以实现高可用性或故障转移。
值得注意的是,这个功能适用于所有用户定义的网络类型,包括叠加网络类型。我们在这些示例中使用桥接来保持示例简单。
第六章:保护容器网络
在本章中,我们将涵盖以下示例:
-
启用和禁用 ICC
-
禁用出站伪装
-
管理 netfilter 到 Docker 集成
-
创建自定义 iptables 规则
-
通过负载均衡器公开服务
介绍
随着您转向基于容器的应用程序,您需要认真考虑的一项内容是网络安全。特别是容器可能导致需要保护的网络端点数量激增。当然,并非所有端点都完全暴露在网络中。然而,默认情况下,那些没有完全暴露的端点会直接相互通信,这可能会引起其他问题。在涉及基于容器的应用程序时,有许多方法可以解决网络安全问题,本章并不旨在解决所有可能的解决方案。相反,本章旨在审查配置选项和相关网络拓扑,这些选项可以根据您自己的网络安全要求以多种不同的方式组合。我们将详细讨论一些我们在早期章节中接触到的功能,如 ICC 模式和出站伪装。此外,我们将介绍一些不同的技术来限制容器的网络暴露。
启用和禁用 ICC
在早期章节中,我们接触到了 ICC 模式的概念,但对其工作机制并不了解。ICC 是 Docker 本地的一种方式,用于隔离连接到同一网络的所有容器。提供的隔离可以防止容器直接相互通信,同时允许它们的暴露端口被发布,并允许出站连接。在这个示例中,我们将审查在默认的docker0桥接上下文以及用户定义的网络中基于 ICC 的配置选项。
准备工作
在这个示例中,我们将使用两个 Docker 主机来演示 ICC 在不同网络配置中的工作方式。假设本实验室中使用的两个 Docker 主机都处于默认配置。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
操作方法…
ICC 模式可以在原生的docker0桥以及使用桥驱动的任何用户定义的网络上进行配置。在本教程中,我们将介绍如何在docker0桥上配置 ICC 模式。正如我们在前几章中看到的,与docker0桥相关的设置需要在服务级别进行。这是因为docker0桥是作为服务初始化的一部分创建的。这也意味着,要对其进行更改,我们需要编辑 Docker 服务配置,然后重新启动服务以使更改生效。在进行任何更改之前,让我们有机会审查默认的 ICC 配置。为此,让我们首先查看docker0桥的配置:
user@docker1:~$ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "d88fa0a96585792f98023881978abaa8c5d05e4e2bbd7b4b44a6e7b0ed7d346b",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Containers": {},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
user@docker1:~$
注意
重要的是要记住,docker network子命令用于管理所有 Docker 网络。一个常见的误解是它只能用于管理用户定义的网络。
正如我们所看到的,docker0桥配置为 ICC 模式(true)。这意味着 Docker 不会干预或阻止连接到这个桥的容器直接相互通信。为了证明这一点,让我们启动两个容器:
user@docker1:~$ docker run -d --name=web1 jonlangemak/web_server_1
417dd2587dfe3e664b67a46a87f90714546bec9c4e35861476d5e4fa77e77e61
user@docker1:~$ docker run -d --name=web2 jonlangemak/web_server_2
a54db26074c00e6771d0676bb8093b1a22eb95a435049916becd425ea9587014
user@docker1:~$
请注意,我们没有指定-P标志,这告诉 Docker 不要发布任何容器暴露的端口。现在,让我们获取每个容器的 IP 地址,以便验证连接:
user@docker1:~$ docker exec **web1** ip addr show dev eth0 | grep inet
inet **172.17.0.2/16** scope global eth0
inet6 fe80::42:acff:fe11:2/64 scope link
user@docker1:~$ docker exec **web2** ip addr show dev eth0 | grep inet
inet **172.17.0.3/16** scope global eth0
inet6 fe80::42:acff:fe11:3/64 scope link
user@docker1:~$
现在我们知道了 IP 地址,我们可以验证每个容器是否可以访问另一个容器在其上监听的任何服务:
user@docker1:~$ docker exec -it **web1** ping **172.17.0.3** -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
56 bytes from 172.17.0.3: icmp_seq=0 ttl=64 time=0.198 ms
56 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.082 ms
--- 172.17.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.082/0.140/0.198/0.058 ms
user@docker1:~$
user@docker1:~$ docker exec **web2** curl -s **http://172.17.0.2
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
根据这些测试,我们可以假设容器被允许在任何监听的协议上相互通信。这是启用 ICC 模式时的预期行为。现在,让我们更改服务级别设置并重新检查我们的配置。为此,在 Docker 服务的 systemd drop in 文件中设置以下配置:
ExecStart=/usr/bin/dockerd --icc=false
现在重新加载 systemd 配置,重新启动 Docker 服务,并检查 ICC 设置:
user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl restart docker
user@docker1:~$ docker network inspect bridge
…<Additional output removed for brevity>…
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "false",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
…<Additional output removed for brevity>…
user@docker1:~$
现在我们已经确认了 ICC 被禁用,让我们再次启动我们的两个容器并运行相同的连接性测试:
user@docker1:~$ docker start web1
web1
user@docker1:~$ docker start web2
web2
user@docker1:~$
user@docker1:~$ docker exec -it **web1** ping **172.17.0.3** -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
user@docker1:~$ docker exec -it **web2** curl -m 1 http://172.17.0.2
curl: (28) connect() timed out!
user@docker1:~$
如您所见,我们的两个容器之间没有连接。但是,Docker 主机本身仍然能够访问服务:
user@docker1:~$ curl **http://172.17.0.2
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$ **curl http://172.17.0.3
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
我们可以检查用于实现 ICC 的 netfilter 规则,方法是查看过滤表的iptables规则FORWARD链:
user@docker1:~$ sudo iptables -S FORWARD
-P FORWARD ACCEPT
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j DROP
user@docker1:~$
前面加粗的规则是防止在docker0桥上进行容器之间通信的。如果在禁用 ICC 之前检查了这个iptables链,我们会看到这个规则设置为ACCEPT,如下所示:
user@docker1:~$ sudo iptables -S FORWARD
-P FORWARD ACCEPT
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
user@docker1:~$
正如我们之前所看到的,链接容器允许您绕过这一规则,允许源容器访问目标容器。如果我们移除这两个容器,我们可以通过以下方式重新启动它们:
user@docker1:~$ **docker run -d --name=web1 jonlangemak/web_server_1
9846614b3bac6a2255e135d19f20162022a40d95bd62a0264ef4aaa89e24592f
user@docker1:~$ **docker run -d --name=web2 --link=web1 jonlangemak/web_server_2
b343b570189a0445215ad5406e9a2746975da39a1f1d47beba4d20f14d687d83
user@docker1:~$
现在,如果我们用iptables检查规则,我们可以看到两个新规则添加到了过滤表中:
user@docker1:~$ sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j DROP
-A DOCKER -s 172.17.0.3/32 -d 172.17.0.2/32 -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER -s 172.17.0.2/32 -d 172.17.0.3/32 -i docker0 -o docker0 -p tcp -m tcp --sport 80 -j ACCEPT
-A DOCKER-ISOLATION -j RETURN
user@docker1:~$
这两个新规则允许web2访问web1的任何暴露端口。请注意,第一个规则定义了从web2(172.17.0.3)到web1(172.17.0.2)的访问,目的端口为80。第二个规则翻转了 IP,并指定端口80作为源端口,允许流量返回到web2。
注意
早些时候,当我们讨论用户定义的网络时,您看到我们可以将 ICC 标志传递给用户定义的桥接。然而,目前不支持使用覆盖驱动程序禁用 ICC 模式。
禁用出站伪装
默认情况下,容器允许通过伪装或隐藏其真实 IP 地址在 Docker 主机的 IP 地址后访问外部网络。这是通过 netfilter masquerade规则实现的,这些规则将容器流量隐藏在下一跳中引用的 Docker 主机接口后面。当我们讨论跨主机的容器之间的连接时,我们在第二章配置和监控 Docker 网络中看到了这方面的详细示例。虽然这种类型的配置在许多方面都是理想的,但在某些情况下,您可能更喜欢禁用出站伪装功能。例如,如果您不希望容器完全具有出站连接性,禁用伪装将阻止容器与外部网络通信。然而,这只是由于缺乏返回路由而阻止了出站流量。更好的选择可能是将容器视为任何其他单独的网络端点,并使用现有的安全设备来定义网络策略。在本教程中,我们将讨论如何禁用 IP 伪装以及如何在容器在外部网络中进行遍历时提供唯一的 IP 地址。
准备工作
在本示例中,我们将使用单个 Docker 主机。假设在此实验中使用的 Docker 主机处于其默认配置中。您还需要访问更改 Docker 服务级别设置。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。我们还将对 Docker 主机连接的网络设备进行更改。
如何做…
您会记得,Docker 中的 IP 伪装是通过 netfilter masquerade规则处理的。在其默认配置中的 Docker 主机上,我们可以通过使用iptables检查规则集来看到这个规则:
user@docker1:~$ sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
user@docker1:~$
此规则指定流量的来源为docker0网桥子网,只有 NAT 流量可以离开主机。MASQUERADE目标告诉主机对 Docker 主机的下一跳接口的流量进行源 NAT。也就是说,如果主机有多个 IP 接口,容器的流量将源 NAT 到下一跳使用的任何接口。这意味着根据 Docker 主机接口和路由表配置,容器流量可能潜在地隐藏在不同的 IP 地址后面。例如,考虑一个具有两个接口的 Docker 主机,如下图所示:

在左侧示例中,流量正在采用默认路由,因为4.2.2.2的目的地在主机的路由表中没有更具体的前缀。在这种情况下,主机执行源 NAT,并在流经 Docker 主机到外部网络时将流量的源从172.17.0.2更改为10.10.10.101。但是,如果目的地落入172.17.0.0/16,容器流量将被隐藏在右侧示例中所示的192.168.10.101接口后面。
Docker 的默认行为可以通过操纵--ip-masq Docker 选项来更改。默认情况下,该选项被认为是true,可以通过指定该选项并将其设置为false来覆盖。我们可以通过在 Docker systemd drop in 文件中指定该选项来实现这一点:
ExecStart=/usr/bin/dockerd --ip-masq=false
现在重新加载 systemd 配置,重新启动 Docker 服务,并检查 ICC 设置:
user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl restart docker
user@docker1:~$
user@docker1:~$ sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKERuser@docker1:~$
注意,masquerade规则现在已经消失。在此主机上生成的容器流量将尝试通过其实际源 IP 地址路由到 Docker 主机外部。在 Docker 主机上进行tcpdump将捕获此流量通过原始容器 IP 地址退出主机的eth0接口:
user@docker1:~$ sudo tcpdump –n -i **eth0** dst 4.2.2.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
09:06:10.243523 IP **172.17.0.2 > 4.2.2.2**: ICMP echo request, id 3072, seq 0, length 56
09:06:11.244572 IP **172.17.0.2 > 4.2.2.2**: ICMP echo request, id 3072, seq 256, length 56
由于外部网络不知道172.17.0.0/16在哪里,这个请求将永远不会收到响应,有效地阻止了容器与外部世界的通信。
虽然这可能是阻止与外部世界通信的一种有用手段,但并不完全理想。首先,您仍然允许流量出去;响应只是不知道要返回到哪里,因为它试图返回到源。此外,您影响了 Docker 主机上所有网络的所有容器。如果docker0桥接分配了一个可路由的子网,并且外部网络知道该子网的位置,您可以使用现有的安全工具来制定安全策略决策。
例如,假设docker0桥接被分配了一个子网172.10.10.0/24,并且我们禁用了 IP 伪装。我们可以通过更改 Docker 选项来指定新的桥接 IP 地址来实现这一点:
ExecStart=/usr/bin/dockerd --ip-masq=false **--bip=172.10.10.1/24
与以前一样,离开容器并前往外部网络的流量在穿过 Docker 主机时不会改变。假设一个小的网络拓扑,如下图所示:

假设从容器到4.2.2.2的流量。在这种情况下,出口流量应该天然工作:
-
容器生成流量到
4.2.2.2,并使用它的默认网关,即docker0桥接 IP 地址 -
Docker 主机进行路由查找,未能找到特定的前缀匹配,并将流量转发到其默认网关,即交换机。
-
交换机进行路由查找,未能找到特定的前缀匹配,并将流量转发到其默认路由,即防火墙。
-
防火墙进行路由查找,未能找到特定的前缀匹配,确保流量在策略中被允许,执行隐藏 NAT 到公共 IP 地址,并将流量转发到其默认路由,即互联网。
因此,没有任何额外的配置,出口流量应该能够到达目的地。问题在于返回流量。当来自互联网目的地的响应返回到防火墙时,它将尝试确定如何返回到源。这个路由查找可能会失败,导致防火墙丢弃流量。
注意
在某些情况下,边缘网络设备(在本例中是防火墙)将所有私有 IP 地址路由回内部(在本例中是交换机)。在这种情况下,防火墙可能会将返回流量转发到交换机,但交换机没有特定的返回路由,导致了同样的问题。
为了使其工作,防火墙和交换机需要知道如何将流量返回到特定的容器。为此,我们需要在每个设备上添加特定的路由,将docker0桥接子网指向docker1主机:

一旦这些路由设置好,在 Docker 主机上启动的容器应该能够连接到外部网络:
user@docker1:~$ docker run -it --name=web1 jonlangemak/web_server_1 /bin/bash
root@132530812e1f:/# **ping 4.2.2.2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=33.805 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=40.431 ms
在 Docker 主机上进行tcpdump将显示流量以原始容器 IP 地址离开:
user@docker1:~$ sudo tcpdump –n **-i eth0 dst 4.2.2.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
10:54:42.197828 IP **172.10.10.2 > 4.2.2.2**: ICMP echo request, id 3328, seq 0, length 56
10:54:43.198882 IP **172.10.10.2 > 4.2.2.2**: ICMP echo request, id 3328, seq 256, length 56
这种类型的配置提供了使用现有安全设备来决定容器是否可以访问外部网络资源的能力。但是,这也取决于安全设备与您的 Docker 主机的距离。例如,在这种配置中,Docker 主机上的容器可以访问连接到交换机的任何其他网络端点。执行点(在本例中是防火墙)只允许您限制容器与互联网的连接。此外,为每个 Docker 主机分配可路由的 IP 空间可能会引入 IP 分配约束,特别是在大规模情况下。
管理 netfilter 到 Docker 的集成
默认情况下,Docker 会为您执行大部分 netfilter 配置。它会处理诸如发布端口和出站伪装之类的事情,并允许您阻止或允许 ICC。但是,这都是可选的,您可以告诉 Docker 不要修改或添加任何现有的iptables规则。如果这样做,您将需要生成自己的规则来提供类似的功能。如果您已经广泛使用iptables规则,并且不希望 Docker 自动更改您的配置,这可能会对您有吸引力。在本教程中,我们将讨论如何禁用 Docker 自动生成iptables规则,并向您展示如何手动创建类似的规则。
准备工作
在本示例中,我们将使用单个 Docker 主机。假设在本实验中使用的 Docker 主机处于其默认配置中。您还需要访问更改 Docker 服务级别的设置。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何做…
正如我们已经看到的,当涉及到网络配置时,Docker 会为您处理很多繁重的工作。它还允许您在需要时自行配置这些内容。在我们自己尝试配置之前,让我们确认一下 Docker 实际上在我们的iptables规则方面为我们配置了什么。让我们运行以下容器:
user@docker1:~$ docker run -dP --name=web1 jonlangemak/web_server_1
f5b7b389890398588c55754a09aa401087604a8aa98dbf55d84915c6125d5e62
user@docker1:~$ docker run -dP --name=web2 jonlangemak/web_server_2
e1c866892e7f3f25dee8e6ba89ec526fa3caf6200cdfc705ce47917f12095470
user@docker1:~$
运行这些容器将产生以下拓扑结构:

注意
稍后给出的示例将不直接使用主机的eth1接口。它只是用来说明 Docker 生成的规则是以涵盖 Docker 主机上的所有物理接口的方式编写的。
正如我们之前提到的,Docker 使用iptables来处理以下项目:
-
出站容器连接(伪装)
-
入站端口发布
-
容器之间的连接
由于我们使用的是默认配置,并且我们已经在两个容器上发布了端口,我们应该能够在iptables中看到这三个项目的配置。让我们首先查看 NAT 表:
注意
在大多数情况下,我更喜欢打印规则并解释它们,而不是将它们列在格式化的列中。每种方法都有权衡,但如果您喜欢列表模式,您可以用-vL替换-S。
user@docker1:~$ sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A POSTROUTING -s 172.17.0.3/32 -d 172.17.0.3/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 32768 -j DNAT --to-destination 172.17.0.2:80
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 32769 -j DNAT --to-destination 172.17.0.3:80
user@docker1:~$
让我们回顾一下前面输出中每个加粗行的重要性。第一个加粗行处理了出站隐藏 NAT 或MASQUERADE:
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
该规则正在寻找符合两个特征的流量:
-
源 IP 地址必须匹配
docker0桥的 IP 地址空间 -
该流量不是通过
docker0桥出口的。也就是说,它是通过其他接口如eth0或eth1离开的
结尾处的跳转语句指定了MASQUERADE的目标,它将根据路由表将容器流量源 NAT 到主机的 IP 接口之一。
接下来的两行加粗的内容提供了类似的功能,并为每个容器提供了所需的 NAT。让我们来看其中一个:
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 32768 -j DNAT --to-destination 172.17.0.2:80
该规则正在寻找符合三个特征的流量:
-
流量不是通过
docker0桥接进入的 -
流量是 TCP
-
流量的目的端口是
32768
最后的跳转语句指定了DNAT的目标和容器的真实服务端口(80)的目的地。请注意,这两条规则在 Docker 主机的物理接口方面是通用的。正如我们之前看到的,主机上的任何接口都可以进行端口发布和出站伪装,除非我们“明确限制范围。
我们要审查的下一个表是过滤表:
user@docker1:~$ sudo iptables -t filter -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER -d 172.17.0.3/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER-ISOLATION -j RETURN
user@docker1:~$
同样,您会注意到默认链的链策略设置为ACCEPT。在过滤表的情况下,这对功能有更严重的影响。这意味着除非在规则中明确拒绝,否则一切都被允许。换句话说,如果没有定义规则,一切仍然可以工作。Docker 在默认策略未设置为ACCEPT的情况下插入这些规则。稍后,当我们手动创建规则时,我们将把默认策略设置为DROP,以便您可以看到规则的影响。前面的规则需要更多的解释,特别是如果您不熟悉iptables规则的工作原理。让我们逐一审查加粗的线。
第一行加粗的线负责允许来自外部网络的流量返回到容器中。在这种情况下,规则是特定于容器本身生成流量并期望来自外部网络的响应的实例:
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
该规则正在寻找符合两个特征的流量:
-
流量是通过
docker0桥接离开的 -
流量具有
RELATED或ESTABLISHED的连接状态。这将包括作为现有流或与之相关的会话
最后的跳转语句引用了ACCEPT的目标,这将允许流量通过。
第二行加粗的线允许容器与外部网络的连接:
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
该规则正在寻找符合两个特征的流量:
-
流量是通过
docker0桥接进入的 -
流量不是通过
docker0桥接离开的
这是一种非常通用的方式来识别来自容器并且通过docker0桥接以外的任何其他接口离开的流量。最后的跳转语句引用了ACCEPT的目标,这将允许流量通过。与第一条规则结合起来,将允许从容器生成的流向外部网络的流量工作。
加粗的第三行允许容器间的连接:
-A FORWARD -i docker0 -o docker0 -j ACCEPT
该规则正在寻找符合两个特征的流量:
-
流量是通过
docker0桥进入的 -
流量通过
docker0桥出口
这是另一种通用的方法来识别源自docker0桥上容器的流量,以及目标是docker0桥上的流量。结尾处的跳转语句引用了一个ACCEPT目标,这将允许流量通过。这与我们在早期章节中看到的在禁用 ICC 模式时转换为DROP目标的规则相同。
最后两行加粗的允许发布的端口到达容器。让我们检查其中一行:
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
该规则正在寻找符合五个特征的流量:
-
流量是发送到已发布端口的容器
-
流量不是通过
docker0桥进入的 -
流量通过
docker0桥出口 -
协议是 TCP
-
端口号是
80
这个规则特别允许发布的端口工作,通过允许访问容器的服务端口(80)。结尾处的跳转语句引用了一个ACCEPT目标,这将允许流量通过。
手动创建所需的 iptables 规则
现在我们已经看到 Docker 如何自动处理规则生成,让我们通过一个示例来了解如何自己建立这种连接。为此,我们首先需要指示 Docker 不创建任何iptables规则。为此,在 Docker systemd drop in 文件中将--iptables Docker 选项设置为false:
ExecStart=/usr/bin/dockerd --iptables=false
我们需要重新加载 systemd drop in 文件并重新启动 Docker 服务,以便 Docker 重新读取服务参数。为了确保从空白状态开始,如果可能的话,重新启动服务器或手动清除所有iptables规则(如果您不熟悉管理iptables规则,最好的方法就是重新启动服务器以清除它们)。在接下来的示例中,我们假设我们正在使用空规则集。一旦 Docker 重新启动,您可以重新启动两个容器,并确保系统上没有iptables规则存在:
user@docker1:~$ docker start web1
web1
user@docker1:~$ docker start web2
web2
user@docker1:~$ sudo iptables -S
-P INPUT **ACCEPT
-P FORWARD **ACCEPT
-P OUTPUT **ACCEPT
user@docker1:~$
如您所见,当前没有定义iptables规则。我们还可以看到过滤表中默认链策略设置为ACCEPT。现在让我们将过滤表中的默认策略更改为每个链的DROP。除此之外,让我们还包括一条规则,允许 SSH 进出主机,以免破坏我们的连接:
user@docker1:~$ sudo iptables -A INPUT -i eth0 -p tcp --dport 22 \
-m state --state NEW,ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -A OUTPUT -o eth0 -p tcp --sport 22 \
-m state --state ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -P INPUT DROP
user@docker1:~$ sudo iptables -P FORWARD DROP
user@docker1:~$ sudo iptables -P OUTPUT DROP
现在让我们再次检查过滤表,以确保规则已被接受:
user@docker1:~$ sudo iptables -S
-P INPUT **DROP
-P FORWARD **DROP
-P OUTPUT **DROP
-A INPUT -i eth0 -p tcp -m tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -o eth0 -p tcp -m tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
user@docker1:~$
此时,容器web1和web2将不再能够相互到达:
user@docker1:~$ docker exec -it web1 ping 172.17.0.3 -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
user@docker1:~$
注意
根据您的操作系统,您可能会注意到此时web1实际上能够 ping 通web2。最有可能的原因是br_netfilter内核模块尚未加载。没有这个模块,桥接的数据包将不会被 netfilter 检查。要解决这个问题,您可以使用sudo modprobe br_netfilter命令手动加载模块。要使模块在每次启动时加载,您还可以将其添加到/etc/modules文件中。当 Docker 管理iptables规则集时,它会负责为您加载模块。
现在,让我们开始构建规则集,以重新创建 Docker 自动为我们构建的连接。我们要做的第一件事是允许容器的入站和出站访问。我们将使用以下两条规则来实现:
user@docker1:~$ sudo iptables -A FORWARD -i docker0 ! \
-o docker0 -j ACCEPT
user@docker1:~$ sudo iptables -A FORWARD -o docker0 \
-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
尽管这两条规则将允许容器从外部网络生成和接收流量,但此时连接仍然无法工作。为了使其工作,我们需要应用masquerade规则,以便容器流量将被隐藏在docker0主机的接口后面。如果我们不这样做,流量将永远不会返回,因为外部网络对容器所在的172.17.0.0/16网络一无所知:
user@docker1:~$ sudo iptables -t nat -A POSTROUTING \
-s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
有了这个设置,容器现在将能够到达外部网络的网络端点:
user@docker1:~$ docker exec -it **web1** ping **4.2.2.2** -c 2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=36.261 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=55.271 ms
--- 4.2.2.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 36.261/45.766/55.271/9.505 ms
user@docker1:~$
然而,容器仍然无法直接相互通信:
user@docker1:~$ docker exec -it web1 ping 172.17.0.3 -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
user@docker1:~$ docker exec -it web1 curl -S http://172.17.0.3
user@docker1:~$
我们需要添加最后一条规则:
sudo iptables -A FORWARD -i docker0 -o docker0 -j ACCEPT
由于容器之间的流量既进入又离开docker0桥,这将允许容器之间的互联:
user@docker1:~$ docker exec -it **web1** ping **172.17.0.3** -c 2
PING 172.17.0.3 (172.17.0.3): 48 data bytes
56 bytes from 172.17.0.3: icmp_seq=0 ttl=64 time=0.092 ms
56 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.086 ms
--- 172.17.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.086/0.089/0.092/0.000 ms
user@docker1:~$
user@docker1:~$ docker exec -it **web1** curl **http://172.17.0.3
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
唯一剩下的配置是提供一个发布端口的机制。我们可以首先在 Docker 主机上配置目标 NAT 来实现这一点。即使 Docker 没有配置 NAT 规则,它仍然会代表你跟踪端口分配。在容器运行时,如果你选择发布一个端口,Docker 会为你分配一个端口映射,即使它不处理发布。明智的做法是使用 Docker 分配的端口以防止重叠:
user@docker1:~$ docker port web1
80/tcp -> 0.0.0.0:**32768
user@docker1:~$ docker port web2
80/tcp -> 0.0.0.0:**32769
user@docker1:~$
user@docker1:~$ sudo iptables -t nat -A PREROUTING ! -i docker0 \
-p tcp -m tcp --dport **32768** -j DNAT --to-destination **172.17.0.2:80
user@docker1:~$ sudo iptables -t nat -A PREROUTING ! -i docker0 \
-p tcp -m tcp --dport **32769** -j DNAT --to-destination **172.17.0.3:80
user@docker1:~$
使用 Docker 分配的端口,我们可以为每个容器定义一个入站 NAT 规则,将入站连接转换为 Docker 主机上的外部端口到真实的容器 IP 和服务端口。最后,我们只需要允许入站流量:
user@docker1:~$ sudo iptables -A FORWARD -d **172.17.0.2/32** ! -i docker0 -o docker0 -p tcp -m tcp --dport **80** -j ACCEPT
user@docker1:~$ sudo iptables -A FORWARD -d **172.17.0.3/32** ! -i docker0 -o docker0 -p tcp -m tcp --dport **80** -j ACCEPT
一旦这些规则配置好了,我们现在可以测试来自 Docker 主机外部的已发布端口的连接:

创建自定义 iptables 规则
在前面的配方中,我们介绍了 Docker 如何处理最常见的容器网络需求的iptables规则。然而,可能会有一些情况,您希望扩展默认的iptables配置,以允许更多的访问或限制连接的范围。在这个配方中,我们将演示如何实现自定义的iptables规则的一些示例。我们将重点放在限制连接到运行在您的容器上的服务的源的范围,以及允许 Docker 主机本身连接到这些服务。
注意
后面提供的示例旨在演示您配置iptables规则集的选项。它们在这些示例中的实现方式可能或可能不适合您的环境,并且可以根据您的安全需求以不同的方式和位置部署。
准备就绪
我们将使用与前一个配方相同的 Docker 主机和相同的配置。Docker 服务应该配置为使用--iptables=false服务选项,并且应该定义两个容器——web1和web2。如果您不确定如何达到这种状态,请参阅前一个配方。为了定义一个新的iptables策略,我们还需要清除 NAT 和 FILTER 表中的所有现有iptables规则。这样做的最简单方法是重新启动主机。
注意
当默认策略为拒绝时刷新iptables规则将断开任何远程管理会话。如果您没有控制台访问权限,要小心不要意外断开自己!
如果您不想重新启动,可以将默认的过滤策略更改回allow。然后,按照以下步骤刷新过滤和 NAT 表:
sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -t filter -F
sudo iptables -t nat -F
怎么做...
此时,您应该再次拥有两个运行的容器和一个空的默认iptables策略的 Docker 主机。首先,让我们再次将默认的过滤策略更改为deny,同时确保我们仍然允许通过 SSH 进行管理连接:
user@docker1:~$ sudo iptables -A INPUT -i eth0 -p tcp --dport 22 \
-m state --state NEW,ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -A OUTPUT -o eth0 -p tcp --sport 22 \
-m state --state ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -P INPUT DROP
user@docker1:~$ sudo iptables -P FORWARD DROP
user@docker1:~$ sudo iptables -P OUTPUT DROP
因为我们将专注于过滤表周围的策略,让我们将 NAT 策略放在上一篇配方中未更改的状态下。这些 NAT 覆盖了每个容器中服务的出站伪装和入站伪装:
user@docker1:~$ sudo iptables -t nat -A POSTROUTING -s \
172.17.0.0/16 ! -o docker0 -j MASQUERADE
user@docker1:~$ sudo iptables -t nat -A PREROUTING ! -i docker0 \
-p tcp -m tcp --dport 32768 -j DNAT --to-destination 172.17.0.2:80
user@docker1:~$ sudo iptables -t nat -A PREROUTING ! -i docker0 \
-p tcp -m tcp --dport 32769 -j DNAT --to-destination 172.17.0.3:80
您可能有兴趣配置的项目之一是限制容器在外部网络上可以访问的范围。您会注意到,在以前的示例中,容器被允许与外部任何东西通信。这是因为过滤规则相当通用:
sudo iptables -A FORWARD -i docker0 ! -o docker0 -j ACCEPT
此规则允许容器与除docker0之外的任何接口上的任何东西通信。与其允许这样做,我们可以指定我们想要允许出站的端口。因此,例如,如果我们发布端口80,然后我们可以定义一个反向或出站规则,只允许特定的返回流量。让我们首先重新创建我们在上一个示例中使用的入站规则:
user@docker1:~$ sudo iptables -A FORWARD -d 172.17.0.2/32 \
! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
user@docker1:~$ sudo iptables -A FORWARD -d 172.17.0.3/32 \
! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
现在我们可以轻松地用特定规则替换更通用的出站规则,只允许端口80上的返回流量。例如,让我们放入一个规则,允许容器web1只在端口80上返回流量:
user@docker1:~$ sudo iptables -A FORWARD -s 172.17.0.2/32 -i \
docker0 ! -o docker0 -p tcp -m tcp --sport 80 -j ACCEPT
如果我们检查一下,我们应该能够从外部网络访问web1上的服务:

然而,此时容器web1除了在端口80上无法与外部网络上的任何东西通信,因为我们没有使用通用的出站规则:
user@docker1:~$ docker exec -it web1 ping 4.2.2.2 -c 2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
user@docker1:~$
为了解决这个问题,我们可以添加特定的规则,允许来自web1容器的 ICMP 之类的东西:
user@docker1:~$ sudo iptables -A FORWARD -s 172.17.0.2/32 -i \
docker0 ! -o docker0 -p icmp -j ACCEPT
上述规则与前一篇配方中的状态感知返回规则相结合,将允许 web1 容器发起和接收返回的 ICMP 流量。
user@docker1:~$ sudo iptables -A FORWARD -o docker0 -m conntrack \
--ctstate RELATED,ESTABLISHED -j ACCEPT
user@docker1:~$ docker exec -it **web1 ping 4.2.2.2** -c 2
PING 4.2.2.2 (4.2.2.2): 48 data bytes
56 bytes from 4.2.2.2: icmp_seq=0 ttl=50 time=33.892 ms
56 bytes from 4.2.2.2: icmp_seq=1 ttl=50 time=34.326 ms
--- 4.2.2.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 33.892/34.109/34.326/0.217 ms
user@docker1:~$
在web2容器的情况下,其 Web 服务器仍然无法从外部网络访问。如果我们希望限制可以与 Web 服务器通信的流量源,我们可以通过更改入站端口80规则或指定出站端口80规则中的目的地来实现。例如,我们可以通过在出口规则中指定目标来将流量源限制为外部网络上的单个设备:
user@docker1:~$ sudo iptables -A FORWARD -s 172.17.0.3/32 **-d \
10.20.30.13** -i docker0 ! -o docker0 -p tcp -m tcp --sport 80 \
-j ACCEPT
现在,如果我们尝试使用外部网络上 IP 地址为10.20.30.13的实验室设备,我们应该能够访问 Web 服务器:
[user@lab1 ~]# ip addr show dev eth0 | grep inet
inet **10.20.30.13/24** brd 10.20.30.255 scope global eth0
[user@lab2 ~]# **curl http://docker1.lab.lab:32769
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
[user@lab1 ~]#
但是,如果我们尝试使用具有不同 IP 地址的不同实验室服务器,连接将失败:
[user@lab2 ~]# ip addr show dev eth0 | grep inet
inet **10.20.30.14/24** brd 10.20.30.255 scope global eth0
[user@lab2 ~]# **curl http://docker1.lab.lab:32769
[user@lab2 ~]#
同样,这条规则可以作为入站规则或出站规则实现。
以这种方式管理iptables规则时,您可能已经注意到 Docker 主机本身不再能够与容器及其托管的服务进行通信:
user@docker1:~$ ping 172.17.0.2 -c 2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
ping: sendmsg: Operation not permitted
ping: sendmsg: Operation not permitted
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms
user@docker1:~$
这是因为我们一直在过滤表中编写的所有规则都在转发链中。转发链仅适用于主机正在转发的流量,而不适用于源自主机或目的地为主机本身的流量。为了解决这个问题,我们可以在过滤表的INPUT和OUTPUT链中放置规则。为了允许容器之间的 ICMP 流量,我们可以指定以下规则:
user@docker1:~$ sudo iptables -A OUTPUT -o docker0 -p icmp -m \
state --state NEW,ESTABLISHED -j ACCEPT
user@docker1:~$ sudo iptables -A INPUT -i docker0 -p icmp -m \
state --state ESTABLISHED -j ACCEPT
添加到输出链的规则查找流向docker0桥(流向容器)的流量,协议为 ICMP,并且是新的或已建立的流量。添加到输入链的规则查找流向docker0桥(流向主机)的流量,协议为 ICMP,并且是已建立的流量。由于流量是从 Docker 主机发起的,这些规则将匹配并允许容器的 ICMP 流量工作:
user@docker1:~$ ping **172.17.0.2** -c 2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.081 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.021 ms
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.021/0.051/0.081/0.030 ms
user@docker1:~$
然而,这仍然不允许容器本身对默认网关进行 ping。这是因为我们添加到输入链的规则仅匹配进入docker0桥的流量,只寻找已建立的会话。为了使其双向工作,您需要向第二条规则添加NEW标志,以便它也可以匹配容器向主机生成的新流量:
user@docker1:~$ sudo iptables -A INPUT -i docker0 -p icmp -m \
state --state NEW,ESTABLISHED -j ACCEPT
由于我们添加到输出链的规则已经指定了新的或已建立的流量,容器到主机的 ICMP 连接现在也将工作:
user@docker1:~$ docker exec -it **web1** ping
PING 172.17.0.1 (172.17.0.1): 48 data bytes
56 bytes from 172.17.0.1: icmp_seq=0 ttl=64 time=0.073 ms
56 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.079 ms
^C--- 172.17.0.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.073/0.076/0.079/0.000 ms
user@docker1:~$
通过负载均衡器公开服务
隔离容器的另一种方法是使用负载均衡器作为前端。这种操作模式有几个优点。首先,负载均衡器可以为多个后端节点提供智能负载均衡。如果一个容器死掉,负载均衡器可以将其从负载均衡池中移除。其次,您实际上是将容器隐藏在负载均衡虚拟 IP(VIP)地址后面。客户端认为他们直接与容器中运行的应用程序进行交互,而实际上他们是在与负载均衡器进行交互。在许多情况下,负载均衡器可以提供或卸载安全功能,如 SSL 和 Web 应用程序防火墙,使基于容器的应用程序更容易以安全的方式进行扩展。在本教程中,我们将学习如何做到这一点以及 Docker 中可用的一些功能,使这更容易实现。
准备工作
在以下示例中,我们将使用多个 Docker 主机。我们还将使用用户定义的覆盖网络。假设您知道如何为覆盖网络配置 Docker 主机。如果不知道,请参阅第三章中的创建用户定义的覆盖网络教程,用户定义的网络。
如何做…
负载均衡不是一个新概念,在物理和虚拟机空间中是一个众所周知的概念。然而,使用容器进行负载均衡增加了额外的复杂性,这可能会使事情变得更加复杂。首先,让我们看看在没有容器的情况下负载均衡通常是如何工作的:

在这种情况下,我们有一个简单的负载均衡器配置,其中负载均衡器为单个后端池成员(192.168.50.150)提供 VIP。流程如下:
-
客户端向托管在负载均衡器上的 VIP(10.10.10.150)发出请求
-
负载均衡器接收请求,确保它具有该 IP 的 VIP,然后代表客户端向后端池成员发出请求
-
服务器接收来自负载均衡器的请求,并直接回应负载均衡器
-
负载均衡器然后回应客户端
在大多数情况下,对话涉及两个不同的会话,一个是客户端和负载均衡器之间的会话,另一个是负载均衡器和服务器之间的会话。每个都是一个独立的 TCP 会话。
现在,让我们展示一个在容器空间中可能如何工作的示例。查看以下图中显示的拓扑:

在这个例子中,我们将使用基于容器的应用服务器作为后端池成员,以及基于容器的负载均衡器。让我们做出以下假设:
-
主机
docker2和docker3将为许多支持许多不同 VIP 的不同网络演示容器提供托管 -
我们将为每个要定义的 VIP 使用一个负载均衡器容器(
haproxy实例) -
每个演示服务器都公开端口
80
鉴于此,我们可以假设主机网络模式对于负载均衡器主机(docker1)以及托管主机(docker2和docker3)都不可行,因为它需要容器在大量端口上公开服务。在用户定义网络引入之前,这将使我们不得不处理docker0桥上的端口映射。
这很快就会成为一个管理和故障排除的问题。例如,拓扑可能真的是这样的:

在这种情况下,负载均衡器 VIP 将是主机docker1上的发布端口,即32769。Web 服务器本身也在发布端口以公开其 Web 服务器。让我们看一下负载均衡请求可能是什么样子:
-
外部网络的客户端生成对
http://docker1.lab.lab:32769的请求。 -
docker1主机接收请求并通过haproxy容器上的发布端口转换数据包。这将把目的地 IP 和端口更改为172.17.0.2:80。 -
haproxy容器接收请求并确定被访问的 VIP 具有包含docker2:23770和docker3:32771的后端池。它选择docker3主机进行此会话,并向docker3:32771发送请求。 -
当请求穿过主机
docker1时,它执行出站MASQUERADE并隐藏容器在主机的 IP 接口后面。 -
请求被发送到主机的默认网关(MLS),然后转发请求到主机
docker3。 -
docker3主机接收请求并通过web2容器上的发布端口转换数据包。这将把目的地 IP 和端口更改为172.17.0.3:80。 -
web2容器接收请求并向docker1回复 -
docker3主机接收到回复,并通过入站发布端口将数据包翻译回去。 -
请求在
docker1接收,通过出站MASQUERADE进行翻译,并传递到haproxy容器。 -
然后
haproxy容器回应客户端。docker1主机将haproxy容器的响应翻译回自己的 IP 地址和端口32769,响应返回到客户端。
虽然可行,但要跟踪这些内容是很多的。此外,负载均衡器节点需要知道每个后端容器的发布端口和 IP 地址。如果容器重新启动,发布端口可能会改变,从而使其无法访问。在大型后端池中进行故障排除也会是一个头痛的问题。
因此,虽然这当然是可行的,但引入用户定义的网络可以使这更加可管理。例如,我们可以利用覆盖类型网络来进行后端池成员的管理,并完全消除大部分端口发布和出站伪装的需要。这种拓扑结构看起来更像这样:

让我们看看构建这种配置需要做些什么。我们需要做的第一件事是在其中一个节点上定义一个用户定义的覆盖类型网络。我们将在docker1上定义它,并称之为presentation_backend:
user@docker1:~$ docker network create -d overlay \
--internal presentation_backend
bd9e9b5b5e064aee2ddaa58507fa6c15f49e4b0a28ea58ffb3da4cc63e6f8908
user@docker1:~$
注意
请注意,当我创建这个网络时,我传递了--internal标志。你会记得在第三章中,用户定义的网络,这意味着只有连接到这个网络的容器才能访问它。
接下来我们要做的是创建两个 Web 容器,它们将作为负载均衡器的后端池成员。我们将在docker2和docker3主机上进行操作:
user@docker2:~$ docker run -dP **--name=web1 --net \
presentation_backend** jonlangemak/web_server_1
6cc8862f5288b14e84a0dd9ff5424a3988de52da5ef6a07ae593c9621baf2202
user@docker2:~$
user@docker3:~$ docker run -dP **--name=web2 --net \
presentation_backend** jonlangemak/web_server_2
e2504f08f234220dd6b14424d51bfc0cd4d065f75fcbaf46c7b6dece96676d46
user@docker3:~$
剩下要部署的组件是负载均衡器。如前所述,haproxy有一个负载均衡器的容器镜像,所以我们将在这个例子中使用它。在运行容器之前,我们需要准备一个配置,以便将其传递给haproxy使用。这是通过将一个卷挂载到容器中来完成的,我们很快就会看到。配置文件名为haproxy.cfg,我的示例配置看起来像这样:
global
log 127.0.0.1 local0
defaults
log global
mode http
option httplog
timeout connect 5000
timeout client 50000
timeout server 50000
stats enable
stats auth user:docker
stats uri /lbstats
frontend all
bind *:80
use_backend pres_containers
backend **pres_containers
balance **roundrobin
server web1 web1:80 check
server web2 web2:80 check
option httpchk HEAD /index.html HTTP/1.0
在前面的配置中有几个值得指出的地方:
-
我们将
haproxy服务绑定到端口80上的所有接口 -
任何命中端口
80的容器的请求都将被负载均衡到名为pres_containers的池中 -
pres_containers池以循环轮询的方式在两个服务器之间进行负载均衡: -
web1的端口80 -
web2的端口80
这里的一个有趣的地方是,我们可以按名称定义池成员。这是与用户定义的网络一起出现的一个巨大优势,这意味着我们不需要担心跟踪容器 IP 地址。
我将这个配置文件放在了我的主目录中名为haproxy的文件夹中:
user@docker1:~/haproxy$ pwd
/home/user/haproxy
user@docker1:~/haproxy$ ls
haproxy.cfg
user@docker1:~/haproxy$
一旦配置文件就位,我们可以按照以下方式运行容器:
user@docker1:~$ docker run -d --name haproxy --net \
presentation_backend -p 80:80 -v \
~/haproxy:/usr/local/etc/haproxy/ haproxy
d34667aa1118c70cd333810d9c8adf0986d58dab9d71630d68e6e15816741d2b
user@docker1:~$
您可能想知道为什么我在连接容器到“内部”类型网络时指定了端口映射。回想一下前几章中提到的端口映射在所有网络类型中都是全局的。换句话说,即使我目前没有使用它,它仍然是容器的一个特性。因此,如果我将来连接一个可以使用端口映射的网络类型到容器中,它就会使用端口映射。在这种情况下,我首先需要将容器连接到覆盖网络,以确保它可以访问后端 web 服务器。如果haproxy容器在启动时无法解析池成员名称,它将无法加载。
此时,haproxy容器已经可以访问其池成员,但我们无法从外部访问haproxy容器。为了做到这一点,我们将连接另一个可以使用端口映射的接口到容器中。在这种情况下,这将是docker0桥:
user@docker1:~$ docker network connect bridge haproxy
user@docker1:~
在这一点上,haproxy容器应该可以在以下 URL 外部访问:
-
负载均衡 VIP:
http://docker1.lab.lab -
HAProxy 统计信息:
http://docker1.lab.lab/lbstats
如果我们检查统计页面,我们应该看到haproxy容器可以通过覆盖网络访问每个后端 web 服务器。我们可以看到每个的健康检查都返回200 OK状态:

现在,如果我们检查 VIP 本身并刷新几次,我们应该看到来自每个容器的网页:

这种拓扑结构为我们提供了比我们最初在容器负载均衡方面的概念更多的优势。基于覆盖网络的使用不仅提供了基于名称的容器解析,还显著减少了流量路径的复杂性。当然,无论哪种情况,流量都会采用相同的物理路径,但我们不需要依赖那么多不同的 NAT 来使流量工作。这也使整个解决方案变得更加动态。这种设计可以很容易地复制,为许多不同的后端覆盖网络提供负载均衡。
第七章:使用 Weave Net
在本章中,我们将涵盖以下操作:
-
安装和配置 Weave
-
运行连接到 Weave 的容器
-
理解 Weave IPAM
-
使用 WeaveDNS
-
Weave 安全性
-
使用 Weave 网络插件
介绍
Weave Net(简称 Weave)是 Docker 的第三方网络解决方案。早期,它为用户提供了 Docker 本身没有提供的额外网络功能。例如,Weave 在 Docker 开始支持用户定义的覆盖网络和嵌入式 DNS 之前,提供了覆盖网络和 WeaveDNS。然而,随着最近的发布,Docker 已经开始从网络的角度获得了与 Weave 相同的功能。也就是说,Weave 仍然有很多可提供的功能,并且是第三方工具如何与 Docker 交互以提供容器网络的有趣示例。在本章中,我们将介绍安装和配置 Weave 的基础知识,以便与 Docker 一起工作,并从网络的角度描述 Weave 的一些功能。虽然我们将花一些时间演示 Weave 的一些功能,但这并不是整个 Weave 解决方案的操作指南。本章不会涵盖 Weave 的许多功能。我建议您查看他们的网站,以获取有关功能和功能的最新信息(www.weave.works/)。
安装和配置 Weave
在这个示例中,我们将介绍安装 Weave 以及如何在 Docker 主机上提供 Weave 服务。我们还将展示 Weave 如何处理希望参与 Weave 网络的主机的连接。
准备工作
在这个示例中,我们将使用与第三章中使用的相同的实验室拓扑,用户定义的网络,在那里我们讨论了用户定义的覆盖网络:

您将需要一些主机,最好其中一些位于不同的子网。假设在本实验中使用的 Docker 主机处于其默认配置中。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何做…
Weave 是通过 Weave CLI 工具安装和管理的。一旦下载,它不仅管理与 Weave 相关的配置,还管理 Weave 服务的提供。在您希望配置的每个主机上,您只需运行以下三个命令:
- 将 Weave 二进制文件下载到您的本地系统:
user@docker1:~$ sudo curl -L git.io/weave -o \
/usr/local/bin/weave
- 使文件可执行:
user@docker1:~$ sudo chmod +x /usr/local/bin/weave
- 运行 Weave:
user@docker1:~$ **weave launch
如果所有这些命令都成功完成,您的 Docker 主机现在已准备好使用 Weave 进行 Docker 网络。要验证,您可以使用weave status命令检查 Weave 状态:
user@docker1:~$ weave status
Version: 1.7.1 (up to date; next check at 2016/10/11 01:26:42)
Service: router
Protocol: weave 1..2
Name: 12:d2:fe:7a:c1:f2(docker1)
Encryption: disabled
PeerDiscovery: enabled
Targets: 0
Connections: 0
Peers: 1
TrustedSubnets: none
Service: ipam
Status: idle
Range: 10.32.0.0/12
DefaultSubnet: 10.32.0.0/12
Service: dns
Domain: weave.local.
Upstream: 10.20.30.13
TTL: 1
Entries: 0
Service: proxy
Address: unix:///var/run/weave/weave.sock
Service: plugin
DriverName: weave
user@docker1:~$
此输出为您提供了有关 Weave 的所有五个与网络相关的服务的信息。它们是router、ipam、dns、proxy和plugin。此时,您可能想知道所有这些服务都在哪里运行。保持与 Docker 主题一致,它们都在主机上的容器内运行:

正如您所看到的,有三个与 Weave 相关的容器在主机上运行。运行weave launch命令生成了所有三个容器。每个容器提供 Weave 用于网络容器的独特服务。weaveproxy容器充当一个 shim 层,允许直接从 Docker CLI 利用 Weave。weaveplugin容器实现了 Docker 的自定义网络驱动程序。"weave"容器通常被称为 Weave 路由器,并提供与 Weave 网络相关的所有其他服务。
每个容器都可以独立配置和运行。使用weave launch命令运行 Weave 意味着您想要使用所有三个容器,并使用一组合理的默认值部署它们。但是,如果您希望更改与特定容器相关的设置,您需要独立启动容器。可以通过以下方式完成:
weave launch-router
weave launch-proxy
weave launch-plugin
如果您希望在特定主机上清理 Weave 配置,可以发出weave reset命令,它将清理所有与 Weave 相关的服务容器。为了开始我们的示例,我们将只使用 Weave 路由器容器。让我们清除 Weave 配置,然后在我们的主机docker1上只启动该容器:
user@docker1:~$ weave reset
user@docker1:~$ weave launch-router
e5af31a8416cef117832af1ec22424293824ad8733bb7a61d0c210fb38c4ba1e
user@docker1:~$
Weave 路由器(weave 容器)是我们需要提供大部分网络功能的唯一容器。让我们通过检查 weave 容器配置来查看默认情况下传递给 Weave 路由器的配置选项:
user@docker1:~$ docker inspect weave
…<Additional output removed for brevity>…
"Args":
"**--port**",
"6783",
"**--name**",
"12:d2:fe:7a:c1:f2",
"**--nickname**",
"docker1",
"**--datapath**",
"datapath",
"**--ipalloc-range**",
"10.32.0.0/12",
"**--dns-effective-listen-address**",
"172.17.0.1",
"**--dns-listen-address**",
"172.17.0.1:53",
"**--http-addr**",
"127.0.0.1:6784",
"**--resolv-conf**",
"/var/run/weave/etc/resolv.conf"
…<Additional output removed for brevity>…
user@docker1:~$
在前面的输出中有一些值得指出的项目。IP 分配范围被给定为10.32.0.0/12。这与我们默认在docker0桥上处理的172.17.0.0/16有很大不同。此外,还定义了一个 IP 地址用作 DNS 监听地址。回想一下,Weave 还提供了 WeaveDNS,可以用来解析 Weave 网络上其他容器的名称。请注意,这个 IP 地址就是主机上docker0桥接口的 IP 地址。
现在让我们将另一个主机配置为 Weave 网络的一部分:
user@docker2:~$ sudo curl -L git.io/weave -o /usr/local/bin/weave
user@docker2:~$ sudo chmod +x /usr/local/bin/weave
user@docker2:~$ **weave launch-router 10.10.10.101
48e5035629b5124c8d3bedf09fca946b333bb54aff56704ceecef009b53dd449
user@docker2:~$
请注意,我们以与之前相同的方式安装了 Weave,但是当我们启动路由器容器时,我们指定了第一个 Docker 主机的 IP 地址。在 Weave 中,这就是我们将多个主机连接在一起的方式。您希望连接到 Weave 网络的任何主机只需指定 Weave 网络上任何现有节点的 IP 地址。如果我们检查新连接的节点上的 Weave 状态,我们应该看到它显示为已连接:
user@docker2:~$ weave status
Version: 1.7.1 (up to date; next check at 2016/10/11 03:36:22)
Service: router
Protocol: weave 1..2
Name: e6:b1:90:cd:76:da(docker2)
Encryption: disabled
PeerDiscovery: enabled
Targets: 1
Connections: 1 (1 established)
Peers: 2 (with 2 established connections)
TrustedSubnets: none
…<Additional output removed for brevity>…
user@docker2:~$
安装了 Weave 后,我们可以继续以相同的方式连接另外两个剩余的节点:
user@**docker3**:~$ weave launch-router **10.10.10.102
user@**docker4**:~$ weave launch-router **192.168.50.101
在每种情况下,我们将先前加入的 Weave 节点指定为我们尝试加入的节点的对等体。在我们的情况下,我们的加入模式看起来像下面的图片所示:

然而,我们也可以让每个节点加入到任何其他现有节点,并且得到相同的结果。也就是说,将节点docker2、docker3和docker4加入到docker1会产生相同的最终状态。这是因为 Weave 只需要与现有节点通信,以获取有关 Weave 网络当前状态的信息。由于所有现有成员都有这些信息,因此无论加入新节点时与哪个节点通信都无所谓。如果现在检查任何 Weave 节点的状态,我们应该看到我们有四个对等体:
user@docker4:~$ weave status
Version: 1.7.1 (up to date; next check at 2016/10/11 03:25:22)
Service: router
Protocol: weave 1..2
Name: 42:ec:92:86:1a:31(docker4)
Encryption: disabled
PeerDiscovery: enabled
Targets: 1
Connections: 3 (3 established)
Peers: 4 (with 12 established connections)
TrustedSubnets: none
…<Additional output removed for brevity>…
user@docker4:~$
我们可以看到这个节点有三个连接,分别连接到其他两个加入的节点。这给我们总共四个对等体,共有十二个连接,每个 Weave 节点有三个连接。因此,尽管只在三个节点之间配置了对等连接,但最终我们得到了所有主机之间的容器连接的全网格:

现在 Weave 的配置已经完成,我们在所有启用 Weave 的 Docker 主机之间建立了一个完整的网状网络。您可以使用weave status connections命令验证每个主机与其他对等体的连接情况。
user@docker1:~$ weave status connections
-> **192.168.50.102**:6783 established fastdp 42:ec:92:86:1a:31(**docker4**)
<- **10.10.10.102**:45632 established fastdp e6:b1:90:cd:76:da(**docker2**)
<- **192.168.50.101**:38411 established fastdp ae:af:a6:36:18:37(**docker3**)
user@docker1:~$
您会注意到,此配置不需要配置独立的键值存储。
还应该注意,可以使用 Weave CLI 的connect和forget命令手动管理 Weave 对等体。如果在实例化 Weave 时未指定 Weave 网络的现有成员,可以使用 Weave connect 手动连接到现有成员。此外,如果从 Weave 网络中删除成员并且不希望其返回,可以使用forget命令告诉网络完全忘记对等体。
运行 Weave 连接的容器
Weave 是一个有趣的例子,展示了第三方解决方案与 Docker 交互的不同方式。它提供了几种不同的与 Docker 交互的方法。第一种是 Weave CLI,通过它不仅可以配置 Weave,还可以像通过 Docker CLI 一样生成容器。第二种是网络插件,它直接与 Docker 绑定,允许您将 Docker 容器配置到 Weave 网络上。在本教程中,我们将演示如何使用 Weave CLI 将容器连接到 Weave 网络。Weave 网络插件将在本章的后续教程中介绍。
注意
Weave 还提供了一个 API 代理服务,允许 Weave 在 Docker 和 Docker CLI 之间透明地插入自己。本章不涵盖该配置,但他们在此链接上有关于该功能的广泛文档。
www.weave.works/docs/net/latest/weave-docker-api/
准备工作
假设您正在构建本章第一个教程中创建的实验室。还假设主机已安装了 Docker 和 Weave。我们还假设在上一章中定义的 Weave 对等体已经就位。
如何做…
使用 Weave CLI 管理容器连接时,有两种方法可以将容器连接到 Weave 网络。
第一种方法是使用weave命令来运行一个容器。Weave 通过将weave run后面指定的任何内容传递给docker run来实现这一点。这种方法的优势在于,Weave 知道了连接,因为它实际上是在告诉 Docker 运行容器。
这使得 Weave 处于一个完美的位置,可以确保容器以适当的配置启动,以便在 Weave 网络上工作。例如,我们可以使用以下语法在主机docker1上启动名为web1的容器:
user@docker1:~$ **weave** run -dP --name=web1 jonlangemak/web_server_1
请注意,run命令的语法与 Docker 的相同。
注意
尽管有相似之处,但有几点不同值得注意。Weave 只能在后台或-d模式下启动容器。此外,您不能指定--rm标志在执行完毕后删除容器。
一旦以这种方式启动容器,让我们看一下容器的接口配置:
user@docker1:~$ docker exec web1 ip addr
…<Loopback interface removed for brevity>…
20: **eth0**@if21: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
22: **ethwe**@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1410 qdisc noqueue state UP
link/ether a6:f2:d0:36:6f:bd brd ff:ff:ff:ff:ff:ff
inet **10.32.0.1/12** scope global ethwe
valid_lft forever preferred_lft forever
inet6 fe80::a4f2:d0ff:fe36:6fbd/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
请注意,容器现在有一个名为ethwe的额外接口,其 IP 地址为10.32.0.1/12。这是 Weave 网络接口,除了 Docker 网络接口(eth0)之外添加的。如果我们检查,我们会注意到,由于我们传递了-P标志,Docker 已经将容器暴露的端口发布到了eth0接口上。
user@docker1:~$ docker port **web1
80**/tcp -> 0.0.0.0:**32785
user@docker1:~$ sudo iptables -t nat -S
…<Additional output removed for brevity>…
-A DOCKER ! -i docker0 -p tcp -m tcp --dport **32768** -j DNAT --to-destination **172.17.0.2:80
user@docker1:~$
这证明了我们之前看到的所有端口发布功能仍然是通过 Docker 网络结构完成的。Weave 接口只是添加到现有的 Docker 本机网络接口中。
连接容器到 Weave 网络的第二种方法可以通过两种不同的方式实现,但基本上产生相同的结果。可以通过使用 Weave CLI 启动当前停止的容器,或者将正在运行的容器附加到 Weave 来将现有的 Docker 容器添加到 Weave 网络。让我们看看每种方法。首先,让我们以与通常使用 Docker CLI 相同的方式在主机docker2上启动一个容器,然后使用 Weave 重新启动它:
user@docker2:~$ **docker** run -dP --name=web2 jonlangemak/web_server_2
5795d42b58802516fba16eed9445950123224326d5ba19202f23378a6d84eb1f
user@docker2:~$ **docker stop web2
web2
user@docker2:~$ **weave start web2
web2
user@docker2:~$ docker exec web2 ip addr
…<Loopback interface removed for brevity>…
15: **eth0**@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
17: **ethwe**@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1410 qdisc noqueue state UP
link/ether e2:22:e0:f8:0b:96 brd ff:ff:ff:ff:ff:ff
inet **10.44.0.0/12** scope global ethwe
valid_lft forever preferred_lft forever
inet6 fe80::e022:e0ff:fef8:b96/64 scope link
valid_lft forever preferred_lft forever
user@docker2:~$
因此,正如您所看到的,当使用 Weave CLI 重新启动容器时,Weave 已经处理了将 Weave 接口添加到容器中。类似地,我们可以在主机docker3上启动我们的web1容器的第二个实例,然后使用weave attach命令动态连接到 Weave 网络:
user@docker3:~$ docker run -dP --name=web1 jonlangemak/web_server_1
dabdf098964edc3407c5084e56527f214c69ff0b6d4f451013c09452e450311d
user@docker3:~$ docker exec web1 ip addr
…<Loopback interface removed for brevity>…
5: **eth0**@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
user@docker3:~$
user@docker3:~$ **weave attach web1
10.36.0.0
user@docker3:~$ docker exec web1 ip addr
…<Loopback interface removed for brevity>…
5: **eth0**@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
15: **ethwe**@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1410 qdisc noqueue state UP
link/ether de:d6:1c:03:63:ba brd ff:ff:ff:ff:ff:ff
inet **10.36.0.0/12** scope global ethwe
valid_lft forever preferred_lft forever
inet6 fe80::dcd6:1cff:fe03:63ba/64 scope link
valid_lft forever preferred_lft forever
user@docker3:~$
正如我们在前面的输出中所看到的,容器在我们手动将其附加到 Weave 网络之前没有 ethwe 接口。附加是动态完成的,无需重新启动容器。除了将容器添加到 Weave 网络外,您还可以使用 weave detach 命令动态将其从 Weave 中移除。
在这一点上,您应该已经连接到了 Weave 网络的所有容器之间的连通性。在我的情况下,它们被分配了以下 IP 地址:
-
web1在主机docker1上:10.32.0.1 -
web2在主机docker2上:10.44.0.0 -
web1在主机docker3上:10.36.0.0
user@docker1:~$ **docker exec -it web1 ping 10.44.0.0 -c 2
PING 10.40.0.0 (10.40.0.0): 48 data bytes
56 bytes from 10.40.0.0: icmp_seq=0 ttl=64 time=0.447 ms
56 bytes from 10.40.0.0: icmp_seq=1 ttl=64 time=0.681 ms
--- 10.40.0.0 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.447/0.564/0.681/0.117 ms
user@docker1:~$ **docker exec -it web1 ping 10.36.0.0 -c 2
PING 10.44.0.0 (10.44.0.0): 48 data bytes
56 bytes from 10.44.0.0: icmp_seq=0 ttl=64 time=1.676 ms
56 bytes from 10.44.0.0: icmp_seq=1 ttl=64 time=0.839 ms
--- 10.44.0.0 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.839/1.257/1.676/0.419 ms
user@docker1:~$
这证明了 Weave 网络正在按预期工作,并且容器位于正确的网络段上。
了解 Weave IPAM
正如我们在前几章中多次看到的那样,IPAM 是任何容器网络解决方案的关键组成部分。当您开始在多个 Docker 主机上使用常见网络时,IPAM 的关键性变得更加清晰。随着 IP 分配数量的增加,能够通过名称解析这些容器也变得至关重要。与 Docker 一样,Weave 为其容器网络解决方案提供了集成的 IPAM。在本章中,我们将展示如何配置和利用 Weave IPAM 来管理 Weave 网络中的 IP 分配。
做好准备
假设您正在基于本章第一个配方中创建的实验室进行构建。还假设主机已安装了 Docker 和 Weave。Docker 应该处于其默认配置状态,Weave 应该已安装但尚未进行对等连接。如果您需要删除先前示例中定义的对等连接,请在每个主机上发出 weave reset 命令。
如何做…
Weave 对 IPAM 的解决方案依赖于整个 Weave 网络使用一个大的子网,然后将其划分为较小的部分,并直接分配给每个主机。然后主机从分配给它的 IP 地址池中分配容器 IP 地址。为了使其工作,Weave 集群必须就要分配给每个主机的 IP 分配达成一致意见。它首先在集群内部达成共识。如果您大致知道您的集群将有多大,您可以在初始化期间向 Weave 提供具体信息,以帮助它做出更好的决定。
注意
本教程的目标不是深入讨论 Weave 与 IPAM 使用的共识算法的细节。有关详细信息,请参阅以下链接:
www.weave.works/docs/net/latest/ipam/
为了这个示例,我们假设您不知道您的集群有多大,我们将假设它将从两个主机开始并从那里扩展。
重要的是要理解,Weave 中的 IPAM 在您首次配置容器之前处于空闲状态。例如,让我们从在主机docker1上配置 Weave 开始:
user@docker1:~$ **weave launch-router --ipalloc-range 172.16.16.0/24
469c81f786ac38618003e4bd08eb7303c1f8fa84d38cc134fdb352c589cbc42d
user@docker1:~$
您应该注意到的第一件事是添加参数--ipalloc-range。正如我们之前提到的,Weave 是基于一个大子网的概念工作。默认情况下,这个子网是10.32.0.0/12。在 Weave 初始化期间,可以通过向 Weave 传递--ipalloc-range标志来覆盖此默认设置。为了使这些示例更容易理解,我决定将默认子网更改为更易管理的内容;在这种情况下,是172.16.16.0/24。
让我们还在主机docker2上运行相同的命令,但是传递主机docker1的 IP 地址,以便它可以立即进行对等连接:
user@docker2:~$ **weave launch-router --ipalloc-range \
172.16.16.0/24 10.10.10.101
9bfb1cb0295ba87fe88b7373a8ff502b1f90149741b2f43487d66898ffad775d
user@docker2:~$
请注意,我再次向 Weave 传递了相同的子网。每个运行 Weave 的主机上的 IP 分配范围相同是至关重要的。只有同意相同 IP 分配范围的主机才能正常运行。现在让我们检查一下 Weave 服务的状态:
user@docker2:~$ weave status
…<Additional output removed for brevity>…
Connections: 1 (1 established)
Peers: 2 (with 2 established connections)
TrustedSubnets: none
Service: **ipam
Status: **idle
Range: **172.16.16.0/24
DefaultSubnet: **172.16.16.0/24
…<Additional output removed for brevity>…
user@docker2:~$
输出显示两个对等点,表明我们对docker1的对等连接成功。请注意,IPAM 服务显示为idle状态。idle状态意味着 Weave 正在等待更多对等点加入,然后才会做出关于哪些主机将获得哪些 IP 分配的决定。让我们看看当我们运行一个容器时会发生什么:
user@docker2:~$ weave run -dP --name=web2 jonlangemak/web_server_2
379402b05db83315285df7ef516e0417635b24923bba3038b53f4e58a46b4b0d
user@docker2:~$
如果我们再次检查 Weave 状态,我们应该看到 IPAM 现在已从idle更改为ready:
user@docker2:~$ weave status
…<Additional output removed for brevity>…
Connections: 1 (1 established)
Peers: 2 (with 2 established connections)
TrustedSubnets: none
Service: **ipam
Status: **ready
Range: 172.16.16.0/24
DefaultSubnet: 172.16.16.0/24
…<Additional output removed for brevity>…
user@docker2:~$
连接到 Weave 网络的第一个容器迫使 Weave 达成共识。此时,Weave 已经决定集群大小为两,并已尽最大努力在主机之间分配可用的 IP 地址。让我们在主机docker1上运行一个容器,然后检查分配给每个容器的 IP 地址:
user@docker1:~$ weave run -dP --name=web1 jonlangemak/web_server_1
fbb3eac42115**9308f41d795638c3a4689c92a9401718fd1988083bfc12047844
user@docker1:~$ **weave ps
weave:expose 12:d2:fe:7a:c1:f2
fbb3eac42115** 02:a7:38:ab:73:23 **172.16.16.1/24
user@docker1:~$
使用weave ps命令,我们可以看到我们刚刚在主机docker1上生成的容器收到了 IP 地址172.16.16.1/24。如果我们检查主机docker2上的容器web2的 IP 地址,我们会看到它获得了 IP 地址172.16.16.128/24:
user@docker2:~$ **weave ps
weave:expose e6:b1:90:cd:76:da
dde411fe4c7b** c6:42:74:89:71:da **172.16.16.128/24
user@docker2:~$
这是非常合理的。Weave 知道网络中有两个成员,所以它直接将分配分成两半,基本上为每个主机提供自己的/25网络分配。docker1开始分配/24的前半部分,docker2则从后半部分开始。
尽管完全分配了整个空间,这并不意味着我们现在用完了 IP 空间。这些初始分配更像是预留,可以根据 Weave 网络的大小进行更改。例如,我们现在可以将主机docker3添加到 Weave 网络,并在其上启动web1容器的另一个实例:
user@docker3:~$ **weave launch-router --ipalloc-range \
172.16.16.0/24 10.10.10.101
8e8739f48854d87ba14b9dcf220a3c33df1149ce1d868819df31b0fe5fec2163
user@docker3:~$ **weave run -dP --name=web1 jonlangemak/web_server_1
0c2193f2d756**9943171764155e0e93272f5715c257adba75ed544283a2794d3e
user@docker3:~$ weave ps
weave:expose ae:af:a6:36:18:37
0c2193f2d756** 76:8d:4c:ee:08:db **172.16.16.224/24
user@docker3:~$
因为网络现在有更多成员,Weave 只是进一步将初始分配分成更小的块。根据分配给每个主机上容器的 IP 地址,我们可以看到 Weave 试图保持分配在有效的子网内。以下图片显示了第三和第四个主机加入 Weave 网络时 IP 分配的情况:

重要的是要记住,尽管分配给每台服务器的分配是灵活的,但当它们为容器分配 IP 地址时,它们都使用与初始分配相同的掩码。这确保容器都假定它们在同一个网络上,并且彼此直接连接,无需有路由指向其他主机。
为了证明初始 IP 分配必须在所有主机上相同,我们可以尝试使用不同的子网加入最后一个主机docker4:
user@docker4:~$ weave launch-router --ipalloc-range 172.64.65.0/24 10.10.10.101
9716c02c66459872e60447a6a3b6da7fd622bd516873146a874214057fe11035
user@docker4:~$ weave status
…<Additional output removed for brevity>…
Service: router
Protocol: weave 1..2
Name: 42:ec:92:86:1a:31(docker4)
Encryption: disabled
PeerDiscovery: enabled
Targets: 1
Connections: 1 (1 failed)
…<Additional output removed for brevity>…
user@docker4:~$
如果我们检查 Weave 路由器容器的日志,我们会发现它无法加入现有的 Weave 网络,因为定义了错误的 IP 分配:
user@docker4:~$ docker logs weave
…<Additional output removed for brevity>…
INFO: 2016/10/11 02:16:09.821503 ->[192.168.50.101:6783|ae:af:a6:36:18:37(docker3)]: **connection shutting down due to error: Incompatible IP allocation ranges (received: 172.16.16.0/24, ours: 172.64.65.0/24)
…<Additional output removed for brevity>…
加入现有的 Weave 网络的唯一方法是使用与所有现有节点相同的初始 IP 分配。
最后,重要的是要指出,不是必须以这种方式使用 Weave IPAM。您可以通过在weave run期间手动指定 IP 地址来手动分配 IP 地址,就像这样:
user@docker1:~$ weave run **1.1.1.1/24** -dP --name=wrongip \
jonlangemak/web_server_1
259004af91e3b0367bede723c9eb9d3fbdc0c4ad726efe7aea812b79eb408777
user@docker1:~$
在指定单个 IP 地址时,您可以选择任何 IP 地址。正如您将在后面的配方中看到的那样,您还可以指定用于分配的子网,并让 Weave 跟踪该子网在 IPAM 中的分配。在从子网分配 IP 地址时,子网必须是初始 Weave 分配的一部分。
如果您希望手动为某些容器分配 IP 地址,可能明智的是在初始 Weave 配置期间配置额外的 Weave 参数,以限制动态分配的范围。您可以在启动时向 Weave 传递--ipalloc-default-subnet参数,以限制动态分配给主机的 IP 地址的范围。例如,您可以传递以下内容:
weave launch-router --ipalloc-range 172.16.16.0/24 \
--ipalloc-default-subnet 172.16.16.0/25
这将配置 Weave 子网为172.16.16.0/25,使较大网络的其余部分可用于手动分配。我们将在后面的教程中看到,这种类型的配置在 Weave 如何处理 Weave 网络上的网络隔离中起着重要作用。
使用 WeaveDNS
自然而然,在 IPAM 之后要考虑的下一件事是名称解析。无论规模如何,都需要一种方法来定位和识别容器,而不仅仅是 IP 地址。与较新版本的 Docker 一样,Weave 为解析 Weave 网络上的容器名称提供了自己的 DNS 服务。在本教程中,我们将审查 WeaveDNS 的默认配置,以及它是如何实现的,以及一些相关的配置设置,让您可以立即开始运行。
准备工作
假设您正在构建本章第一个教程中创建的实验室。还假设主机已安装了 Docker 和 Weave。Docker 应该处于默认配置状态,并且 Weave 应该已成功地与所有四个主机进行了对等连接,就像我们在本章的第一个教程中所做的那样。
如何做…
如果您一直在本章中跟随到这一点,您已经配置了 WeaveDNS。WeaveDNS 随 Weave 路由器容器一起提供,并且默认情况下已启用。我们可以通过查看 Weave 状态来看到这一点:
user@docker1:~$ weave status
…<Additional output removed for brevity>…
Service: dns
Domain: weave.local.
Upstream: 10.20.30.13
TTL: 1
Entries: 0
…<Additional output removed for brevity>…
当 Weave 配置 DNS 服务时,它会从一些合理的默认值开始。在这种情况下,它检测到我的主机 DNS 服务器是10.20.30.13,因此将其配置为上游解析器。它还选择了weave.local作为域名。如果我们使用 weave run 语法启动容器,Weave 将确保容器以允许其使用此 DNS 服务的方式进行配置。例如,让我们在主机docker1上启动一个容器:
user@docker1:~$ weave run -dP --name=web1 jonlangemak/web_server_1
c0cf29fb07610b6ffc4e55fdd4305f2b79a89566acd0ae0a6de09df06979ef36
user@docker1:~$ docker exec –t web1 more /etc/resolv.conf
nameserver 172.17.0.1
user@docker1:~$
启动容器后,我们可以看到 Weave 已经配置了容器的resolv.conf文件,与 Docker 的默认配置不同。回想一下,默认情况下,在非用户定义的网络中,Docker 会给容器分配与 Docker 主机本身相同的 DNS 配置。在这种情况下,Weave 给容器分配了一个名为172.17.0.1的名称服务器,默认情况下是分配给docker0桥接口的 IP 地址。您可能想知道 Weave 如何期望容器通过与docker0桥接口通信来解析自己的 DNS 系统。解决方案非常简单。Weave 路由器容器以主机模式运行,并绑定到端口53的服务。
user@docker1:~$ docker network inspect **host
…<Additional output removed for brevity>…
"Containers": { "03e3e82a5e0ced0b973e2b31ed9c2d3b8fe648919e263965d61ee7c425d9627c": {
"Name": "**weave**",
…<Additional output removed for brevity>…
如果我们检查主机上绑定的端口,我们可以看到 weave 路由器正在暴露端口53:
user@docker1:~$ sudo netstat -plnt
Active Internet connections (only servers)
…<some columns removed to increase readability>…
Proto Local Address State PID/Program name
tcp **172.17.0.1:53** LISTEN **2227/weaver
这意味着 Weave 容器中的 WeaveDNS 服务将在docker0桥接口上监听 DNS 请求。让我们在主机docker2上启动另一个容器:
user@docker2:~$ **weave run -dP --name=web2 jonlangemak/web_server_2
b81472e86d8ac62511689185fe4e4f36ac4a3c41e49d8777745a60cce6a4ac05
user@docker2:~$ **docker exec -it web2 ping web1 -c 2
PING web1.weave.local (10.32.0.1): 48 data bytes
56 bytes from 10.32.0.1: icmp_seq=0 ttl=64 time=0.486 ms
56 bytes from 10.32.0.1: icmp_seq=1 ttl=64 time=0.582 ms
--- web1.weave.local ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.486/0.534/0.582/0.048 ms
user@docker2:~$
只要两个容器都在 Weave 网络上并且具有适当的设置,Weave 将自动生成一个包含容器名称的 DNS 记录。我们可以使用weave status dns命令从任何 Weave 启用的主机上查看 Weave 知道的所有名称记录:
user@docker2:~$ weave status dns
web1 10.32.0.1 86029a1305f1 12:d2:fe:7a:c1:f2
web2 10.44.0.0 56927d3bf002 e6:b1:90:cd:76:da
user@docker2:~$
在这里,我们可以看到目标主机的 Weave 网络接口的容器名称、IP 地址、容器 ID 和 MAC 地址。
这很有效,但依赖于容器配置了适当的设置。这是另一种情况,使用 Weave CLI 会非常有帮助,因为它确保这些设置在容器运行时生效。例如,如果我们在主机docker3上使用 Docker CLI 启动另一个容器,然后将其连接到 Docker,它将不会获得 DNS 记录:
user@docker3:~$ docker run -dP --name=web1 jonlangemak/web_server_1
cd3b043bd70c0f60a03ec24c7835314ca2003145e1ca6d58bd06b5d0c6803a5c
user@docker3:~$ **weave attach web1
10.36.0.0
user@docker3:~$ docker exec -it **web1 ping web2
ping: unknown host
user@docker3:~$
这有两个原因不起作用。首先,容器不知道在哪里查找 Weave DNS,并且试图通过 Docker 提供的 DNS 服务器来解析它。在这种情况下,这是在 Docker 主机上配置的一个:
user@docker3:~$ **docker exec -it web1 more /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.20.30.13
search lab.lab
user@docker3:~$
其次,当容器被连接时,Weave 没有在 WeaveDNS 中注册记录。为了使 Weave 在 WeaveDNS 中为容器生成记录,容器必须在相同的域中。为此,当 Weave 通过其 CLI 运行容器时,它会传递容器的主机名以及域名。当我们在 Docker 中运行容器时,我们可以通过提供主机名来模拟这种行为。例如:
user@docker3:~$ docker stop web1
user@docker3:~$ docker rm web1
user@docker3:~$ docker run -dP **--hostname=web1.weave.local \
--name=web1 jonlangemak/web_server_1
04bb1ba21b692b4117a9b0503e050d7f73149b154476ed5a0bce0d049c3c9357
user@docker3:~$
现在当我们将容器连接到 Weave 网络时,我们应该看到为其生成的 DNS 记录:
user@docker3:~$ weave attach web1
10.36.0.0
user@docker3:~$ weave status dns
web1 10.32.0.1 86029a1305f1 12:d2:fe:7a:c1:f2
web1 10.36.0.0 5bab5eae10b0 ae:af:a6:36:18:37
web2 10.44.0.0 56927d3bf002 e6:b1:90:cd:76:da
user@docker3:~$
注意
如果您希望该容器还能够解析 WeaveDNS 中的记录,还需要向容器传递--dns=172.17.0.1标志,以确保其 DNS 服务器设置为docker0桥的 IP 地址。
您可能已经注意到,我们现在在 WeaveDNS 中有相同容器名称的两个条目。这是 Weave 在 Weave 网络中提供基本负载平衡的方式。例如,如果我们回到docker2主机,让我们尝试多次 ping 名称web1:
user@docker2:~$ **docker exec -it web2 ping web1 -c 1
PING **web1.weave.local (10.32.0.1):** 48 data bytes
56 bytes from 10.32.0.1: icmp_seq=0 ttl=64 time=0.494 ms
--- web1.weave.local ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.494/0.494/0.494/0.000 ms
user@docker2:~$ **docker exec -it web2 ping web1 -c 1
PING **web1.weave.local (10.36.0.0):** 48 data bytes
56 bytes from 10.36.0.0: icmp_seq=0 ttl=64 time=0.796 ms
--- web1.weave.local ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.796/0.796/0.796/0.000 ms
user@docker2:~$ **docker exec -it web2 ping web1 -c 1
PING **web1.weave.local (10.32.0.1):** 48 data bytes
56 bytes from 10.32.0.1: icmp_seq=0 ttl=64 time=0.507 ms
--- web1.weave.local ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.507/0.507/0.507/0.000 ms
user@docker2:~$
请注意,在第二次 ping 尝试期间,容器解析为不同的 IP 地址。由于 WeaveDNS 中有相同名称的多个记录,我们可以仅使用 DNS 提供基本负载平衡功能。Weave 还将跟踪容器的状态,并将死掉的容器从 WeaveDNS 中移除。例如,如果我们在docker3主机上杀死容器,我们应该看到web1记录中的一个被移出 DNS,只留下web1的单个记录:
user@docker3:~$ docker stop web1
web1
user@docker3:~$ weave status dns
web1 10.32.0.1 86029a1305f1 12:d2:fe:7a:c1:f2
web2 10.44.0.0 56927d3bf002 e6:b1:90:cd:76:da
user@docker3:~$
注意
有许多不同的配置选项可供您自定义 WeaveDNS 的工作方式。要查看完整指南,请查看[www.weave.works/docs/net/latest/weavedns/](http:// https://www.weave.works/docs/net/latest/weavedns/)上的文档。
Weave 安全性
Weave 提供了一些属于安全性范畴的功能。由于 Weave 是一种基于覆盖的网络解决方案,它提供了在物理或底层网络中传输覆盖流量的加密能力。当您的容器可能需要穿越公共网络时,这可能特别有用。此外,Weave 允许您在某些网络段内隔离容器。Weave 依赖于为每个隔离的段使用不同的子网来实现此目的。在本配方中,我们将介绍如何配置覆盖加密以及如何为 Weave 网络中的不同容器提供隔离。
准备工作
假设您正在构建本章第一个配方中创建的实验室。还假设主机已安装了 Docker 和 Weave。Docker 应该处于默认配置状态,Weave 应该已安装但尚未进行对等连接。如果您需要删除先前示例中定义的对等连接,请在每个主机上发出weave reset命令。
如何做…
配置 Weave 以加密覆盖网络相当容易实现;但是,必须在 Weave 的初始配置期间完成。使用前面配方中的相同实验拓扑,让我们运行以下命令来构建 Weave 网络:
- 在主机
docker1上:
weave launch-router **--password notverysecurepwd \
--trusted-subnets 192.168.50.0/24** --ipalloc-range \
172.16.16.0/24 --ipalloc-default-subnet 172.16.16.128/25
- 在主机
docker2,docker3和docker4上:
weave launch-router **--password notverysecurepwd \
--trusted-subnets 192.168.50.0/24** --ipalloc-range \
172.16.16.0/24 --ipalloc-default-subnet \
172.16.16.128/25 10.10.10.101
您会注意到我们在主机上运行的命令基本相同,只是最后三个主机指定docker1作为对等体以构建 Weave 网络。在任何情况下,在 Weave 初始化期间,我们传递了一些额外的参数给路由器:
-
--password:这是启用 Weave 节点之间通信加密的参数。与我的示例不同,您应该选择一个非常安全的密码来使用。这需要在运行 weave 的每个节点上相同。 -
--trusted-subnets:这允许您定义主机子网为受信任的,这意味着它们不需要加密通信。当 Weave 进行加密时,它会退回到比通常使用的更慢的数据路径。由于使用--password参数会打开端到端的加密,定义一些子网不需要加密可能是有意义的 -
--ipalloc-range:在这里,我们定义更大的 Weave 网络为172.16.16.0/24。我们在之前的配方中看到了这个命令的使用: -
--ipalloc-default-subnet:这指示 Weave 默认从更大的 Weave 分配中的较小子网中分配容器 IP 地址。在这种情况下,那就是172.16.16.128/25。
现在,让我们在每个主机上运行以下容器:
docker1:
weave run -dP --name=web1tenant1 jonlangemak/web_server_1
docker2:
weave run -dP --name=web2tenant1 jonlangemak/web_server_2
docker3:
weave run **net:172.16.16.0/25** -dP --name=web1tenant2 \
jonlangemak/web_server_1
docker4:
weave run **net:172.16.16.0/25** -dP --name=web2tenant2 \
jonlangemak/web_server_2
请注意,在主机docker3和docker4上,我添加了net:172.16.16.0/25参数。回想一下,当我们启动 Weave 网络时,我们告诉 Weave 默认从172.16.16.128/25中分配 IP 地址。只要在更大的 Weave 网络范围内,我们可以在容器运行时覆盖这一设置,并为 Weave 提供一个新的子网来使用。在这种情况下,docker1和docker2上的容器将获得172.16.16.128/25内的 IP 地址,因为这是默认设置。docker3和docker4上的容器将获得172.16.16.0/25内的 IP 地址,因为我们覆盖了默认设置。一旦您启动了所有容器,我们可以确认这一点:
user@docker4:~$ weave status dns
web1tenant1 172.16.16.129 26c58ef399c3 12:d2:fe:7a:c1:f2
web1tenant2 172.16.16.64 4c569073d663 ae:af:a6:36:18:37
web2tenant1 172.16.16.224 211c2e0b388e e6:b1:90:cd:76:da
web2tenant2 172.16.16.32 191207a9fb61 42:ec:92:86:1a:31
user@docker4:~$
正如我之前提到的,使用不同的子网是 Weave 提供容器分割的方式。在这种情况下,拓扑将如下所示:

虚线象征着 Weave 在覆盖网络中为我们提供的隔离。由于tenant1容器位于与tenant2容器不同的子网中,它们将无法连接。这样,Weave 使用基本的网络来实现容器隔离。我们可以通过一些测试来证明这一点:
user@docker4:~$ docker exec -**it web2tenant2** curl http://**web1tenant2
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker4:~$ docker exec -it **web2tenant2** curl http://**web1tenant1
user@docker4:~$ docker exec -it **web2tenant2** curl http://**web2tenant1
user@docker4:~$
user@docker4:~$ **docker exec -it web2tenant2 ping web1tenant1 -c 1
PING web1tenant1.weave.local (172.16.16.129): 48 data bytes
--- web1tenant1.weave.local ping statistics ---
1 packets transmitted, 0 packets received, **100% packet loss
user@docker4:~$
当web2tenant2容器尝试访问其自己租户(子网)中的服务时,它按预期工作。尝试访问tenant1中的服务将不会收到响应。但是,由于 DNS 在 Weave 网络中是共享的,容器仍然可以解析tenant1中容器的 IP 地址。
这也说明了加密的例子,以及我们如何指定某些主机为受信任的。无论容器位于哪个子网中,Weave 仍然在所有主机之间建立连接。由于我们在 Weave 初始化期间启用了加密,所有这些连接现在应该是加密的。但是,我们还指定了一个受信任的网络。受信任的网络定义了不需要在它们之间进行加密的节点。在我们的情况下,我们指定192.168.50.0/24为受信任的网络。由于有两个具有这些 IP 地址的节点,docker3和docker4,我们应该看到它们之间的连接是未加密的。我们可以在主机上使用 weave status connections 命令来验证这一点。我们应该得到以下响应:
docker1(截断输出):
<- 10.10.10.102:45888 established encrypted sleeve
<- 192.168.50.101:57880 established encrypted sleeve
<- 192.168.50.102:45357 established encrypted sleeve
docker2(截断输出):
<- 192.168.50.101:35207 established encrypted sleeve
<- 192.168.50.102:34640 established encrypted sleeve
-> 10.10.10.101:6783 established encrypted sleeve
docker3(截断输出):
-> 10.10.10.101:6783 established encrypted sleeve
-> 192.168.50.102:6783 established unencrypted fastdp
-> 10.10.10.102:6783 established encrypted sleeve
docker4(截断输出):
-> 10.10.10.102:6783 established encrypted sleeve
<- 192.168.50.101:36315 established unencrypted fastdp
-> 10.10.10.101:6783 established encrypted sleeve
您可以看到所有的连接都显示为加密,除了主机docker3(192.168.50.101)和主机docker4(192.168.50.102)之间的连接。由于两个主机需要就受信任的网络达成一致,主机docker1和docker2将永远不会同意它们的连接是未加密的。
使用 Weave 网络插件
Weave 的一个特点是它可以以几种不同的方式操作。正如我们在本章的前几个示例中看到的,Weave 有自己的 CLI,我们可以使用它直接将容器配置到 Weave 网络中。虽然这当然是一种紧密集成的工作方式,但它要求您利用 Weave CLI 或 Weave API 代理与 Docker 集成。除了这些选项,Weave 还编写了一个原生的 Docker 网络插件。这个插件允许您直接从 Docker 中使用 Weave。也就是说,一旦插件注册,您就不再需要使用 Weave CLI 将容器配置到 Weave 中。在本示例中,我们将介绍如何安装和使用 Weave 网络插件。
准备工作
假设您正在基于本章第一个示例中创建的实验室进行构建。还假设主机已经安装了 Docker 和 Weave。Docker 应该处于默认配置状态,Weave 应该已安装,并且所有四个主机已成功互联,就像我们在本章的第一个示例中所做的那样。
如何做…
与 Weave 的其他组件一样,利用 Docker 插件非常简单。您只需要告诉 Weave 启动它。例如,如果我决定在主机docker1上使用 Docker 插件,我可以这样启动插件:
user@docker1:~$ **weave launch-plugin
3ef9ee01cc26173f2208b667fddc216e655795fd0438ef4af63dfa11d27e2546
user@docker1:~$
就像其他服务一样,该插件以容器的形式存在。在运行了前面的命令之后,您应该看到插件作为名为weaveplugin的容器运行:

一旦运行,您还应该看到它注册为一个网络插件:
user@docker1:~$ docker info
…<Additional output removed for brevity>…
Plugins:
Volume: local
Network: host **weavemesh** overlay bridge null
…<Additional output removed for brevity>…
user@docker1:~$
我们还可以将其视为使用docker network子命令定义的网络类型:
user@docker1:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
79105142fbf0 bridge bridge local
bb090c21339c host host local
9ae306e2af0a none null local
20864e3185f5 weave weavemesh local
user@docker1:~$
在这一点上,通过 Docker 直接将容器连接到 Weave 网络可以直接完成。您只需要在启动容器时指定weave的网络名称。例如,我们可以运行:
user@docker1:~$ docker run -dP --name=web1 --**net=weave** \
jonlangemak/web_server_1
4d84cb472379757ae4dac5bf6659ec66c9ae6df200811d56f65ffc957b10f748
user@docker1:~$
如果我们查看容器接口,我们应该看到我们习惯在 Weave 连接的容器中看到的两个接口:
user@docker1:~$ docker exec web1 ip addr
…<loopback interface removed for brevity>…
83: **ethwe**0@if84: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1410 qdisc noqueue state UP
link/ether 9e:b2:99:c4:ac:c4 brd ff:ff:ff:ff:ff:ff
inet **10.32.0.1/12** scope global ethwe0
valid_lft forever preferred_lft forever
inet6 fe80::9cb2:99ff:fec4:acc4/64 scope link
valid_lft forever preferred_lft forever
86: **eth1**@if87: <BROADCAST,MULTICAST,UP,LOWER_UP> 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** scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe12:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
但是,您可能会注意到eth1的 IP 地址不在docker0桥上,而是在我们在早期章节中看到的docker_gwbridge上使用的。使用网关桥而不是docker0桥的好处是,网关桥默认情况下已禁用 ICC。这可以防止 Weave 连接的容器意外地在docker0桥上跨通信,如果您启用了 ICC 模式。
插件方法的一个缺点是 Weave 不在中间告诉 Docker 有关 DNS 相关的配置,这意味着容器没有注册它们的名称。即使它们注册了,它们也没有接收到解析 WeaveDNS 所需的正确名称解析设置。我们可以指定容器的正确设置的两种方法。在任何一种情况下,我们都需要在容器运行时手动指定参数。第一种方法涉及手动指定所有必需的参数。手动完成如下:
user@docker1:~$ docker run -dP --name=web1 \
--hostname=web1.weave.local --net=weave --dns=172.17.0.1 \
--dns-search=weave.local** jonlangemak/web_server_1
6a907ee64c129d36e112d0199eb2184663f5cf90522ff151aa10c2a1e6320e16
user@docker1:~$
为了注册 DNS,您需要在前面的代码中显示的四个加粗设置:
-
--hostname=web1.weave.local:如果您不将容器的主机名设置为weave.local中的名称,DNS 服务器将不会注册该名称。 -
--net=weave:它必须在 Weave 网络上才能正常工作。 -
--dns=172.17.0.1:我们需要告诉它使用在docker0桥 IP 地址上监听的 Weave DNS 服务器。但是,您可能已经注意到,该容器实际上并没有在docker0桥上拥有 IP 地址。相反,由于我们连接到docker-gwbridge,我们在172.18.0.0/16网络中有一个 IP 地址。在任何一种情况下,由于两个桥都有 IP 接口,容器可以通过docker_gwbridge路由到docker0桥上的 IP 接口。 -
--dns-search=weave.local:这允许容器解析名称而无需指定完全限定域名(FQDN)。
一旦使用这些设置启动容器,您应该看到 WeaveDNS 中注册的记录:
user@docker1:~$ weave status dns
web1 10.32.0.1 7b02c0262786 12:d2:fe:7a:c1:f2
user@docker1:~$
第二种解决方案仍然是手动的,但涉及从 Weave 本身提取 DNS 信息。您可以从 Weave 中注入 DNS 服务器和搜索域,而不是指定它。Weave 有一个名为dns-args的命令,将为您返回相关信息。因此,我们可以简单地将该命令作为容器参数的一部分注入,而不是指定它,就像这样:
user@docker2:~$ docker run --hostname=web2.weave.local \
--net=weave **$(weave dns-args)** --name=web2 -dP \
jonlangemak/web_server_2
597ffde17581b7203204594dca84c9461c83cb7a9076ed3d1ed3fcb598c2b77d
user@docker2:~$
当然,这并不妨碍需要指定网络或容器的 FQDN,但它确实减少了一些输入。此时,您应该能够看到 WeaveDNS 中定义的所有记录,并能够通过名称访问 Weave 网络上的服务。
user@docker1:~$ weave status dns
web1 10.32.0.1 7b02c0262786 12:d2:fe:7a:c1:f2
web2 10.32.0.2 b154e3671feb 12:d2:fe:7a:c1:f2
user@docker1:~$
user@docker2:~$ **docker exec -it web2 ping web1 -c 2
PING web1 (10.32.0.1): 48 data bytes
56 bytes from 10.32.0.1: icmp_seq=0 ttl=64 time=0.139 ms
56 bytes from 10.32.0.1: icmp_seq=1 ttl=64 time=0.130 ms
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.130/0.135/0.139/0.000 ms
user@docker1:~$
您可能注意到这些容器的 DNS 配置并不完全符合您的预期。例如,resolv.conf文件并未显示我们在容器运行时指定的 DNS 服务器。
user@docker1:~$ docker exec web1 more /etc/resolv.conf
::::::::::::::
/etc/resolv.conf
::::::::::::::
search weave.local
nameserver 127.0.0.11
options ndots:0
user@docker1:~$
然而,如果您检查容器的配置,您会看到正确的 DNS 服务器被正确定义。
user@docker1:~$ docker inspect web1
…<Additional output removed for brevity>…
"**Dns**": [
"**172.17.0.1**"
],
…<Additional output removed for brevity>…
user@docker1:~$
请记住,用户定义的网络需要使用 Docker 的嵌入式 DNS 系统。我们在容器的resolv.conf文件中看到的 IP 地址引用了 Docker 的嵌入式 DNS 服务器。反过来,当我们为容器指定 DNS 服务器时,嵌入式 DNS 服务器会将该服务器添加为嵌入式 DNS 中的转发器。这意味着,尽管请求仍然首先到达嵌入式 DNS 服务器,但请求会被转发到 WeaveDNS 进行解析。
请注意
Weave 插件还允许您使用 Weave 驱动程序创建额外的用户定义网络。然而,由于 Docker 将其视为全局范围,它们需要使用外部密钥存储。如果您有兴趣以这种方式使用 Weave,请参考www.weave.works/docs/net/latest/plugin/上的 Weave 文档。
第八章:使用 Flannel
在本章中,我们将涵盖以下配方:
-
安装和配置 Flannel
-
将 Flannel 与 Docker 集成
-
使用 VXLAN 后端
-
使用主机网关后端
-
指定 Flannel 选项
介绍
Flannel 是由CoreOS团队开发的 Docker 的第三方网络解决方案。Flannel 是早期旨在为每个容器提供唯一可路由 IP 地址的项目之一。这消除了跨主机容器到容器通信需要使用发布端口的要求。与我们审查过的其他一些解决方案一样,Flannel 使用键值存储来跟踪分配和各种其他配置设置。但是,与 Weave 不同,Flannel 不提供与 Docker 服务的直接集成,也不提供插件。相反,Flannel 依赖于您告诉 Docker 使用 Flannel 网络来配置容器。在本章中,我们将介绍如何安装 Flannel 以及其各种配置选项。
安装和配置 Flannel
在这个教程中,我们将介绍安装 Flannel。Flannel 需要安装一个密钥存储和 Flannel 服务。由于每个服务的依赖关系,它们需要在 Docker 主机上配置为实际服务。为此,我们将利用systemd单元文件来定义每个相应的服务。
准备工作
在本示例中,我们将使用与第三章中使用的相同的实验拓扑,用户定义的网络,在那里我们讨论了用户定义的覆盖网络:

你需要一对主机,最好其中一些位于不同的子网上。假设在这个实验中使用的 Docker 主机处于它们的默认配置中。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何做…
如前所述,Flannel 依赖于一个键值存储来向参与 Flannel 网络的所有节点提供信息。在其他示例中,我们运行了基于容器的键值存储,如 Consul,以提供此功能。由于 Flannel 是由 CoreOS 构建的,我们将利用他们的键值存储etcd。虽然etcd以容器格式提供,但由于 Flannel 工作所需的一些先决条件,我们无法轻松使用基于容器的版本。也就是说,我们将下载etcd和 Flannel 的二进制文件,并在我们的主机上将它们作为服务运行。
让我们从etcd开始,因为它是 Flannel 的先决条件。你需要做的第一件事是下载代码。在这个例子中,我们将利用etcd版本 3.0.12,并在主机docker1上运行键值存储。要下载二进制文件,我们将运行以下命令:
user@docker1:~$ curl -LO \
https://github.com/coreos/etcd/releases/download/v3.0.12/\
etcd-v3.0.12-linux-amd64.tar.gz
下载完成后,我们可以使用以下命令从存档中提取二进制文件:
user@docker1:~$ tar xzvf etcd-v3.0.12-linux-amd64.tar.gz
然后我们可以将需要的二进制文件移动到正确的位置,使它们可以执行。在这种情况下,位置是/usr/bin,我们想要的二进制文件是etcd服务本身以及其命令行工具etcdctl:
user@docker1:~$ cd etcd-v3.0.12-linux-amd64
user@docker1:~/etcd-v2.3.7-linux-amd64$ sudo mv etcd /usr/bin/
user@docker1:~/etcd-v2.3.7-linux-amd64$ sudo mv etcdctl /usr/bin/
现在我们已经把所有的部件都放在了正确的位置,我们需要做的最后一件事就是在系统上创建一个服务,来负责运行etcd。由于我们的 Ubuntu 版本使用systemd,我们需要为etcd服务创建一个 unit 文件。要创建服务定义,您可以在/lib/systemd/system/目录中创建一个服务 unit 文件:
user@docker1:~$ sudo vi /lib/systemd/system/etcd.service
然后,您可以创建一个运行etcd的服务定义。etcd服务的一个示例 unit 文件如下所示:
[Unit]
Description=etcd key-value store
Documentation=https://github.com/coreos/etcd
After=network.target
[Service]
Environment=DAEMON_ARGS=
Environment=ETCD_NAME=%H
Environment=ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
Environment=ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
Environment=ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2378
Environment=ETCD_DATA_DIR=/var/lib/etcd/default
Type=notify
ExecStart=/usr/bin/etcd $DAEMON_ARGS
Restart=always
RestartSec=10s
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
注意
请记住,systemd可以根据您的要求以许多不同的方式进行配置。前面给出的 unit 文件演示了配置etcd作为服务的一种方式。
一旦 unit 文件就位,我们可以重新加载systemd,然后启用并启动服务:
user@docker1:~$ sudo systemctl daemon-reload
user@docker1:~$ sudo systemctl enable etcd
user@docker1:~$ sudo systemctl start etcd
如果由于某种原因服务无法启动或保持启动状态,您可以使用systemctl status etcd命令来检查服务的状态:
user@docker1:~$ systemctl status etcd
etcd.service - etcd key-value store
Loaded: loaded (/lib/systemd/system/etcd.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2016-10-11 13:41:01 CDT; 1h 30min ago
Docs: https://github.com/coreos/etcd
Main PID: 17486 (etcd)
Tasks: 8
Memory: 8.5M
CPU: 22.095s
CGroup: /system.slice/etcd.service
└─17486 /usr/bin/etcd
Oct 11 13:41:01 docker1 etcd[17486]: setting up the initial cluster version to 3.0
Oct 11 13:41:01 docker1 etcd[17486]: published {Name:docker1 **ClientURLs:[http://0.0.0.0:2379]}** to cluster cdf818194e3a8c32
Oct 11 13:41:01 docker1 etcd[17486]: ready to serve client requests
Oct 11 13:41:01 docker1 etcd[17486]: **serving insecure client requests on 0.0.0.0:2379, this is strongly iscouraged!
Oct 11 13:41:01 docker1 systemd[1]: Started etcd key-value store.
Oct 11 13:41:01 docker1 etcd[17486]: set the initial cluster version to 3.0
Oct 11 13:41:01 docker1 etcd[17486]: enabled capabilities for version 3.0
Oct 11 15:04:20 docker1 etcd[17486]: start to snapshot (applied: 10001, lastsnap: 0)
Oct 11 15:04:20 docker1 etcd[17486]: saved snapshot at index 10001
Oct 11 15:04:20 docker1 etcd[17486]: compacted raft log at 5001
user@docker1:~$
稍后,如果您在使用启用 Flannel 的节点与etcd通信时遇到问题,请检查并确保etcd允许在所有接口(0.0.0.0)上访问,如前面加粗的输出所示。这在示例单元文件中有定义,但如果未定义,etcd将默认仅在本地环回接口(127.0.0.1)上侦听。这将阻止远程服务器访问该服务。
注意
由于键值存储配置是明确为了演示 Flannel 而进行的,我们不会涵盖键值存储的基础知识。这些配置选项足以让您在单个节点上运行,并且不打算在生产环境中使用。在将其用于生产环境之前,请确保您了解etcd的工作原理。
一旦启动了etcd服务,我们就可以使用etcdctl命令行工具来配置 Flannel 的一些基本设置:
user@docker1:~$ etcdctl mk /coreos.com/network/config \
'{"Network":"10.100.0.0/16"}'
我们将在以后的教程中讨论这些配置选项,但现在只需知道我们定义为Network参数的子网定义了 Flannel 的全局范围。
现在我们已经配置了etcd,我们可以专注于配置 Flannel 本身。将 Flannel 配置为系统服务与我们刚刚为etcd所做的非常相似。主要区别在于我们将在所有四个实验室主机上进行相同的配置,而键值存储只在单个主机上配置。我们将展示在单个主机docker4上安装 Flannel,但您需要在实验室环境中的每个主机上重复这些步骤,以便成为 Flannel 网络的成员:
首先,我们将下载 Flannel 二进制文件。在本例中,我们将使用版本 0.5.5:
user@docker4:~$ cd /tmp/
user@docker4:/tmp$ curl -LO \
https://github.com/coreos/flannel/releases/download/v0.6.2/\
flannel-v0.6.2-linux-amd64.tar.gz
然后,我们需要从存档中提取文件并将flanneld二进制文件移动到正确的位置。请注意,与etcd一样,没有命令行工具与 Flannel 交互:
user@docker4:/tmp$ tar xzvf flannel-v0.6.2-linux-amd64.tar.gz
user@docker4:/tmp$ sudo mv flanneld /usr/bin/
与etcd一样,我们希望定义一个systemd单元文件,以便我们可以在每个主机上将flanneld作为服务运行。要创建服务定义,您可以在/lib/systemd/system/目录中创建另一个服务单元文件:
user@docker4:/tmp$ sudo vi /lib/systemd/system/flanneld.service
然后,您可以创建一个运行etcd的服务定义。etcd服务的示例单元文件如下所示:
[Unit]
Description=Flannel Network Fabric
Documentation=https://github.com/coreos/flannel
Before=docker.service
After=etcd.service
[Service]
Environment='DAEMON_ARGS=--etcd-endpoints=http://10.10.10.101:2379'
Type=notify
ExecStart=/usr/bin/flanneld $DAEMON_ARGS
Restart=always
RestartSec=10s
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
一旦单元文件就位,我们可以重新加载systemd,然后启用并启动服务:
user@docker4:/tmp$ sudo systemctl daemon-reload
user@docker4:/tmp$ sudo systemctl enable flanneld
user@docker4:/tmp$ sudo systemctl start flanneld
如果由于某种原因服务无法启动或保持启动状态,您可以使用systemctl status flanneld命令来检查服务的状态:
user@docker4:/tmp$ systemctl status flanneld
flanneld.service - Flannel Network Fabric
Loaded: loaded (/lib/systemd/system/flanneld.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2016-10-12 08:50:54 CDT; 6s ago
Docs: https://github.com/coreos/flannel
Main PID: 25161 (flanneld)
Tasks: 6
Memory: 3.3M
CPU: 12ms
CGroup: /system.slice/flanneld.service
└─25161 /usr/bin/flanneld --etcd-endpoints=http://10.10.10.101:2379
Oct 12 08:50:54 docker4 systemd[1]: Starting Flannel Network Fabric...
Oct 12 08:50:54 docker4 flanneld[25161]: I1012 08:50:54.409928 25161 main.go:126] Installing signal handlers
Oct 12 08:50:54 docker4 flanneld[25161]: I1012 08:50:54.410384 25161 manager.go:133] Determining IP address of default interface
Oct 12 08:50:54 docker4 flanneld[25161]: I1012 08:50:54.410793 25161 manager.go:163] Using 192.168.50.102 as external interface
Oct 12 08:50:54 docker4 flanneld[25161]: I1012 08:50:54.411688 25161 manager.go:164] Using 192.168.50.102 as external endpoint
Oct 12 08:50:54 docker4 flanneld[25161]: I1012 08:50:54.423706 25161 local_manager.go:179] **Picking subnet in range 10.100.1.0 ... 10.100.255.0
Oct 12 08:50:54 docker4 flanneld[25161]: I1012 08:50:54.429636 25161 manager.go:246] **Lease acquired: 10.100.15.0/24
Oct 12 08:50:54 docker4 flanneld[25161]: I1012 08:50:54.430507 25161 network.go:98] Watching for new subnet leases
Oct 12 08:50:54 docker4 systemd[1]: **Started Flannel Network Fabric.
user@docker4:/tmp$
您应该在日志中看到类似的输出,表明 Flannel 在您配置的etcd全局范围分配中找到了一个租约。这些租约对每个主机都是本地的,我经常将它们称为本地范围或网络。下一步是在其余主机上完成此配置。通过检查每个主机上的 Flannel 日志,我可以知道为每个主机分配了哪些子网。在我的情况下,我得到了以下结果:
-
docker1:10.100.93.0/24 -
docker2:10.100.58.0/24 -
docker3:10.100.90.0/24 -
docker4:10.100.15.0/24
此时,Flannel 已经完全配置好了。在下一个教程中,我们将讨论如何配置 Docker 来使用 Flannel 网络。
将 Flannel 与 Docker 集成
正如我们之前提到的,目前 Flannel 和 Docker 之间没有直接集成。也就是说,我们需要找到一种方法将容器放入 Flannel 网络,而 Docker 并不直接知道正在发生的事情。在这个教程中,我们将展示如何做到这一点,讨论导致我们当前配置的一些先决条件,并了解 Flannel 如何处理主机之间的通信。
准备工作
假设您正在构建上一个教程中描述的实验室。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何做到这一点...
在上一个教程中,我们配置了 Flannel,但我们并没有从网络的角度实际检查 Flannel 配置到底做了什么。让我们快速查看一下我们的一个启用了 Flannel 的主机的配置,看看发生了什么变化:
user@docker4:~$ ip addr
…<loopback interface removed for brevity>…
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether d2:fe:5e:b2:f6:43 brd ff:ff:ff:ff:ff:ff
inet 192.168.50.102/24 brd 192.168.50.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::d0fe:5eff:feb2:f643/64 scope link
valid_lft forever preferred_lft forever
3: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN group default qlen 500
link/none
inet 10.100.15.0/16 scope global flannel0
valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:16:78:74:cf brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 scope global docker0
valid_lft forever preferred_lft forever
user@docker4:~$
您会注意到一个名为flannel0的新接口的添加。您还会注意到它具有分配给此主机的/24本地范围内的 IP 地址。如果我们深入挖掘一下,我们可以使用ethtool来确定这个接口是一个虚拟的tun接口。
user@docker4:~$ ethtool -i flannel0
driver: tun
version: 1.6
firmware-version:
bus-info: tun
supports-statistics: no
supports-test: no
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: no
user@docker4:~$
Flannel 在运行 Flannel 服务的每个主机上创建了这个接口。请注意,flannel0接口的子网掩码是/16,它覆盖了我们在etcd中定义的整个全局范围分配。尽管为主机分配了/24范围,但主机认为整个/16都可以通过flannel0接口访问:
user@docker4:~$ ip route
default via 192.168.50.1 dev eth0
10.100.0.0/16 dev flannel0 proto kernel scope link src 10.100.93.0
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.50.0/24 dev eth0 proto kernel scope link src 192.168.50.102
user@docker4:~$
有了接口后,就会创建这条路由,确保前往其他主机上分配的本地范围的流量通过flannel0接口。我们可以通过 ping 其他主机上的其他flannel0接口来证明这一点:
user@docker4:~$ **ping 10.100.93.0 -c 2
PING 10.100.93.0 (10.100.93.0) 56(84) bytes of data.
64 bytes from 10.100.93.0: icmp_seq=1 ttl=62 time=0.901 ms
64 bytes from 10.100.93.0: icmp_seq=2 ttl=62 time=0.930 ms
--- 10.100.93.0 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.901/0.915/0.930/0.033 ms
user@docker4:~$
由于物理网络对10.100.0.0/16网络空间一无所知,Flannel 必须在流经物理网络时封装流量。为了做到这一点,它需要知道哪个物理 Docker 主机分配了给定的范围。回想一下我们在上一篇示例中检查的 Flannel 日志,Flannel 根据主机的默认路由为每个主机选择了一个外部接口:
I0707 09:07:01.733912 02195 main.go:130] **Determining IP address of default interface
I0707 09:07:01.734374 02195 main.go:188] **Using 192.168.50.102 as external interface
这些信息以及分配给每个主机的范围都在键值存储中注册。使用这些信息,Flannel 可以确定哪个主机分配了哪个范围,并可以使用该主机的外部接口作为发送封装流量的目的地。
注意
Flannel 支持多个后端或传输机制。默认情况下,它会在端口8285上使用 UDP 封装流量。在接下来的示例中,我们将讨论其他后端选项。
既然我们知道了 Flannel 的工作原理,我们需要解决如何将实际的 Docker 容器放入 Flannel 网络中。最简单的方法是让 Docker 使用分配的范围作为docker0桥接的子网。Flannel 将范围信息写入一个文件,保存在/run/flannel/subnet.env中:
user@docker4:~$ more /run/flannel/subnet.env
FLANNEL_NETWORK=10.100.0.0/16
FLANNEL_SUBNET=10.100.15.1/24
FLANNEL_MTU=1472
FLANNEL_IPMASQ=false
user@docker4:~$
利用这些信息,我们可以配置 Docker 使用正确的子网作为其桥接接口。Flannel 提供了两种方法来实现这一点。第一种方法涉及使用随 Flannel 二进制文件一起提供的脚本生成新的 Docker 配置文件。该脚本允许您输出一个使用subnet.env文件中信息的新 Docker 配置文件。例如,我们可以使用该脚本生成一个新的配置,如下所示:
user@docker4:~$ cd /tmp
user@docker4:/tmp$ ls
flannel-v0.6.2-linux-amd64.tar.gz **mk-docker-opts.sh** README.md
user@docker4:~/flannel-0.5.5$ ./**mk-docker-opts.sh -c -d \
example_docker_config
user@docker4:/tmp$ more example_docker_config
DOCKER_OPTS=" --bip=10.100.15.1/24 --ip-masq=true --mtu=1472"
user@docker4:/tmp$
在不使用systemd的系统中,Docker 在大多数情况下会自动检查/etc/default/docker文件以获取服务级选项。这意味着我们可以简单地让 Flannel 将前面提到的配置文件写入/etc/default/docker,这样当服务重新加载时,Docker 就可以使用新的设置。然而,由于我们的系统使用systemd,这种方法需要更新我们的 Docker drop-in 文件(/etc/systemd/system/docker.service.d/docker.conf),使其如下所示:
[Service]
EnvironmentFile=**/etc/default/docker
ExecStart=
ExecStart=/usr/bin/dockerd **$DOCKER_OPTS
加粗的行表示服务应该检查文件etc/default/docker,然后加载变量$DOCKER_OPTS以在运行时传递给服务。如果您使用此方法,为了简单起见,定义所有服务级选项都在etc/default/docker中可能是明智的。
注意:
应该注意的是,这种第一种方法依赖于运行脚本来生成配置文件。如果您手动运行脚本来生成文件,则有可能如果 Flannel 配置更改,配置文件将过时。稍后显示的第二种方法更加动态,因为/run/flannel/subnet.env文件由 Flannel 服务更新。
尽管第一种方法当然有效,但我更喜欢使用一个略有不同的方法,我只是从/run/flannel/subnet.env文件中加载变量,并在 drop-in 文件中使用它们。为了做到这一点,我们将我们的 Docker drop-in 文件更改为如下所示:
[Service]
EnvironmentFile=/run/flannel/subnet.env
ExecStart=
ExecStart=/usr/bin/dockerd --bip=${FLANNEL_SUBNET} --mtu=${FLANNEL_MTU}
通过将/run/flannel/subnet.env指定为EnvironmentFile,我们使文件中定义的变量可供服务定义中使用。然后,我们只需在服务启动时将它们用作选项传递给服务。如果我们在我们的 Docker 主机上进行这些更改,重新加载systemd配置,并重新启动 Docker 服务,我们应该看到我们的docker0接口现在反映了 Flannel 子网:
user@docker4:~$ ip addr show dev docker0
8: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:24:0a:e3:c8 brd ff:ff:ff:ff:ff:ff
inet **10.100.15.1/24** scope global docker0
valid_lft forever preferred_lft forever
user@docker4:~$
您还可以根据 Flannel 配置手动更新 Docker 服务级参数。只需确保您使用/run/flannel/subnet.env文件中的信息。无论您选择哪种方法,请确保docker0桥在所有四个 Docker 主机上都使用 Flannel 指定的配置。我们的拓扑现在应该是这样的:

由于每个 Docker 主机只使用其子网的 Flannel 分配范围,因此每个主机都认为全局 Flannel 网络中包含的剩余子网仍然可以通过flannel0接口访问。只有分配的本地范围的特定/24可以通过docker0桥在本地访问:
user@docker4:~$ ip route
default via 192.168.50.1 dev eth0 onlink
10.100.0.0/16 dev flannel0** proto kernel scope link src 10.100.15.0
10.100.15.0/24 dev docker0** proto kernel scope link src 10.100.15.1
192.168.50.0/24 dev eth0 proto kernel scope link src 192.168.50.102
user@docker4:~$
我们可以通过在两个不同的主机上运行两个不同的容器来验证 Flannel 的操作:
user@**docker1**:~$ docker run -dP **--name=web1** jonlangemak/web_server_1
7e44a55c7ea7704d97a8804bfa211344c66f9fb83b3ac17f697c504b3b193e2d
user@**docker1**:~$
user@**docker4**:~$ docker run -dP **--name=web2** jonlangemak/web_server_2
39a47920588b5e0d77ca9d2838988e2d8de893dee6198759f9ddbd3b38cea80d
user@**docker4**:~$
现在,我们可以通过 IP 地址直接访问每个容器上运行的服务。首先,找到一个容器的 IP 地址:
user@**docker1**:~$ docker exec -it **web1 ip addr show dev eth0
12: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue state UP
link/ether 02:42:0a:64:5d:02 brd ff:ff:ff:ff:ff:ff
inet **10.100.93.2/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe64:5d02/64 scope link
valid_lft forever preferred_lft forever
user@**docker1**:~$
然后,从第二个容器访问服务:
user@**docker4**:~$ docker exec -it web2 curl http://**10.100.93.2
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@**docker4**:~$
连接正常工作。现在我们已经将整个 Flannel 配置与 Docker 一起工作,重要的是要指出我们做事情的顺序。我们查看的其他解决方案能够将其解决方案的某些部分容器化。例如,Weave 能够以容器格式提供其服务,而不需要像我们使用 Flannel 那样需要本地服务。对于 Flannel,每个组件都有一个先决条件才能工作。
例如,我们需要在 Flannel 注册之前运行etcd服务。这本身并不是一个很大的问题,如果etcd和 Flannel 都在容器中运行,你可以相当容易地解决这个问题。然而,由于 Docker 需要对其桥接 IP 地址进行的更改是在服务级别完成的,所以 Docker 在启动之前需要知道有关 Flannel 范围的信息。这意味着我们不能在 Docker 容器中运行etcd和 Flannel 服务,因为我们无法在没有从etcd读取密钥生成的 Flannel 信息的情况下启动 Docker。在这种情况下,了解每个组件的先决条件是很重要的。
注意
在 CoreOS 中运行 Flannel 时,他们能够在容器中运行这些组件。解决方案在他们的文档中详细说明了这一点,在底层部分的这一行:
coreos.com/flannel/docs/latest/flannel-config.html
使用 VXLAN 后端
如前所述,Flannel 支持多种不同的后端配置。后端被认为是 Flannel 在启用 Flannel 的主机之间传递流量的手段。默认情况下,这是通过 UDP 完成的,就像我们在前面的示例中看到的那样。然而,Flannel 也支持 VXLAN。使用 VXLAN 而不是 UDP 的优势在于,较新的主机支持内核中的 VXLAN。在这个示例中,我们将演示如何将 Flannel 后端类型更改为 VXLAN。
准备工作
假设您正在构建本章前面示例中描述的实验室。您将需要与 Docker 集成的启用了 Flannel 的主机,就像本章的前两个示例中描述的那样。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何做…
在你首次在etcd中实例化网络时,你希望使用的后端类型是被定义的。由于我们在定义网络10.100.0.0/16时没有指定类型,Flannel 默认使用 UDP 后端。这可以通过更新我们最初在etcd中设置的配置来改变。回想一下,我们的 Flannel 网络是通过这个命令首次定义的:
etcdctl mk /coreos.com/network/config '{"Network":"10.10.0.0/16"}'
注意我们如何使用etcdctl的mk命令来创建键。如果我们想将后端类型更改为 VXLAN,我们可以运行这个命令:
etcdctl set /coreos.com/network/config '{"Network":"10.100.0.0/16", "Backend": {"Type": "vxlan"}}'
请注意,由于我们正在更新对象,我们现在使用set命令代替mk。虽然在纯文本形式下有时很难看到,但我们传递给etcd的格式正确的 JSON 看起来像这样:
{
"Network": "10.100.0.0/16",
"Backend": {
"Type": "vxlan",
}
}
这将定义这个后端的类型为 VXLAN。虽然前面的配置本身足以改变后端类型,但有时我们可以指定作为后端的一部分的额外参数。例如,当将类型定义为 VXLAN 时,我们还可以指定VXLAN 标识符(VNI)和 UDP 端口。如果未指定,VNI 默认为1,端口默认为8472。为了演示,我们将默认值作为我们配置的一部分应用:
user@docker1:~$ etcdctl set /coreos.com/network/config \
'{"Network":"10.100.0.0/16", "Backend": {"Type": "vxlan","VNI": 1, "Port": 8472}}'
这在格式正确的 JSON 中看起来像这样:
{
"Network": "10.100.0.0/16",
"Backend": {
"Type": "vxlan",
"VNI": 1,
"Port": 8472
}
}
如果我们运行命令,本地etcd实例的配置将被更新。我们可以通过etcdctl命令行工具查询etcd,以验证etcd是否具有正确的配置。要读取配置,我们可以使用etcdctl get子命令:
user@docker1:~$ etcdctl get /coreos.com/network/config
{"Network":"10.100.0.0/16", "Backend": {"Type": "vxlan", "VNI": 1, "Port": 8472}}
user@docker1:~$
尽管我们已成功更新了etcd,但每个节点上的 Flannel 服务不会根据这个新配置进行操作。这是因为每个主机上的 Flannel 服务只在服务启动时读取这些变量。为了使这个更改生效,我们需要重新启动每个节点上的 Flannel 服务:
user@docker4:~$ sudo systemctl restart flanneld
确保您重新启动每个主机上的 Flannel 服务。如果有些主机使用 VXLAN 后端,而其他主机使用 UDP 后端,主机将无法通信。重新启动后,我们可以再次检查我们的 Docker 主机的接口:
user@docker4:~$ ip addr show
…<Additional output removed for brevity>…
11: **flannel.1**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
link/ether 2e:28:e7:34:1a:ff brd ff:ff:ff:ff:ff:ff
inet **10.100.15.0/16** scope global flannel.1
valid_lft forever preferred_lft forever
inet6 fe80::2c28:e7ff:fe34:1aff/64 scope link
valid_lft forever preferred_lft forever
在这里,我们可以看到主机现在有一个名为flannel.1的新接口。如果我们使用ethtool检查接口,我们可以看到它正在使用 VXLAN 驱动程序:
user@docker4:~$ **ethtool -i flannel.1
driver: **vxlan
version: 0.1
firmware-version:
bus-info:
supports-statistics: no
supports-test: no
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: no
user@docker4:~$
而且我们应该仍然能够使用 Flannel IP 地址访问服务:
user@**docker4**:~$ docker exec -it **web2 curl http://10.100.93.2
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@**docker4**:~$
注意
如果您指定了不同的 VNI,Flannel 接口将被定义为flannel.<VNI 编号>。
重要的是要知道,Flannel 不会清理旧配置的遗留物。例如,如果您更改了etcd中的 VXLAN ID 并重新启动 Flannel 服务,您将得到两个接口在同一个网络上。您需要手动删除使用旧 VNI 命名的旧接口。此外,如果更改了分配给 Flannel 的子网,您需要在重新启动 Flannel 服务后重新启动 Docker 服务。请记住,Docker 在加载 Docker 服务时从 Flannel 读取配置变量。如果这些变化,您需要重新加载配置才能生效。
使用主机网关后端
正如我们已经看到的,Flannel 支持两种类型的覆盖网络。使用 UDP 或 VXLAN 封装,Flannel 可以在 Docker 主机之间构建覆盖网络。这样做的明显优势是,您可以在不触及物理底层网络的情况下,在不同的 Docker 节点之间提供网络。然而,某些类型的覆盖网络也会引入显著的性能惩罚,特别是对于在用户空间执行封装的进程。主机网关模式旨在通过不使用覆盖网络来解决这个问题。然而,这也带来了自己的限制。在这个示例中,我们将回顾主机网关模式可以提供什么,并展示如何配置它。
准备工作
在这个示例中,我们将稍微修改我们一直在使用的实验室。实验室拓扑将如下所示:

在这种情况下,主机docker3和docker4现在具有与docker1和docker2相同子网的 IP 地址。也就是说,所有主机现在都是相互的二层邻接,并且可以直接通信,无需通过网关进行路由。一旦您将主机在此拓扑中重新配置,我们将希望清除 Flannel 配置。要做到这一点,请执行以下步骤:
- 在运行
etcd服务的主机上:
sudo systemctl stop etcd
sudo rm -rf /var/lib/etcd/default
sudo systemctl start etcd
- 在所有运行 Flannel 服务的主机上:
sudo systemctl stop flanneld
sudo ip link delete flannel.1
sudo systemctl --no-block start flanneld
注意
您会注意到我们在启动flanneld时传递了systemctl命令和--no-block参数。由于我们从etcd中删除了 Flannel 配置,Flannel 服务正在搜索用于初始化的配置。由于服务的定义方式(类型为通知),传递此参数是必需的,以防止命令在 CLI 上挂起。
如何做…
此时,您的 Flannel 节点将正在搜索其配置。由于我们删除了etcd数据存储,目前缺少告诉 Flannel 节点如何配置服务的密钥,Flannel 服务将继续轮询etcd主机,直到我们进行适当的配置。我们可以通过检查其中一个主机的日志来验证这一点:
user@docker4:~$ journalctl -f -u flanneld
-- Logs begin at Wed 2016-10-12 12:39:35 CDT. –
Oct 12 12:39:36 docker4 flanneld[873]: I1012 12:39:36.843784 00873 manager.go:163] **Using 10.10.10.104 as external interface
Oct 12 12:39:36 docker4 flanneld[873]: I1012 12:39:36.844160 00873 manager.go:164] **Using 10.10.10.104 as external endpoint
Oct 12 12:41:22 docker4 flanneld[873]: E1012 12:41:22.102872 00873 network.go:106] **failed to retrieve network config: 100: Key not found (/coreos.com)** [4]
Oct 12 12:41:23 docker4 flanneld[873]: E1012 12:41:23.104904 00873 network.go:106] **failed to retrieve network config: 100: Key not found (/coreos.com)** [4]
重要的是要注意,此时 Flannel 已经通过查看哪个接口支持主机的默认路由来决定其外部端点 IP 地址:
user@docker4:~$ ip route
default via 10.10.10.1 dev eth0
10.10.10.0/24 dev eth0 proto kernel scope link src 10.10.10.104
user@docker4:~$
由于这恰好是eth0,Flannel 选择该接口的 IP 地址作为其外部地址。要配置主机网关模式,我们可以将以下配置放入etcd:
{
"Network":"10.100.0.0/16",
"Backend":{
"Type":"host-gw"
}
}
正如我们以前看到的,我们仍然指定一个网络。唯一的区别是我们提供了type为host-gw。将其插入etcd的命令如下:
user@docker1:~$ etcdctl set /coreos.com/network/config \
'{"Network":"10.100.0.0/16", "Backend": {"Type": "host-gw"}}'
在我们插入此配置后,Flannel 节点应该都会接收到新的配置。让我们检查主机docker4上 Flannel 的服务日志以验证这一点:
user@docker4:~$ journalctl -r -u flanneld
-- Logs begin at Wed 2016-10-12 12:39:35 CDT, end at Wed 2016-10-12 12:55:38 CDT. --
Oct 12 12:55:06 docker4 flanneld[873]: I1012 12:55:06.797289 00873 network.go:83] **Subnet added: 10.100.23.0/24 via 10.10.10.103
Oct 12 12:55:06 docker4 flanneld[873]: I1012 12:55:06.796982 00873 network.go:83] **Subnet added: 10.100.20.0/24 via 10.10.10.101
Oct 12 12:55:06 docker4 flanneld[873]: I1012 12:55:06.796468 00873 network.go:83] **Subnet added: 10.100.43.0/24 via 10.10.10.102
Oct 12 12:55:06 docker4 flanneld[873]: I1012 12:55:06.785464 00873 network.go:51] **Watching for new subnet leases
Oct 12 12:55:06 docker4 flanneld[873]: I1012 12:55:06.784436 00873 manager.go:246] **Lease acquired: 10.100.3.0/24
Oct 12 12:55:06 docker4 flanneld[873]: I1012 12:55:06.779349 00873 local_manager.go:179] **Picking subnet in range 10.100.1.0 ... 10.100.255.0
注意
journalctl命令对于查看由systemd管理的服务的所有日志非常有用。在前面的示例中,我们传递了-r参数以倒序显示日志(最新的在顶部)。我们还传递了-u参数以指定我们要查看日志的服务。
我们看到的最旧的日志条目是这个主机的 Flannel 服务在10.100.0.0/16子网内选择并注册范围。这与 UDP 和 VXLAN 后端的工作方式相同。接下来的三个日志条目显示 Flannel 检测到其他三个 Flannel 节点范围的注册。由于etcd正在跟踪每个 Flannel 节点的外部 IP 地址,以及它们注册的范围,所有 Flannel 主机现在都知道可以用什么外部 IP 地址来到达每个注册的 Flannel 范围。在覆盖模式(UDP 或 VXLAN)中,此外部 IP 地址被用作封装流量的目的地。在主机网关模式中,此外部 IP 地址被用作路由目的地。如果我们检查路由表,我们可以看到每个主机的路由条目:
user@docker4:~$ ip route
default via 10.10.10.1 dev eth0 onlink
10.10.10.0/24 dev eth0 proto kernel scope link src 10.10.10.104
10.100.20.0/24 via 10.10.10.101 dev eth0
10.100.23.0/24 via 10.10.10.103 dev eth0
10.100.43.0/24 via 10.10.10.102 dev eth0
user@docker4:~$
在这种配置中,Flannel 只是依赖基本路由来提供对所有 Flannel 注册范围的可达性。在这种情况下,主机docker4有路由到所有其他 Docker 主机的路由,以便到达它们的 Flannel 网络范围:

这不仅比处理覆盖网络要简单得多,而且比要求每个主机为覆盖网络进行封装要更高效。这种方法的缺点是每个主机都需要在同一网络上有一个接口才能正常工作。如果主机不在同一网络上,Flannel 无法添加这些路由,因为这将需要上游网络设备(主机的默认网关)也具有有关如何到达远程主机的路由信息。虽然 Flannel 节点可以在其默认网关上指定静态路由,但物理网络对10.100.0.0/16网络一无所知,并且无法传递流量。其结果是主机网关模式限制了您可以放置启用 Flannel 的 Docker 主机的位置。
最后,重要的是要指出,Flannel 在 Docker 服务已经运行后可能已经改变状态。如果是这种情况,您需要重新启动 Docker,以确保它从 Flannel 中获取新的变量。如果在重新配置网络接口时重新启动了主机,则可能只需要启动 Docker 服务。系统启动时,服务可能因缺少 Flannel 配置信息而未能加载,现在应该已经存在。
注意
Flannel 还为各种云提供商(如 GCE 和 AWS)提供了后端。您可以查看它们的文档,以获取有关这些后端类型的更多具体信息。
指定 Flannel 选项
除了配置不同的后端类型,您还可以通过etcd和 Flannel 客户端本身指定其他选项。这些选项允许您限制 IP 分配范围,并指定用作 Flannel 节点外部 IP 端点的特定接口。在本教程中,我们将审查您在本地和全局都可以使用的其他配置选项。
做好准备
我们将继续构建上一章中的实验,在那里我们配置了主机网关后端。但是,实验拓扑将恢复到以前的配置,其中 Docker 主机docker3和docker4位于192.168.50.0/24子网中:

一旦您在这个拓扑中配置了您的主机,我们将想要清除 Flannel 配置。为此,请执行以下步骤:
- 在运行
etcd服务的主机上:
sudo systemctl stop etcd
sudo rm -rf /var/lib/etcd/default
sudo systemctl start etcd
- 在所有运行 Flannel 服务的主机上:
sudo systemctl stop flanneld
sudo ip link delete flannel.1
sudo systemctl --no-block start flanneld
在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何做…
之前的示例展示了如何指定整体 Flannel 网络或全局范围,并改变后端网络类型。我们还看到一些后端网络类型允许额外的配置选项。除了我们已经看到的选项之外,我们还可以全局配置其他参数,来决定 Flannel 的整体工作方式。有三个其他主要参数可以影响分配给 Flannel 节点的范围:
-
SubnetLen: 此参数以整数形式指定,并规定了分配给每个节点的范围的大小。正如我们所见,这默认为/24 -
SubnetMin: 此参数以字符串形式指定,并规定了范围分配应该开始的起始 IP 范围 -
SubnetMax: 此参数以字符串形式指定,并规定了子网分配应该结束的 IP 范围的末端
将这些选项与Network标志结合使用时,我们在分配网络时具有相当大的灵活性。例如,让我们使用这个配置:
{
"Network":"10.100.0.0/16",
"SubnetLen":25,
"SubnetMin":"10.100.0.0",
"SubnetMax":"10.100.1.0",
"Backend":{
"Type":"host-gw"
}
}
这定义了每个 Flannel 节点应该获得一个/25的范围分配,第一个子网应该从10.100.0.0开始,最后一个子网应该结束于10.100.1.0。您可能已经注意到,在这种情况下,我们只有空间来容纳三个子网:
-
10.100.0.0/25 -
10.100.0.128./25 -
10.100.1.0/25
这是故意为了展示当 Flannel 在全局范围内空间不足时会发生什么。现在让我们使用这个命令将这个配置放入etcd中:
user@docker1:~$ etcdctl set /coreos.com/network/config \
'{"Network":"10.100.0.0/16","SubnetLen": 25, "SubnetMin": "10.100.0.0", "SubnetMax": "10.100.1.0", "Backend": {"Type": "host-gw"}}'
一旦放置,您应该会看到大多数主机接收到本地范围的分配。但是,如果我们检查我们的主机,我们会发现有一个主机未能接收到分配。在我的情况下,那就是主机docker4。我们可以在 Flannel 服务的日志中看到这一点:
user@docker4:~$ journalctl -r -u flanneld
-- Logs begin at Wed 2016-10-12 12:39:35 CDT, end at Wed 2016-10-12 13:17:42 CDT. --
Oct 12 13:17:42 docker4 flanneld[1422]: E1012 13:17:42.650086 01422 network.go:106] **failed to register network: failed to acquire lease: out of subnets
Oct 12 13:17:42 docker4 flanneld[1422]: I1012 13:17:42.649604 01422 local_manager.go:179] Picking subnet in range 10.100.0.0 ... 10.100.1.0
由于我们在全局范围内只允许了三个分配空间,第四个主机无法接收本地范围,并将继续请求,直到有一个可用。这可以通过更新SubnetMax参数为10.100.1.128并重新启动未能接收本地范围分配的主机上的 Flannel 服务来解决。
正如我所提到的,我们还可以将配置参数传递给每个主机上的 Flannel 服务。
注意
Flannel 客户端支持各种参数,所有这些参数都可以通过运行flanneld --help来查看。这些参数涵盖了新的和即将推出的功能,以及与基于 SSL 的通信相关的配置,在在运行这些类型的服务时,这些配置将是重要的。
从网络的角度来看,也许最有价值的配置选项是--iface参数,它允许您指定要用作 Flannel 外部端点的主机接口。为了了解其重要性,让我们看一个我们的多主机实验室拓扑的快速示例:

如果你还记得,在主机网关模式下,Flannel 要求所有 Flannel 节点都是二层相邻的,或者在同一个网络上。在这种情况下,左侧有两个主机在10.10.10.0/24网络上,右侧有两个主机在192.168.50.0/24网络上。为了彼此通信,它们需要通过多层交换机进行路由。这种情况通常需要一个覆盖后端模式,可以通过多层交换机隧道传输容器流量。然而,如果主机网关模式是性能或其他原因的要求,如果您可以为主机提供额外的接口,您可能仍然可以使用它。例如,想象一下,这些主机实际上是虚拟机,相对容易为我们在每个主机上提供另一个接口,称之为eth1:

这个接口可以专门用于 Flannel 流量,允许每个主机仍然在 Flannel 流量的情况下保持二层相邻,同时保持它们通过eth0的现有默认路由。然而,仅仅配置接口是不够的。请记住,Flannel 默认通过引用主机的默认路由来选择其外部端点接口。由于在这种模型中默认路由没有改变,Flannel 将无法添加适当的路由:
user@docker4:~$ journalctl -ru flanneld
-- Logs begin at Wed 2016-10-12 14:24:51 CDT, end at Wed 2016-10-12 14:31:14 CDT. --
Oct 12 14:31:14 docker4 flanneld[1491]: E1012 14:31:14.463106 01491 network.go:116] **Error adding route to 10.100.1.128/25 via 10.10.10.102: network is unreachable
Oct 12 14:31:14 docker4 flanneld[1491]: I1012 14:31:14.462801 01491 network.go:83] Subnet added: 10.100.1.128/25 via 10.10.10.102
Oct 12 14:31:14 docker4 flanneld[1491]: E1012 14:31:14.462589 01491 network.go:116] **Error adding route to 10.100.0.128/25 via 10.10.10.101: network is unreachable
Oct 12 14:31:14 docker4 flanneld[1491]: I1012 14:31:14.462008 01491 network.go:83] Subnet added: 10.100.0.128/25 via 10.10.10.101
由于 Flannel 仍然使用eth0接口作为其外部端点 IP 地址,它知道另一个子网上的主机是无法直接到达的。我们可以通过向 Flannel 服务传递--iface选项来告诉 Flannel 使用eth1接口来解决这个问题。
例如,我们可以通过更新 Flannel 服务定义(/lib/systemd/system/flanneld.service)来更改 Flannel 配置,使其如下所示:
[Unit]
Description=Flannel Network Fabric
Documentation=https://github.com/coreos/flannel
Before=docker.service
After=etcd.service
[Service]
Environment= 'DAEMON_ARGS=--etcd-endpoints=http://10.10.10.101:2379 **--iface=eth1'
Type=notify
ExecStart=/usr/bin/flanneld $DAEMON_ARGS
Restart=always
RestartSec=10s
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
有了这个配置,Flannel 将使用eth1接口作为其外部端点,从而使所有主机能够直接在10.11.12.0/24网络上进行通信。然后,您可以通过重新加载systemd配置并在所有主机上重新启动服务来加载新配置:
sudo systemctl daemon-reload
sudo systemctl restart flanneld
请记住,Flannel 使用外部端点 IP 地址来跟踪 Flannel 节点。更改这意味着 Flannel 将为每个 Flannel 节点分配一个新的范围。最好在加入 Flannel 节点之前配置这些选项。在我们的情况下,由于etcd已经配置好,我们将再次删除现有的etcd配置,并重新配置它,以便范围变得可用。
user@docker1:~$ sudo systemctl stop etcd
user@docker1:~$ sudo rm -rf /var/lib/etcd/default
user@docker1:~$ sudo systemctl start etcd
user@docker1:~$ etcdctl set /coreos.com/network/config \
'{"Network":"10.100.0.0/16","SubnetLen": 25, "SubnetMin": "10.100.0.0", "SubnetMax": "10.100.1.128", "Backend": {"Type": "host-gw"}}'
如果您检查主机,现在应该看到它有三个 Flannel 路由——每个路由对应其他三个主机的分配范围之一:
user@docker1:~$ ip route
default via 10.10.10.1 dev eth0 onlink
10.10.10.0/24 dev eth0 proto kernel scope link src 10.10.10.101
10.11.12.0/24 dev eth1 proto kernel scope link src 10.11.12.101
10.100.0.0/25 via 10.11.12.102 dev eth1
10.100.1.0/25 via 10.11.12.104 dev eth1
10.100.1.128/25 via 10.11.12.103 dev eth1
10.100.0.128/25 dev docker0 proto kernel scope link src 10.100.75.1
user@docker1:~$
此外,如果您将通过 NAT 使用 Flannel,您可能还想查看--public-ip选项,该选项允许您定义节点的公共 IP 地址。这在云环境中尤为重要,因为服务器的真实 IP 地址可能被隐藏在 NAT 后面。
第九章:探索网络功能
在本章中,我们将涵盖以下内容:
-
使用 Docker 的预发布版本
-
理解 MacVLAN 接口
-
使用 Docker MacVLAN 网络驱动程序
-
理解 IPVLAN 接口
-
使用 Docker IPVLAN 网络驱动程序
-
使用 MacVLAN 和 IPVLAN 网络标记 VLAN ID
介绍
尽管我们在前几章讨论过的许多功能自从一开始就存在,但许多功能是最近才引入的。Docker 是一个快速发展的开源软件,有许多贡献者。为了管理功能的引入、测试和潜在发布,Docker 以几种不同的方式发布代码。在本章中,我们将展示如何探索尚未包含在软件生产或发布版本中的功能。作为其中的一部分,我们将回顾 Docker 引入的两个较新的网络功能。其中一个是 MacVLAN,最近已经合并到软件的发布版本中,版本号为 1.12。第二个是 IPVLAN,仍然处于预发布软件渠道中。在我们回顾如何使用 Docker 预发布软件渠道之后,我们将讨论 MacVLAN 和 IPVLAN 网络接口的基本操作,然后讨论它们在 Docker 中作为驱动程序的实现方式。
使用 Docker 的预发布版本
Docker 提供了两种不同的渠道,您可以在其中预览未发布的代码。这使用户有机会审查既定发布的功能,也可以审查完全实验性的功能,这些功能可能永远不会进入实际发布版本。审查这些功能并对其提供反馈是开源软件开发的重要组成部分。Docker 认真对待收到的反馈,许多在这些渠道中测试过的好主意最终会进入生产代码发布中。在本篇中,我们将回顾如何安装测试和实验性的 Docker 版本。
准备工作
在本教程中,我们将使用一个新安装的 Ubuntu 16.04 主机。虽然这不是必需的,但建议您在当前未安装 Docker 的主机上安装 Docker 的预发布版本。如果安装程序检测到 Docker 已经安装,它将警告您不要安装实验或测试代码。也就是说,我建议在专用的开发服务器上进行来自这些渠道的软件测试。在许多情况下,虚拟机用于此目的。如果您使用虚拟机,我建议您安装基本操作系统,然后对 VM 进行快照,以便为自己创建还原点。如果安装出现问题,您可以始终恢复到此快照以从已知良好的系统开始。
正如 Docker 在其文档中所指出的:
实验性功能尚未准备好投入生产。它们提供给您在沙盒环境中进行测试和评估。
请在使用非生产代码的任何一列火车时牢记这一点。强烈建议您在 GitHub 上就任何渠道中存在的所有功能提供反馈。
如何做…
如前所述,终端用户可以使用两个不同的预发布软件渠道。
-
experimental.docker.com/:这是下载和安装 Docker 实验版本的脚本的 URL。该版本包括完全实验性的功能。其中许多功能可能在以后的某个时候集成到生产版本中。然而,许多功能不会这样做,而是仅用于实验目的。 -
test.docker.com/:这是下载和安装 Docker 测试版本的脚本的 URL。Docker 还将其称为发布候选(RC)版本的代码。这些代码具有计划发布但尚未集成到 Docker 生产或发布版本中的功能。
要安装任一版本,您只需从 URL 下载脚本并将其传递给 shell。例如:
- 要安装实验版,请运行此命令:
curl -sSL https://experimental.docker.com/ | sh
- 要安装测试版或候选发布版,请运行此命令:
curl -sSL https://test.docker.com/ | sh
注意
值得一提的是,您也可以使用类似的配置来下载 Docker 的生产版本。除了test.docker.com/和experimental.docker.com/之外,还有get.docker.com/,它将安装软件的生产版本。
如前所述,这些脚本的使用应该在当前未安装 Docker 的机器上进行。安装后,您可以通过检查docker info的输出来验证是否安装了适当的版本。例如,在安装实验版本时,您可以在输出中看到实验标志已设置:
user@docker-test:~$ sudo docker info
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 1.12.2
…<Additional output removed for brevity>…
Experimental: true
Insecure Registries:
127.0.0.0/8
user@docker-test:~$
在测试或 RC 版本中,您将看到类似的输出;但是,在 Docker info 的输出中不会列出实验变量:
user@docker-test:~$ sudo docker info
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 1.12.2-rc3
…<Additional output removed for brevity>…
Insecure Registries:
127.0.0.0/8
user@docker-test:~$
通过脚本安装后,您会发现 Docker 已安装并运行,就好像您通过操作系统的默认软件包管理器安装了 Docker 一样。虽然脚本应该在安装的最后提示您,但建议将您的用户帐户添加到 Docker 组中。这样可以避免您在使用 Docker CLI 命令时需要提升权限使用sudo。要将您的用户帐户添加到 Docker 组中,请使用以下命令:
user@docker-test:~$ sudo usermod -aG docker <your username>
确保您注销并重新登录以使设置生效。
请记住,这些脚本也可以用于更新任一渠道的最新版本。在这些情况下,脚本仍会提示您有关在现有 Docker 安装上安装的可能性,但它将提供措辞以指示您可以忽略该消息:
user@docker-test:~$ **curl -sSL https://test.docker.com/ | sh
Warning: the "docker" command appears to already exist on this system.
If you already have Docker installed, this script can cause trouble, which is why we're displaying this warning and provide the opportunity to cancel the installation.
If you installed the current Docker package using this script and are using it again to update Docker, you can safely ignore this message.
You may press Ctrl+C now to abort this script.
+ sleep 20
虽然这不是获取测试和实验代码的唯一方法,但肯定是最简单的方法。您也可以下载预构建的二进制文件或自行构建二进制文件。有关如何执行这两种操作的信息可在 Docker 的 GitHub 页面上找到:github.com/docker/docker/tree/master/experimental。
理解 MacVLAN 接口
我们将要看的第一个特性是 MacVLAN。在这个教程中,我们将在 Docker 之外实现 MacVLAN,以更好地理解它的工作原理。了解 Docker 之外的 MacVLAN 如何工作对于理解 Docker 如何使用 MacVLAN 至关重要。在下一个教程中,我们将介绍 Docker 网络驱动程序对 MacVLAN 的实现。
准备工作
在这个示例中,我们将使用两台 Linux 主机(net1和net2)来演示 MacVLAN 功能。我们的实验室拓扑将如下所示:

假设主机处于基本配置状态,每台主机都有两个网络接口。 eth0接口将有一个静态 IP 地址,并作为每个主机的默认网关。 eth1接口将配置为没有 IP 地址。 供参考,您可以在每个主机的网络配置文件(/etc/network/interfaces)中找到以下内容:
net1.lab.lab
auto eth0
iface eth0 inet static
address 172.16.10.2
netmask 255.255.255.0
gateway 172.16.10.1
dns-nameservers 10.20.30.13
dns-search lab.lab
auto eth1
iface eth1 inet manual
net2.lab.lab
auto eth0
iface eth0 inet static
address 172.16.10.3
netmask 255.255.255.0
gateway 172.16.10.1
dns-nameservers 10.20.30.13
dns-search lab.lab
auto eth1
iface eth1 inet manual
注意
虽然我们将在这个示例中涵盖创建拓扑所需的所有步骤,但如果有些步骤不清楚,您可能希望参考第一章, Linux 网络构造。第一章, Linux 网络构造,更深入地介绍了基本的 Linux 网络构造和 CLI 工具。
如何操作…
MacVLAN 代表一种完全不同的接口配置方式,与我们到目前为止所见过的方式完全不同。我们之前检查的 Linux 网络配置依赖于松散模仿物理网络结构的构造。MacVLAN 接口在逻辑上是绑定到现有网络接口的,并且被称为父接口,可以支持一个或多个 MacVLAN 逻辑接口。让我们快速看一下在我们的实验室主机上配置 MacVLAN 接口的一个示例。
配置 MacVLAN 类型接口的方式与 Linux 网络接口上的所有其他类型非常相似。使用ip命令行工具,我们可以使用link子命令来定义接口:
user@net1:~$ sudo ip link add macvlan1 link eth0 type macvlan
这个语法应该对你来说很熟悉,因为我们在书的第一章中定义了多种不同的接口类型。创建后,下一步是为其配置 IP 地址。这也是通过ip命令完成的:
user@net1:~$ sudo ip address add 172.16.10.5/24 dev macvlan1
最后,我们需要确保启动接口。
user@net1:~$ sudo ip link set dev macvlan1 up
接口现在已经启动,我们可以使用ip addr show命令来检查配置:
user@net1:~$ ip addr show
1: …<loopback interface configuration removed for brevity>…
2: **eth0**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:2d:dd:79 brd ff:ff:ff:ff:ff:ff
inet **172.16.10.2/24** brd 172.16.10.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe2d:dd79/64 scope link
valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:2d:dd:83 brd ff:ff:ff:ff:ff:ff
inet6 fe80::20c:29ff:fe2d:dd83/64 scope link
valid_lft forever preferred_lft forever
4: **macvlan1@eth0**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
link/ether da:aa:c0:18:55:4a brd ff:ff:ff:ff:ff:ff
inet **172.16.10.5/24** scope global macvlan1
valid_lft forever preferred_lft forever
inet6 fe80::d8aa:c0ff:fe18:554a/64 scope link
valid_lft forever preferred_lft forever
user@net1:~$
现在我们已经配置了接口,有几个有趣的地方需要指出。首先,MacVLAN 接口的名称使得很容易识别接口的父接口。回想一下,我们提到每个 MacVLAN 接口都必须与一个父接口关联。在这种情况下,我们可以通过查看 MacVLAN 接口名称中macvlan1@后面列出的名称来知道这个 MacVLAN 接口的父接口是eth0。其次,分配给 MacVLAN 接口的 IP 地址与父接口(eth0)处于相同的子网中。这是有意为之,以允许外部连接。让我们在同一个父接口上定义第二个 MacVLAN 接口,以演示允许的连接性:
user@net1:~$ sudo ip link add macvlan2 link eth0 type macvlan
user@net1:~$ sudo ip address add 172.16.10.6/24 dev macvlan2
user@net1:~$ sudo ip link set dev macvlan2 up
我们的网络拓扑如下:

我们有两个 MacVLAN 接口绑定到 net1 的eth0接口。如果我们尝试从外部子网访问任一接口,连接性应该如预期般工作:
user@test_server:~$** ip addr show dev **eth0** |grep inet
inet **10.20.30.13/24** brd 10.20.30.255 scope global eth0
user@test_server:~$ ping 172.16.10.5 -c 2
PING 172.16.10.5 (172.16.10.5) 56(84) bytes of data.
64 bytes from 172.16.10.5: icmp_seq=1 ttl=63 time=0.423 ms
64 bytes from 172.16.10.5: icmp_seq=2 ttl=63 time=0.458 ms
--- 172.16.10.5 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.423/0.440/0.458/0.027 ms
user@test_server:~$ ping 172.16.10.6 -c 2
PING 172.16.10.6 (172.16.10.6) 56(84) bytes of data.
64 bytes from 172.16.10.6: icmp_seq=1 ttl=63 time=0.510 ms
64 bytes from 172.16.10.6: icmp_seq=2 ttl=63 time=0.532 ms
--- 172.16.10.6 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.510/0.521/0.532/0.011 ms
在前面的输出中,我尝试从net1主机的子网外部的测试服务器上到达172.16.10.5和172.16.10.6。在这两种情况下,我们都能够到达 MacVLAN 接口的 IP 地址,这意味着路由正在按预期工作。这就是为什么我们给 MacVLAN 接口分配了服务器eth0接口现有子网内的 IP 地址。由于多层交换机知道172.16.10.0/24位于 VLAN 10 之外,它只需为 VLAN 10 上的新 IP 地址发出 ARP 请求,以获取它们的 MAC 地址。Linux 主机已经有一个指向允许返回流量到达测试服务器的交换机的默认路由。然而,这绝不是 MacVLAN 接口的要求。我本可以轻松选择另一个 IP 子网用于接口,但那将阻止外部路由的固有工作。
另一个需要指出的地方是父接口不需要有关联的 IP 地址。例如,让我们通过在主机net1上建立两个更多的 MacVLAN 接口来扩展拓扑。一个在主机net1上,另一个在主机net2上:
user@net1:~$ sudo ip link add macvlan3 link eth1 type macvlan
user@net1:~$ sudo ip address add 192.168.10.5/24 dev macvlan3
user@net1:~$ sudo ip link set dev macvlan3 up
user@net2:~$ sudo ip link add macvlan4 link eth1 type macvlan
user@net2:~$ sudo ip address add 192.168.10.6/24 dev macvlan4
user@net2:~$ sudo ip link set dev macvlan4 up
我们的拓扑如下:

尽管在物理接口上没有定义 IP 地址,但主机现在将192.168.10.0/24网络视为已定义,并认为该网络是本地连接的:
user@net1:~$ ip route
default via 172.16.10.1 dev eth0
172.16.10.0/24 dev eth0 proto kernel scope link src 172.16.10.2
172.16.10.0/24 dev macvlan1 proto kernel scope link src 172.16.10.5
172.16.10.0/24 dev macvlan2 proto kernel scope link src 172.16.10.6
192.168.10.0/24 dev macvlan3 proto kernel scope link src 192.168.10.5
user@net1:~$
这意味着两个主机可以直接通过它们在该子网上的关联 IP 地址相互到达:
user@**net1**:~$ ping **192.168.10.6** -c 2
PING 192.168.10.6 (192.168.10.6) 56(84) bytes of data.
64 bytes from 192.168.10.6: icmp_seq=1 ttl=64 time=0.405 ms
64 bytes from 192.168.10.6: icmp_seq=2 ttl=64 time=0.432 ms
--- 192.168.10.6 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.405/0.418/0.432/0.024 ms
user@net1:~$
此时,您可能会想知道为什么要使用 MacVLAN 接口类型。从外观上看,它似乎与创建逻辑子接口没有太大区别。真正的区别在于接口的构建方式。通常,子接口都使用相同的父接口的 MAC 地址。您可能已经注意到在先前的输出和图表中,MacVLAN 接口具有与其关联的父接口不同的 MAC 地址。我们也可以在上游多层交换机(网关)上验证这一点:
switch# show ip arp vlan 10
Protocol Address Age (min) Hardware Addr Type Interface
Internet 172.16.10.6 8 a2b1.0cd4.4e73 ARPA Vlan10
Internet 172.16.10.5 8 4e19.f07f.33e0 ARPA Vlan10
Internet 172.16.10.2 0 000c.292d.dd79 ARPA Vlan10
Internet 172.16.10.3 62 000c.2959.caca ARPA Vlan10
Internet 172.16.10.1 - 0021.d7c5.f245 ARPA Vlan10
注意
在测试中,您可能会发现 Linux 主机对于配置中的每个 IP 地址都呈现相同的 MAC 地址。根据您运行的操作系统,您可能需要更改以下内核参数,以防止主机呈现相同的 MAC 地址:
echo 1 | sudo tee /proc/sys/net/ipv4/conf/all/arp_ignore
echo 2 | sudo tee /proc/sys/net/ipv4/conf/all/arp_announce
echo 2 | sudo tee /proc/sys/net/ipv4/conf/all/rp_filter
请记住,以这种方式应用这些设置不会在重新启动后持久存在。
从 MAC 地址来看,我们可以看到父接口(172.16.10.2)和两个 MacVLAN 接口(172.16.10.5和6)具有不同的 MAC 地址。MacVLAN 允许您使用不同的 MAC 地址呈现多个接口。其结果是您可以拥有多个 IP 接口,每个接口都有自己独特的 MAC 地址,但都使用同一个物理接口。
由于父接口负责多个 MAC 地址,它需要处于混杂模式。当选择为父接口时,主机应自动将接口置于混杂模式。您可以通过检查 ip 链接详细信息来验证:
user@net2:~$ ip -d link
…<output removed for brevity>…
2: **eth1**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:59:ca:d4 brd ff:ff:ff:ff:ff:ff **promiscuity 1
…<output removed for brevity>…
注意
如果父接口处于混杂模式是一个问题,您可能会对本章后面讨论的 IPVLAN 配置感兴趣。
与我们见过的其他 Linux 接口类型一样,MacVLAN 接口也支持命名空间。这可以导致一些有趣的配置选项。现在让我们来看看如何在独立的网络命名空间中部署 MacVLAN 接口。
让我们首先删除所有现有的 MacVLAN 接口:
user@net1:~$ sudo ip link del macvlan1
user@net1:~$ sudo ip link del macvlan2
user@net1:~$ sudo ip link del macvlan3
user@net2:~$ sudo ip link del macvlan4
就像我们在第一章中所做的那样,Linux 网络构造,我们可以创建一个接口,然后将其移入一个命名空间。我们首先创建命名空间:
user@net1:~$ sudo ip netns add namespace1
然后,我们创建 MacVLAN 接口:
user@net1:~$ sudo ip link add macvlan1 link eth0 type macvlan
接下来,我们将接口移入新创建的网络命名空间:
user@net1:~$ sudo ip link set macvlan1 netns namespace1
最后,从命名空间内部,我们为其分配一个 IP 地址并将其启动:
user@net1:~$ sudo ip netns exec namespace1 ip address \
add 172.16.10.5/24 dev macvlan1
user@net1:~$ sudo ip netns exec namespace1 ip link set \
dev macvlan1 up
让我们也在第二个命名空间中创建一个第二个接口,用于测试目的:
user@net1:~$ sudo ip netns add namespace2
user@net1:~$ sudo ip link add macvlan2 link eth0 type macvlan
user@net1:~$ sudo ip link set macvlan2 netns namespace2
user@net1:~$ sudo ip netns exec namespace2 ip address \
add 172.16.10.6/24 dev macvlan2
user@net1:~$ sudo ip netns exec namespace2 ip link set \
dev macvlan2 up
注意
当您尝试不同的配置时,通常会多次创建和删除相同的接口。这样做时,您可能会生成具有相同 IP 地址但不同 MAC 地址的接口。由于我们将这些 MAC 地址呈现给上游物理网络,因此请务必确保上游设备或网关具有要到达的 IP 的最新 ARP 条目。许多交换机和路由器在长时间内不会为新的 MAC 条目 ARP 而具有长的 ARP 超时值是很常见的。
此时,我们的拓扑看起来是这样的:

父接口(eth0)像以前一样有一个 IP 地址,但这次,MacVLAN 接口存在于它们自己独特的命名空间中。尽管位于不同的命名空间中,但它们仍然共享相同的父接口,因为这是在将它们移入命名空间之前完成的。
此时,您应该注意到外部主机无法再 ping 通所有 IP 地址。相反,您只能到达172.16.10.2的eth0 IP 地址。原因很简单。正如您所记得的,命名空间类似于虚拟路由和转发(VRF),并且有自己的路由表。如果您检查一下两个命名空间的路由表,您会发现它们都没有默认路由:
user@net1:~$ sudo ip netns exec **namespace1** ip route
172.16.10.0/24 dev macvlan1 proto kernel scope link src 172.16.10.5
user@net1:~$ sudo ip netns exec **namespace2** ip route
172.16.10.0/24 dev macvlan2 proto kernel scope link src 172.16.10.6
user@net1:~$
为了使这些接口在网络外可达,我们需要为每个命名空间指定一个默认路由,指向该子网上的网关(172.16.10.1)。同样,这是将 MacVLAN 接口 addressing 在与父接口相同的子网中的好处。路由已经存在于物理网络上。添加路由并重新测试:
user@net1:~$ sudo ip netns exec namespace1 ip route \
add 0.0.0.0/0 via 172.16.10.1
user@net1:~$ sudo ip netns exec namespace2 ip route \
add 0.0.0.0/0 via 172.16.10.1
从外部测试主机(为简洁起见删除了一些输出):
user@test_server:~$** ping 172.16.10.2 -c 2
PING 172.16.10.2 (172.16.10.2) 56(84) bytes of data.
64 bytes from 172.16.10.2: icmp_seq=1 ttl=63 time=0.459 ms
64 bytes from 172.16.10.2: icmp_seq=2 ttl=63 time=0.441 ms
user@test_server:~$** ping 172.16.10.5 -c 2
PING 172.16.10.5 (172.16.10.5) 56(84) bytes of data.
64 bytes from 172.16.10.5: icmp_seq=1 ttl=63 time=0.521 ms
64 bytes from 172.16.10.5: icmp_seq=2 ttl=63 time=0.528 ms
user@test_server:~$** ping 172.16.10.6 -c 2
PING 172.16.10.6 (172.16.10.6) 56(84) bytes of data.
64 bytes from 172.16.10.6: icmp_seq=1 ttl=63 time=0.524 ms
64 bytes from 172.16.10.6: icmp_seq=2 ttl=63 time=0.551 ms
因此,虽然外部连接似乎按预期工作,但请注意,这些接口都无法相互通信:
user@net1:~$ sudo ip netns exec **namespace2** ping **172.16.10.5
PING 172.16.10.5 (172.16.10.5) 56(84) bytes of data.
--- 172.16.10.5 ping statistics ---
5 packets transmitted, 0 received, **100% packet loss**, time 0ms
user@net1:~$ sudo ip netns exec **namespace2** ping **172.16.10.2
PING 172.16.10.2 (172.16.10.2) 56(84) bytes of data.
--- 172.16.10.2 ping statistics ---
5 packets transmitted, 0 received, **100% packet loss**, time 0ms
user@net1:~$
这似乎很奇怪,因为它们都共享相同的父接口。问题在于 MacVLAN 接口的配置方式。MacVLAN 接口类型支持四种不同的模式:
-
VEPA:虚拟以太网端口聚合器(VEPA)模式强制所有源自 MacVLAN 接口的流量从父接口出去,无论目的地如何。即使流量的目的地是共享同一父接口的另一个 MacVLAN 接口,也会受到此策略的影响。在第 2 层场景中,由于标准生成树规则,两个 MacVLAN 接口之间的通信可能会被阻止。您可以在上游路由器上在两者之间进行路由。
-
桥接:MacVLAN 桥接模式模仿标准 Linux 桥接。允许在同一父接口上的两个 MacVLAN 接口之间直接进行通信,而无需经过主机的父接口。这对于您期望在同一父接口上跨接口进行高级别通信的情况非常有用。
-
私有:此模式类似于 VEPA 模式,具有完全阻止在同一父接口上的接口之间通信的功能。即使允许流量经过父接口然后回流到主机,通信也会被丢弃。
-
透传:旨在直接将父接口与 MacVLAN 接口绑定。在此模式下,每个父接口只允许一个 MacVLAN 接口,并且 MacVLAN 接口继承父接口的 MAC 地址。
如果不知道在哪里查找,很难分辨出来,我们的 MacVLAN 接口碰巧是 VEPA 类型,这恰好是默认值。我们可以通过向ip命令传递详细信息(-d)标志来查看这一点:
user@net1:~$ sudo ip netns exec namespace1 ip -d link show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0
20: **macvlan1@if2**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether 36:90:37:f6:08:cc brd ff:ff:ff:ff:ff:ff promiscuity 0
macvlan mode vepa
user@net1:~$
在我们的情况下,VEPA 模式阻止了两个命名空间接口直接通信。更常见的是,MacVLAN 接口被定义为类型bridge,以允许在同一父接口上的接口之间进行通信。然而,即使在这种模式下,子接口也不被允许直接与直接分配给父接口的 IP 地址(在本例中为172.16.10.2)进行通信。这应该是一个单独的段落。
user@net1:~$ sudo ip netns del namespace1
user@net1:~$ sudo ip netns del namespace2
现在我们可以重新创建两个接口,为每个 MacVLAN 接口指定bridge模式:
user@net1:~$ sudo ip netns add namespace1
user@net1:~$ sudo ip link add macvlan1 link eth0 type \
macvlan **mode bridge
user@net1:~$ sudo ip link set macvlan1 netns namespace1
user@net1:~$ sudo ip netns exec namespace1 ip address \
add 172.16.10.5/24 dev macvlan1
user@net1:~$ sudo ip netns exec namespace1 ip link set \
dev macvlan1 up
user@net1:~$ sudo ip netns add namespace2
user@net1:~$ sudo ip link add macvlan2 link eth0 type \
macvlan **mode bridge
user@net1:~$ sudo ip link set macvlan2 netns namespace2
user@net1:~$ sudo ip netns exec namespace2 sudo ip address \
add 172.16.10.6/24 dev macvlan2
user@net1:~$ sudo ip netns exec namespace2 ip link set \
dev macvlan2 up
在指定了bridge模式之后,我们可以验证这两个接口可以直接互连:
user@net1:~$ sudo ip netns exec **namespace1 ping 172.16.10.6 -c 2
PING 172.16.10.6 (172.16.10.6) 56(84) bytes of data.
64 bytes from 172.16.10.6: icmp_seq=1 ttl=64 time=0.041 ms
64 bytes from 172.16.10.6: icmp_seq=2 ttl=64 time=0.030 ms
--- 172.16.10.6 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.030/0.035/0.041/0.008 ms
user@net1:~$
然而,我们也注意到我们仍然无法到达在父接口(eth0)上定义的主机 IP 地址:
user@net1:~$ sudo ip netns exec **namespace1 ping 172.16.10.2 -c 2
PING 172.16.10.2 (172.16.10.2) 56(84) bytes of data.
--- 172.16.10.2 ping statistics ---
2 packets transmitted, 0 received, **100% packet loss**, time 1008ms
user@net1:~$
使用 Docker MacVLAN 网络驱动程序
当我开始写这本书时,Docker 的当前版本是 1.10,那时 MacVLAN 功能已经包含在 Docker 的候选版本中。自那时起,1.12 版本已经发布,将 MacVLAN 推入软件的发布版本。也就是说,使用 MacVLAN 驱动程序的唯一要求是确保您安装了 1.12 或更新版本的 Docker。在本章中,我们将讨论如何为从 Docker 创建的容器使用 MacVLAN 网络驱动程序。
准备工作
在本教程中,我们将使用两台运行 Docker 的 Linux 主机。我们的实验拓扑将包括两个生活在同一网络上的 Docker 主机。它将如下所示:

假设每个主机都运行着 1.12 或更高版本的 Docker,以便可以访问 MacVLAN 驱动程序。主机应该有一个单独的 IP 接口,并且 Docker 应该处于默认配置状态。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何做…
就像所有其他用户定义的网络类型一样,MacVLAN 驱动程序是通过docker network子命令处理的。创建 MacVLAN 类型网络与创建任何其他网络类型一样简单,但有一些特定于此驱动程序的事项需要牢记。
-
在定义网络时,您需要指定上游网关。请记住,MacVLAN 接口显示在父接口的相同接口上。它们需要主机或接口的上游网关才能访问外部子网。
-
在其他用户定义的网络类型中,如果您决定不指定一个子网,Docker 会为您生成一个子网供您使用。虽然 MacVLAN 驱动程序仍然是这种情况,但除非您指定父接口所在的网络,否则它将无法正常工作。就像我们在上一个教程中看到的那样,MacVLAN 依赖于上游网络设备知道如何路由 MacVLAN 接口。这是通过在与父接口相同的子网上定义容器的 MacVLAN 接口来实现的。您还可以选择使用没有定义 IP 地址的父接口。在这些情况下,只需确保您在 Docker 中定义网络时指定的网关可以通过父接口到达。
-
作为驱动程序的选项,您需要指定希望用作所有连接到 MacVLAN 接口的容器的父接口的接口。如果不将父接口指定为选项,Docker 将创建一个虚拟网络接口并将其用作父接口。这将阻止该网络与外部网络的任何通信。
-
使用 MacVLAN 驱动程序创建网络时,可以使用
--internal 标志。当指定时,父接口被定义为虚拟接口,阻止流量离开主机。 -
MacVLAN 用户定义网络与父接口之间是一对一的关系。也就是说,您只能在给定的父接口上定义一个 MacVLAN 类型网络。
-
一些交换机供应商限制每个端口可以学习的 MAC 地址数量。虽然这个数字通常非常高,但在使用这种网络类型时,请确保考虑到这一点。
-
与其他用户定义的网络类型一样,您可以指定 IP 范围或一组辅助地址,希望 Docker 的 IPAM 不要分配给容器。在 MacVLAN 模式下,这些设置更为重要,因为您直接将容器呈现到物理网络上。
考虑到我们当前的实验室拓扑,我们可以在每个主机上定义网络如下:
user@docker1:~$ docker network create -d macvlan \
--subnet 10.10.10.0/24 --ip-range 10.10.10.0/25 \
--gateway=10.10.10.1 --aux-address docker1=10.10.10.101 \
--aux-address docker2=10.10.10.102 -o parent=eth0 macvlan_net
user@docker2:~$ docker network create -d macvlan \
--subnet 10.10.10.0/24 --ip-range 10.10.10.128/25 \
--gateway=10.10.10.1 --aux-address docker1=10.10.10.101 \
--aux-address docker2=10.10.10.102 -o parent=eth0 macvlan_net
使用这种配置,网络上的每个主机将使用可用子网的一半,本例中为/25。由于 Docker 的 IPAM 自动为我们保留网关 IP 地址,因此无需通过将其定义为辅助地址来阻止其分配。但是,由于 Docker 主机接口本身确实位于此范围内,我们确实需要使用辅助地址来保留这些地址。
现在,我们可以在每个主机上定义容器,并验证它们是否可以彼此通信:
user@docker1:~$ docker run -d --name=web1 \
--net=macvlan_net jonlangemak/web_server_1
user@docker1:~$ **docker exec web1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
7: **eth0@if2**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
link/ether 02:42:0a:0a:0a:02 brd ff:ff:ff:ff:ff:ff
inet **10.10.10.2/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe0a:a02/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
user@docker2:~$ docker run -d --name=web2 \
--net=macvlan_net jonlangemak/web_server_2
user@docker2:~$ **docker exec web2 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
4: **eth0@if2**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
link/ether 02:42:0a:0a:0a:80 brd ff:ff:ff:ff:ff:ff
inet **10.10.10.128/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe0a:a80/64 scope link
valid_lft forever preferred_lft forever
user@docker2:~$
请注意,在容器运行时不需要发布端口。由于容器此时具有唯一可路由的 IP 地址,因此不需要进行端口发布。任何容器都可以在其自己的唯一 IP 地址上提供任何服务。
与其他网络类型一样,Docker 为每个容器创建一个网络命名空间,然后将容器的 MacVLAN 接口映射到其中。此时,我们的拓扑如下所示:

注意
可以通过检查容器本身或链接 Docker 的netns目录来找到命名空间名称,就像我们在前面的章节中看到的那样,因此ip netns子命令可以查询 Docker 定义的网络命名空间。
从一个生活在子网之外的外部测试主机,我们可以验证每个容器服务都可以通过容器的 IP 地址访问到:
user@test_server:~$ **curl http://10.10.10.2
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #1 - Running on port 80**</span>
</h1>
</body>
</html>
user@test_server:~$ **curl http://10.10.10.128
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
[root@tools ~]#
但是,您会注意到连接到 MacVLAN 网络的容器尽管位于同一接口上,但无法从本地主机访问:
user@docker1:~$ **ping 10.10.10.2
PING 10.10.10.2 (10.10.10.2) 56(84) bytes of data.
From 10.10.10.101 icmp_seq=1 **Destination Host Unreachable
--- 10.10.10.2 ping statistics ---
5 packets transmitted, 0 received, +1 errors, **100% packet loss**, time 0ms
user@docker1:~$
Docker 当前的实现仅支持 MacVLAN 桥接模式。我们可以通过检查容器内接口的详细信息来验证 MacVLAN 接口的操作模式:
user@docker1:~$ docker exec web1 ip -d link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: **eth0@if2**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
link/ether 02:42:0a:0a:0a:02 brd ff:ff:ff:ff:ff:ff
macvlan mode bridge
user@docker1:~$
理解 IPVLAN 接口
IPVLAN 是 MacVLAN 的一种替代方案。IPVLAN 有两种模式。第一种是 L2 模式,它的操作方式与 MacVLAN 非常相似,唯一的区别在于 MAC 地址的分配方式。在 IPVLAN 模式下,所有逻辑 IP 接口使用相同的 MAC 地址。这使得您可以保持父 NIC 不处于混杂模式,并且还可以防止您遇到任何可能的 NIC 或交换机端口 MAC 限制。第二种模式是 IPVLAN 层 3。在层 3 模式下,IPVLAN 就像一个路由器,转发 IPVLAN 连接网络中的单播数据包。在本文中,我们将介绍基本的 IPVLAN 网络结构,以了解它的工作原理和实现方式。
准备工作
在本文中,我们将使用本章中“理解 MacVLAN 接口”食谱中的相同 Linux 主机(net1和net2)。有关拓扑结构的更多信息,请参阅本章中“理解 MacVLAN”食谱的“准备工作”部分。
注意
较旧版本的iproute2工具集不包括对 IPVLAN 的完全支持。如果 IPVLAN 配置的命令不起作用,很可能是因为您使用的是不支持的较旧版本。您可能需要更新以获取具有完全支持的新版本。较旧的版本对 IPVLAN 有一些支持,但缺乏定义模式(L2 或 L3)的能力。
操作步骤
如前所述,IPVLAN 的 L2 模式在功能上几乎与 MacVLAN 相同。主要区别在于 IPVLAN 利用相同的 MAC 地址连接到同一主机的所有 IPVLAN 接口。您会记得,MacVLAN 接口利用不同的 MAC 地址连接到同一父接口的每个 MacVLAN 接口。
我们可以创建与 MacVLAN 配方中相同的接口,以显示接口地址是使用相同的 MAC 地址创建的:
user@net1:~$ sudo ip link add ipvlan1 link eth0 **type ipvlan mode l2
user@net1:~$ sudo ip address add 172.16.10.5/24 dev ipvlan1
user@net1:~$ sudo ip link set dev ipvlan1 up
user@net1:~$ sudo ip link add ipvlan2 link eth0 **type ipvlan mode l2
user@net1:~$ sudo ip address add 172.16.10.6/24 dev ipvlan2
user@net1:~$ sudo ip link set dev ipvlan2 up
请注意,配置中唯一的区别是我们将类型指定为 IPVLAN,模式指定为 L2。在 IPVLAN 的情况下,默认模式是 L3,因此我们需要指定 L2 以使接口以这种方式运行。由于 IPVLAN 接口继承了父接口的 MAC 地址,我们的拓扑应该是这样的:

我们可以通过检查接口本身来证明这一点:
user@net1:~$ ip -d link
…<loopback interface removed for brevity>…
2: **eth0**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether **00:0c:29:2d:dd:79** brd ff:ff:ff:ff:ff:ff promiscuity 1 addrgenmode eui64
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:2d:dd:83 brd ff:ff:ff:ff:ff:ff promiscuity 0 addrgenmode eui64
28: **ipvlan1@eth0**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether **00:0c:29:2d:dd:79** brd ff:ff:ff:ff:ff:ff promiscuity 0
ipvlan mode l2** addrgenmode eui64
29: **ipvlan2@eth0**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether **00:0c:29:2d:dd:79** brd ff:ff:ff:ff:ff:ff promiscuity 0
ipvlan mode l2** addrgenmode eui64
user@net1:~$
如果我们从本地子网外部向这些 IP 发起流量,我们可以通过检查上游网关的 ARP 表来验证每个 IP 报告相同的 MAC 地址:
switch#show ip arp vlan 10
Protocol Address Age (min) Hardware Addr Type Interface
Internet 172.16.10.6 0 000c.292d.dd79 ARPA Vlan30
Internet 172.16.10.5 0 000c.292d.dd79 ARPA Vlan30
Internet 172.16.10.2 111 000c.292d.dd79 ARPA Vlan30
Internet 172.16.10.3 110 000c.2959.caca ARPA Vlan30
Internet 172.16.10.1 - 0021.d7c5.f245 ARPA Vlan30
虽然我们在这里不会展示一个例子,但是 IPVLAN 接口在 L2 模式下也像我们在最近几个配方中看到的 MacVLAN 接口类型一样具有命名空间感知能力。唯一的区别在于接口 MAC 地址,就像我们在前面的代码中看到的那样。与父接口无法与子接口通信以及反之的相同限制也适用。
现在我们知道了 IPVLAN 在 L2 模式下的工作原理,让我们讨论一下 IPVLAN L3 模式。L3 模式与我们到目前为止所见到的情况有很大不同。正如 L3 模式的名称所暗示的那样,这种接口类型在所有附加的子接口之间路由流量。这在命名空间配置中最容易理解。例如,让我们看一下这个快速实验的拓扑:

在上图中,您可以看到我在我们的两个实验主机上创建了四个独立的命名空间。我还创建了四个独立的 IPVLAN 接口,将它们映射到不同的命名空间,并为它们分配了各自独特的 IP 地址。由于这些是 IPVLAN 接口,您会注意到所有 IPVLAN 接口共享父接口的 MAC 地址。为了构建这个拓扑,我在每个相应的主机上使用了以下配置:
user@net1:~$ sudo ip link del dev ipvlan1
user@net1:~$ sudo ip link del dev ipvlan2
user@net1:~$ sudo ip netns add namespace1
user@net1:~$ sudo ip netns add namespace2
user@net1:~$ sudo ip link add ipvlan1 link eth0 type ipvlan mode l3
user@net1:~$ sudo ip link add ipvlan2 link eth0 type ipvlan mode l3
user@net1:~$ sudo ip link set ipvlan1 netns namespace1
user@net1:~$ sudo ip link set ipvlan2 netns namespace2
user@net1:~$ sudo ip netns exec namespace1 ip address \
add 10.10.20.10/24 dev ipvlan1
user@net1:~$ sudo ip netns exec namespace1 ip link set dev ipvlan1 up
user@net1:~$ sudo ip netns exec namespace2 sudo ip address \
add 10.10.30.10/24 dev ipvlan2
user@net1:~$ sudo ip netns exec namespace2 ip link set dev ipvlan2 up
user@net2:~$ sudo ip netns add namespace3
user@net2:~$ sudo ip netns add namespace4
user@net2:~$ sudo ip link add ipvlan3 link eth0 type ipvlan mode l3
user@net2:~$ sudo ip link add ipvlan4 link eth0 type ipvlan mode l3
user@net2:~$ sudo ip link set ipvlan3 netns namespace3
user@net2:~$ sudo ip link set ipvlan4 netns namespace4
user@net2:~$ sudo ip netns exec namespace3 ip address \
add 10.10.40.10/24 dev ipvlan3
user@net2:~$ sudo ip netns exec namespace3 ip link set dev ipvlan3 up
user@net2:~$ sudo ip netns exec namespace4 sudo ip address \
add 10.10.40.11/24 dev ipvlan4
user@net2:~$ sudo ip netns exec namespace4 ip link set dev ipvlan4 up
一旦配置完成,您会注意到唯一可以相互通信的接口是主机net2上的那些接口(10.10.40.10和10.10.40.11)。让我们逻辑地看一下这个拓扑,以理解其中的原因:

从逻辑上看,它开始看起来像一个路由网络。你会注意到所有分配的 IP 地址都是唯一的,没有重叠。正如我之前提到的,IPVLAN L3 模式就像一个路由器。从概念上看,你可以把父接口看作是那个路由器。如果我们从三层的角度来看,只有命名空间 3 和 4 中的接口可以通信,因为它们在同一个广播域中。其他命名空间需要通过网关进行路由才能相互通信。让我们检查一下所有命名空间的路由表,看看情况如何:
user@net1:~$ sudo ip netns exec **namespace1** ip route
10.10.20.0/24** dev ipvlan1 proto kernel scope link src 10.10.20.10
user@net1:~$ sudo ip netns exec **namespace2** ip route
10.10.30.0/24** dev ipvlan2 proto kernel scope link src 10.10.30.10
user@net2:~$ sudo ip netns exec **namespace3** ip route
10.10.40.0/24** dev ipvlan3 proto kernel scope link src 10.10.40.10
user@net2:~$ sudo ip netns exec **namespace4** ip route
10.10.40.0/24** dev ipvlan4 proto kernel scope link src 10.10.40.11
如预期的那样,每个命名空间只知道本地网络。因此,为了让这些接口进行通信,它们至少需要一个默认路由。这就是事情变得有点有趣的地方。IPVLAN 接口不允许广播或组播流量。这意味着如果我们将接口的网关定义为上游交换机,它永远也无法到达,因为它无法进行 ARP。然而,由于父接口就像一种路由器,我们可以让命名空间使用 IPVLAN 接口本身作为网关。我们可以通过以下方式添加默认路由来实现这一点:
user@net1:~$ sudo ip netns exec namespace1 ip route add \
default dev ipvlan1
user@net1:~$ sudo ip netns exec namespace2 ip route add \
default dev ipvlan2
user@net2:~$ sudo ip netns exec namespace3 ip route add \
default dev ipvlan3
user@net2:~$ sudo ip netns exec namespace4 ip route add \
default dev ipvlan4
在添加这些路由之后,你还需要在每台 Linux 主机上添加路由,告诉它们如何到达这些远程子网。由于这个示例中的两台主机是二层相邻的,最好在主机本身进行这些操作。虽然你也可以依赖默认路由,并在上游网络设备上配置这些路由,但这并不理想。你实际上会在网关上的同一个 L3 接口上进行路由,这不是一个很好的网络设计实践。如果主机不是二层相邻的,那么在多层交换机上添加路由就是必需的。
user@net1:~$ sudo ip route add 10.10.40.0/24 via 172.16.10.3
user@net2:~$ sudo ip route add 10.10.20.0/24 via 172.16.10.2
user@net2:~$ sudo ip route add 10.10.30.0/24 via 172.16.10.2
在安装了所有路由之后,你应该能够从任何一个命名空间到达所有其他命名空间。
user@net1:~$ **sudo ip netns exec namespace1 ping 10.10.30.10 -c 2
PING 10.10.30.10 (10.10.30.10) 56(84) bytes of data.
64 bytes from 10.10.30.10: icmp_seq=1 ttl=64 time=0.047 ms
64 bytes from 10.10.30.10: icmp_seq=2 ttl=64 time=0.033 ms
--- 10.10.30.10 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.033/0.040/0.047/0.007 ms
user@net1:~$ **sudo ip netns exec namespace1 ping 10.10.40.10 -c 2
PING 10.10.40.10 (10.10.40.10) 56(84) bytes of data.
64 bytes from 10.10.40.10: icmp_seq=1 ttl=64 time=0.258 ms
64 bytes from 10.10.40.10: icmp_seq=2 ttl=64 time=0.366 ms
--- 10.10.40.10 ping statistics ---
2 packets transmitted, 2 received, +3 duplicates, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.258/0.307/0.366/0.042 ms
user@net1:~$ **sudo ip netns exec namespace1 ping 10.10.40.11 -c 2
PING 10.10.40.11 (10.10.40.11) 56(84) bytes of data.
64 bytes from 10.10.40.11: icmp_seq=1 ttl=64 time=0.246 ms
64 bytes from 10.10.40.11: icmp_seq=2 ttl=64 time=0.366 ms
--- 10.10.40.11 ping statistics ---
2 packets transmitted, 2 received, +3 duplicates, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.246/0.293/0.366/0.046 ms
user@net1:~$ s
正如你所看到的,IPVLAN L3 模式与我们到目前为止所见到的不同。与 MacVLAN 或 IPVLAN L2 不同,你需要告诉网络如何到达这些新接口。
使用 Docker IPVLAN 网络驱动程序
正如我们在前一个配方中所看到的,IPVLAN 提供了一些有趣的操作模式,这些模式可能与大规模容器部署相关。目前,Docker 在其实验软件通道中支持 IPVLAN。在本配方中,我们将审查如何使用 Docker IPVLAN 驱动程序消耗附加 IPVLAN 的容器。
准备工作
在本配方中,我们将使用两台运行 Docker 的 Linux 主机。我们的实验拓扑将如下所示:

假设每个主机都在运行 Docker 的实验通道,以便访问实验性的 IPVLAN 网络驱动程序。请参阅有关使用和消费实验软件通道的第 1 个配方。主机应该有一个单独的 IP 接口,并且 Docker 应该处于默认配置。在某些情况下,我们所做的更改可能需要您具有系统的根级访问权限。
如何操作…
一旦您的主机运行了实验性代码,请通过查看docker info的输出来验证您是否处于正确的版本:
user@docker1:~$ docker info
…<Additional output removed for brevity>…
Server Version: 1.12.2
…<Additional output removed for brevity>…
Experimental: true
user@docker1:~$
在撰写本文时,您需要在 Docker 的实验版本上才能使用 IPVLAN 驱动程序。
Docker IPVLAN 网络驱动程序提供了层 2 和层 3 操作模式。由于 IPVLAN L2 模式与我们之前审查的 MacVLAN 配置非常相似,因此我们将专注于在本配方中实现 L3 模式。我们需要做的第一件事是定义网络。在这样做之前,在使用 IPVLAN 网络驱动程序时需要记住一些事情:
-
虽然它允许您在定义网络时指定网关,但该设置将被忽略。请回想一下前一个配方,您需要使用 IPVLAN 接口本身作为网关,而不是上游网络设备。Docker 会为您配置这个。
-
作为驱动程序的一个选项,您需要指定要用作所有附加 IPVLAN 接口的父接口的接口。如果您不将父接口指定为选项,Docker 将创建一个虚拟网络接口,并将其用作父接口。这将阻止该网络与外部网络进行通信。
-
在使用 IPVLAN 驱动程序创建网络时,可以使用
--internal标志。当指定时,父接口被定义为虚拟接口,阻止流量离开主机。 -
如果您没有指定子网,Docker IPAM 将为您选择一个。这是不建议的,因为这些是可路由的子网。不同 Docker 主机上的 IPAM 可能会选择相同的子网。请始终指定您希望定义的子网。
-
IPVLAN 用户定义网络和父接口之间是一对一的关系。也就是说,在给定的父接口上只能定义一个 IPVLAN 类型的网络。
您会注意到,许多前面的观点与适用于 Docker MacVLAN 驱动程序的观点相似。一个重要的区别在于,我们不希望使用与父接口相同的网络。在我们的示例中,我们将在主机docker1上使用子网10.10.20.0/24,在主机docker3上使用子网10.10.30.0/24。现在让我们在每台主机上定义网络:
user@docker1:~$ docker network create -d ipvlan -o parent=eth0 \
--subnet=10.10.20.0/24 -o ipvlan_mode=l3 ipvlan_net
16a6ed2b8d2bdffad04be17e53e498cc48b71ca0bdaed03a565542ba1214bc37
user@docker3:~$ docker network create -d ipvlan -o parent=eth0 \
--subnet=10.10.30.0/24 -o ipvlan_mode=l3 ipvlan_net
6ad00282883a83d1f715b0f725ae9115cbd11034ec59347524bebb4b673ac8a2
创建后,我们可以在每个使用 IPVLAN 网络的主机上启动一个容器:
user@docker1:~$ docker run -d --name=web1 --net=ipvlan_net \
jonlangemak/web_server_1
93b6be9e83ee2b1eaef26abd2fb4c653a87a75cea4b9cd6bf26376057d77f00f
user@docker3:~$ docker run -d --name=web2 --net=ipvlan_net \
jonlangemak/web_server_2
89b8b453849d12346b9694bb50e8376f30c2befe4db8836a0fd6e3950f57595c
您会注意到,我们再次不需要处理发布端口。容器被分配了一个完全可路由的 IP 地址,并且可以在该 IP 上提供任何服务。分配给容器的 IP 地址将来自指定的子网。在这种情况下,我们的拓扑结构如下:

一旦运行起来,您会注意到容器没有任何连接。这是因为网络不知道如何到达每个 IPVLAN 网络。为了使其工作,我们需要告诉上游网络设备如何到达每个子网。为此,我们将在多层交换机上添加以下路由:
ip route 10.10.20.0 255.255.255.0 10.10.10.101
ip route 10.10.30.0 255.255.255.0 192.168.50.101
一旦建立了这种路由,我们就能够路由到远程容器并访问它们提供的任何服务:
user@docker1:~$ **docker exec web1 curl -s http://10.10.30.2
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
您会注意到,在这种模式下,容器还可以访问主机接口:
user@docker1:~$ **docker exec -it web1 ping 10.10.10.101 -c 2
PING 10.10.10.101 (10.10.10.101): 48 data bytes
56 bytes from 10.10.10.101: icmp_seq=0 ttl=63 time=0.232 ms
56 bytes from 10.10.10.101: icmp_seq=1 ttl=63 time=0.321 ms
--- 10.10.10.101 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.232/0.277/0.321/0.045 ms
user@docker1:~$
虽然这样可以工作,但重要的是要知道这是通过遍历父接口到多层交换机然后再返回来实现的。如果我们尝试在相反的方向进行 ping,上游交换机(网关)会生成 ICMP 重定向。
user@docker1:~$ ping 10.10.20.2 -c 2
PING 10.10.20.2 (10.10.20.2) 56(84) bytes of data.
From **10.10.10.1**: icmp_seq=1 **Redirect Host(New nexthop: 10.10.10.101)
64 bytes from 10.10.20.2: icmp_seq=1 ttl=64 time=0.270 ms
From **10.10.10.1**: icmp_seq=2 **Redirect Host(New nexthop: 10.10.10.101)
64 bytes from 10.10.20.2: icmp_seq=2 ttl=64 time=0.368 ms
--- 10.10.20.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.270/0.319/0.368/0.049 ms
user@docker1:~$
因此,虽然主机到容器的连接是有效的,但如果您需要主机与本地容器通信,则这不是最佳模型。
使用 MacVLAN 和 IPVLAN 网络标记 VLAN ID
MacVLAN 和 IPVLAN Docker 网络类型都具有的一个特性是能够在特定 VLAN 上标记容器。这是可能的,因为这两种网络类型都利用了一个父接口。在这个教程中,我们将向您展示如何创建支持 VLAN 标记或 VLAN 感知的 Docker 网络类型。由于这个功能在任一网络类型的情况下都是相同的,我们将重点介绍如何在 MacVLAN 类型网络中配置这个功能。
准备工作
在这个教程中,我们将使用单个 Docker 主机来演示 Linux 主机如何向上游网络设备发送 VLAN 标记帧。我们的实验拓扑将如下所示:

假设这个主机正在运行 1.12 版本。主机有两个网络接口,eth0的 IP 地址是10.10.10.101,eth1是启用的,但没有配置 IP 地址。
操作步骤…
MacVLAN 和 IPVLAN 网络驱动程序带来的一个有趣特性是能够提供子接口。子接口是通常物理接口的逻辑分区。对物理接口进行分区的标准方法是利用 VLAN。你通常会听到这被称为 dot1q 干线或 VLAN 标记。为了做到这一点,上游网络接口必须准备好接收标记帧并能够解释标记。在我们之前的所有示例中,上游网络端口都是硬编码到特定的 VLAN。这就是这台服务器的eth0接口的情况。它插入了交换机上的一个端口,该端口静态配置为 VLAN 10。此外,交换机还在 VLAN 10 上有一个 IP 接口,我们的情况下是10.10.10.1/24。它充当服务器的默认网关。从服务器的eth0接口发送的帧被交换机接收并最终进入 VLAN 10。这一点非常简单明了。
另一个选择是让服务器告诉交换机它希望在哪个 VLAN 中。为此,我们在服务器上创建一个特定于给定 VLAN 的子接口。离开该接口的流量将被标记为 VLAN 号并发送到交换机。为了使其工作,交换机端口需要配置为干线。干线是可以携带多个 VLAN 并且支持 VLAN 标记(dot1q)的接口。当交换机接收到帧时,它会引用帧中的 VLAN 标记,并根据标记将流量放入正确的 VLAN 中。从逻辑上讲,您可以将干线配置描述如下:

我们将eth1接口描述为一个宽通道,可以支持连接到大量 VLAN。我们可以看到干线端口可以连接到所有可能的 VLAN 接口,这取决于它接收到的标记。eth0接口静态绑定到 VLAN 10 接口。
注意
在生产环境中,限制干线端口上允许的 VLAN 是明智的。不这样做意味着某人可能只需指定正确的 dot1q 标记就可以潜在地访问交换机上的任何 VLAN。
这个功能已经存在很长时间了,Linux 系统管理员可能熟悉用于创建 VLAN 标记子接口的手动过程。有趣的是,Docker 现在可以为您管理这一切。例如,我们可以创建两个不同的 MacVLAN 网络:
user@docker1:~$ docker network create -d macvlan **-o parent=eth1.19 \
--subnet=10.10.90.0/24 --gateway=10.10.90.1 vlan19
8f545359f4ca19ee7349f301e5af2c84d959e936a5b54526b8692d0842a94378
user@docker1:~$ docker network create -d macvlan **-o parent=eth1.20 \
--subnet=192.168.20.0/24 --gateway=192.168.20.1 vlan20
df45e517a6f499d589cfedabe7d4a4ef5a80ed9c88693f255f8ceb91fe0bbb0f
user@docker1:~$
接口的定义与任何其他 MacVLAN 接口一样。不同的是,我们在父接口名称上指定了.19和.20。在接口名称后面指定带有数字的点是定义子接口的常见语法。如果我们查看主机网络接口,我们应该会看到两个新接口的添加:
user@docker1:~$ ip -d link show
…<Additional output removed for brevity>…
5: **eth1.19@eth1**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 00:0c:29:50:b8:d6 brd ff:ff:ff:ff:ff:ff promiscuity 0
vlan protocol 802.1Q id 19** <REORDER_HDR> addrgenmode eui64
6: **eth1.20@eth1**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 00:0c:29:50:b8:d6 brd ff:ff:ff:ff:ff:ff promiscuity 0
vlan protocol 802.1Q id 20** <REORDER_HDR> addrgenmode eui64
user@docker1:~$
从这个输出中我们可以看出,这些都是 MacVLAN 或 IPVLAN 接口,其父接口恰好是物理接口eth1。
如果我们在这两个网络上启动容器,我们会发现它们最终会进入基于我们指定的网络的 VLAN 19 或 VLAN 20 中:
user@docker1:~$ **docker run --net=vlan19 --name=web1 -d \
jonlangemak/web_server_1
7f54eec28098eb6e589c8d9601784671b9988b767ebec5791540e1a476ea5345
user@docker1:~$
user@docker1:~$ **docker run --net=vlan20 --name=web2 -d \
jonlangemak/web_server_2
a895165c46343873fa11bebc355a7826ef02d2f24809727fb4038a14dd5e7d4a
user@docker1:~$
user@docker1:~$ **docker exec web1 ip addr show dev eth0
7: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
link/ether 02:42:0a:0a:5a:02 brd ff:ff:ff:ff:ff:ff
inet **10.10.90.2/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe0a:5a02/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
user@docker1:~$ **docker exec web2 ip addr show dev eth0
8: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
link/ether 02:42:c0:a8:14:02 brd ff:ff:ff:ff:ff:ff
inet **192.168.20.2/24** scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:c0ff:fea8:1402/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
如果我们尝试向它们的网关发送流量,我们会发现两者都是可达的:
user@docker1:~$ **docker exec -it web1 ping 10.10.90.1 -c 2
PING 10.10.90.1 (10.10.90.1): 48 data bytes
56 bytes from 10.10.90.1: icmp_seq=0 ttl=255 time=0.654 ms
56 bytes from 10.10.90.1: icmp_seq=1 ttl=255 time=0.847 ms
--- 10.10.90.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.654/0.750/0.847/0.097 ms
user@docker1:~$ **docker exec -it web2 ping 192.168.20.1 -c 2
PING 192.168.20.1 (192.168.20.1): 48 data bytes
56 bytes from 192.168.20.1: icmp_seq=0 ttl=255 time=0.703 ms
56 bytes from 192.168.20.1: icmp_seq=1 ttl=255 time=0.814 ms
--- 192.168.20.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.703/0.758/0.814/0.056 ms
user@docker1:~$
如果我们捕获服务器发送的帧,甚至能够在第 2 层标头中看到 dot1q(VLAN)标记:

与 Docker 创建的其他网络结构一样,Docker 也会在您删除这些用户定义的网络时进行清理。此外,如果您更喜欢自己建立子接口,Docker 可以使用您已经创建的接口,只要名称与您指定的父接口相同即可。
能够在用户定义的网络中指定 VLAN 标签是一件大事,这使得将容器呈现给物理网络变得更加容易。
第十章:利用 IPv6
在本章中,我们将涵盖以下教程:
-
IPv6 命令行基础知识
-
在 Docker 中启用 IPv6 功能
-
使用 IPv6 启用的容器
-
配置 NDP 代理
-
用户定义的网络和 IPv6
介绍
在本书的这一部分,我们一直专注于 IPv4 网络。然而,IPv4 并不是我们唯一可用的 IP 协议。尽管 IPv4 仍然是最广为人知的协议,但 IPv6 开始引起了重大关注。公共 IPv4 空间已经耗尽,许多人开始预见到私有 IPv4 分配用尽的问题。IPv6 看起来可以通过定义更大的可用 IP 空间来解决这个问题。然而,IPv6 与 IPv4 有一些不同之处,使一些人认为实施 IPv6 将会很麻烦。我认为,当你考虑部署容器技术时,你也应该考虑如何有效地利用 IPv6。尽管 IPv6 是一个不同的协议,但它很快将成为许多网络的要求。随着容器代表着在你的网络上引入更多 IP 端点的可能性,尽早进行过渡是一个好主意。在本章中,我们将看看 Docker 目前支持的 IPv6 功能。
IPv6 命令行基础知识
即使你了解 IPv6 协议的基础知识,第一次在 Linux 主机上使用 IPv6 可能会有点令人畏惧。与 IPv4 类似,IPv6 有其独特的一套命令行工具,可以用来配置和排除 IPv6 连接问题。其中一些工具与我们在 IPv4 中使用的相同,只是语法略有不同。其他工具则是完全独特于 IPv6。在这个教程中,我们将介绍如何配置和验证基本的 IPv6 连接。
准备工作
在这个教程中,我们将使用由两个 Linux 主机组成的小型实验室:

每台主机都分配了一个 IPv4 地址和一个 IPv6 地址给其物理接口。你需要 root 级别的访问权限来对每台主机进行网络配置更改。
注意
这个教程的目的不是教授 IPv6 或 IPv6 网络设计的基础知识。本教程中的示例仅供举例。虽然在示例中我们可能会涵盖一些基础知识,但假定读者已经对 IPv6 协议的工作原理有基本的了解。
如何做…
如前图所示,每台 Linux 主机都被分配了 IPv4 和 IPv6 IP 地址。这些都是作为主机网络配置脚本的一部分进行配置的。以下是两台实验主机的示例配置:
net1.lab.lab
auto eth0
iface eth0 inet static
address 172.16.10.2
netmask 255.255.255.0
gateway 172.16.10.1
dns-nameservers 10.20.30.13
dns-search lab.lab
iface eth0 inet6 static
address 2003:ab11::1
netmask 64
net2.lab.lab
auto eth0
iface eth0 inet static
address 172.16.10.3
netmask 255.255.255.0
gateway 172.16.10.1
dns-nameservers 10.20.30.13
dns-search lab.lab
iface eth0 inet6 static
address 2003:ab11::2
netmask 64
请注意,在每种情况下,我们都将 IPv6 地址添加到现有的物理网络接口上。在这种类型的配置中,IPv4 和 IPv6 地址共存于同一个网卡上。这通常被称为运行双栈,因为两种协议共享同一个物理适配器。配置完成后,您需要重新加载接口以使配置生效。然后,您应该能够通过使用ifconfig工具或ip(iproute2)工具集来确认每台主机是否具有正确的配置:
user@net1:~$ **ifconfig eth0
eth0 Link encap:Ethernet HWaddr 00:0c:29:2d:dd:79
inet addr:172.16.10.2 Bcast:172.16.10.255 Mask:255.255.255.0
inet6 addr: fe80::20c:29ff:fe2d:dd79/64 Scope:Link
inet6 addr: 2003:ab11::1/64 Scope:Global
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:308 errors:0 dropped:0 overruns:0 frame:0
TX packets:348 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:32151 (32.1 KB) TX bytes:36732 (36.7 KB)
user@net1:~$
user@net2:~$ ip -6 addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qlen 1000
inet6 2003:ab11::2/64 scope global
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe59:caca/64 scope link
valid_lft forever preferred_lft forever
user@net2:~$
使用较旧的ifconfig工具的优势在于您可以同时看到 IPv4 和 IPv6 接口信息。当使用ip工具时,您需要通过传递-6标志来指定您希望看到 IPv6 信息。当我们在后面使用ip工具配置 IPv6 接口时,我们将看到这种情况是一样的。
在任一情况下,两台主机现在似乎都已经在它们的eth0接口上配置了 IPv6。但是,请注意,实际上我们定义了两个 IPv6 地址。您会注意到一个地址的范围是本地的,另一个地址的范围是全局的。在 IPv6 中,每个 IP 接口都被分配了全局和本地 IPv6 地址。本地范围的接口仅对其分配的链路上的通信有效,并且通常用于到达同一段上的相邻设备。在大多数情况下,链路本地地址是由主机自己动态确定的。这意味着几乎每个启用 IPv6 的接口都有一个链路本地 IPv6 地址,即使您没有在接口上专门配置全局 IPv6 地址。使用链路本地 IP 地址的数据包永远不会被路由器转发,这将限制它们在定义的段上。在我们的大部分讨论中,我们将专注于全局地址。
注意
任何对 IPv6 地址的进一步引用都是指全局范围的 IPv6 地址,除非另有说明。
由于我们的两台主机都在同一个子网上,我们应该能够使用 IPv6 从一台服务器到达另一台服务器:
user@net1:~$ **ping6 2003:ab11::2 -c 2
PING 2003:ab11::2(2003:ab11::2) 56 data bytes
64 bytes from 2003:ab11::2: icmp_seq=1 ttl=64 time=0.422 ms
64 bytes from 2003:ab11::2: icmp_seq=2 ttl=64 time=0.401 ms
--- 2003:ab11::2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.401/0.411/0.422/0.022 ms
user@net1:~$
请注意,我们使用ping6工具而不是标准的 ping 工具来验证 IPv6 的可达性。
我们想要检查的最后一件事是邻居发现表。IPv6 的另一个重大变化是它不使用 ARP 来查找 IP 端点的硬件或 MAC 地址。这样做的主要原因是 IPv6 不支持广播流量。ARP 依赖广播来工作,因此不能在 IPv6 中使用。相反,IPv6 使用邻居发现,它利用多播。
话虽如此,当排除本地网络故障时,您需要查看邻居发现表,而不是 ARP 表。为此,我们可以使用熟悉的iproute2工具集:
user@net1:~$ **ip -6 neighbor show
fe80::20c:29ff:fe59:caca dev eth0 lladdr 00:0c:29:59:ca:ca DELAY
2003:ab11::2 dev eth0 lladdr 00:0c:29:59:ca:ca REACHABLE
user@net1:~$
与 ARP 表类似,邻居表向我们显示了我们希望到达的 IPv6 地址的硬件或 MAC 地址。请注意,与之前一样,我们向ip命令传递了-6标志,告诉它我们需要 IPv6 信息。
现在我们已经建立了基本的连接性,让我们在每个主机上添加一个新的 IPv6 接口。为此,我们几乎可以按照添加 IPv4 接口时所做的相同步骤进行操作。例如,添加虚拟接口几乎是相同的:
user@net1:~$ sudo ip link add ipv6_dummy type dummy
user@net1:~$ sudo ip -6 address add 2003:cd11::1/64 dev ipv6_dummy
user@net1:~$ sudo ip link set ipv6_dummy up
请注意,唯一的区别是我们需要再次传递-6标志,告诉iproute2我们正在指定一个 IPv6 地址。在其他方面,配置与我们在 IPv4 中所做的方式完全相同。让我们也在第二个主机上配置另一个虚拟接口:
user@net2:~$ sudo ip link add ipv6_dummy type dummy
user@net2:~$ sudo ip -6 address add 2003:ef11::1/64 dev ipv6_dummy
user@net2:~$ sudo ip link set ipv6_dummy up
此时,我们的拓扑现在如下所示:

现在让我们检查每个主机的 IPv6 路由表。与之前一样,我们也可以使用iproute2工具来检查 IPv6 路由表:
user@net1:~$ ip -6 route
2003:ab11::/64 dev eth0 proto kernel metric 256 pref medium
2003:cd11::/64 dev ipv6_dummy proto kernel metric 256 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
fe80::/64 dev ipv6_dummy proto kernel metric 256 pref medium
user@net1:~$
user@net2:~$ ip -6 route
2003:ab11::/64 dev eth0 proto kernel metric 256 pref medium
2003:ef11::/64 dev ipv6_dummy proto kernel metric 256 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
fe80::/64 dev ipv6_dummy proto kernel metric 256 pref medium
user@net2:~$
正如我们所看到的,每个主机都知道自己直接连接的接口,但不知道其他主机的虚拟接口。为了使任何一个主机能够到达其他主机的虚拟接口,我们需要进行路由。由于这些主机是直接连接的,可以通过添加默认的 IPv6 路由来解决。每个默认路由将引用另一个主机作为下一跳。虽然这是可行的,但让我们改为向每个主机添加特定路由,引用虚拟接口所在的网络:
user@net1:~$ sudo ip -6 route add **2003:ef11::/64 via 2003:ab11::2
user@net2:~$ sudo ip -6 route add **2003:cd11::/64 via 2003:ab11::1
添加这些路由后,任何一个主机都应该能够到达其他主机的ipv6_dummy接口:
user@net1:~$ **ping6 2003:ef11::1 -c 2
PING 2003:ef11::1(2003:ef11::1) 56 data bytes
64 bytes from 2003:ef11::1: icmp_seq=1 ttl=64 time=0.811 ms
64 bytes from 2003:ef11::1: icmp_seq=2 ttl=64 time=0.408 ms
--- 2003:ef11::1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.408/0.609/0.811/0.203 ms
user@net1:~$
注意
您可能会注意到,只在单个主机上添加一个路由就可以使该主机到达另一个主机上的虚拟接口。这是因为我们只需要路由来将流量从发起主机上移出。流量将由主机的 eth0 接口(2003:ab11::/64)发出,而另一个主机知道如何到达它。如果 ping 是从虚拟接口发出的,您需要这两个路由才能使其正常工作。
现在我们已经配置并验证了基本的连接性,让我们迈出最后一步,使用网络命名空间重建这些接口。为此,让我们首先清理虚拟接口,因为我们将在命名空间内重用这些 IPv6 子网:
user@net1:~$ sudo ip link del dev ipv6_dummy
user@net2:~$ sudo ip link del dev ipv6_dummy
我们要配置的配置如下:

虽然与上一个配置非常相似,但有两个主要区别。您会注意到我们现在使用网络命名空间来封装新接口。这样做,我们已经为 VETH 对的一端配置了新接口的 IPv6 地址。VETH 对的另一端位于默认网络命名空间中的主机上。
注意
如果您对一些 Linux 网络构造不太熟悉,请查看第一章中的 Linux 网络构造,在那里我们会更详细地讨论命名空间和 VETH 接口。
要进行配置,我们将应用以下配置:
添加一个名为 net1_ns 的新网络命名空间:
user@net1:~$ sudo ip netns add net1_ns
创建一个名为 host_veth1 的 VETH 对,并将另一端命名为 ns_veth1:
user@net1:~$ sudo ip link add host_veth1 type veth peer name ns_veth1
将 VETH 对的命名空间端移入命名空间:
user@net1:~$ sudo ip link set dev ns_veth1 netns net1_ns
在命名空间内,给 VETH 接口分配一个 IP 地址:
user@net1:~$ sudo ip netns exec net1_ns ip -6 address \
add 2003:cd11::2/64 dev ns_veth1
在命名空间内,启动接口:
user@net1:~$ sudo ip netns exec net1_ns ip link set ns_veth1 up
在命名空间内,添加一个路由以到达另一个主机上的命名空间:
user@net1:~$ sudo ip netns exec net1_ns ip -6 route \
add 2003:ef11::/64 via 2003:cd11::1
给 VETH 对的主机端分配一个 IP 地址:
user@net1:~$ sudo ip -6 address add 2003:cd11::1/64 dev host_veth1
启动 VETH 接口的主机端:
user@net1:~$ sudo ip link set host_veth1 up
注意
请注意,我们只在命名空间内添加了一个路由以到达另一个命名空间。我们没有在 Linux 主机上添加相同的路由。这是因为我们之前已经在配方中添加了这个路由,以便到达虚拟接口。如果您删除了该路由,您需要将其添加回来才能使其正常工作。
我们现在必须在第二个主机上执行类似的配置:
user@net2:~$ sudo ip netns add net2_ns
user@net2:~$ sudo ip link add host_veth1 type veth peer name ns_veth1
user@net2:~$ sudo ip link set dev ns_veth1 netns net2_ns
user@net2:~$ sudo ip netns exec net2_ns ip -6 address add \
2003:ef11::2/64 dev ns_veth1
user@net2:~$ sudo ip netns exec net2_ns ip link set ns_veth1 up
user@net2:~$ sudo ip netns exec net2_ns ip -6 route add \
2003:cd11::/64 via 2003:ef11::1
user@net2:~$ sudo ip -6 address add 2003:ef11::1/64 dev host_veth1
user@net2:~$ sudo ip link set host_veth1 up
添加后,您应该能够验证每个命名空间是否具有到达其他主机命名空间所需的路由信息:
user@net1:~$ sudo ip netns exec net1_ns ip -6 route
2003:cd11::/64 dev ns_veth1 proto kernel metric 256 pref medium
2003:ef11::/64 via 2003:cd11::1 dev ns_veth1 metric 1024 pref medium
fe80::/64 dev ns_veth1 proto kernel metric 256 pref medium
user@net1:~$
user@net2:~$ sudo ip netns exec net2_ns ip -6 route
2003:cd11::/64 via 2003:ef11::1 dev ns_veth1 metric 1024 pref medium
2003:ef11::/64 dev ns_veth1 proto kernel metric 256 pref medium
fe80::/64 dev ns_veth1 proto kernel metric 256 pref medium
user@net2:~$
但是当我们尝试从一个命名空间到另一个命名空间时,连接失败:
user@net1:~$ **sudo ip netns exec net1_ns ping6 2003:ef11::2 -c 2
PING 2003:ef11::2(2003:ef11::2) 56 data bytes
--- 2003:ef11::2 ping statistics ---
2 packets transmitted, 0 received, **100% packet loss**, time 1007ms
user@net1:~$
这是因为我们现在正在尝试将 Linux 主机用作路由器。如果您回忆起早期章节,当我们希望 Linux 内核转发或路由数据包时,我们必须启用该功能。这是通过更改每个主机上的这两个内核参数来完成的:
user@net1:~$ sudo sysctl **net.ipv6.conf.default.forwarding=1
net.ipv6.conf.default.forwarding = 1
user@net1:~$ sudo sysctl **net.ipv6.conf.all.forwarding=1
net.ipv6.conf.all.forwarding = 1
注意
请记住,以这种方式定义的设置在重新启动时不会持久保存。
一旦在两个主机上进行了这些设置,您的 ping 现在应该开始工作:
user@net1:~$ **sudo ip netns exec net1_ns ping6 2003:ef11::2 -c 2
PING 2003:ef11::2(2003:ef11::2) 56 data bytes
64 bytes from 2003:ef11::2: icmp_seq=1 ttl=62 time=0.540 ms
64 bytes from 2003:ef11::2: icmp_seq=2 ttl=62 time=0.480 ms
--- 2003:ef11::2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.480/0.510/0.540/0.030 ms
user@net1:~$
有趣的是,在启用内核中的 IPv6 转发后,检查主机上的邻居表:
user@net1:~$ ip -6 neighbor
2003:ab11::2 dev eth0 lladdr 00:0c:29:59:ca:ca router STALE
2003:cd11::2 dev host_veth1 lladdr a6:14:b5:39:da:96 STALE
fe80::20c:29ff:fe59:caca dev eth0 lladdr 00:0c:29:59:ca:ca router STALE
fe80::a414:b5ff:fe39:da96 dev host_veth1 lladdr a6:14:b5:39:da:96 STALE
user@net1:~$
您是否注意到另一个 Linux 主机的邻居条目有什么不同之处?现在,它的邻居定义中包含router标志。当 Linux 主机在内核中启用 IPv6 转发时,它会在该段上作为路由器进行广告。
启用 Docker 中的 IPv6 功能
Docker 中默认禁用 IPv6 功能。与我们之前审查的其他功能一样,要启用它需要在服务级别进行设置。一旦启用,Docker 将为与 Docker 关联的主机接口以及容器本身提供 IPv6 地址。
准备就绪
在这个示例中,我们将使用由两个 Docker 主机组成的小型实验室:

每个主机都有分配给其物理接口的 IPv4 地址和 IPv6 地址。您需要对每个主机进行网络配置更改的根级访问权限。假定已安装了 Docker,并且它是默认配置。
如何做…
如前所述,除非告知,Docker 不会为容器提供 IPv6 地址。要在 Docker 中启用 IPv6,我们需要向 Docker 服务传递一个服务级标志。
注意
如果您需要复习定义 Docker 服务级参数,请参阅第二章中的最后一个示例,配置和监视 Docker 网络,在那里我们讨论了在运行systemd的系统上配置这些参数。
除了启用 IPv6 功能,您还需要为docker0桥定义一个子网。为此,我们将修改 Docker 的systemd附加文件,并确保它具有以下选项:
- 在主机
docker1上:
ExecStart=/usr/bin/dockerd --ipv6 --fixed-cidr-v6=2003:cd11::/64
- 在主机
docker2上:
ExecStart=/usr/bin/dockerd --ipv6 --fixed-cidr-v6=2003:ef11::/64
如果我们应用此配置,在每个主机上重新加载systemd配置并重新启动 Docker 服务,我们应该会看到docker0桥已经从定义的 IPv6 CIDR 范围中获取了第一个可用的 IP 地址:
user@docker1:~$ ip -6 addr show dev docker0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500
inet6 2003:cd11::1/64 scope global tentative
valid_lft forever preferred_lft forever
inet6 fe80::1/64 scope link tentative
valid_lft forever preferred_lft forever
user@docker1:~$
user@docker2:~$ ip -6 addr show dev docker0
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500
inet6 2003:ef11::1/64 scope global tentative
valid_lft forever preferred_lft forever
inet6 fe80::1/64 scope link tentative
valid_lft forever preferred_lft forever
user@docker2:~$
此时,我们的拓扑结构看起来很像第一个配方中的样子:

Docker 将为其创建的每个容器分配一个 IPv6 地址和一个 IPv4 地址。让我们在第一个主机上启动一个容器,看看我的意思是什么:
user@docker1:~$ **docker run -d --name=web1 jonlangemak/web_server_1
50d522d176ebca2eac0f7e826ffb2e36e754ce27b3d3b4145aa8a11c6a13cf15
user@docker1:~$
请注意,我们没有向容器传递-P标志来发布容器暴露的端口。如果我们在本地测试,我们可以验证主机可以从容器的 IPv4 和 IPv6 地址访问容器内的服务:
user@docker1:~$ docker exec web1 ifconfig eth0
eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:02
inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
inet6 addr: 2003:cd11::242:ac11:2/64 Scope:Global
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:16 errors:0 dropped:0 overruns:0 frame:0
TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1792 (1.7 KB) TX bytes:648 (648.0 B)
user@docker1:~$ **curl http://172.17.0.2
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">Web Server #1 - Running on port 80</span>
</h1>
</body>
</html>
user@docker1:~$ **curl -g http://[2003:cd11::242:ac11:2]
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">Web Server #1 - Running on port 80</span>
</h1>
</body>
</html>
user@docker1:~$
注意
在使用带有 IPv6 地址的curl时,您需要将 IPv6 地址放在方括号中,然后通过传递-g标志告诉curl不要进行全局匹配。
正如我们所看到的,IPv6 地址的行为与 IPv4 地址的行为相同。随之而来,同一主机上的容器可以使用其分配的 IPv6 地址直接相互通信,跨过docker0桥。让我们在同一主机上启动第二个容器:
user@docker1:~$ docker run -d --name=web2 jonlangemak/web_server_2
快速验证将向我们证明这两个容器可以像预期的那样使用其 IPv6 地址直接相互通信:
user@docker1:~$ docker exec **web2** ip -6 addr show dev eth0
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
inet6 **2003:cd11::242:ac11:3/64** scope global nodad
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:3/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
user@docker1:~$ **docker exec -it web1 curl -g \
http://[2003:cd11::242:ac11:3]
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
使用 IPv6 启用的容器
在上一个配方中,我们看到了 Docker 如何处理启用 IPv6 的容器的基本分配。到目前为止,我们看到的行为与之前章节中处理 IPv4 地址容器时所看到的行为非常相似。然而,并非所有网络功能都是如此。Docker 目前在 IPv4 和 IPv6 之间并没有完全的功能对等。特别是,正如我们将在这个配方中看到的,Docker 对于启用 IPv6 的容器并没有iptables(ip6tables)集成。在本章中,我们将回顾一些我们之前在仅启用 IPv4 的容器中访问过的网络功能,并看看在使用 IPv6 寻址时它们的表现如何。
准备工作
在这个配方中,我们将继续构建上一个配方中构建的实验室。您需要 root 级别的访问权限来对每个主机进行网络配置更改。假设 Docker 已安装,并且是默认配置。
操作步骤
如前所述,Docker 目前没有针对 IPv6 的主机防火墙,特别是 netfilter 或iptables的集成。这意味着我们以前依赖 IPv4 的几个功能在处理容器的 IPv6 地址时会有所不同。让我们从一些基本功能开始。在上一个示例中,我们看到了在连接到docker0桥接器的同一主机上的两个容器可以直接相互通信。
这种行为是预期的,并且在使用 IPv4 地址时的方式基本相同。如果我们想要阻止这种通信,我们可能会考虑在 Docker 服务中禁用容器间通信(ICC)。让我们更新主机docker1上的 Docker 选项,将 ICC 设置为false:
ExecStart=/usr/bin/dockerd --icc=false --ipv6 --fixed-cidr-v6=2003:cd11::/64
然后,我们可以重新加载systemd配置,重新启动 Docker 服务,并重新启动容器:
user@docker1:~$ **docker start web1
web1
user@docker1:~$ **docker start web2
web2
user@docker1:~$ docker exec web2 ifconfig eth0
eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:03
inet addr:172.17.0.3** Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:3/64 Scope:Link
inet6 addr: 2003:cd11::242:ac11:3**/64 Scope:Global
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:12 errors:0 dropped:0 overruns:0 frame:0
TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1128 (1.1 KB) TX bytes:648 (648.0 B)
user@docker1:~$
user@docker1:~$ **docker exec -it web1 curl http://172.17.0.3
curl: (7) couldn't connect to host
user@docker1:~$ **docker exec -it web1 curl -g \
http://[2003:cd11::242:ac11:3]
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
正如我们所看到的,IPv4 尝试失败,随后的 IPv6 尝试成功。由于 Docker 没有管理与容器的 IPv6 地址相关的防火墙规则,因此没有任何阻止 IPv6 地址之间直接连接的内容。
由于 Docker 没有管理与 IPv6 相关的防火墙规则,您可能还会认为出站伪装和端口发布等功能也不再起作用。虽然这在某种意义上是正确的,即 Docker 不会创建 IPv6 相关的 NAT 规则和防火墙策略,但这并不意味着容器的 IPv6 地址无法从外部网络访问。让我们通过一个示例来向您展示我的意思。让我们在第二个 Docker 主机上启动一个容器:
user@docker2:~$ docker run -dP --name=web2 jonlangemak/web_server_2
5e2910c002db3f21aa75439db18e5823081788e69d1e507c766a0c0233f6fa63
user@docker2:~$
user@docker2:~$ docker port web2
80/tcp -> 0.0.0.0:32769
user@docker2:~$
请注意,当我们在主机docker2上运行容器时,我们传递了-P标志,告诉 Docker 发布容器的暴露端口。如果我们检查端口映射,我们可以看到主机选择了端口32768。请注意,端口映射指示 IP 地址为0.0.0.0,通常表示任何 IPv4 地址。让我们从另一个 Docker 主机执行一些快速测试,以验证工作和不工作的内容:
user@docker1:~$ **curl http://10.10.10.102:32769
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
如预期的那样,IPv4 端口映射起作用。通过利用iptables NAT 规则将端口32769映射到实际服务端口80,我们能够通过 Docker 主机的 IPv4 地址访问容器的服务。现在让我们尝试相同的示例,但使用主机的 IPv6 地址:
user@docker1:~$ **curl -g http://[2003:ab11::2]:32769
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
令人惊讶的是,这也起作用。您可能想知道这是如何工作的,考虑到 Docker 不管理或集成任何主机 IPv6 防火墙策略。答案实际上非常简单。如果我们查看第二个 Docker 主机的开放端口,我们会看到有一个绑定到端口32769的docker-proxy服务:
user@docker2:~$ sudo netstat -plnt
…<output removed for brevity>…
Active Internet connections (only servers)
Local Address Foreign Address State PID/Program name
0.0.0.0:22 0.0.0.0:* LISTEN 1387/sshd
127.0.0.1:6010 0.0.0.0:* LISTEN 3658/0
:::22 :::* LISTEN 1387/sshd
::1:6010 :::* LISTEN 3658/0
:::32769 :::* LISTEN 2390/docker-proxy
user@docker2:~$
正如我们在前几章中看到的,docker-proxy服务促进了容器之间和发布端口的连接。为了使其工作,docker-proxy服务必须绑定到容器发布的端口。请记住,监听所有 IPv4 接口的服务使用0.0.0.0的语法来表示所有 IPv4 接口。类似地,IPv6 接口使用:::的语法来表示相同的事情。您会注意到docker-proxy端口引用了所有 IPv6 接口。尽管这可能因操作系统而异,但绑定到所有 IPv6 接口也意味着绑定到所有 IPv4 接口。也就是说,前面的docker-proxy服务实际上正在监听所有主机的 IPv4 和 IPv6 接口。
注意
请记住,docker-proxy通常不用于入站服务。这些依赖于iptables NAT 规则将发布的端口映射到容器。但是,在这些规则不存在的情况下,主机仍然在其所有接口上监听端口32769的流量。
这样做的最终结果是,尽管没有 IPv6 NAT 规则,我仍然能够通过 Docker 主机接口访问容器服务。以这种方式,具有 IPv6 的发布端口仍然有效。但是,只有在使用docker-proxy时才有效。尽管这种操作模式仍然是默认的,但打算在 hairpin NAT 的支持下移除。我们可以通过将--userland-proxy=false参数传递给 Docker 作为服务级选项来在 Docker 主机上启用 hairpin NAT。这样做将阻止这种 IPv6 端口发布方式的工作。
最后,缺乏防火墙集成也意味着我们不再支持出站伪装功能。在 IPv4 中,这个功能允许容器与外部网络通信,而不必担心路由或 IP 地址重叠。离开主机的容器流量总是隐藏在主机 IP 接口之一的后面。然而,这并不是一个强制性的配置。正如我们在前几章中看到的,您可以非常容易地禁用出站伪装功能,并为docker0桥接分配一个可路由的 IP 地址和子网。只要外部或外部网络知道如何到达该子网,容器就可以非常容易地拥有一个独特的可路由 IP 地址。
IPv6 出现的一个原因是 IPv4 地址的迅速枯竭。IPv4 中的 NAT 作为一个相当成功的,尽管同样麻烦的临时缓解了地址枯竭问题。这意味着许多人认为,我们不应该在 IPv6 方面实施任何形式的 NAT。相反,所有 IPv6 前缀都应该是本地可路由和可达的,而不需要 IP 转换的混淆。缺乏 IPv6 防火墙集成,直接将 IPv6 流量路由到每个主机是 Docker 实现跨多个 Docker 主机和外部网络可达性的当前手段。这要求每个 Docker 主机使用唯一的 IPv6 CIDR 范围,并且 Docker 主机知道如何到达所有其他 Docker 主机定义的 CIDR 范围。虽然这通常需要物理网络具有网络可达性信息,在我们简单的实验室示例中,每个主机只需要对其他主机的 CIDR 添加静态路由。就像我们在第一个配方中所做的那样,我们将在每个主机上添加一个 IPv6 路由,以便两者都知道如何到达另一个docker0桥接的 IPv6 子网:
user@docker1:~$ sudo ip -6 route add 2003:ef11::/64 via 2003:ab11::2
user@docker2:~$ sudo ip -6 route add 2003:cd11::/64 via 2003:ab11::1
添加路由后,每个 Docker 主机都知道如何到达另一个主机的 IPv6 docker0桥接子网:

如果我们现在检查,我们应该在每个主机上的容器之间有可达性:
user@docker2:~$ docker exec web2 ifconfig eth0
eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:02
inet addr:172.17.0.2** Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
inet6 addr: 2003:ef11::242:ac11:2/64** Scope:Global
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:43 errors:0 dropped:0 overruns:0 frame:0
TX packets:34 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:3514 (3.5 KB) TX bytes:4155 (4.1 KB)
user@docker2:~$
user@docker1:~$ **docker exec -it web1 curl -g http://[2003:ef11::242:ac11:2]
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">**Web Server #2 - Running on port 80**</span>
</h1>
</body>
</html>
user@docker1:~$
正如我们所看到的,主机docker1上的容器能够成功地直接路由到运行在主机docker2上的容器。只要每个 Docker 主机具有适当的路由信息,容器就能够直接路由到彼此。
这种方法的缺点是容器现在是一个完全暴露的网络端点。我们不再能够通过 Docker 发布的端口仅暴露某些端口到外部网络的优势。如果您希望确保仅在 IPv6 接口上暴露某些端口,那么用户态代理可能是您目前的最佳选择。在设计围绕 IPv6 连接的服务时,请记住这些选项。
配置 NDP 代理
正如我们在上一个教程中看到的,Docker 中 IPv6 支持的一个主要区别是缺乏防火墙集成。没有这种集成,我们失去了出站伪装和完整端口发布功能。虽然这在所有情况下可能并非必要,但当不使用时会失去一定的便利因素。例如,在仅运行 IPv4 模式时,管理员可以安装 Docker 并立即将容器连接到外部网络。这是因为容器只能通过 Docker 主机的 IP 地址进行入站(发布端口)和出站(伪装)连接。这意味着无需通知外部网络有关额外子网的信息,因为外部网络只能看到 Docker 主机的 IP 地址。在 IPv6 模型中,外部网络必须知道容器子网才能路由到它们。在本章中,我们将讨论如何配置 NDP 代理作为解决此问题的方法。
准备工作
在本教程中,我们将使用以下实验拓扑:

您需要 root 级别的访问权限来对每个主机进行网络配置更改。假设 Docker 已安装,并且是默认配置。
如何做…
前面的拓扑图显示我们的主机是双栈连接到网络的,但是 Docker 还没有配置为使用 IPv6。就像我们在上一个教程中看到的那样,配置 Docker 以支持 IPv6 通常意味着在外部网络上配置路由,以便它知道如何到达您为docker0桥定义的 IPv6 CIDR。然而,假设一会儿这是不可能的。假设您无法控制外部网络,这意味着您无法向其他网络端点广告或通知有关 Docker 主机上任何新定义的 IPv6 子网。
假设虽然您无法广告任何新定义的 IPv6 网络,但您可以在现有网络中保留额外的 IPv6 空间。例如,主机当前在2003:ab11::/64网络中定义了接口。如果我们划分这个空间,我们可以将其分割成四个/66网络:
-
2003:ab11::/66 -
2003:ab11:0:0:4000::/66 -
2003:ab11:0:0:8000::/66 -
2003:ab11:0:0:c000::/66
假设我们被允许为我们的使用保留最后两个子网。我们现在可以在 Docker 中启用 IPv6,并将这两个网络分配为 IPv6 CIDR 范围。以下是每个 Docker 主机的配置选项:
docker1
ExecStart=/usr/bin/dockerd --ipv6 --fixed-cidr-v6=2003:ab11:0:0:8000::/66
docker2
ExecStart=/usr/bin/dockerd --ipv6 --fixed-cidr-v6=2003:ab11:0:0:c000::/66
将新配置加载到systemd中并重新启动 Docker 服务后,我们的实验室拓扑现在看起来是这样的:

让我们在两个主机上启动一个容器:
user@docker1:~$ docker run -d --name=web1 jonlangemak/web_server_1
user@docker2:~$ docker run -d --name=web2 jonlangemak/web_server_2
现在确定web1容器的分配的 IPv6 地址:
user@docker1:~$ docker exec web1 ip -6 addr show dev eth0
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
inet6 **2003:ab11::8000:242:ac11:2/66** scope global nodad
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
user@docker1:~$
现在,让我们尝试从web2容器到达该容器:
user@docker2:~$ **docker exec -it web2 ping6 \
2003:ab11::8000:242:ac11:2** -c 2
PING 2003:ab11::8000:242:ac11:2 (2003:ab11::8000:242:ac11:2): 48 data bytes
56 bytes from 2003:ab11::c000:0:0:1: Destination unreachable: Address unreachable
56 bytes from 2003:ab11::c000:0:0:1: Destination unreachable: Address unreachable
--- 2003:ab11::8000:242:ac11:2 ping statistics ---
2 packets transmitted, 0 packets received, **100% packet loss
user@docker2:~$
这失败是因为 Docker 主机认为目标地址直接连接到它们的eth0接口。当web2容器尝试连接时,会发生以下操作:
-
容器进行路由查找,并确定地址
2003:ab11::8000:242:ac11:2不在其本地子网2003:ab11:0:0:c000::1/66内,因此将流量转发到其默认网关(docker0桥接口) -
主机接收流量并进行路由查找,确定
2003:ab11::8000:242:ac11:2的目标地址落在其本地子网2003:ab11::/64(eth0)内,并使用 NDP 尝试找到具有该目标 IP 地址的主机 -
主机对此查询没有响应,流量失败
我们可以通过检查docker2主机的 IPv6 邻居表来验证这一点:
user@docker2:~$ ip -6 neighbor show
fe80::20c:29ff:fe50:b8cc dev eth0 lladdr 00:0c:29:50:b8:cc STALE
2003:ab11::c000:242:ac11:2 dev docker0 lladdr 02:42:ac:11:00:02 REACHABLE
2003:ab11::8000:242:ac11:2 dev eth0 FAILED
fe80::42:acff:fe11:2 dev docker0 lladdr 02:42:ac:11:00:02 REACHABLE
user@docker2:~$
按照正常的路由逻辑,一切都按预期工作。然而,IPv6 有一个叫做 NDP 代理的功能,可以帮助解决这个问题。熟悉 IPv4 中代理 ARP 的人会发现 NDP 代理提供了类似的功能。基本上,NDP 代理允许主机代表另一个端点回答邻居请求。在我们的情况下,我们可以告诉两个 Docker 主机代表容器回答。为了做到这一点,我们首先需要在主机上启用 NDP 代理。这是通过启用内核参数net.ipv6.conf.eth0.proxy_ndp来完成的,如下面的代码所示:
user@docker1:~$ sudo sysctl net.ipv6.conf.eth0.proxy_ndp=1
net.ipv6.conf.eth0.proxy_ndp = 1
user@docker1:~$
user@docker2:~$ sudo sysctl net.ipv6.conf.eth0.proxy_ndp=1
net.ipv6.conf.eth0.proxy_ndp = 1
user@docker2:~$
注意
请记住,以这种方式定义的设置在重启后不会持久保存。
一旦启用了这个功能,我们需要手动告诉每个主机要回答哪个 IPv6 地址。我们通过向每个主机的邻居表添加代理条目来实现这一点。在前面的例子中,我们需要为源容器和目标容器都这样做,以便允许双向流量。首先,在主机docker1上为目标添加条目:
user@docker1:~$ sudo ip -6 neigh add proxy \
2003:ab11::8000:242:ac11:2** dev eth0
然后,确定web2容器的 IPv6 地址,它将作为流量的源,并在主机docker2上为其添加代理条目:
user@docker2:~$ docker exec web2 ip -6 addr show dev eth0
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
inet6 **2003:ab11::c000:242:ac11:2/66** scope global nodad
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
user@docker2:~$
user@docker2:~$ sudo ip -6 neigh add proxy \
2003:ab11::c000:242:ac11:2** dev eth0
这将告诉每个 Docker 主机代表容器回复邻居请求。Ping 测试现在应该按预期工作:
user@docker2:~$ **docker exec -it web2 ping6 \
2003:ab11::8000:242:ac11:2** -c 2
PING 2003:ab11::8000:242:ac11:2 (2003:ab11::8000:242:ac11:2): 48 data bytes
56 bytes from 2003:ab11::8000:242:ac11:2: icmp_seq=0 ttl=62 time=0.462 ms
56 bytes from 2003:ab11::8000:242:ac11:2: icmp_seq=1 ttl=62 time=0.660 ms
--- 2003:ab11::8000:242:ac11:2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.462/0.561/0.660/0.099 ms
user@docker2:~$
我们应该在每个主机上看到相关的邻居条目:
user@docker1:~$ ip -6 neighbor show
fe80::20c:29ff:fe7f:3d64 dev eth0 lladdr 00:0c:29:7f:3d:64 router REACHABLE
2003:ab11::8000:242:ac11:2 dev docker0 lladdr 02:42:ac:11:00:02 REACHABLE
fe80::42:acff:fe11:2 dev docker0 lladdr 02:42:ac:11:00:02 DELAY
2003:ab11::c000:242:ac11:2 dev eth0 lladdr 00:0c:29:7f:3d:64 REACHABLE
user@docker1:~$
user@docker2:~$ ip -6 neighbor show
fe80::42:acff:fe11:2 dev docker0 lladdr 02:42:ac:11:00:02 REACHABLE
2003:ab11::c000:242:ac11:2 dev docker0 lladdr 02:42:ac:11:00:02 REACHABLE
fe80::20c:29ff:fe50:b8cc dev eth0 lladdr 00:0c:29:50:b8:cc router REACHABLE
2003:ab11::8000:242:ac11:2 dev eth0 lladdr 00:0c:29:50:b8:cc REACHABLE
user@docker2:~$
就像代理 ARP 一样,NDP 代理是通过主机在邻居发现请求中提供自己的 MAC 地址来工作的。我们可以看到,在这两种情况下,邻居表中的 MAC 地址实际上是每个主机的eth0 MAC 地址。
user@docker1:~$ ip link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether **00:0c:29:50:b8:cc** brd ff:ff:ff:ff:ff:ff
user@docker1:~$
user@docker2:~$ ip link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether **00:0c:29:7f:3d:64** brd ff:ff:ff:ff:ff:ff
user@docker2:~$
这种方法在无法将 Docker IPv6 子网广告传播到外部网络的情况下效果相当不错。然而,它依赖于每个希望代理的 IPv6 地址的单独代理条目。对于每个生成的容器,您都需要生成一个额外的 IPv6 代理地址。
用户定义的网络和 IPv6
就像我们在 IPv4 中看到的那样,用户定义的网络可以利用 IPv6 寻址。也就是说,所有与网络相关的参数都与 IPv4 和 IPv6 相关。在本章中,我们将介绍如何定义用户定义的 IPv6 网络,并演示一些相关的配置选项。
准备工作
在这个示例中,我们将使用一个单独的 Docker 主机。假设 Docker 已安装并处于默认配置。不需要使用--ipv6服务级参数启用 Docker 服务,以便在用户定义的网络上使用 IPv6 寻址。
如何做到这一点...
在使用用户定义的网络时,我们可以为 IPv4 和 IPv6 定义配置。此外,当我们运行容器时,我们可以指定它们的 IPv4 和 IPv6 地址。为了演示这一点,让我们首先定义一个具有 IPv4 和 IPv6 寻址的用户定义网络:
user@docker1:~$ docker network create -d bridge \
--subnet 2003:ab11:0:0:c000::/66 --subnet 192.168.127.0/24 \
--ipv6 ipv6_bridge
这个命令的语法应该对你来说很熟悉,来自第三章用户定义的网络,在那里我们讨论了用户定义的网络。然而,有几点需要指出。
首先,您会注意到我们定义了--subnet参数两次。这样做,我们既定义了一个 IPv4 子网,也定义了一个 IPv6 子网。当定义 IPv4 和 IPv6 地址时,--gateway和--aux-address字段可以以类似的方式使用。其次,我们定义了一个选项来在此网络上启用 IPv6。如果您不定义此选项以启用 IPv6,则主机的网关接口将不会被定义。
一旦定义好,让我们在网络上启动一个容器,看看我们的配置是什么样的:
user@docker1:~$ docker run -d --name=web1 --net=ipv6_bridge \
--ip 192.168.127.10 --ip6 2003:ab11::c000:0:0:10 \
jonlangemak/web_server_1
这个语法对你来说也应该很熟悉。请注意,我们指定这个容器应该是用户定义网络ipv6_bridge的成员。这样做,我们还可以使用--ip和--ip6参数为容器定义 IPv4 和 IPv6 地址。
如果我们检查网络,我们应该看到容器附加以及与网络定义以及容器网络接口相关的所有相关信息:
user@docker1:~$ docker network inspect ipv6_bridge
[
{
"Name": "ipv6_bridge",
"Id": "0c6e760998ea6c5b99ba39f3c7ce63b113dab2276645e5fb7a2207f06273401a",
"Scope": "local",
"Driver": "**bridge**",
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "**192.168.127.0/24**"
},
{
"Subnet": "**2003:ab11:0:0:c000::/66**"
}
]
},
"Containers": {
"38e7ac1a0d0ce849a782c5045caf770c3310aca42e069e02a55d0c4a601e6b5a": {
"Name": "web1",
"EndpointID": "a80ac4b00d34d462ed98084a238980b3a75093591630b5832f105d400fabb4bb",
"MacAddress": "02:42:c0:a8:7f:0a",
"IPv4Address": "**192.168.127.10/24**",
"IPv6Address": "**2003:ab11::c000:0:0:10/66**"
}
},
"Options": {
"**com.docker.network.enable_ipv6": "true"
}
}
]
user@docker1:~$
通过检查主机的网络配置,我们应该看到已创建了一个与这些网络匹配的新桥:
user@docker1:~$ ip addr show
…<Additional output removed for brevity>…
9: br-0b2efacf6f85: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:09:bc:9f:77 brd ff:ff:ff:ff:ff:ff
inet **192.168.127.1/24** scope global br-0b2efacf6f85
valid_lft forever preferred_lft forever
inet6 **2003:ab11::c000:0:0:1/66** scope global
valid_lft forever preferred_lft forever
inet6 fe80::42:9ff:febc:9f77/64 scope link
valid_lft forever preferred_lft forever
inet6 fe80::1/64 scope link
valid_lft forever preferred_lft forever
…<Additional output removed for brevity>…
user@docker1:~$
如果我们检查容器本身,我们会注意到这些接口是这个网络上的容器将用于其 IPv4 和 IPv6 默认网关的接口:
user@docker1:~$ docker exec web1 **ip route
default via 192.168.127.1 dev eth0
192.168.127.0/24 dev eth0 proto kernel scope link src 192.168.127.10
user@docker1:~$ docker exec web1 **ip -6 route
2003:ab11:0:0:c000::/66 dev eth0 proto kernel metric 256
fe80::/64 dev eth0 proto kernel metric 256
default via 2003:ab11::c000:0:0:1 dev eth0 metric 1024
user@docker1:~$
就像默认网络模式一样,用户定义的网络不支持主机防火墙集成,以支持出站伪装或入站端口发布。关于 IPv6 的连接,主机内外的情况与docker0桥相同,需要原生路由 IPv6 流量。
您还会注意到,如果您在主机上启动第二个容器,嵌入式 DNS 将同时适用于 IPv4 和 IPv6 寻址。
user@docker1:~$ docker run -d --name=web2 --net=ipv6_bridge \
jonlangemak/web_server_1
user@docker1:~$
user@docker1:~$ **docker exec -it web2 ping web1 -c 2
PING web1 (192.168.127.10): 48 data bytes
56 bytes from 192.168.127.10: icmp_seq=0 ttl=64 time=0.113 ms
56 bytes from 192.168.127.10: icmp_seq=1 ttl=64 time=0.111 ms
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.111/0.112/0.113/0.000 ms
user@docker1:~$
user@docker1:~$ **docker exec -it web2 ping6 web1 -c 2
PING web1 (2003:ab11::c000:0:0:10): 48 data bytes
56 bytes from web1.ipv6_bridge: icmp_seq=0 ttl=64 time=0.113 ms
56 bytes from web1.ipv6_bridge: icmp_seq=1 ttl=64 time=0.127 ms
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.113/0.120/0.127/0.000 ms
user@docker1:~$
第十一章:故障排除 Docker 网络
在本章中,我们将涵盖以下示例:
-
使用 tcpdump 验证网络路径
-
验证 VETH 对
-
验证发布的端口和出站伪装
-
验证名称解析
-
构建一个测试容器
-
重置本地 Docker 网络数据库
介绍
正如我们在前几章中看到的,Docker 利用了一系列相对知名的 Linux 网络构造来提供容器网络。在本书中,我们已经看过许多不同的方式,您可以配置、使用和验证 Docker 网络配置。我们还没有概述当您遇到问题时可以使用的故障排除和验证方法。在故障排除容器网络时,重要的是要理解并能够排除用于提供端到端连接的每个特定网络组件。本章的目标是提供在需要验证或故障排除 Docker 网络问题时可以采取的具体步骤。
使用 tcpdump 验证网络路径
尽管我们在之前的章节中简要介绍了它的用法,但任何在基于 Linux 的系统上使用网络的人都应该熟悉tcpdump。tcpdump允许您在主机上的一个或多个接口上捕获网络流量。在这个示例中,我们将介绍如何使用tcpdump来验证几种不同的 Docker 网络场景中的容器网络流量。
准备工作
在这个示例中,我们将使用一个单独的 Docker 主机。假设 Docker 已安装并处于默认配置。您还需要 root 级别的访问权限,以便检查和更改主机的网络和防火墙配置。您还需要安装tcpdump实用程序。如果您的系统上没有它,您可以使用以下命令安装它:
sudo apt-get install tcpdump
如何做…
tcpdump是一个令人惊叹的故障排除工具。当正确使用时,它可以让您详细查看 Linux 主机上接口上的数据包。为了演示,让我们在我们的 Docker 主机上启动一个单个容器:
user@docker1:~$ docker run -dP --name web1 jonlangemak/web_server_1
ea32565ece0c0c22eace935113b6697bebe837f0b5ddf31724f371220792fb15
user@docker1:~$
由于我们没有指定任何网络参数,这个容器将在docker0桥上运行,并且任何暴露的端口都将发布到主机接口上。从容器生成的流量也将隐藏在主机的 IP 接口后,因为流量朝向外部网络。使用tcpdump,我们可以在每个阶段看到这个流量。
让我们首先检查进入主机的流量:
user@docker1:~$ docker port web1
80/tcp -> 0.0.0.0:32768
user@docker1:~$
在我们的案例中,这个容器暴露了端口80,现在已经发布到主机接口的端口32768上。让我们首先确保流量进入主机的正确端口。为了做到这一点,我们可以在主机的eth0接口上捕获到目标端口为32768的流量:
user@docker1:~$ **sudo tcpdump -qnn -i eth0 dst port 32768
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
15:46:07.629747 IP 10.20.30.41.55939 > 10.10.10.101.32768: tcp 0
15:46:07.629997 IP 10.20.30.41.55940 > 10.10.10.101.32768: tcp 0
15:46:07.630257 IP 10.20.30.41.55939 > 10.10.10.101.32768: tcp 0
要使用tcpdump捕获这个入站流量,我们使用了一些不同的参数:
-
q:这告诉tcpdump保持安静,或者不要生成太多输出。因为我们只想看到第 3 层和第 4 层的信息,这样可以清理输出得很好 -
nn:这告诉tcpdump不要尝试将 IP 解析为 DNS 名称。同样,我们想在这里看到 IP 地址 -
i:这指定了我们要捕获的接口,在这种情况下是eth0 -
src port:告诉tcpdump过滤具有目的端口为32768的流量
注意
dst参数可以从此命令中删除。这样做将过滤任何端口为32768的流量,从而显示整个流量,包括返回流量。
如前面的代码所示,我们可以看到主机在其物理接口(10.10.10.101)上接收到来自远程源(10.20.30.41)的端口32768的流量。在这种情况下,10.20.30.41是一个测试服务器,它正在向容器的发布端口发出流量。
既然我们已经看到流量到达主机,让我们看看它是如何穿过docker0桥的:
user@docker1:~$ **sudo tcpdump -qnn -i docker0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 65535 bytes
16:34:54.193822 IP 10.20.30.41.53846 > 172.17.0.2.80: tcp 0
16:34:54.193848 IP 10.20.30.41.53847 > 172.17.0.2.80: tcp 0
16:34:54.193913 IP 172.17.0.2.80 > 10.20.30.41.53846: tcp 0
16:34:54.193940 IP 172.17.0.2.80 > 10.20.30.41.53847: tcp 0
在这种情况下,我们可以通过只在docker0桥接口上过滤流量来看到流量。正如预期的那样,我们看到相同的流量,具有相同的源,但现在反映了容器中运行的服务的准确目的地 IP 和端口,这要归功于发布端口功能。
虽然这当然是捕获流量的最简单方法,但如果您在docker0桥上运行多个容器,这种方法并不是非常有效。当前的过滤器将为您提供桥上所有的流量,而不仅仅是您正在寻找的特定容器。在这种情况下,您还可以在过滤器中指定 IP 地址,就像这样:
user@docker1:~$ **sudo tcpdump -qnn -i docker0 dst 172.17.0.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 65535 bytes
16:42:22.332555 IP 10.20.30.41.53878 > 172.17.0.2.80: tcp 0
16:42:22.332940 IP 10.20.30.41.53878 > 172.17.0.2.80: tcp 0
注意
我们在这里将目的地 IP 指定为过滤器。如果我们希望看到源和目的地都是该 IP 地址的流量,我们可以用host替换dst。
这种数据包捕获对于验证端口发布等功能是否按预期工作至关重要。捕获可以在大多数接口类型上进行,包括那些没有与其关联的 IP 地址的接口。这种接口的一个很好的例子是用于将容器命名空间连接回默认命名空间的 VETH 对的主机端。在排除容器连接问题时,能够将到达docker0桥的流量与特定主机端 VETH 接口相关联可能会很方便。我们可以通过从多个地方相关数据来实现这一点。例如,假设我们执行以下tcpdump:
user@docker1:~$ **sudo tcpdump -qnne -i docker0 host 172.17.0.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 65535 bytes
16:59:33.334941 **02:42:ab:27:0e:3e** > **02:42:ac:11:00:02**, IPv4, length 66: 10.20.30.41.57260 > 172.17.0.2.80: tcp 0
16:59:33.335012 **02:42:ac:11:00:02** > **02:42:ab:27:0e:3e**, IPv4, length 66: 172.17.0.2.80 > 10.20.30.41.57260: tcp 0
请注意,在这种情况下,我们向tcpdump传递了e参数。这告诉tcpdump显示每个帧的源和目的 MAC 地址。在这种情况下,我们可以看到我们有两个 MAC 地址。其中一个将是与docker0桥相关联的 MAC 地址,另一个将是与容器相关联的 MAC 地址。我们可以查看docker0桥信息来确定其 MAC 地址是什么:
user@docker1:~$ ip link show dev docker0
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether **02:42:ab:27:0e:3e** brd ff:ff:ff:ff:ff:ff
user@docker1:~$
这将留下地址02:42:ac:11:00:02。使用作为iproute2工具集的一部分的 bridge 命令,我们可以确定这个 MAC 地址存在于哪个接口上:
user@docker1:~$ bridge fdb show | grep **02:42:ac:11:00:02
02:42:ac:11:00:02 dev vetha431055
user@docker1:~$
在这里,我们可以看到容器的 MAC 地址可以通过名为vetha431055的接口访问。在该接口上进行捕获将确认我们是否正在查看正确的接口:
user@docker1:~$ **sudo tcpdump -qnn -i vetha431055
tcpdump: WARNING: vetha431055: no IPv4 address assigned
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on vetha431055, link-type EN10MB (Ethernet), capture size 65535 bytes
21:01:24.503939 IP 10.20.30.41.58035 > **172.17.0.2.80**: tcp 0
21:01:24.503990 IP **172.17.0.2.80** > 10.20.30.41.58035: tcp 0
tcpdump可以成为验证容器通信的重要工具。花一些时间了解该工具以及使用其不同参数过滤流量的不同方式是明智的。
验证 VETH 对
在本书中我们审查过的所有 Linux 网络构造中,VETH 对可能是最重要的。它们是命名空间感知的,允许您将一个唯一命名空间中的容器连接到包括默认命名空间在内的任何其他命名空间。虽然 Docker 会为您处理所有这些,但能够确定 VETH 对的端点位于何处并将它们相关联以确定 VETH 对的用途是很有用的。在本教程中,我们将深入研究如何找到和相关 VETH 对的端点。
准备工作
在本教程中,我们将使用单个 Docker 主机。假定 Docker 已安装并处于默认配置。您还需要 root 级别访问权限,以便检查和更改主机的网络和防火墙配置。
如何做…
Docker 中 VETH 对的主要用例是将容器的网络命名空间连接回默认网络命名空间。它通过将 VETH 对中的一个放置在docker0桥上,另一个放置在容器中来实现这一点。VETH 对的容器端被分配了一个 IP 地址,然后重命名为eth0。
当寻找匹配容器的 VETH 对端时,有两种情况。第一种是当你从默认命名空间开始,第二种是当你从容器命名空间开始。让我们逐步讨论每种情况以及如何将它们联系在一起。
让我们首先从了解接口的主机端开始。例如,假设我们正在寻找这个接口的容器端:
user@docker1:~$ ip -d link show
…<Additional output removed for brevity>…
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:ab:27:0e:3e brd ff:ff:ff:ff:ff:ff promiscuity 0
bridge
6: vetha431055@if5**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether 82:69:cb:b6:9a:db brd ff:ff:ff:ff:ff:ff promiscuity 1
veth
user@docker1:~$
这里有几件事情需要指出。首先,将-d参数传递给ip link子命令会显示有关接口的额外详细信息。在这种情况下,它确认了接口是一个 VETH 对。其次,VETH 对的命名通常遵循<end1>@<end2>的命名约定。在这种情况下,我们可以看到vetha431055端是本地接口,而if5是另一端。if5代表接口 5 或主机上第 5 个接口的索引 ID。由于 VETH 接口总是成对创建的,可以合理地假设具有索引 6 的 VETH 对端很可能是索引 5 或 7。在这种情况下,命名表明它是 5,但我们可以使用ethtool命令来确认:
user@docker1:~$ sudo ethtool -S **vetha431055
NIC statistics:
peer_ifindex: 5
user@docker1:~$
正如你所看到的,这个 VETH 对的另一端具有接口索引 5,正如名称所示。现在找到具有 5 的容器是困难的部分。为了做到这一点,我们需要检查每个容器的特定接口号。如果你运行了很多容器,这可能是一个挑战。你可以使用 Linux 的xargs循环遍历它们,而不是手动检查每个容器。例如,看看这个命令:
docker ps -q | xargs --verb -I {} docker exec {} ip link | grep ⁵:
我们在这里要做的是返回所有正在运行的容器的容器 ID 列表,然后将该列表传递给xargs。反过来,xargs正在使用这些容器 ID 在容器内部运行docker exec命令。该命令恰好是ip link命令,它将返回所有接口及其关联的索引号的列表。如果返回的任何信息以5:开头,表示接口索引为 5,我们将把它打印到屏幕上。为了查看哪个容器具有相关接口,我们必须以详细模式(--verb)运行xargs命令,这将显示每个命令的运行情况。输出将如下所示:
user@docker1:~$ **docker ps -q | xargs --verb -I {} docker exec {} ip link | grep ⁵:
docker exec 4b521df22184 ip link
docker exec 772e12b15c92 ip link
docker exec d8f3e7936690 ip link
docker exec a2e3201278e2 ip link
docker exec f9216233ba56 ip link
docker exec ea32565ece0c ip link
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
user@docker1:~$
如您所见,此主机上有六个容器正在运行。直到最后一个容器,我们才找到了我们要找的接口 ID。有了容器 ID,我们就可以知道哪个容器具有 VETH 接口的另一端。
注意
您可以通过运行docker exec -it ea32565ece0c ip link命令来确认这一点。
现在,让我们尝试另一个例子,从 VETH 对的容器端开始。这稍微容易一些,因为接口的命名告诉我们主机端匹配接口的索引:
user@docker1:~$ docker exec web1 ip -d link show dev eth0
5: **eth0@if6**: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
veth
user@docker1:~$
然后,我们可以通过再次使用ethtool来验证主机上索引为 6 的接口是否与容器中索引为 5 的接口匹配:
user@docker1:~$ ip -d link show | grep ^**6:
6: vetha431055**@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
user@docker1:~$ sudo ethtool -S **vetha431055
[sudo] password for user:
NIC statistics:
peer_ifindex: 5
user@docker1:~$
验证已发布的端口和出站伪装
Docker 网络中涉及的较困难的部分之一是iptables。iptables/netfilter 集成在提供端口发布和出站伪装等功能方面发挥着关键作用。然而,如果您对其不熟悉,iptables可能很难理解和排除故障。在本教程中,我们将审查如何详细检查iptables配置,并验证连接是否按预期工作。
准备工作
在本教程中,我们将使用单个 Docker 主机。假设 Docker 已安装并处于默认配置中。您还需要 root 级别的访问权限才能检查iptables规则集。
如何做…
正如我们在前几章中看到的,Docker 在代表您管理主机防火墙规则方面做得非常出色。您可能很少需要查看或修改与 Docker 相关的iptables规则。然而,能够验证配置以排除iptables可能是容器网络故障的一个好主意。
为了演示遍历iptables规则集,我们将检查一个发布端口的示例容器。我们执行这些步骤很容易转移到检查任何其他 Docker 集成iptables用例的规则。为此,我们将运行一个简单的容器,该容器公开端口80以进行发布:
user@docker1:~$ docker run -dP --name web1 jonlangemak/web_server_1
由于我们告诉 Docker 发布任何公开的端口,我们知道该容器应该将其公开的端口80发布到主机。为了验证端口是否真的被发布,我们可以检查iptables规则集。我们想要做的第一件事是确保端口发布所需的目标 NAT 已经就位。为了检查iptables表,我们可以使用iptables命令并传递以下参数:
-
n:告诉iptables在输出中使用数值信息,如地址和端口 -
L:告诉iptables你想输出一个规则列表 -
v:告诉iptables提供详细输出,这样我们就可以看到所有的规则信息以及规则计数器 -
t:告诉iptables仅显示特定表的信息
将所有这些放在一起,我们可以使用命令sudo iptables -nL -t nat来查看主机 NAT 表中的规则:

注意
请注意,我们将在本教程中检查的所有默认表和链策略都是“接受”。如果默认链策略是“接受”,这意味着即使我们没有匹配规则,流量仍将被允许。无论默认策略设置为什么,Docker 都将创建规则。
如果你对iptables不太熟悉,解释这个输出可能有点令人生畏。即使我们正在查看 NAT 表,我们也需要知道哪个链正在处理进入主机的通信。在我们的情况下,由于流量进入主机,我们感兴趣的链是PREROUTING链。让我们来看一下表是如何处理的:
-
PREROUTING链中的第一行寻找目的地为LOCAL或主机本身的流量。由于流量的目的地是主机接口之一的 IP 地址,我们匹配了这条规则并执行了引用跳转到一个名为DOCKER的新链的动作。 -
在
DOCKER链中,我们命中了第一条规则,该规则正在寻找进入docker0桥的流量。由于这个流量并没有进入docker0桥,所以规则被跳过,我们继续移动到链中的下一条规则。 -
DOCKER链中的第二条规则正在寻找并非进入docker0桥且目的端口为 TCP32768的流量。我们匹配了这条规则并执行了将目的 NAT 转换为172.17.0.2端口80的动作。
表中的处理看起来是这样的:

在前面的图像中的箭头表示流量在穿过 NAT 表时的流动。在这个例子中,我们只有一个运行在主机上的容器,所以很容易看出哪些规则正在被处理。
注意
您可以将这种输出与watch命令配合使用,以获得计数器的实时输出,例如:
sudo watch --interval 0 iptables -vnL -t nat
现在我们已经穿过了 NAT 表,接下来我们需要担心的是过滤表。我们可以以与查看 NAT 表相同的方式查看过滤表:

乍一看,我们可以看到这个表的布局与 NAT 表略有不同。例如,这个表中的链与 NAT 表中的不同。在我们的情况下,我们对入站发布端口通信感兴趣的链是 forward 链。这是因为主机正在转发或路由流量到容器。流量将按以下方式穿过这个表:
-
转发链中的第一行直接将流量发送到
DOCKER-ISOLATION链。 -
在这种情况下,
DOCKER-ISOLATION链中唯一的规则是将流量发送回来,所以我们继续审查FORWARD表中的规则。 -
转发表中的第二条规则表示,如果流量要离开
docker0桥,则将流量发送到DOCKER链。由于我们的目的地(172.17.0.20)位于docker0桥外,我们匹配了这条规则并跳转到DOCKER链。 -
在
DOCKER链中,我们检查第一条规则,并确定它正在寻找目的地为容器 IP 地址,端口为 TCP80的流量,且是从docker0桥接口出去而不是进来的。我们匹配到了这条规则,流量被接受了。
表中的处理如下:

通过过滤表是发布端口流量必须经过的最后一步,以便到达容器。然而,我们现在只是到达了容器。我们仍然需要考虑从容器返回到与发布端口通信的主机的返回流量。因此,现在,我们需要讨论容器发起的流量如何由iptables处理。
我们将遇到的第一个表是出站流量的过滤表。再次,来自容器的流量将使用过滤表的转发链。流程大致如下:
-
转发链中的第一条规则直接将流量发送到
DOCKER-ISOLATION链。 -
在这种情况下,
DOCKER-ISOLATION链中唯一的规则是将流量发送回去,所以我们继续查看转发表中的规则。 -
转发表中的第二条规则表示,如果流量是从
docker0桥接口出去的,就将流量发送到DOCKER链。由于我们的流量是进入docker0桥接口而不是出去,所以这条规则被跳过,我们继续查看链中的下一条规则。 -
转发表中的第三条规则表示,如果流量是从
docker0桥接口出去,并且连接状态是RELATED或ESTABLISHED,则应该接受该流量。这个流量是进入docker0桥接口的,所以我们也不会匹配到这条规则。然而,值得指出的是,这条规则用于允许容器发起的流量的返回流量。它只是作为初始出站连接的一部分而没有被命中,因为那代表了一个新的流量。 -
转发表中的第四条规则表示,如果流量经过
docker0桥接口,但不是从docker0桥接口出去,就接受它。因为我们的流量是进入docker0桥接口的,所以我们匹配到了这条规则,流量被接受了。
表中的处理如下:

我们将命中的下一个表是 NAT 表。这一次,我们要查看POSTROUTING链。在这种情况下,我们匹配链的第一条规则,该规则寻找不是从docker0桥出去的流量,并且源自docker0桥子网(172.17.0.0/16)的流量。

这条规则的操作是MASQUERADE,它将根据主机的路由表隐藏流量在主机接口之一后面。
采用相同的方法,您可以轻松验证与 Docker 相关的其他iptables流。当然,随着容器数量的增加,这变得更加困难。然而,由于大多数规则是按照每个容器的基础编写的,命中计数器将对每个容器都是唯一的,这使得缩小范围变得更容易。
注意
有关iptables表和链的处理顺序的更多信息,请查看这个iptables网页和相关的流程图www.iptables.info/en/structure-of-iptables.html。
验证名称解析
容器的 DNS 解析一直都很简单。容器接收与主机相同的 DNS 配置。然而,随着用户定义网络和嵌入式 DNS 服务器的出现,这现在变得有点棘手。我见过的许多 DNS 问题中的一个常见问题是不理解嵌入式 DNS 服务器的工作原理以及如何验证它是否正常工作。在这个教程中,我们将逐步介绍容器 DNS 配置,以验证它使用哪个 DNS 服务器来解析特定的命名空间。
准备工作
在这个教程中,我们将使用一个单独的 Docker 主机。假设 Docker 已安装并处于默认配置。您还需要 root 级别的访问权限,以便检查和更改主机的网络和防火墙配置。
如何操作…
没有用户定义网络的情况下,Docker 的标准 DNS 配置是将主机的 DNS 配置简单地复制到容器中。在这些情况下,DNS 解析很简单:
user@docker1:~$ docker run -dP --name web1 jonlangemak/web_server_1
e5735b30ce675d40de8c62fffe28e338a14b03560ce29622f0bb46edf639375f
user@docker1:~$
user@docker1:~$ **docker exec web1 more /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver **<your local DNS server>
search lab.lab
user@docker1:~$
user@docker1:~$ more **/etc/resolv.conf
nameserver <your local DNS server>
search lab.lab
user@docker1:~$
在这些情况下,所有的 DNS 请求都会直接发送到定义的 DNS 服务器。这意味着我们的容器可以解析任何我们的主机可以解析的 DNS 记录:
user@docker1:~$ docker exec -it web1 **ping docker2.lab.lab** -c 2
PING docker2.lab.lab (10.10.10.102): 48 data bytes
56 bytes from 10.10.10.102: icmp_seq=0 ttl=63 time=0.471 ms
56 bytes from 10.10.10.102: icmp_seq=1 ttl=63 time=0.453 ms
--- docker2.lab.lab ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.453/0.462/0.471/0.000 ms
user@docker1:~$
再加上 Docker 将这些流量伪装成主机本身的 IP 地址,这就成为了一个简单且易于维护的解决方案。
然而,当我们开始使用用户定义的网络时,情况就会变得有些棘手。这是因为用户定义的网络提供了容器名称解析。也就是说,一个容器可以解析另一个容器的名称,而无需使用静态或手动主机文件条目和链接。这是一个很棒的功能,但如果您不了解容器如何接收其 DNS 配置,可能会导致一些混乱。例如,现在让我们创建一个新的用户定义网络:
user@docker1:~$ docker network create -d bridge mybridge1
e8afb0e506298e558baf5408053c64c329b8e605d6ad12efbf10e81f538df7b9
user@docker1:~$
现在让我们在这个网络上启动一个名为web2的新容器:
user@docker1:~$ docker run -dP --name web2 --net \
mybridge1 jonlangemak/web_server_2
1b38ad04c3c1be7b0f1af28550bf402dcde1515899234e4b09e482da0a560a0a
user@docker1:~$
现在,如果我们将现有的web1容器连接到这个桥接器,我们应该会发现web1可以通过名称解析容器web2:
user@docker1:~$ docker network connect mybridge1 web1
user@docker1:~$ docker exec -it web1 ping web2 -c 2
PING web2 (172.18.0.2): 48 data bytes
56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.100 ms
56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.086 ms
--- web2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.086/0.093/0.100/0.000 ms
user@docker1:~$
问题在于,为了实现这一点,Docker 必须更改web1容器的 DNS 配置。这样做会在容器的 DNS 请求中间注入嵌入式 DNS 服务器。因此,在此之前,当我们直接与主机的 DNS 服务器通信时,现在我们是在与嵌入式 DNS 服务器通信:
user@docker1:~$ docker exec -t web1 more /etc/resolv.conf
search lab.lab
nameserver 127.0.0.11
options ndots:0
user@docker1:~$
这对于容器的 DNS 解析是必需的,但它有一个有趣的副作用。嵌入式 DNS 服务器会读取主机的/etc/resolv.conf文件,并使用该文件中定义的任何名称服务器作为嵌入式 DNS 服务器的转发器。这样做的净效果是,您不会注意到嵌入式 DNS 服务器,因为它仍然将无法回答的请求转发给主机的 DNS 服务器。但是,它只会在这些转发器被定义时进行编程。如果它们不存在或设置为127.0.0.1,那么 Docker 会将转发器设置为 Google 的公共 DNS 服务器(8.8.8.8和8.4.4.4)。
尽管这是有道理的,但在某些罕见情况下,您的本地 DNS 服务器恰好是127.0.0.1。例如,您可能在同一台主机上运行某种类型的本地 DNS 解析器,或者使用 DNS 转发应用程序,比如DNSMasq。在这些情况下,Docker 将容器的 DNS 请求转发到前面提到的外部 DNS 服务器,而不是本地定义的 DNS 服务器,可能会引起一些复杂情况。换句话说,内部 DNS 区域将不再可解析:
user@docker1:~$ docker exec -it web1 ping docker2.lab.lab
ping: unknown host
user@docker1:~$
注意
这也可能导致一般的解析问题,因为通常会阻止 DNS 流量到外部 DNS 服务器,而是更倾向于强制内部端点使用内部 DNS 服务器。
在这些情景中,有几种方法可以解决这个问题。您可以在容器运行时通过传递 DNS 标志来指定运行容器的特定 DNS 服务器:
user@docker1:~$ docker run -dP --name web2 --net mybridge1 \
--dns <your local DNS server> jonlangemak/web_server_2
否则,您可以在 Docker 服务级别设置 DNS 服务器,然后嵌入式 DNS 服务器将使用它作为转发器:
ExecStart=/usr/bin/dockerd --dns=<your local DNS server>
无论哪种情况,如果您遇到容器解析问题,请始终检查并查看容器在其/etc/resolv.conf文件中配置了什么。如果是127.0.0.11,那表明您正在使用 Docker 嵌入式 DNS 服务器。如果是这样,并且您仍然遇到问题,请确保验证主机 DNS 配置,以确定嵌入式 DNS 服务器正在使用什么作为转发器。如果没有定义或者是127.0.0.1,那么请确保告诉 Docker 服务应该将哪个 DNS 服务器传递给容器,可以使用前面定义的两种方式之一。
构建一个测试容器
构建 Docker 容器的原则之一是保持它们小巧精悍。在某些情况下,这可能会限制您的故障排除选项,因为容器的镜像中可能没有许多常见的 Linux 网络工具。虽然不是理想的情况,但有时候有一个安装了这些工具的容器镜像是很好的,这样您就可以从容器的角度来排查网络问题。在本章中,我们将讨论如何专门为此目的构建 Docker 镜像。
准备工作
在本示例中,我们将使用单个 Docker 网络主机。假设 Docker 已安装并处于默认配置状态。您还需要 root 级别访问权限,以便检查和更改主机的网络和防火墙配置。
如何做…
Docker 镜像是通过定义 Dockerfile 来构建的。Dockerfile 定义了要使用的基础镜像以及容器内部要运行的命令。在我的示例中,我将定义 Dockerfile 如下:
FROM ubuntu:16.04
MAINTAINER Jon Langemak jon@interubernet.com
RUN apt-get update && apt-get install -y apache2 net-tools \
inetutils-ping curl dnsutils vim ethtool tcpdump
ADD index.html /var/www/html/index.html
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2
ENV APACHE_PID_FILE /var/run/apache2/apache2.pid
ENV APACHE_LOCK_DIR /var/run/apache2
RUN mkdir -p /var/run/apache2
RUN chown www-data:www-data /var/run/apache2
EXPOSE 80
CMD ["/usr/sbin/apache2", "-D", "FOREGROUND"]
这个镜像的目标是双重的。首先,我希望能够以分离模式运行容器,并且让它提供一个服务。这将允许我定义容器并验证诸如端口发布之类的功能是否在主机上正常工作。这个容器镜像为我提供了一个已知良好的容器,将在端口80上发布一个服务。为此,我们使用 Apache 来托管一个简单的索引页面。
索引文件在构建时被拉入镜像中,并且可以由您自定义。我使用一个简单的 HTML 页面index.html,显示大红色的字体,如下所示:
<body>
<html>
<h1><span style="color:#FF0000;font-size:72px;">Test Web Server - Running on port 80</span>
</h1>
</body>
</html>
其次,镜像中安装了许多网络工具。您会注意到我正在安装以下软件包:
-
net-tools:提供网络实用程序以查看和配置接口 -
inetutils-ping:提供 ping 功能 -
curl:这是从其他网络端点拉取文件 -
dnsutils:这是用于解析 DNS 名称和其他 DNS 跟踪 -
ethtool:这是从接口获取信息和统计信息 -
tcpdump:这是从容器内进行数据包捕获
如果您定义了这个 Dockerfile,以及它所需的支持文件(一个索引页面),您可以按以下方式构建图像:
sudo docker build -t <tag name for image> <path files ('.' If local)>
注意
在构建图像时,您可以定义很多选项。查看docker build --help以获取更多信息。
然后 Docker 将处理 Dockerfile,如果成功,它将生成一个docker image文件,然后您可以将其推送到您选择的容器注册表,以便在其他主机上使用docker pull进行消费。
构建完成后,您可以运行它并验证工具是否按预期工作。在容器内有ethtool意味着我们可以轻松确定 VETH 对的主机端 VETH 端:
user@docker1:~$ docker run -dP --name nettest jonlangemak/net_tools
user@docker1:~$ docker exec -it nettest /bin/bash
root@2ef59fcc0f60:/# **ethtool -S eth0
NIC statistics:
peer_ifindex: 5
root@2ef59fcc0f60:/#
我们还可以执行本地的tcpdump操作来验证到达容器的流量:
root@2ef59fcc0f60:/# tcpdump -qnn -i eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
15:17:43.442243 IP 10.20.30.41.54974 > 172.17.0.3.80: tcp 0
15:17:43.442286 IP 172.17.0.3.80 > 10.20.30.41.54974: tcp 0
随着您的用例的改变,您可以修改 Dockerfile,使其更符合您自己的用例。在容器内进行故障排除时,能够进行故障排除可能会在诊断连接问题时提供很大帮助。
注意
这个图像只是一个例子。有很多方法可以使它更加轻量级。我决定使用 Ubuntu 作为基础图像,只是为了熟悉起见。前面描述的图像因此相当沉重。
重置本地 Docker 网络数据库
随着用户定义网络的出现,用户可以为其容器定义自定义网络类型。一旦定义,这些网络将在系统重新启动时持久存在,直到被管理员删除。为了使这种持久性工作,Docker 需要一些地方来存储与您的用户定义网络相关的信息。答案是一个本地主机的数据库文件。在一些罕见的情况下,这个数据库可能与主机上容器的当前状态不同步,或者变得损坏。这可能会导致与删除容器、删除网络和启动 Docker 服务相关的问题。在这个教程中,我们将向您展示如何删除数据库以将 Docker 恢复到其默认网络配置。
准备工作
在本教程中,我们将使用单个 Docker 网络主机。假设 Docker 已安装并处于默认配置状态。您还需要 root 级别访问权限,以便检查和更改主机的网络和防火墙配置。
如何操作…
Docker 将与用户定义网络相关的信息存储在本地主机上的数据库中。当定义网络时,会将数据写入该数据库,并在服务启动时从中读取。在极少数情况下,如果该数据库不同步或损坏,您可以删除数据库并重新启动 Docker 服务,以重置 Docker 用户定义网络并恢复三种默认网络类型(桥接、主机和无)。
注意
警告:删除此数据库会删除主机上的任何 Docker 用户定义网络。最好只在万不得已且有能力重新创建先前定义的网络时才这样做。在尝试此操作之前,应该追求所有其他故障排除选项,并在删除之前创建文件的备份。
该数据库的名称为local-kv.db,存储在路径/var/lib/network/files/中。访问或删除该文件需要 root 级别访问权限。为了方便浏览这个受保护的目录,我们将切换到 root 用户:
user@docker1:~$ sudo su
[sudo] password for user:
root@docker1:/home/user# cd **/var/lib/docker/network/files
root@docker1:/var/lib/docker/network/files# ls -al
total 72
drwxr-x--- 2 root root 32768 Aug 9 21:27 .
drwxr-x--- 3 root root 4096 Apr 3 21:04 ..
-rw-r--r-- 1 root root 65536 Aug 9 21:27 **local-kv.db
root@docker1:/var/lib/docker/network/files#
为了演示删除此文件时会发生什么,让我们首先创建一个新的用户定义网络并将一个容器连接到它:
root@docker1:~# **docker network create -d bridge mybridge
c765f1d24345e4652b137383839aabdd3b01b1441d1d81ad4b4e17229ddca7ac
root@docker1:~# **docker run -d --name web1 --net mybridge jonlangemak/web_server_1
24a6497e99de9e114b617b65673a8a50492655e9869dbf7f7930dd7f9f930b5e
root@docker1:~#
现在让我们删除文件local-db.kv:
root@docker1:/var/lib/docker/network/files# rm local-kv.db
尽管这对正在运行的容器没有立即影响,但它阻止我们向与此用户定义网络关联的新容器添加、删除或启动:
root@docker1:/~# docker run -d --name web2 --net mybridge \
jonlangemak/web_server_2
2ef7e52f44c93412ea7eaa413f523020a65f1a9fa6fd6761ffa6edea157c2623
docker: Error response from daemon: failed to update store for object type *libnetwork.endpointCnt: Key not found in store.
root@docker1:~#
删除boltdb数据库文件local-kv.db后,您需要重新启动 Docker 服务,以便 Docker 使用默认设置重新创建它:
root@docker1:/var/lib/docker/network/files# cd
root@docker1:~# systemctl restart docker
root@docker1:~# ls /var/lib/docker/network/files
local-kv.db
root@docker1:~# docker network ls
NETWORK ID NAME DRIVER
bfd1ba1175a9 none null
0740840aef37 host host
97cbc0e116d7 bridge bridge
root@docker1:/var/lib/docker/network/files#
现在文件已重新创建,您将再次能够创建用户定义网络。但是,以前连接到先前配置的用户定义网络的任何容器现在将无法启动:
root@docker1:~# docker start web1
Error response from daemon: network mybridge not found
Error: failed to start containers: web1
root@docker1:~#
这是预期行为,因为 Docker 仍然认为容器应该在该网络上有一个接口:
root@docker1:~# docker inspect web1
…<Additional output removed for brevity>…
"Networks": {
"**mybridge**": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "c765f1d24345e4652b137383839aabdd3b01b1441d1d81ad4b4e17229ddca7ac",
…<Additional output removed for brevity>…
root@docker1:~#
为了解决这个问题,你有两个选择。首先,你可以使用与最初配置时相同的配置选项重新创建名为mybridge的用户定义网络。如果这不起作用,你唯一的选择就是删除容器并重新启动一个新实例,引用新创建的或默认网络。
注意:…
:GitHub 上已经讨论过 Docker 的新版本是否支持在使用docker network disconnect子命令时使用--force标志。在 1.10 版本中,存在这个参数,但仍然不喜欢用户定义的网络不存在。如果你正在运行一个更新的版本,这也许值得一试。






浙公网安备 33010602011771号