我们已经从浅入深的总结了两篇文章,来分析docker 如何利用cni 创建网络。今天我们来到终结篇,分析下kubelet 是如何利用cni 创建网络。
1. kubelet 创建容器的过程
我们把这张图放在这里。kubelet 是一个本地的服务,会定期watch 分配到自己头上的pod,当发现自己需要处理一个addpod 的动作的时候。会通过一个kubecontainer.Runtime去执行pod 的生命周期管理,这个Runtime 的一个实现就是kuberuntime.NewKubeGenericRuntimeManager,代码位于/kubelet/kuberuntime/kuberuntime_manager.go。我们看下SyncPod 这个方法。它的注释说的很清楚:
// 1. Compute sandbox and container changes.
// 2. Kill pod sandbox if necessary.
// 3. Kill any containers that should not be running.
// 4. Create sandbox if necessary.
// 5. Create ephemeral containers.
// 6. Create init containers.
// 7. Create normal containers.
代码中注释的也非常清楚。我们重点看下NewKubeGenericRuntimeManager.runtimeService 这个字段。因为这个字段,是真正去访问运行时服务的client。我们看到图中,cri 会发起一个grpc请求,请求的对象是一个被称之为dockershim(垫钱)的grpc server。这个server是在kubelet启动时run起来的,它全权代理了针对docker.sock 的本地请求,至于请求发给了docker 以后,数据怎么走,我们并不关心。 参见我之前的文章,dockerd,containerd,runc 和 container-shim 的关系。其中我们所关心的pod 的网络设置,也就是设置在pod 的沙箱中的网络设置,发生在步骤4。我们下面重点跟踪。
2. 网络的创建
其创建pod 的第一步 就是创建pod 的沙箱环境也就是pause 容器,我们重点看下创建沙箱的过程。
Step 1: Pull the image for the sandbox.
Step 2: Create the sandbox container.
Step 3: Create Sandbox Checkpoint.
Step 4: Start the sandbox container.
Step 5: Setup networking for the sandbox.
实现在:runtimeService.RunPodSandbox,dockershim 就会先调用Docker API创建并启动infra容器。最终会来到:r.runtimeClient.RunPodSandbox,这是一个grpc 请求,请求的server 真实响应在代码在
kubernetes\pkg\kubelet\dockershim\docker_sandbox.go: RunPodSandbox中实现的。
第5步的时候就会给pod设置网络,CNI插件在pod启动的时候设置pod网络,分配IP地址,设置路由关系,创建网卡等。这个实现在这里:ds.network.SetUpPod。这个方法会调用kubelet 在启动时曾经初始化过的cni插件的相关接口方法,这个接口就是CNI接口,
plugin.addToNetwork
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)
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)
ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error)
}
CNI的插件有一段很简单的代码,调用了CNI插件的相关代码处理。
// AddNetworkList executes a sequence of plugins with the ADD command
func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
var err error
var result types.Result
for _, net := range list.Plugins {
result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
if err != nil {
return nil, err
}
}
if err = setCachedResult(result, list.Name, rt); err != nil {
return nil, fmt.Errorf("failed to set network %q cached result: %v", list.Name, err)
}
return result, nil
}
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)
if err != nil {
return nil, err
}
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 driver 会根据配置调用具体的 cni 插件,cni 插件将给 pause 容器配置正确的网络,其处理就像我们在前一篇文章中所写的一样,这里只是用代码的方式调用而已。
3. 其他容器的绑定
这一步很简单了,因为pod 是容器组,所以当pause 容器的网络设置好了以后,只要把sandboxid 传递给接下来的容器创建接口就好了。
很简单,这里的接口在创建容器时只是传入的上一步的sandboxID。
containerID, err := m.runtimeService.CreateContainer(podSandboxID, containerConfig, podSandboxConfig)
4. 多节点的容器互通
我们忽略了一步,当我们安装插件的逻辑,设置好了当前容器的network namespace后,多节点的互通由谁负责呢?这一步的解决,我们以flannel 为例。
我们在k8s环境上用命令安装的flannel,又是干什么用的呢?
https://raw.githubusercontent.com/coreos/flannel/a70459be0084506e4ec919aa1c114638878db11b/Documentation/kube-flannel.yml
从yaml文件上看有两个容器,分别为install-cni和 kube-flannel
install-cni:为初始化容器,执行后就消失了,用于从etcd 获取当前节点的网段信息,生成该主机/etc/cni/net.d/的flannel配置,我们看到它也的确做了mount。k8s创建、删除容器的时候就可以按照CNI流程挂载和删除网卡。
kube-flannel:主要启动flanneld,flanneld启动的时候会传入两个参数ip-masq和kube-subnet-mgr。
第一个参数–ip-masq,是为容器配置snat,容器默认都是可以直接访问外网的。MASQUERADE和SNAT都是用于将内网地址出局时转变为外部地址的方式,但MASQ本质上还是SNAT,只不过特殊一点。网络出口为动态分配IP时,MASQ是唯一的选择,它先判断当前系统的默认网关,再根据默认网关找到本机对应的出口IP地址,再做SNAT。
第二个参数–kube-subnet-mgr代表其使用kube类型的subnet-manager。该类型有别于使用etcd的local-subnet-mgr类型,使用kube类型后,flannel上各Node的IP子网分配均基于K8S Node的spec.podCIDR属性。
flanneld初始化创建基本的网络接口flannel,打通不同节点上的网络。CNI就负责将各个pod挂载到这个网络上。 flanneld 就是来去封装和解包vxlan的软件组件。数据到了vxlan 以后,容器就可以通过这种overlay 的方式,达到了集群内容器与容器的直接可达。
5. 总结
这里的过程总结可以看到,kubelet 做了大量的解耦的工作,调用CNI插件也是通过二进制直接操作的方式,以后当我们编写新的CNI插件时可以直接学习flannel的做法了。