容器间通信的可靠性和安全性相当重要,即使容器分属于不同网络中的不同主机。这也是覆盖网络大展拳脚的地方,它允许创建扁平的、安全的二层网络来连接多个主机,容器可以连接到覆盖网络并直接互相通信。

Docker 提供了原生覆盖网络的支持,易于配置且非常安全。其背后是基于 Libnetwork 以及相应的驱动来构建的。

Libnetwork 是 CNM 的典型实现,从而可以通过插拔驱动的方式来实现不同的网络技术和拓扑结构。

Docker 提供了一些诸如 Overlay 的原生驱动,同时第三方也可以提供驱动。

在 2015 年 3 月,Docker 公司收购了一个叫作 Socket Plane 的网络初创企业。收购的原因有二,首先是因为这会给 Docker 带来真正意义的网络架构,其次是让容器间联网变得非常简单,以至于开发人员都可以配置它。

Docker 公司在这两点上都取得了巨大的成功。但是,简洁的网络命令实际由大量的组件构成。这部分内容是在进行生产环境部署和问题定位前必须要了解的。

在 Swarm 模式下构建并测试 Docker 覆盖网络

要完成下面的示例,需要两台 Docker 主机,并通过一个路由器上两个独立的二层网络连接在一起。如下图所示,注意节点位于不同网络之上。

docker层叠 docker cp覆盖_docker层叠

 1) 构建 Swarm
首先需要将两台主机配置为包含两个节点的 Swarm 集群。接下来会在 node1 节点上运行 docker swarm init 命令使其成为管理节点,然后在 node2 节点上运行 docker swarm join 命令来使其成为工作节点。

在 node1 节点上运行下面的命令。

$ docker swarm init \
--advertise-addr=172.31.1.5 \
--listen-addr=172.31.1.5:2377

在 node2 上运行下面的命令。

$ docker swarm join \
--token SWMTKN-1-0hz2ec...2vye \
172.31.1.5:2377

现在就已经创建好了包含管理节点 node1 和工作节点 node2 两个节点的 Swarm 集群了。

 2) 创建新的覆盖网络
创建一个名为 uber-net 的覆盖网络。

在 node1(管理节点)节点上运行下面的命令。

$ docker network create -d overlay uber-net

刚刚创建了一个崭新的覆盖网络,能连接 Swarm 集群内的所有主机,并且该网络还包括一个 TLS 加密的控制层!如果还想对数据层加密的话,只需在命令中增加 -o encrypted 参数。

可以通过 docker network ls 命令列出每个节点上的全部网络。

如果在 node2 节点上运行 docker network ls 命令,就会发现无法看到 uber-net 网络。这是因为只有当运行中的容器连接到覆盖网络的时候,该网络才变为可用状态。这种延迟生效策略通过减少网络梳理,提升了网络的扩展性。

 3) 将服务连接到覆盖网络
现在覆盖网络已经就绪,接下来新建一个 Docker 服务并连接到该网络。Docker 服务会包含两个副本(容器),一个运行 node1 节点上,一个运行在 node2 节点上。这样会自动将 node2 节点接入 uber-net 网络。

在 node1 节点上运行下面的命令。

Linux 示例如下。

$ docker service create --name test \
--network uber-net \
--replicas 2 \
ubuntu sleep infinity

该命令创建了名为 test 的新服务,连接到了 uber-net 这个覆盖网络,并且还基于指定的镜像创建了两个副本(容器)。在两个示例中,均在容器中采用 sleep 命令来保持容器运行,并在休眠结束后退出该容器。

由于运行了两个副本(容器),而 Swarm 包含两个节点,因此每个节点上都会运行一个副本。

可以通过 docker service ps 命令来确认上面的操作。

$ docker service ps test

当 Swarm 在覆盖网络之上启动容器时,会自动将容器运行所在节点加入到网络当中。此时在 node2 节点上就可以看到 uber-net 网络了。

