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协议的资料,可以参考:

CNI协议实现

下面我们重点来看看CNI这个协议具体是如何实现的,包括容器运行时和网络插件是如何实现CNI协议的。

CNI libcni 库

CNI定义了一个lib库,libcni,该lib库其实就是CNI协议的具体实现,容器运行时可以直接引用libcni来实现CNI协议,它里面就包含两个文件:api.go和conf.go。

conf.go主要是用来解析json格式的网络配置文件,生成下面的对象:

1
2
3
4
5
6
7
8
9
10
11
12
type NetworkConfig struct {
Network *types.NetConf
Bytes []byte
}

type NetworkConfigList struct {
Name string
CNIVersion string
DisableCheck bool
Plugins []*NetworkConfig
Bytes []byte
}

api.go则使用上面生成的配置对象,调用对应的网络插件进行网络配置,它里面定义了如下的接口和结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type CNI interface {
AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
GetNetworkListCachedConfig(net *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error)

AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
GetNetworkCachedConfig(net *NetworkConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error)

ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error)
}

type CNIConfig struct {
Path []string
exec invoke.Exec
cacheDir string
}

CNIConfig实现了上面定义的接口,这些接口就包含了CNI协议中定义的3种操作:ADD, DEL, CHECK,此外还额外实现了GetVersionInfo()方法,用来支持VERSION操作,AddNetworkList()方法就是链式插件的实现,循环调用配置文件中定义的插件,将前一个插件的执行结果作为后一个插件的参数输入,而AddNetwork()是单独调用一个网络插件,其核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
c.ensureExec()
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
......
newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
if err != nil {
return nil, err
}

return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)
}

容器运行时CNI实现

容器运行时可以调用libcni中的conf.go去解析配置文件,调用api.go去构造CNIConfig对象,然后就可以调用对应的方法去给网络插件发送相应的操作了。我们以cri-o容器运行时为例,来介绍下它是如何实现CNI的,cri-o是RedHat主导实现的为Kubernetes量身定制的容器运行时,它实现了CRI和CNI协议,非常具有代表性,但它并没有直接调用libcni,而是定义了另外一个项目,叫ocicni,去间接调用libcni,这个项目作为一个中间层,可以为符合OCI规范的容器运行时提供CNI支持,而不是仅仅为cri-o使用,在这个项目里定义了如下的接口和结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// ocicni/pkg/ocicni/types.go
type CNIPlugin interface {
// Name returns the plugin's name. This will be used when searching
// for a plugin by name, e.g.
Name() string

// GetDefaultNetworkName returns the name of the plugin's default
// network.
GetDefaultNetworkName() string

// SetUpPod is the method called after the sandbox container of
// the pod has been created but before the other containers of the
// pod are launched.
SetUpPod(network PodNetwork) ([]NetResult, error)

// SetUpPodWithContext is the same as SetUpPod but takes a context
SetUpPodWithContext(ctx context.Context, network PodNetwork) ([]NetResult, error)

// TearDownPod is the method called before a pod's sandbox container will be deleted
TearDownPod(network PodNetwork) error

// TearDownPodWithContext is the same as TearDownPod but takes a context
TearDownPodWithContext(ctx context.Context, network PodNetwork) error

// GetPodNetworkStatus is the method called to obtain the ipv4 or ipv6 addresses of the pod sandbox
GetPodNetworkStatus(network PodNetwork) ([]NetResult, error)

// GetPodNetworkStatusWithContext is the same as GetPodNetworkStatus but takes a context
GetPodNetworkStatusWithContext(ctx context.Context, network PodNetwork) ([]NetResult, error)

// NetworkStatus returns error if the network plugin is in error state
Status() error

// Shutdown terminates all driver operations
Shutdown() error
}

// ocicni/pkg/ocicni/ocicni.go
type cniNetworkPlugin struct {
cniConfig *libcni.CNIConfig

sync.RWMutex
defaultNetName netName
networks map[string]*cniNetwork

nsManager *nsManager
confDir string
binDirs []string

shutdownChan chan struct{}
watcher *fsnotify.Watcher
done *sync.WaitGroup
......
}

cniNetworkPlugin结构体实现了上面定义的CNIPlugin接口,在初始化这个结构体时,会调用libcni中的方法来从json格式的网络配置文件构造NetworkConfig对象,然后在SetUpPod()方法中,又会调用libcni中的方法去执行添加网络等操作,如下:

1
2
3
4
5
6
7
8
9
10
11
// ocicni/pkg/ocicni/ocicni.go
func (network *cniNetwork) addToNetwork(ctx context.Context, rt *libcni.RuntimeConf, cni *libcni.CNIConfig) (cnitypes.Result, error) {
logrus.Infof("About to add CNI network %s (type=%v)", network.name, network.config.Plugins[0].Network.Type)
res, err := cni.AddNetworkList(ctx, network.config, rt)
if err != nil {
logrus.Errorf("Error adding network: %v", err)
return nil, err
}

return res, nil
}

所以ocicni这个项目,又在Pod这个维度进一步对libcni进行封装,使得容器运行时在实现CNI时进一步简化,这样在cri-o中,只需要初始化一个cniNetworkPlugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
// cri-o/pkg/config/config.go
func (c *NetworkConfig) Validate(onExecution bool) error {
// Init CNI plugin
cniPlugin, err := ocicni.InitCNI(
c.CNIDefaultNetwork, c.NetworkDir, c.PluginDirs...,
)

c.cniPlugin = cniPlugin
}

func (c *NetworkConfig) CNIPlugin() ocicni.CNIPlugin {
return c.cniPlugin
}

然后在实现CRI协议中定义的RunPodSandbox()接口时,直接调用cniPlugin的SetUpPod()方法就行了:

1
2
// cri-o/server/sandbox_network.go
_, err = s.config.CNIPlugin().SetUpPodWithContext(startCtx, podNetwork)

以上是倒叙的方式从底层向上层梳理了一遍,如果反过来,从上层到底层的调用顺序就是: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
2
3
func cmdAdd(args *skel.CmdArgs) error {}
func cmdCheck(args *skel.CmdArgs) error {}
func cmdDel(args *skel.CmdArgs) error {}

分别对应ADD, DEL, CHECK操作,然后其main()方法如下:

1
2
3
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("bridge"))
}

skel.PluginMain()方法就来自于cni项目中的skel模块,在该模块中定义了从环境变量中读取传递的参数的公共方法,然后根据环境变量,回调注册进来的对应方法,利用skel这个框架,网络插件就可以只实现具体的动作,而不用去重复实现变量处理等操作了。

总结

本篇文章,首先简单介绍了下CNI协议的内容,然后重点介绍了容器运行时和网络插件是如何实现CNI协议的,容器运行时主要依赖libcni去实现,但是可以引入一个ocicni中间层,做进一步抽象,而网络插件,则可以利用cni项目提供的skel框架,实现了所有网络插件都需要用到的公共代码,让网络插件只实现具体的动作即可,简化了插件开发。

作者

hackerain

发布于

2021-09-20

更新于

2023-10-26

许可协议