本来预计两篇文章就能讲清楚 Kata containers 网络模型,但是写着写着内容不断扩展,内容覆盖了从 containerd 到 Kata containers 网络配置全链路。
- Kata Containers 网络模型(一)针对 containerd 如何配置网络这一主题详细展开,使用大量篇幅陈述如何初始化 CNI 插件,这部分内容有助于理解 CNI 插件的配置文件与网络的关系。然后详细描述了 containerd 在创建 sandbox 的时候是如何创建 netns 以及如何调用 CNI 插件为 pod 配置网络。
- Kata Containers 网络模型(二)详细展开了 CNI 插件的基本使用方法,以 bridge 插件为例剖析了代码实现,在最后展示了网络配置效果。
- Kata Containers 网络模型(三)将会聚焦 Kata containers 如何为安全容器连接网络,这里我们以 Go(2.0)版本的 runtime 作为例子,Rust(3.0)版本在原理上大同小异。还有部分网络更新的逻辑在 agent 组件中,不可避免的还是要看一些 Rust 源码。
Table of Contents
不管怎样我们先开始吧!还记得我们在第一篇文章中给出的网络架构图吗?在前两篇中,我们的网络已经初具雏形,而且前两篇文章介绍的内容是通用的,不管是 Kata 容器还是 runc 容器它们的网络都是这么被设置的。现在我们开始讲 Kata containers 独占的内容,本质的区别是多了一个虚拟机。
先不考虑容器场景,在普通虚拟机使用网桥连接网络的场景下,一般也会使用 "bridge + tap" 组合作为虚拟机网络模型,标绿色的部分和 Kata containers 网络架构的虚线框一样。一般来说,tap 设备连接虚拟机是惯用做法。
Kata Containers
在 runtime 被调用之前,containerd 和 CNI 插件已经轮流发威,"eth0 (veth)" 设备已经被安装在 pod netns 下了,它的 IP 地址是 172.19.0.95/24。
Kata containers 网络设置的大致流程如上图所示。Containerd 通过 rpc 请求告知 runtime 需要创建一个 sandbox。Runtime 会进入当前 pod 的 netns,扫描在 pod netns 下的全部设备。在我们的例子中,pod netns 下只有一个 veth 设备。随后 runtime 会将这个 veth 设备绑定到 hypervisor 中(如 QEMU),然后启动 hypervisor。最后 runtime 会与工作在虚拟机内部的 agent 通信让它更新接口、路由和 ARP 邻居信息。
接口/数据结构
NetworkInfo 结构体完整的描述一个网络设备,这个结构体是在扫描网络设备的时候获得的,具体信息的内容可以参考 "ip addr show" 指令执行结果。
NetworkInterface 结构体定义了一个设备接口的最基本的结构,包括设备的名字、mac 地址和 IP 地址。
TapInterface 结构体在 NetworkInterface 的基础上拓展了几个与 tap 设备相关的属性。
VMFds 和 VhostFds 最终都会被转换为 QEMU 命令,如 "-netdev tap,fds=x:y:z,vhostfds=x':y':z'" [1],其中:
fds=x:y:z
: 用于连接多队列(multiqueue)tap 设备。vhostfds=x':y':z'
: 用于连接多个已打开的 vhost net 设备。
NetworkInterfacePair 结构体顾名思义就是一对设备接口。为了连接虚拟机,一端必须是一个 tap 设备,所以这个结构体是基于 TapInterface 定义的。VirtIface 是虚拟机的网卡信息,虚拟机的网卡名称是与 pod netns 内的设备一一对应的,比如在我们的例子中,有一个名为 "eth0" 的 veth 设备,那么虚拟机也会被添加一个 "eth0" 的网络设备。
一个 NetworkInterfacePair 可以确定三个设备:(1)pod netns 的一个网络设备;(2)一个 tap 设备;(3)与 pod netns 网络设备对应的虚拟机网络设备,一图胜千言,最终效果如下图所示。
NetInterworkingModel 字段表示网络模型,可选值包括:
值 | 说明 |
---|---|
NetXConnectDefaultModel | 默认模型,默认值为 NetXConnectTCFilterModel。 |
NetXConnectMacVtapModel | MacVtap 模型。 |
NetXConnectTCFilterModel | TCFilter 模型,当前例子下就是 "veth + tc filter" 模型。 |
NetXConnectNoneModel | 无模型。 |
NetXConnectInvalidModel | 无效模型。 |
Endpoint 接口规定了网络设备的各种行为,比如如何绑定到 hypervisor 等。每种网络设备都需要实现一个 Endpoint 接口,比如 veth 设备需要实现一个 VethEndpoint 数据结构,后续章节中也会详细剖析 VethEndpoint 的逻辑。
Runtime 侧
Runtime 侧几乎负责了所有的网络配置,最开始展示的时序图是一个非常简化的版本,屏蔽了复杂的调用和细节。为了讲清楚 runtime 侧在配置网络上做了什么,需要在上面的图上进一步扩充调用关系,扩充后的时序图如下图所示。逻辑较重的地方多存在于 LinuxNetwork 和 VethEndpoint 之间,这也是我们重点关注的地方。
Runtime 在接收到 containerd 的创建 sandbox 指令,就着手创建 sandbox。这里的 sandbox 是指一个虚拟机(hypervisor)。下面是 QEMU 虚拟机使用 tap 设备的启动指令示例 [2],在虚拟机启动之前(步骤 10) runtime 就需要知道哪些网络需要被添加给虚拟机。
Runtime 首先调用了 createNetwork() 方法创建网络,这个方法会接着调用入口函数 AddEndpoints() ,最终会调用到内部方法 addAllEndpoints() 添加网络。
该方法的做的第一件事情了解什么网络设备需要被添加进 sandbox,这里使用 LinkList() 方法扫描 netns 下的全部设备,相当于执行了 "sudo ip netns exec {netns name} ip link show" 命令,这个方法是 vishvananda/netlink 库[3] 提供的方法。还记得吗,当前 pod netns 里只有一个名为 eth0 的 veth 设备。接着,该方法会将这些设备一个个地添加到 sandbox。步骤 5 使用 doNetNS 闭包进入 pod netns,步骤 6 执行了 addSingleEndpoint() 方法创建一个具体的 endpoint 实例并其绑定到 sandbox 中。
addSingleEndpoint() 方法除了 context 共传入了三个参数,分别是 sandbox 实例(s)、设备信息(netInfo)和是否支持热插拔(hotplug)。它做了两件事:(1)根据设备类型创建一个对应的 Endpoint;(2)将这个 Endpoint 绑定到 sandbox 中。代码已经非常直接和简单了,就不在展开了。
创建 VethEndpoint 的过程还是值得展开讲讲的,在这个过程中会创建一个用于连接虚拟机的 tap 设备,这部分逻辑都集中在 createVethNetworkEndpoint() 方法。
从上面的代码上看,它围绕着创建 VethEndpoint 实例做了 3 件事。第 1 步调用了 createNetworkInterfacePair() 方法创建了一个 NetworkInterfacePair 结构体,用一个 tap 设备与 netns 的网络设备相连,使其能够连接到虚拟机上。该结构体包含一个代表 tap 设备的 TapInterface 结构,它的名称是 "tap%d_kata",其中这个 %d 是目前设备的数量,如果只有一个设备则名字是 "tap0_kata"。该结构体还包含一个 VirtIface 代表虚拟机网络接口。createNetworkInterfacePair() 方法并不是 VethEndpoint 独占的,其他的 Endpoint 也需要用一个 tap 设备中转连接到虚拟机上。第 2 步就是用上一步创建出来的 netPair 组装了一个 VethEndpoint。第 3 步则保证虚拟机网卡名称 VirtIface 与 pod netns 的网络设备的名称一致。
时序图的前 7 步已经把有关 Endpoint 的一切信息/结构体都准备好了,第 8 步是将 Endpoint 绑定到 sandbox (或者可以理解为 hypervisor)中,这是靠调用 Endpoint 接口的 Attach() 方法。还有一个用于热插拔的 HotAttach(),有兴趣的同学可以自行研究,这里就不再占用篇幅了。
每个 Endpoint 实现都需要自己实现 Attach() 方法,这里还是以 VethEndpoint 的 Attach() 方法为例。
Attach() 方法主要做了两件事情,一个是调用 xConnectVMNetwork() 方法让 veth 设备连接到 tap0_kata 设备(aka 虚拟机网络,如绿色链路所示),一个是调用 hypervisor 的 AddDevice() 方法,让启动 QEMU 虚拟机时增加一条 netdev 设置,让 tap 设备连接到虚拟机(如蓝色链路所示)。
有关 xConnectVMNetwork() 方法的代码就不贴了,它的重要作用时调用了 setupTCFiltering() 方法。这个方法实质上地创建了 "tap0_kata" 这个 tap 设备(之前都是在创建结构体),通过执行 tc 命令的 filter 子命令 [4] 使得到达 "eth0 (veth)" 的数据无条件重定向到 tap0_kata 设备上,相反地,到达 tap0_kata 设备的数据也会被无条件重定向到 "eth0 (veth)" 设备。在这种模型下,tap0_kata 是不需要 IP 地址的。
第 10 步调用 startVM() 方法拉起虚拟机,此时虚拟机的 eth0 网卡并没有设置任何信息,因此在拓扑图上虚拟机 eth0 网卡的上方暂时标记无 ip 地址。
虚拟机侧
业务容器都运行在虚拟机内部,因此虚拟机的 eth0 网卡的 IP 地址、路由信息等也需要被正确配置。在时序图的第 11 和 12 步中,runtime 和 agent 相互配合共同配置虚拟机网卡。
在这部分不会在深入具体的代码了,我们就只讲网络是怎么通的。现在 "eth0 (veth)" 和 tap0_kata 这两个设备可以看成另一种形式的一对 veth 设备,而 tap0_kata 与虚拟机的 eth0 也可以被看作是一对 veth 设备。在这种体系下,"eth0 (veth)" 设备的数据会被无条件转发到虚拟机 eth0 网卡,反之依然。所以虚拟机 eth0 网卡的接口信息可以被设置为 "eth0 (veth)"。从结果上来说,可以把这三个设备粗略的看成同一个设备。
虚拟机 eth0 接口信息被 updateInterface() 方法设置为:
设备 | name | IPv4 地址 | mac 地址 |
---|---|---|---|
eth0 | eth0 | 172.19.0.95/24 | 62:4d:c9:a1:64:0f |
值得一提的是 "eth0 (veth)" 设备的 mac 地址也是 "62:4d:c9:a1:64:0f"。
虚拟机 eth0 路由信息被 updateRoutes() 方法设置为:
dest | gateway | device | source | scope | family |
---|---|---|---|---|---|
"" | 172.19.0.1 | eth0 | "" | 0 | v4 |
这是 rpc 调用发送的路由信息,不是标准意义上的 Linux 路由结构。这里面只包含一条默认路由:dest 为 0.0.0.0,默认网关为 172.19.0.1(也就是 cni0 网桥的 IP 地址),走 eth0 网卡。
终于!Kata containers 实现了“村网通”,完整网络拓扑结构如下所示。
References
- https://blog.csdn.net/wozaiyizhideng/article/details/116993949
- https://gist.github.com/extremecoders-re/e8fd8a67a515fee0c873dcafc81d811c
- https://github.com/vishvananda/netlink
- https://man7.org/linux/man-pages/man8/tc.8.html