目前已经成功在两个由物理网络连接的节点上创建了新的覆盖网络。同时,还将两个容器连接到了该网络当中。
4) 测试覆盖网络
现在使用 ping 命令来测试覆盖网络。

如下图所示,在两个独立的网络中分别有一台 Docker 主机,并且两者都接入了同一个覆盖网络。目前在每个节点上都有一个容器接入了覆盖网络。测试一下两个容器之间是否可以 ping 通。

docker层叠 docker cp覆盖_IP_02

为了执行该测试,需要知道每个容器的 IP 地址(为了测试,暂时忽略相同覆盖网络上的容器可以通过名称来互相 ping 通的事实)。

运行 docker network inspect 查看被分配给覆盖网络的 Subnet。

$ docker network inspect uber-net

uber-net 的子网是 10.0.0.0/24。注意,这与两个节点的任意底层物理网络 IP 均不相符(172.31.1.0/24 和 192.168.1.0/24)。

在 node1 和 node2 节点上运行下面两条命令。这两条命令可以获取到容器 ID 和 IP 地址。在第二条命令中一定要使用读者自己的环境中的容器 ID。

需要在两台节点上分别运行上述命令,获取两个容器的 ID 和 IP 地址。

下图展示了配置现状。在运行环境中,子网和 IP 地址信息可能不同。

docker层叠 docker cp覆盖_docker_03

由图可知,一个二层覆盖网络横跨两台主机,并且每个容器在覆盖网络中都有自己的 IP 地址。这意味着 node1 节点上的容器可以通过 node2 节点上容器的 IP 地址 10.0.0.4 来 ping 通,该 IP 地址属于覆盖网络。尽管两个节点分属于不同的二层网络,还是可以直接 ping 通。接下来验证这一点。

登录到 node1 的容器,并 ping 另一个的容器。

在 Linux Ubuntu 容器中执行该操作的话,需要安装 ping 工具包。

还可以在容器内部跟踪 ping 命令的路由信息。路由信息只有一条,证明容器间通信确实通过覆盖网络直连。

如果希望 Linux 示例中的 traceroute 可执行,需要安装 traceroute 包。

Linux 示例如下。

$ root@396c8b142a85:/# traceroute 10.0.0.4

到目前为止,已经通过单条命令创建了覆盖网络,并向该网络中接入了容器。这些容器分布在两个不同的主机上,两台主机分属于不同的二层网络。在找出两台容器的 IP 之后,验证了容器可以通过覆盖网络完成直连。

工作原理
1) VXLAN 入门
Docker 使用 VXLAN 隧道技术创建了虚拟二层覆盖网络。

在 VXLAN 的设计中,允许用户基于已经存在的三层网络结构创建虚拟的二层网络。在前面的示例中创建了一个子网掩码为 10.0.0.0/24 的二层网络,该网络是基于一个三层 IP 网络实现的,三层 IP 网络由 172.31.1.0/24 和 192.168.1.0/24 这两个二层网络构成。具体如下图所示。

docker层叠 docker cp覆盖_IP_04

VXLAN 的美妙之处在于它是一种封装技术,能使现存的路由器和网络架构看起来就像普通的 IP/UDP 包一样,并且处理起来毫无问题。

为了创建二层覆盖网络,VXLAN 基于现有的三层 IP 网络创建了隧道。基础网络(Underlay Network)这个术语,它用于指代三层之下的基础部分。

VXLAN 隧道两端都是 VXLAN 隧道终端(VXLAN Tunnel Endpoint, VTEP)。VTEP 完成了封装和解压的步骤,以及一些功能实现所必需的操作,如下图所示。

docker层叠 docker cp覆盖_docker_05

 2) 梳理一下两个容器的示例
在前面的示例中,通过 IP 网络将两台主机连接起来。每个主机运行了一个容器,之后又为容器连接创建了一个 VXLAN 覆盖网络。

为了实现上述场景,在每台主机上都新建了一个 Sandbox(网络命名空间)。Sandbox 就像一个容器,但其中运行的不是应用,而是当前主机上独立的网络栈。

