深入浅出Docker 读书笔记(六)

                   第12章:Docker 覆盖网络

容器间通信的可靠性和安全性相当重要,即使容器分属于不同网络中的不同主机。这也是覆盖网络大展拳脚的地方,它允许创建扁平的、安全的二层网络来连接多个主机,容器可以连接到覆盖网络并直接互相通信。Docker提供了原生覆盖网络的支持,易于配置且非常安全,是基于 Libnetwork 以及相应的驱动来构建的。Libnetwork 是 CNM 的典型实现,从而可以通过插拔驱动的方式来实现不同的网络技术和拓扑结构。Docker 提供了一些诸如 Overlay 的原生驱动,同时第三方也可以提供驱动。以在 Swarm 模式下构建并测试 Docker 覆盖网络为例:要完成图示的示例,需要两台 Docker 主机,并通过一个路由器上两个独立的二层网络连接在一起。节点位于不同网络之上。

连接网络

流程:1) 构建 Swarm:首先需要将两台主机配置为包含两个节点的 Swarm 集群。在 node1 节点上运行 docker swarm init 命令使其成为管理节点,然后在 node2 节点上运行 docker swarm join 命令来使其成为工作节点。2) 创建一个名为 uber-net 的覆盖网络。在 node1(管理节点)节点上运行$ docker network create -d overlay uber-net的命令。若要这些命令在 Windows 上也能运行,需要在 Windows Docker 节点上添加 4789/udp 规则。创建新的覆盖网络,能连接 Swarm 集群内的所有主机,并且该网络还包括一个 TLS 加密的控制层!如果还想对数据层加密的话,只需在命令中增加 -o encrypted 参数。可以通过 docker network ls 命令列出每个节点上的全部网络。如果在 node2 节点上运行 docker network ls 命令,就会发现无法看到uber-net 网络。这是因为只有当运行中的容器连接到覆盖网络的时候,该网络才变为可用状态。这种延迟生效策略通过减少网络梳理,提升了网络的扩展性。3) 将服务连接到覆盖网络,新建一个 Docker 服务并连接到该网络。Docker 服务会包含两个副本(容器),一个运行 node1 节点上,一个运行在 node2 节点上。这样会自动将 node2 节点接入 uber-net 网络。当 Swarm 在覆盖网络之上启动容器时,会自动将容器运行所在节点加入到网络当中。这意味着此时在 node2 节点上就可以看到 uber-net 网络了。目前已经成功在两个由物理网络连接的节点上创建了新的覆盖网络。同时,还将两个容器连接到了该网络当中。4) 测试覆盖网络,用 ping 命令来测试覆盖网络。为了执行该测试,需要知道每个容器的 IP 地址(为了测试,暂时忽略相同覆盖网络上的容器可以通过名称来互相 ping 通的事实)。运行 docker network inspect 查看被分配给覆盖网络的 Subnet。

配置现状


由图可知,一个二层覆盖网络横跨两台主机,并且每个容器在覆盖网络中都有自己的 IP 地址。这意味着 node1 节点上的容器可以通过 node2 节点上容器的 IP 地址 10.0.0.4 来 ping 通,该 IP 地址属于覆盖网络。尽管两个节点分属于不同的二层网络,还是可以直接 ping 通(ping 10.0.0.4)。还可以在容器内部跟踪 ping 命令的路由信息。路由信息只有一条,证明容器间通信确实通过覆盖网络直连。如Linux 示例如下。$ root@396c8b142a85:/# traceroute 10.0.0.4。

       覆盖网络工作背后的技术原理:1) VXLAN 入门:Docker 使用 VXLAN 隧道技术创建了虚拟二层覆盖网络。在 VXLAN 的设计中,允许用户基于已经存在的三层网络结构创建虚拟的二层网络,具体如下图所示。
 

创建虚拟的二层网络


VXLAN 的美妙之处在于它是一种封装技术,能使现存的路由器和网络架构看起来就像普通的 IP/UDP 包一样,并且处理起来毫无问题。为了创建二层覆盖网络,VXLAN 基于现有的三层 IP 网络创建了隧道。基础网络(Underlay Network)这个术语,它用于指代三层之下的基础部分。VXLAN 隧道两端都是 VXLAN 隧道终端(VXLAN Tunnel Endpoint, VTEP)。VTEP 完成了封装和解压的步骤,以及一些功能实现所必需的操作,如下图所示。
 

VXLAN设计

2) 在前面的示例中,通过 IP 网络将两台主机连接起来,每个主机运行了一个容器,之后又为容器连接创建了一个 VXLAN 覆盖网络。为了实现上述场景,在每台主机上都新建了一个 Sandbox(网络命名空间)。正如前文所讲,Sandbox 就像一个容器,但其中运行的不是应用,而是当前主机上独立的网络栈。在 Sandbox 内部创建了一个名为 Br0 的虚拟交换机(又称做虚拟网桥)。同时 Sandbox 内部还创建了一个 VTEP,其中一端接入到名为 Br0 的虚拟交换机当中,另一端接入主机网络栈(VTEP)。在主机网络栈中的终端从主机所连接的基础网络中获取到 IP 地址,并以 UDP Socket 的方式绑定到 4789 端口。不同主机上的两个 VTEP 通过 VXLAN 隧道创建了一个覆盖网络,如下图所示。
 

