容器跨主机网络(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.1docker0 网桥的 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 直接通信,无需经过网关。

网络架构总结

  1. 外部通信:通过 eth0 和默认网关 10.168.0.1 访问互联网或其他外部网络。
  2. 物理局域网10.168.0.0/24 网段的设备通过 eth0 直接互通。
  3. 容器网络
    • 本节点内容器:通过 docker0 网桥(子网 100.96.1.0/24)通信。
    • 跨节点容器:通过 Flannel 覆盖网络(100.96.0.0/16)路由,由 flannel0 接口处理封装和跨节点转发(如 VXLAN)。

流量示例

  • 容器访问互联网
    1. 容器 → docker0(网关 100.96.1.1)→ 默认路由 → eth0 网关 10.168.0.1 → 互联网。
  • 跨节点容器通信
    1. 容器 → docker0 → Flannel 路由规则 → flannel0(封装为 VXLAN)→ 目标节点的 flannel0 → 解封装 → 目标容器。

潜在问题检查

  1. Flannel 与 Docker 子网一致性:确保 docker0 的子网(100.96.1.0/24)是 Flannel 分配的子网(100.96.0.0/16)的一部分。
  2. 网关可达性:验证 10.168.0.1 是否可达,确保外部通信正常。
  3. 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 对 CIDR 100.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 地址。

posted @   guixiang  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示