Kubernetes Kubelet CNI 机制解析
CNI简介
这里说Kubelet CNI,其实说法有些不准确,在Kubernetes Kubelet机制概述中就介绍过,Kubelet并没有直接跟CNI交互,而是通过容器运行时跟外部网络进行交互的,换句话说,CNI解决的是容器网络插件化的问题,跟Kubernetes这种容器编排系统并没有直接关系,但是有很多文章都说Kubelet支持了CNI,包括Kubernets官方的文档Network Plugins,其实这里是特指的Docker,因为Docker本身并不支持CNI,kubelet将对Docker网络环境的创建和删除,通过CNI的方式,放在了dockershim中,如果Kubernetes移除了对Docker的支持,就会移除dockershim,那么kubelet对CNI的支持也就会移除,这样两者就完全没有关系了。
现在Kubelet标准的做法是只对接支持CRI的容器运行时,CRI协议中定义了PodSandbox的概念,代表的是容器运行的网络环境,比如linux network namespace或者是一个虚拟机,在为容器创建PodSandbox时,如果该容器运行时同时也支持CNI,那么就会调用对应的网络插件去给network namespace或者虚拟机配置网卡,路由,DNS等网络信息,通过不同的网络插件,就可以选择不同的网络方案,所以,为PodSandbox配置网络,就是CNI发挥作用的地方,我们这里讨论的CNI机制也主要是针对容器运行时和网络插件的,示意图如下:
CNI的协议在这里查看,从v0.1.0到v1.0.0,已经发布了6个版本,该协议中定义了容器运行时和网络插件交互的标准规范,该协议主要规定了以下一些内容:
- 网络插件都是放到指定目录下的可执行文件,并且以json格式的配置文件来描述网络配置,当需要设置容器网络时,由容器运行时来调用网络插件,需要在环境变量中指定对应的操作,Container ID以及Namespace ID等,然后从标准输入(stdin)读入json格式的配置文件,执行成功的话,将结果输出到标准输出(stdout)中,执行失败的话,将错误输出到标准错误(stderr)中。
- 目前定义了4种操作:ADD, DEL, CHECK, VERSION,分别表示将容器添加到网络或者对现有的网络配置进行更改,从网络中删除容器或者取消对应修改,检查网络配置是否符合预期,查看网络插件支持的CNI版本,这4种操作由容器运行时通过环境变量的方式传给网络插件,决定了网络插件要做什么操作。
- CNI协议还规定了这些网络插件的链式调用关系,即chained plugin,在配置文件中,可以指定一组网络插件,第一个插件叫”Interface Plugin”,主要是用来在network namespace中创建网卡,设置IP等基本的网络配置,其它的插件叫做”Chained Plugin”,它使用前一个插件的输出作为输入,对已有的网卡做进一步配置,比如做一些调优设置等,这种链式结构带来的好处就是可以使网络插件的功能单一化,多个网络插件配合使用,可以组合出来更多的功能。
- 此外,CNI还规定了一种”Delegation Plugin”,与它对应的叫”Main Plugin”,它是将主Plugin的一部分功能抽出来单独组成了一个Plugin,典型的就是IPAM Plugin,因为分配IP,路由,网关等操作不同的网络插件可能是相同的,所以将这个功能单独抽出来,可以被主Plugin引用,即Delegation,这样就避免了各个网络插件去重复实现相同的功能。
以上是CNI协议的一些简介,下面还有一些不错的介绍CNI协议的资料,可以参考:
- https://juejin.cn/post/6986495816949039141
- https://cizixs.com/2017/05/23/container-network-cni/
- https://www.redhat.com/sysadmin/cni-kubernetes
CNI协议实现
下面我们重点来看看CNI这个协议具体是如何实现的,包括容器运行时和网络插件是如何实现CNI协议的。
CNI libcni 库
CNI定义了一个lib库,libcni,该lib库其实就是CNI协议的具体实现,容器运行时可以直接引用libcni来实现CNI协议,它里面就包含两个文件:api.go和conf.go。
conf.go主要是用来解析json格式的网络配置文件,生成下面的对象:
1 | type NetworkConfig struct { |
api.go则使用上面生成的配置对象,调用对应的网络插件进行网络配置,它里面定义了如下的接口和结构体:
1 | type CNI interface { |
CNIConfig实现了上面定义的接口,这些接口就包含了CNI协议中定义的3种操作:ADD, DEL, CHECK,此外还额外实现了GetVersionInfo()方法,用来支持VERSION操作,AddNetworkList()方法就是链式插件的实现,循环调用配置文件中定义的插件,将前一个插件的执行结果作为后一个插件的参数输入,而AddNetwork()是单独调用一个网络插件,其核心代码如下:
1 | func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) { |
容器运行时CNI实现
容器运行时可以调用libcni中的conf.go去解析配置文件,调用api.go去构造CNIConfig对象,然后就可以调用对应的方法去给网络插件发送相应的操作了。我们以cri-o容器运行时为例,来介绍下它是如何实现CNI的,cri-o是RedHat主导实现的为Kubernetes量身定制的容器运行时,它实现了CRI和CNI协议,非常具有代表性,但它并没有直接调用libcni,而是定义了另外一个项目,叫ocicni,去间接调用libcni,这个项目作为一个中间层,可以为符合OCI规范的容器运行时提供CNI支持,而不是仅仅为cri-o使用,在这个项目里定义了如下的接口和结构体:
1 | // ocicni/pkg/ocicni/types.go |
cniNetworkPlugin结构体实现了上面定义的CNIPlugin接口,在初始化这个结构体时,会调用libcni中的方法来从json格式的网络配置文件构造NetworkConfig对象,然后在SetUpPod()方法中,又会调用libcni中的方法去执行添加网络等操作,如下:
1 | // ocicni/pkg/ocicni/ocicni.go |
所以ocicni这个项目,又在Pod这个维度进一步对libcni进行封装,使得容器运行时在实现CNI时进一步简化,这样在cri-o中,只需要初始化一个cniNetworkPlugin:
1 | // cri-o/pkg/config/config.go |
然后在实现CRI协议中定义的RunPodSandbox()接口时,直接调用cniPlugin的SetUpPod()方法就行了:
1 | // cri-o/server/sandbox_network.go |
以上是倒叙的方式从底层向上层梳理了一遍,如果反过来,从上层到底层的调用顺序就是:cri-o –> ocicni –> libcni
网络插件CNI实现
CNI还实现了一些基础的网络插件,包含了bridge, ipvlan, macvlan等主插件,还有dhcp, host-local, static等IPAM插件,以及tuning, portmap等其它插件。我们以bridge为例,来看看网络插件是如何实现CNI的,bridge用来将容器桥接到网桥上,并且在network namespace里面添加网卡,设置IP,配置路由等,是一种很基础很常用的容器网络模型。网络插件的实现,主要是实现CNI协议中定义的4种操作(ADD, DEL, CHECK, VERSION)的具体方法,比如bridge插件中,实现了下面三个方法:
1 | func cmdAdd(args *skel.CmdArgs) error {} |
分别对应ADD, DEL, CHECK操作,然后其main()方法如下:
1 | func main() { |
skel.PluginMain()方法就来自于cni项目中的skel模块,在该模块中定义了从环境变量中读取传递的参数的公共方法,然后根据环境变量,回调注册进来的对应方法,利用skel这个框架,网络插件就可以只实现具体的动作,而不用去重复实现变量处理等操作了。
总结
本篇文章,首先简单介绍了下CNI协议的内容,然后重点介绍了容器运行时和网络插件是如何实现CNI协议的,容器运行时主要依赖libcni去实现,但是可以引入一个ocicni中间层,做进一步抽象,而网络插件,则可以利用cni项目提供的skel框架,实现了所有网络插件都需要用到的公共代码,让网络插件只实现具体的动作即可,简化了插件开发。
Kubernetes Kubelet CNI 机制解析
https://hackerain.me/2021/09/20/kubernetes/kube-kubelet-cni.html