不同主机的VTEP创建覆盖网络


这是 VXLAN 上层网络创建和使用所必需的。接下来每个容器都会有自己的虚拟以太网(veth)适配器,并接入本地 Br0 虚拟交换机。目前拓扑结构如下图所示,虽然是在主机所属网络互相独立的情况下,但这样能更容易看出两个分别位于不同主机上的容器之间是如何通过 VXLAN 上层网络进行通信的。3) 通信示例中,将 node1 上的容器称为 C1,node2 上的容器称为 C2,如下图所示。假设 C1 希望 ping 通 C2。
 

为容器设置了IP地址


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 中创建两个虚拟交换机,默认支持路由。

                                  第13章:卷与持久化数据

数据主要分为两类,持久化的与非持久化的。持久化数据是需要保存的数据。例如客户信息、财务、预定、审计日志以及某些应用日志数据。非持久化数据是不需要保存的那些数据。每个 Docker 容器都有自己的非持久化存储。非持久化存储自动创建,从属于容器,生命周期与容器相同。这意味着删除容器也会删除全部非持久化数据。如果希望自己的容器数据保留下来(持久化),则需要将数据存储在卷上。卷与容器是解耦的,从而可以独立地创建并管理卷,并且卷并未与任意容器生命周期绑定。最终效果即用户可以删除一个关联了卷的容器,但是卷并不会被删除。对于微服务设计模式来说,容器是不错的选择。通常与微服务挂钩的词有暂时以及无状态。所以微服务就是无状态的、临时的工作负载,同时容器即微服务,就会认为容器就是用于临时场景。但这种说法是完全错误的。毫无疑问,容器擅长无状态和非持久化事务。每个容器都被自动分配了本地存储。默认情况下,这是容器全部文件和文件系统保存的地方。之前我们可能听过一些非持久存储相关的名称,如本地存储、GraphDriver 存储以及 SnapShotter 存储。总之,非持久存储属于容器的一部分,并且与容器的生命周期一致,容器创建时会创建非持久化存储,同时该存储也会随容器的删除而删除。在Linux系统中,该存储的目录在 /var/lib/docker/<storage-driver>/ 之下,是容器的一部分。在 Windows 系统中位于 C\ProgramData\Docker\windowsfilter\ 目录之下。如果在生产环境中使用 Linux 运行 Docker,需要确认当前存储驱动(GraphDriver)与 Linux 版本是否相符。下面列举了一些指导建议。

  • RedHat Enterprise Linux:Docker 17.06 或者更高的版本中使用 Overlay2 驱动。在更早的版本中,使用 Device Mapper 驱动。这适用于 Oracle Linux 以及其他 Red Hat 相关发行版。
  • Ubuntu:使用 Overlay2 或者 AUFS 驱动。如果正在使用 Linux4.x 或者更高版本的内核,建议使用 Overlay2。
  • SUSE Linux Enterprise Server:使用 Btrfs 存储驱动。
  • Windows:Windows 只有一种驱动,已经默认设置。

上述清单只作为建议。随着时间发展,Overlay2 驱动正在逐渐流行,可能在未来会成为大多数平台上的推荐存储驱动。如果使用 Docker 企业版(EE),并且有技术支持合约,建议通过咨询获取最新的兼容矩阵。默认情况下,容器的所有存储都使用本地存储。所以默认情况下容器全部目录都是用该存储。

容器中持久化数据:采用卷方式持久化数据,用户创建卷,然后创建容器,接着将卷挂载到容器上。卷会挂载到容器文件系统的某个目录之下,任何写到该目录下的内容都会写到卷中。即使容器被删除,卷与其上面的数据仍然存在。如下图所示,Docker 卷挂载到容器的 /code 目录。任何写入 /code 目录的数据都会保存到卷当中,并且在容器删除后依然存在。

Docker卷挂载到容器的/code目录


上图中,/code 目录是一个 Docker 卷。容器其他目录均使用临时的本地存储。卷与目录 /code 之间采用带箭头的虚线连接,这是为了表明卷与容器是非耦合的关系。1) 创建和管理容器卷:Docker 中卷属于一等公民。抛开其他原因,这意味着卷在 API 中拥有一席之地,并且有独立的 docker volume 子命令。使用$ docker volume create myvol命令创建名为 myvol 的新卷。默认情况下,Docker 创建新卷时采用内置的 local 驱动。恰如其名本地卷只能被所在节点的容器使用。使用 -d 参数可以指定不同的驱动。第三方驱动可以通过插件方式接入。这些驱动提供了高级存储特性,并为 Docker 集成了外部存储系统。截止到目前为止,已经存在 25 种卷插件,涵盖了块存储、文件存储、对象存储等。

  • 块存储:相对性能更高,适用于对小块数据的随机访问负载。目前支持 Docker 卷插件的块存储例子包括 HPE 3PAR、Amazon EBS 以及 OpenStack 块存储服务(Cinder)。
  • 文件存储:包括 NFS 和 SMB 协议的系统,同样在高性能场景下表现优异。支持 Docker 卷插件的文件存储系统包括 NetApp FAS、Azure 文件存储以及 Amazon EFS。
  • 对象存储:适用于较大且长期存储的、很少变更的二进制数据存储。通常对象存储是根据内容寻址,并且性能较低。支持 Docker 卷驱动的例子包括 Amazon S3、Ceph 以及 Minio。

