容器跨主机网络(20250218)
容器跨主机网络(20250218)
Flannel 框架
在 Docker 的默认配置下,不同宿主机上的容器通过 IP 地址进行互相访问是根本做不到的。
要理解容器“跨主通信”的原理,就一定要先从 Flannel 这个项目说起
事实上,Flannel 项目本身只是一个框架,真正为我们提供容器网络功能的,是 Flannel 的后端实现。目前,Flannel 支持三种后端实现,分别是:
- VXLAN;
- host-gw;
- UDP
UDP 模式,是 Flannel 项目最早支持的一种方式,却也是性能最差的一种方式。所以,这个模式目前已经被弃用。
我有两台宿主机
- 宿主机 Node 1 上有一个容器 container-1,它的 IP 地址是 100.96.1.2,对应的 docker0 网桥的地址是:100.96.1.1/24。
- 宿主机 Node 2 上有一个容器 container-2,它的 IP 地址是 100.96.2.3,对应的 docker0 网桥的地址是:100.96.2.1/24。
我们现在的任务,就是让 container-1 访问 container-2。
container-1 容器里的进程发起的 IP 包,其源地址就是 100.96.1.2,目的地址就是 100.96.2.3。由于目的地址 100.96.2.3 并不在 Node 1 的 docker0 网桥的网段里,所以这个 IP 包会被交给默认路由规则,通过容器的网关进入 docker0 网桥(如果是同一台宿主机上的容器间通信,走的是直连规则),从而出现在宿主机上。
这时候,这个 IP 包的下一个目的地,就取决于宿主机上的路由规则了。
# 在Node 1上
$ ip route
default via 10.168.0.1 dev eth0
100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.1.0
100.96.1.0/24 dev docker0 proto kernel scope link src 100.96.1.1
10.168.0.0/24 dev eth0 proto kernel scope link src 10.168.0.2
1. 默认路由
default via 10.168.0.1 dev eth0
- 含义:所有目标地址不在其他路由规则中的流量(如互联网流量),将通过网关
10.168.0.1
发送,使用eth0
物理网卡。- 作用:提供节点的默认出口,通常用于连接外部网络。
2. Flannel 覆盖网络路由
100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.1.0
- 目标网络:
100.96.0.0/16
,可能是 Kubernetes 集群 Pod 的 IP 地址范围。- 接口:
flannel0
,由 Flannel(容器网络插件)创建的虚拟接口,用于跨节点容器通信。- 关键字段:
proto kernel
:路由由内核自动生成(可能因接口配置而触发)。scope link
:仅在直接连接的链路上有效。src 100.96.1.0
:发送数据包时使用的源 IP(可能是 Flannel 分配给当前节点的子网)。- 作用:允许节点上的容器/Pod 通过 Flannel 网络与其他节点的容器/Pod 通信(如 VXLAN 封装)。
3. Docker 网桥路由
100.96.1.0/24 dev docker0 proto kernel scope link src 100.96.1.1
- 目标网络:
100.96.1.0/24
,Docker 容器的子网。- 接口:
docker0
,Docker 默认的虚拟网桥,用于本节点内容器间通信。- 关键字段:
src 100.96.1.1
:docker0
网桥的 IP,作为容器的默认网关。- 作用:本节点内的容器通过
docker0
网桥互通,而跨节点容器流量会通过flannel0
转发。
4. 本地物理网络路由
10.168.0.0/24 dev eth0 proto kernel scope link src 10.168.0.2
- 目标网络:
10.168.0.0/24
,节点所在的物理局域网。- 接口:
eth0
,节点的物理网卡。- 关键字段:
src 10.168.0.2
:节点在物理网络中的 IP 地址。- 作用:同一局域网内的其他设备(如
10.168.0.1
网关或其他节点)通过eth0
直接通信,无需经过网关。
网络架构总结
- 外部通信:通过
eth0
和默认网关10.168.0.1
访问互联网或其他外部网络。- 物理局域网:
10.168.0.0/24
网段的设备通过eth0
直接互通。- 容器网络:
- 本节点内容器:通过
docker0
网桥(子网100.96.1.0/24
)通信。- 跨节点容器:通过 Flannel 覆盖网络(
100.96.0.0/16
)路由,由flannel0
接口处理封装和跨节点转发(如 VXLAN)。
流量示例
- 容器访问互联网:
- 容器 →
docker0
(网关100.96.1.1
)→ 默认路由 →eth0
网关10.168.0.1
→ 互联网。- 跨节点容器通信:
- 容器 →
docker0
→ Flannel 路由规则 →flannel0
(封装为 VXLAN)→ 目标节点的flannel0
→ 解封装 → 目标容器。
潜在问题检查
- Flannel 与 Docker 子网一致性:确保
docker0
的子网(100.96.1.0/24
)是 Flannel 分配的子网(100.96.0.0/16
)的一部分。- 网关可达性:验证
10.168.0.1
是否可达,确保外部通信正常。- Flannel 健康状态:确认
flannel0
接口和 Flannel 服务正常运行,保障跨节点容器通信。
由于我们的 IP 包的目的地址是 100.96.2.3,它匹配不到本机 docker0 网桥对应的 100.96.1.0/24 网段,只能匹配到第二条、也就是 100.96.0.0/16 对应的这条路由规则,从而进入到一个叫作 flannel0 的设备中。
而这个 flannel0 设备的类型就比较有意思了:它是一个 TUN 设备(Tunnel 设备)。
TUN 设备的功能非常简单,即:在操作系统内核和用户应用程序之间传递 IP 包。
以 flannel0 设备为例:像上面提到的情况,当操作系统将一个 IP 包发送给 flannel0 设备之后,flannel0 就会把这个 IP 包,交给创建这个设备的应用程序,也就是 Flannel 进程。这是一个从内核态(Linux 操作系统)向用户态(Flannel 进程)的流动方向。
反之,如果 Flannel 进程向 flannel0 设备发送了一个 IP 包,那么这个 IP 包就会出现在宿主机网络栈中,然后根据宿主机的路由表进行下一步处理。这是一个从用户态向内核态的流动方向。
当 IP 包从容器经过 docker0 出现在宿主机,然后又根据路由表进入 flannel0 设备后,宿主机上的 flanneld 进程(Flannel 项目在每个宿主机上的主进程),就会收到这个 IP 包。然后,flanneld 看到了这个 IP 包的目的地址,是 100.96.2.3,就把它发送给了 Node 2 宿主机。
子网(Subnet)
flanneld 又是如何知道这个 IP 地址对应的容器,是运行在 Node 2 上的
Flannel 项目里一个非常重要的概念:子网(Subnet)
在由 Flannel 管理的容器网络里,一台宿主机上的所有容器,都属于该宿主机被分配的一个“子网”。在我们的例子中,Node 1 的子网是 100.96.1.0/24,container-1 的 IP 地址是 100.96.1.2。Node 2 的子网是 100.96.2.0/24,container-2 的 IP 地址是 100.96.2.3。
而这些子网与宿主机的对应关系,正是保存在 Etcd 当中,如下所示:
$ etcdctl ls /coreos.com/network/subnets
/coreos.com/network/subnets/100.96.1.0-24
/coreos.com/network/subnets/100.96.2.0-24
/coreos.com/network/subnets/100.96.3.0-24
输出显示了 Flannel 在 etcd 中注册的集群子网分配信息
/coreos.com/network/subnets/<子网>
是 Flannel 在 etcd 中存储子网信息的固定路径。子网表示:
100.96.1.0-24
是 Flannel 对 CIDR100.96.1.0/24
的转义写法(etcd 键名不允许使用/
,因此替换为-
)。每个子网对应一个 Kubernetes/Flannel 节点。
例如:
100.96.1.0/24
→ Node 1(当前节点的 Pod 子网,由docker0
网桥管理)。100.96.2.0/24
→ Node 2 的子网。100.96.3.0/24
→ Node 3 的子网。
flanneld 进程在处理由 flannel0 传入的 IP 包时,就可以根据目的 IP 的地址(比如 100.96.2.3),匹配到对应的子网(比如 100.96.2.0/24),从 Etcd 中找到这个子网对应的宿主机的 IP 地址是 10.168.0.3
$ etcdctl get /coreos.com/network/subnets/100.96.2.0-24
{"PublicIP":"10.168.0.3"}
对于 flanneld 来说,只要 Node 1 和 Node 2 是互通的,那么 flanneld 作为 Node 1 上的一个普通进程,就一定可以通过上述 IP 地址(10.168.0.3)访问到 Node 2,这没有任何问题。
所以说,flanneld 在收到 container-1 发给 container-2 的 IP 包之后,就会把这个 IP 包直接封装在一个 UDP 包里,然后发送给 Node 2。不难理解,这个 UDP 包的源地址,就是 flanneld 所在的 Node 1 的地址,而目的地址,则是 container-2 所在的宿主机 Node 2 的地址。
当然,这个请求得以完成的原因是,每台宿主机上的 flanneld,都监听着一个 8285 端口,所以 flanneld 只要把 UDP 包发往 Node 2 的 8285 端口即可。
# 在Node 2上
$ ip route
default via 10.168.0.1 dev eth0
100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.2.0
100.96.2.0/24 dev docker0 proto kernel scope link src 100.96.2.1
10.168.0.0/24 dev eth0 proto kernel scope link src 10.168.0.3
由于这个 IP 包的目的地址是 100.96.2.3,它跟第三条、也就是 100.96.2.0/24 网段对应的路由规则匹配更加精确。所以,Linux 内核就会按照这条路由规则,把这个 IP 包转发给 docker0 网桥。
docker0 网桥会扮演二层交换机的角色,将数据包发送给正确的端口,进而通过 Veth Pair 设备进入到 container-2 的 Network Namespace 里。
而 container-2 返回给 container-1 的数据包,则会经过与上述过程完全相反的路径回到 container-1 中。
上述流程要正确工作还有一个重要的前提,那就是 docker0 网桥的地址范围必须是 Flannel 为宿主机分配的子网。这个很容易实现,以 Node 1 为例,你只需要给它上面的 Docker Daemon 启动时配置如下所示的 bip 参数即可:
$ FLANNEL_SUBNET=100.96.1.1/24
$ dockerd --bip=$FLANNEL_SUBNET ...
它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。这就好比,Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用 IP 地址进行通信,而无需关心容器和宿主机的分布情况。
UDP 模式有严重的性能问题,所以已经被废弃了。通过我上面的讲述,你有没有发现性能问题出现在了哪里呢?
实际上,相比于两台宿主机之间的直接通信,基于 Flannel UDP 模式的容器通信多了一个额外的步骤,即 flanneld 的处理过程。而这个过程,由于使用到了 flannel0 这个 TUN 设备,仅在发出 IP 包的过程中,就需要经过三次用户态与内核态之间的数据拷贝,如下所示:
- 第一次,用户态的容器进程发出的 IP 包经过 docker0 网桥进入内核态;
- 第二次,IP 包根据路由表进入 TUN(flannel0)设备,从而回到用户态的 flanneld 进程;
- 第三次,flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去。
Flannel 进行 UDP 封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在用户态完成的。在 Linux 操作系统中,上述这些上下文切换和用户态操作的代价其实是比较高的,这也正是造成 Flannel UDP 模式性能不好的主要原因。
我们在进行系统级编程的时候,有一个非常重要的优化原则,就是要减少用户态到内核态的切换次数,并且把核心的处理逻辑都放在内核态进行这也是为什么,Flannel 后来支持的VXLAN 模式,逐渐成为了主流的容器网络方案的原因。
VXLAN 的覆盖网络的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网(LAN)里那样自由通信。当然,实际上,这些“主机”可能分布在不同的宿主机上,甚至是分布在不同的物理机房里。
而为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。
而 VTEP 设备的作用,其实跟前面的 flanneld 进程非常相似。只不过,它进行封装和解封装的对象,是二层数据帧(Ethernet frame);而且这个工作的执行流程,全部是在内核里完成的(因为 VXLAN 本身就是 Linux 内核中的一个模块)。
每台宿主机上名叫 flannel.1 的设备,就是 VXLAN 所需的 VTEP 设备,它既有 IP 地址,也有 MAC 地址。