为了支持网络协议栈的多个实例,Linux 在网络协议栈中引入了网络命名空间。这些独立的协议栈被隔离到不同的命名空间中,处于不同命名空间中的网络协议栈是完全隔离的,彼此无法通信。通过对网络资源的隔离,就能在一台宿主机上虚拟多个不同的网络环境,Docker 正是利用了网络的命名空间特性,实现不同容器之间的网络隔离。
在 Linux 的网络命名空间中,每个空间都拥有独立的路由表和 iptables 设置,负责包转发、网络地址转换(NAT)和 IP 包过滤等功能。由于网络命名空间代表独立的协议栈,它们相互隔离且无法直接通信。但 Docker 通过虚拟接口 veth 实现了容器之间以及容器与宿主机之间的通信。虚拟以太网(veth)以成对的形式出现,就像管道的两端一样。数据从一个 veth 进入,经过协议栈后,从另一个 veth 出去,打通了互相隔离的协议栈之间的壁垒。通过连接两个网络命名空间或全局命名空间,veth 可以将物理网卡存在的命名空间内进行通信。要在两个网络命名空间之间实现通信,必须有一对虚拟接口对。
2、Linux 网络虚拟化
Docker 利用了 Linux 上的网络命名空间和虚拟网络设备来实现本地网络功能。了解 Linux 网络虚拟化技术有助于理解 Docker 网络的实现过程,其中 Linux 网络虚拟化是 LXC 项目的一部分。LXC 包括文件系统虚拟化、进程空间虚拟化、用户虚拟化和网络虚拟化等。
Docker 使用了LXC的网络虚拟化来模拟多个网络环境。Linux网络虚拟化主要有以下类型:
- 桥接:创建虚拟桥设备(网桥),将虚拟机连接至桥设备上,再配置桥设备的 IP 地址,即可与外部通信。此时虚拟机使用的是公网地址
- 隔离:将需要通信的虚拟机的网卡添加到同一个虚拟桥设备上,实现虚拟机之间的通信,与外网仍然隔离
- 路由:将虚拟机关联至虚拟桥设备上,配置与虚拟机同段的 IP 地址作为虚拟机的网关,最后打开物理主机的核心转达功能,实现虚拟机与外部主机的通信
- NAT:在路由模型的基础上,配置源地址转换(SNAT)规则,实现虚拟机与外网通信,但自己使用的是内网地址。外网无法主动访问内网的虚拟机,需要额外配置目标地址转换(DNAT)规则
Docker 中的网络接口默认为虚拟接口,其最大优势是转发效率高。Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发,即直接将发送接口的数据包复制到接收接口的缓存中,无需通过外部物理网络设备进行交换。对于本地系统和容器内系统而言,虚拟接口和普通以太网卡没有区别,但速度更快。
Docker 容器网络利用了 Linux 虚拟网络技术,通过在本地宿主机和容器内部创建虚拟接口 veth 并连接二者,这样的一对虚拟接口称为 veth pair。
通常情况下,Docker 创建容器时执行以下操作:
- 创建一对虚拟接口,一个放在本地宿主机,另一个放在新容器的命名空间中
- 将本地宿主机端的虚拟接口连接到默认的网桥 docker0(实际上是一个 Linux 网桥)或指定的网桥上,并赋予一个以 veth 开头的唯一名称,如 veth18
- 将容器端的虚拟接口放置到新创建的容器中,并将其名称修改为 eth0。此接口仅在容器的命名空间中可见
- 从网桥可用地址段中获取一个空闲地址,分配给容器的 eth0 接口(如172.10.0.22/16),并配置默认路由网关为 docker0 网桥的内部接口IP地址(如172.10.1.1/16)
完成上述操作后,Docker 在宿主机和所有容器之间创建了一个虚拟共享网络。容器可以使用其可见的 eth0 虚拟网卡连接其他容器并访问外部网络。在使用 docker [container] run 命令启动容器时,可以通过--net参数指定容器的网络配置。目前有5个可选值,分别是 bridge、container、host、none 和自定义网络。
3、Docker 网络架构
前面介绍了使用 -p 参数实现 Docker 容器与宿主机端口的映射,以及通过--link参数实现容器之间的网络通信。随着 Docker 的不断发展,其网络架构也在逐步演进。
与虚拟机或物理机不同,Docker 容器内部运行的应用程序需要大量不同类型的网络交互,这要求 Docker 具备强大的网络功能。幸运的是,Docker 提供了一套完整的解决方案,用于容器之间、容器与外部网络以及虚拟局域网(VLAN)之间的连接。这对于需要与外部系统(如虚拟机和物理机)进行通信的容器化应用至关重要。
Docker 1.9 版本引入了一套完整的 docker network 子命令和跨主机网络支持。用户可以通过 docker network --help 查看 Docker 网络命令的详细说明。这使得用户可以根据其应用程序的拓扑架构创建虚拟网络,并将容器连接到相应的网络上。事实上,在 Docker 1.7 版本中,网络部分代码就已经被抽离并单独形成了 Docker 的网络库,即 Libnetwork。随后,容器的网络模式被抽象成了统一接口的驱动。
为了标准化网络驱动的开发流程并支持多种网络驱动,Docker 在 Libnetwork 中采用了容器网络模型(CNM)。CNM 定义了构建容器虚拟化网络的模型,并提供了用于开发多种网络驱动的标准接口和组件。Libnetwork 是 Docker 对 CNM 的一种实现,提供了 Docker 核心网络架构的全部功能。不同的驱动可以通过插拔的方式接入 Libnetwork,以提供定制化的网络拓扑。为了实现即插即用的效果,Docker 封装了一系列本地驱动,覆盖了大部分常见的网络需求,包括单机桥接网络、多机覆盖网络,以及支持接入现有 VLAN。Libnetwork 与 Docker 守护进程及各个网络驱动之间的关系如图所示。
Docker 网络架构主要由 CNM、Libnetwork 和驱动三个主要部分组成。Docker 守护进程通过调用 Libnetwork 提供的 API 完成网络的创建和管理等功能;Libnetwork 则使用 CNM 来实现网络功能的提供;而 CNM 主要包括沙盒、接入点和网络三种组件。
Libnetwork 内置了 5 种驱动,为 Libnetwork 提供了不同类型的网络服务。
4、容器网络模型
Libnetwork 中的容器网络模型(CNM)简洁而精致,规定了 Docker 网络的基础组成要素,使得上层容器可以最大程度地摆脱底层实现的烦恼。CNM模型包括以下三种基本组件:
- 沙盒:代表容器独立的网络栈,包括以太网接口、端口、路由表以及 DNS 配置
- 接入点:在网络上代表可以连接容器的虚拟接口,会分配 IP 地址。类似于普通网络接入点,接入点主要负责创建连接,在 CNM 模型中,接入点负责将沙盒连接到网络
- 网络:是一个虚拟子网,可以连接多个接入点,是 IEEE 802.1d 网桥的软件实现
Docker 环境中最小的调度单位是容器,而 CNM 则专注于为容器提供网络功能。下图展示了 CNM 组件如何与容器进行关联,沙盒被置于容器内部,为容器提供网络连接。
接入点类似于常见的网络适配器,一个接入点只能连接到一个网络。如果容器需要连接到多个网络,则需要多个接入点。例如,容器 A 只有一个接入点并连接到网络 A,容器 B 有两个接口分别连接到网络 A 和 B。容器 A 和 B 可以相互通信,因为它们都连接到了同一个网络 A。但是,如果没有三层路由器的支持,容器B的两个接入点之间是无法相互通信的。
通过面向接口的分层解耦设计,CNM 实现了关注点的分离,使得容器管理系统无需关心底层网络的实现细节和不同子网之间的隔离。只要插件能够提供网络和接入点,容器管理系统只需连接或断开容器,剩余的工作就交由插件自行处理,实现了容器和网络功能的解耦,具有极高的灵活性。
CNM 网络的创建过程是,Libnetwork 网络驱动将自身注册到网络控制器,网络控制器使用驱动类型创建网络,然后在创建的网络上创建接口,最后将容器连接到接口上。销毁过程则相反,首先从接入点上卸载容器,然后删除接入点和网络即可。
目前 CNM 支持四种网络驱动类型:
- null:不提供网络服务,容器启动后无网络连接
- bridge:Docker 传统的单机网络,基于 Linux 网桥和 iptables 实现
- overlay:跨宿主机容器网络,利用虚拟扩展局域网(VXLAN)隧道技术实现
- remote:扩展类型,为其他外部实现的方案保留,由第三方编写网络驱动,例如 Calico、Contiv、Kuryr、Weave 等
Libnetwork 位于 Docker 上方支持 Docker,下方支持网络插件,处于十分关键的中间层。熟悉计算机网络协议模型的读者可以将 Libnetwork 视为传输控制协议/网际协议(TCP/IP)层的核心。每个驱动负责管理其上的所有网络资源。为了满足复杂且多变的环境需求,Libnetwork 支持同时激活多个网络驱动,从而支持庞大的异构网络。
5、单机桥接网络
最简单的 Docker 网络是单机桥接网络,从名称中即可了解以下两点:
- 单机:该网络仅在单个 Docker 宿主机上运行,只能连接到该宿主机上的容器
- 桥接:这意味着它是 IEEE 802.1d 桥接的一种实现,即二层交换机
每个 Docker 宿主机都默认拥有一个单机桥接网络。在 Linux 上,网络名为 bridge;在 Windows 上,网络名为 NAT。除非在创建容器时明确指定了 --network
参数,否则新创建的容器都会自动连接到该网络。
在 Linux 宿主机上,Docker 网络由 bridge 驱动创建,底层基于 Linux 内核的 Linux 网桥技术。这意味着 bridge 网络具有高性能和稳定性,并且可以通过标准的 Linux 工具进行管理。
默认情况下,Linux Docker 宿主机上的 bridge 网络映射到内核中的 docker0 Linux 网桥。通过 docker network inspect bridge
命令可以查看该虚拟网络的详细信息。
docker0 网桥类似于交换机,用于转发连接到其上的设备的数据帧。网桥上的 veth 网卡设备相当于交换机上的端口,可连接多个容器或虚拟机,工作在二层,无需配置 IP 信息。
在 Docker 的桥接网络模式中,docker0 的 IP 地址作为连接到其上的容器的默认网关地址存在。
在宿主机上安装 Docker 引擎时,Docker 默认会创建 bridge 网络,并且宿主机上的所有容器默认都会连接到此网络。可通过 docker network create
命令创建新的单机桥接网络,并通过在容器启动时使用 docker run
命令的 --network
参数指定容器要连接的网络。
在同一网络中,不同容器可以通过容器名相互访问,因为所有容器都注册在指定的 Docker DNS 服务上。在单机桥接网络模式下,不同网络中的容器要相互访问,则需要使用端口映射来避免容器名解析限制。通过在容器启动时使用 docker run
命令的 -d
参数指定容器与宿主机的端口映射,将容器端口映射到 Docker 宿主机端口上,从而实现访问。
6、多机覆盖网络
覆盖网络适用于跨多个宿主机的环境,它创建了一个扁平、安全的二层网络,用于连接这些宿主机。容器可以连接到覆盖网络并直接相互通信,实现链路层的数据传输。覆盖网络是实现容器间通信的理想方式,具有良好的网络伸缩性。
Docker 提供了本地驱动来支持覆盖网络,该驱动是基于 Libnetwork 和 overlay 驱动构建的,使得创建覆盖网络非常简单,只需在 `docker network create` 命令中添加 `--d overlay` 参数。
overlay 驱动默认使用 VXLAN 协议,在可相互访问的多个宿主机之间建立隧道,从而实现容器之间的互相访问。Docker 使用 VXLAN 隧道技术创建虚拟的二层覆盖网络。VXLAN 是一种封装技术,能够使现有的路由器和网络架构看起来像普通的 IP/UDP 包一样,并且可以无缝处理。为了创建二层覆盖网络,VXLAN 基于现有的三层 IP 网络创建了隧道。
在 VXLAN 的设计中,每个节点之间建立了 VXLAN 隧道,这些节点称为 VXLAN 隧道终端(VTEP)。VTEP 完成了封装和解压缩的操作,以及一些必要的功能实现。在两个节点之间建立 VXLAN 隧道后,为了实现跨节点的容器间互联,我们需要在每个宿主机上创建一个沙盒(网络命名空间)。沙盒类似于容器,但其中运行的是当前宿主机上独立的网络栈。在沙盒内部,创建一个名为 Br0 的虚拟交换机(也称为虚拟网桥)。同时,在沙盒内部创建一个 VTEP,其中一端连接到名为 Br0 的虚拟交换机,另一端连接到宿主机的网络栈(VTEP)。
在宿主机网络栈中,VTEP 从基础网络中获取 IP 地址,并通过 UDP 套接字绑定到 4789 端口(IANA 规定的 UDP 端口)。不同宿主机上的两个 VTEP 通过 VXLAN 隧道创建了一个覆盖网络。
每个容器都有自己的虚拟以太网适配器(veth),并连接到本地虚拟交换机 Br0。目前的拓扑结构如图所示。
虽然宿主机网络相互独立,但位于不同宿主机上的容器可以通过 VXLAN 覆盖网络进行通信。例如,容器 C1(位于节点1)希望 ping 通容器 C2(位于节点2)。C1 发起 ping 请求,目标 IP 为 C2 的地址 10.1.0.8。请求流量通过连接到虚拟交换机 Br0 的 veth 发送出去。Br0 不知道将数据包发送到哪里,因此会将其发送到所有端口。连接到 Br0 的 VTEP 接口知道如何转发这个数据帧,并将自己的 MAC 地址返回给 Br0。这是一个代理 ARP 响应,使得 Br0 学会了如何转发该包。之后,Br0 更新了自己的 ARP 映射表,将 10.1.0.8 映射到本地 VTEP 的 MAC 地址。现在,Br0 已经学会如何转发到 C2 的流量,之后所有发送到 C2 的包都将直接转发到 VTEP 接口。
当包到达节点2后,内核发现目标端口为 UDP Socket 端口 4789,并且知道存在 VTEP 接
口绑定到该 Socket。因此,内核将包发送给 VTEP,VTEP 读取 VNID,解压缩包信息,并根据 VNID 发送到本地连接到 VLAN 的虚拟交换机 Br0。在 Br0 上,包被发送给容器 C2。
7、混合互联网络
在先前的介绍中,讨论了单节点和跨节点下多个 Docker 容器之间的互联互通。然而,在企业的实际 IT 架构中,由于历史原因,大量传统架构的应用需要进行集成。因此,将容器化应用连接到外部系统和物理网络是至关重要的能力。一个常见的场景是部分容器化的应用需要与运行在物理网络和 VLAN 上的未容器化应用进行通信。这就引入了虚拟网络和物理网络之间混合互联的网络架构。
Docker 内置的 macvlan 驱动就是为这种场景而设计的。通过为容器提供 MAC 地址和 IP 地址,macvlan 允许容器在物理网络上成为“一等公民”。
macvlan 本身是一个 Linux 内核模块,其功能是允许在同一个物理网卡上配置多个 MAC 地址,即多个接口,每个接口可以配置自己的 IP 地址。macvlan 本质上是一种网卡虚拟化技术,其优点在于性能优异,因为无需端口映射或额外桥接,可以直接通过主机接口(或子接口)访问容器接口。但缺点是需要将主机网络接口卡(Network Interface Card,NIC)设置为混杂模式(promiscuous mode),而这在大部分公有云平台上是不允许的。因此,macvlan 对于公司内部的数据中心网络来说非常实用(假设公司网络组能接受 NIC 设置为混杂模式),但在公有云上则不可行。
创建 macvlan 网络非常简单,只需在 Docker 创建网络命令 docker network create
中添加 -d macvlan
参数即可。创建的 macvlan 网络可以是 bridge 模式或 802.1q trunk bridge 模式:
- 在 bridge 模式中,macvlan 流量通过主机上的物理设备
- 在 802.1q trunk bridge 模式中,流量通过 Docker 在运行中创建的 802.1q 子接口,用户可以更细粒度地控制路由和过滤
8、网络访问控制
Docker 容器的网络访问控制主要通过 Linux 系统上的 iptables 防火墙软件来进行管理和实现。iptables 是 Linux 系统中流行的防火墙软件,在大多数发行版中都自带。
安装完 Docker 后,宿主机系统会默认添加一些 iptables 规则,用于管理 Docker 容器之间以及容器与外部世界的通信。iptables 的工作原理是基于规则,这些规则实际上是由网络管理员预定义的条件。一般情况下,规则定义了如果数据包头符合某种条件,就对这个数据包采取相应的处理方式。这些规则存储在内核空间的信息包过滤表中,其中包括了源地址、目的地址、传输协议(如TCP、UDP、ICMP)以及服务类型(如HTTP、FTP、SMTP)等信息。当数据包与规则匹配时,iptables 会根据规则所定义的方法来处理这些数据包,比如允许通过(accept)、拒绝(reject)或丢弃(drop)等。
配置防火墙的主要任务就是添加、修改和删除这些规则。您可以使用 iptables-save命令来查看宿主机中Docker引擎为我们设置的iptables规则信息。