可以通过 docker volume ls 命令查看卷是否成功,还可以使用 docker volume inspect 命令查看详情。使用 local 驱动创建的卷在 Docker 主机上均有其专属目录,这意味着可以在 Docker 主机文件系统中查看卷,甚至在 Docker 主机中对其进行读取数据或者写入数据操作。有两种方式删除 Docker 卷,docker volume prune和docker volume rm。其中docker volume prune 会删除未装入到某个容器或者服务的所有卷,所以谨慎使用!docker volume rm 允许删除指定卷。两种删除命令都不能删除正在被容器或者服务使用的卷。此外可以通过在 Dockerfile 中使用 VOLUME 指令的方式部署卷。具体的格式为 VOLUME <container-mount-point。但是,在 Dockerfile 中无法指定主机目录。这是因为主机目录通常情况下是相对主机的一个目录,意味着这个目录在不同主机间会变化,并且可能导致构建失败。如果通过 Dockerfile 指定,那么每次部署时都需要指定主机目录。2) 演示卷在容器和服务中的使用,使用下面的命令创建一个新的独立容器,并挂载一个名为 bizvol 的卷。Windows 示例如下。所有的 Windows 示例都在 PowerShell 中执行,请注意反引号(`)用于将命令拆至多行。

> docker container run -dit --name voltainer `
--mount source=bizvol,target=c:\vol `
microsoft/powershell:nanoserver

即使系统中没有叫作 bizvol 的卷,命令也应该能够成功运行。这里引出了很有意思的一点。如果指定了已经存在的卷,Docker 会使用该卷。如果指定的卷不存在,Docker 会创建一个卷。可以通过 docker volume ls 命令查看卷。尽管容器和卷各自拥有独立的生命周期,Docker 也不允许删除正在被容器使用的卷。目前卷是空的执行 exec 连接到容器并向卷中写入一部分数据。示例引用的是 Linux,如使用 Windows 示例,则将exec 命令结尾的sh换为 pwsh.exe。$ docker container exec -it voltainer sh
写入内容保存输入 exit 命令返回到 Docker 主机 Shell 中,然后删除容器但卷依旧存在。由于卷仍然存在,因此可以进入到其在主机的挂载点并查看前面写入的数据是否还在。在 Docker 主机的终端上执行$ ls -l /var/lib/docker/volumes/bizvol/_data/命令证明文件依然存在。$ cat /var/lib/docker/volumes/bizvol/_data/file1展示了文件的内容。甚至将 bizvol 挂载到一个新的服务或者容器都是可以的。

       在集群节点间共享存储:Docker 能够集成外部存储系统,使得集群间节点共享外部存储数据变得简单。例如,独立存储 LUN 或者 NFS 共享可以应用到多个 Docker 主机,因此无论容器或者服务副本运行在哪个节点上,都可以共享该存储。下图展示了位于共享存储的卷被两个 Docker 节点共享的场景。接下来这些 Docker 节点可以将共享卷应用到容器之上。

位于共享存储的卷被两个Docker节点共享


构建这样的环境需要外部存储系统的相关知识,并了解应用如何从共享存储读取或者写入数据。这种配置主要关注数据损坏(Data Corruption)。基于上图,设想下面的场景:Node 1 上的容器 A 在共享卷中更新了部分数据。但是为了快速返回,数据实际写入了本地缓存而不是卷中。此时,容器 A 认为数据已经更新。但是,在 Node 1 的容器 A 将缓存数据刷新并提交到卷前,Node 2 的容器 B 更新了相同部分的数据,但是值不同,并且更新方式为直接写入卷中。此时,两个容器均认为自己已经将数据写入卷中,但实际上只有容器 B 写入了。容器 A 会在稍后将自己的缓存数据写入缓存,覆盖了 Node 2 的容器 B 所做的一些变更。但是 Node 2 上的容器 B 对此一无所知。数据损坏就是这样发生的。为了避免这种情况,需要在应用程序中进行控制。数据卷得常用命令小结。1) docker volume create命令用于创建新卷。默认情况下,新卷创建使用 local 驱动,但是可以通过 -d 参数来指定不同的驱动。2) docker volume ls列出本地Docker主机上的全部卷。3) docker volume inspect用于查看卷的详细信息。可以使用该命令查看卷在 Docker 主机文件系统中的具体位置。4) docker volume prune会删除未被容器或者服务副本使用的全部卷。5) docker volume rm删除未被使用的指定卷。

发布了105 篇原创文章 · 获赞 86 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/dingyahui123/article/details/104563197