在 Sandbox 内部创建了一个名为 Br0 的虚拟交换机(又称做虚拟网桥)。同时 Sandbox 内部还创建了一个 VTEP,其中一端接入到名为 Br0 的虚拟交换机当中,另一端接入主机网络栈(VTEP)。

在主机网络栈中的终端从主机所连接的基础网络中获取到 IP 地址,并以 UDP Socket 的方式绑定到 4789 端口。不同主机上的两个 VTEP 通过 VXLAN 隧道创建了一个覆盖网络,如下图所示。

docker层叠 docker cp覆盖_docker_06

这是 VXLAN 上层网络创建和使用所必需的。

接下来每个容器都会有自己的虚拟以太网(veth)适配器,并接入本地 Br0 虚拟交换机。目前拓扑结构如下图所示,虽然是在主机所属网络互相独立的情况下,但这样能更容易看出两个分别位于不同主机上的容器之间是如何通过 VXLAN 上层网络进行通信的。

docker层叠 docker cp覆盖_docker层叠_07

 3) 通信示例
在本例中,将 node1 上的容器称为 C1,node2 上的容器称为 C2,如下图所示。假设 C1 希望 ping 通 C2。

docker层叠 docker cp覆盖_Docker_08

C1 发起 ping 请求,目标 IP 为 C2 的地址 10.0.0.4。该请求的流量通过连接到 Br0 虚拟交换机 veth 接口发出。虚拟交换机并不知道将包发送到哪里,因为在虚拟交换机的 MAC 地址映射表(ARP 映射表)中并没有与当前目的 IP 对应的 MAC 地址。

虚拟交换机会将该包发送到其上的全部端口。连接到 Br0 的 VTEP 接口知道如何转发这个数据帧,会将自己的 MAC 地址返回。这就是一个代理 ARP 响应,并且虚拟交换机 Br0 根据返回结果学会了如何转发该包。接下来虚拟交换机会更新自己的 ARP 映射表,将 10.0.0.4 映射到本地 VTEP 的 MAC 地址上。

现在 Br0 交换机已经学会如何转发目标为 C2 的流量,接下来所有发送到 C2 的包都会被直接转发到 VTEP 接口。VTEP 接口知道 C2,是因为所有新启动的容器都会将自己的网络详情采用网络内置 Gossip 协议发送给相同 Swarm 集群内的其他节点。

交换机会将包转发到 VTEP 接口,VTEP 完成数据帧的封装,这样就能在底层网络传输。封装操作就是把 VXLAN Header 信息添加以太帧当中。

VXLAN Header 信息包含了 VXLAN 网络 ID(VNID),其作用是记录 VLAN 到 VXLAN 的映射关系。每个 VLAN 都对应一个 VNID,以便包可以在解析后被转发到正确的 VLAN。

封装的时候会将数据帧放到 UDP 包中,并设置 UDP 的目的 IP 字段为 node2 节点的 VTEP 的 IP 地址,同时设置 UDP Socket 端口为 4789。这种封装方式保证了底层网络即使不知道任何关于 VXLAN 的信息,也可以完成数据传输。

当包到达 node2 之后,内核发现目的端口为 UDP 端口 4789,同时还知道存在 VTEP 接口绑定到该 Socket。所以内核将包发给 VTEP,由 VTEP 读取 VNID,解压包信息,并根据 VNID 发送到本地名为 Br0 的连接到 VLAN 的交换机。在该交换机上,包被发送给容器 C2。

以上大体介绍了 Docker 覆盖网络是如何利用 VXLAN 技术的。

最后一件需要注意的是,Docker 支持使用同样的覆盖网络实现三层路由。可以创建包含两个子网的覆盖网络,Docker 会负责子网间的路由。创建的命令如 docker network create --subnet=10.1.1.0/24 --subnet=11.1.1.0/24 -d overlay prod-net。该命令会在 Sandbox 中创建两个虚拟交换机,默认支持路由。