Kubernetes 学习笔记之 Calico CNI Plugin 源码解析 (一)

  • 2021-05-10
  • 本文字数:5726 字

    阅读完需:约 19 分钟

Overview

之前在Kubernetes学习笔记之kube-proxy service实现原理学习到 calico 会在 worker 节点上为 pod 创建路由 route 和虚拟网卡 virtual interface,并为 pod 分配 pod ip,以及为 worker 节点分配 pod cidr 网段。

我们生产 k8s 网络插件使用 calico cni,在安装时会安装两个插件:calico 和 calico-ipam,官网安装文档 Install the plugin(https://docs.projectcalico.org/getting-started/kubernetes/hardway/install-cni-plugin#install-the-plugin)也说到了这一点,而这两个插件代码在 calico.go(https://github.com/projectcalico/cni-plugin/blob/release-v3.17/cmd/calico/calico.go) ,代码会编译出两个二进制文件:calico 和 calico-ipam。calico 插件主要用来创建 route 和 virtual interface,而 calico-ipam 插件主要用来分配 pod ip 和为 worker 节点分配 pod cidr。

重要问题是,calico 是如何做到的?

Sandbox container

kubelet 进程在开始启动时,会调用容器运行时 SyncPod(https://github.com/kubernetes/kubernetes/blob/release-1.17/pkg/kubelet/kubelet.go#L1692) 来创建 pod 内相关容器,

主要做了几件事情 L657-L856(https://github.com/kubernetes/kubernetes/blob/release-1.17/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L657-L856)

  • 创建 sandbox container,这里会调用 cni 插件创建 network 等步骤,同时考虑了边界条件,创建失败会 kill sandbox container 等等。

  • 创建 ephemeral containers、init containers 和普通的 containers。

这里只关注创建 sandbox container 过程,只有这一步会创建 pod network,这个 sandbox container 创建好后,其余 container 都会和其共享同一个 network namespace,所以一个 pod 内各个容器看到的网络协议栈是同一个,ip 地址都是相同的,通过 port 来区分各个容器。具体创建过程,会调用容器运行时服务创建容器,这里会先准备好 pod 的相关配置数据,创建 network namespace 时也需要这些配置数据 L36-L138(https://github.com/kubernetes/kubernetes/blob/release-1.17/pkg/kubelet/kuberuntime/kuberuntime_sandbox.go#L36-L138) :

func (m *kubeGenericRuntimeManager) createPodSandbox(pod *v1.Pod, attempt uint32) (string, string, error) {    // 生成pod相关配置数据    podSandboxConfig, err := m.generatePodSandboxConfig(pod, attempt)    // ...    // 这里会在宿主机上创建pod logs目录,在/var/log/pods/{namespace}_{pod_name}_{uid}目录下    err = m.osInterface.MkdirAll(podSandboxConfig.LogDirectory, 0755)    // ...    // 调用容器运行时创建sandbox container,我们生产k8s这里是docker创建    podSandBoxID, err := m.runtimeService.RunPodSandbox(podSandboxConfig, runtimeHandler)    // ...    return podSandBoxID, "", nil}
复制代码

k8s 使用 cri(container runtime interface)来抽象出标准接口,目前 docker 还不支持 cri 接口,所以 kubelet 做了个适配模块 dockershim,代码在 pkg/kubelet/dockershim。上面代码中的 runtimeService 对象就是 dockerService 对象,所以可以看下 dockerService.RunPodSandbox()代码实现 L76-L197(https://github.com/kubernetes/kubernetes/blob/release-1.17/pkg/kubelet/dockershim/docker_sandbox.go#L76-L197)

// 创建sandbox container,以及为该container创建networkfunc (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {    config := r.GetConfig()    // Step 1: Pull the image for the sandbox.    // 1. 拉取镜像    image := defaultSandboxImage    podSandboxImage := ds.podSandboxImage    if len(podSandboxImage) != 0 {        image = podSandboxImage    }    if err := ensureSandboxImageExists(ds.client, image); err != nil {        return nil, err    }        // Step 2: Create the sandbox container.    // 2. 创建sandbox container    createResp, err := ds.client.CreateContainer(*createConfig)    // ...    resp := &runtimeapi.RunPodSandboxResponse{PodSandboxId: createResp.ID}    ds.setNetworkReady(createResp.ID, false)        // Step 3: Create Sandbox Checkpoint.    // 3. 创建checkpoint    if err = ds.checkpointManager.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config)); err != nil {        return nil, err    }        // Step 4: Start the sandbox container.    // Assume kubelet's garbage collector would remove the sandbox later, if    // startContainer failed.    // 4. 启动容器    err = ds.client.StartContainer(createResp.ID)    // ...    // Step 5: Setup networking for the sandbox.    // All pod networking is setup by a CNI plugin discovered at startup time.    // This plugin assigns the pod ip, sets up routes inside the sandbox,    // creates interfaces etc. In theory, its jurisdiction ends with pod    // sandbox networking, but it might insert iptables rules or open ports    // on the host as well, to satisfy parts of the pod spec that aren't    // recognized by the CNI standard yet.    // 5. 这一步为sandbox container创建网络,主要是调用calico cni插件创建路由和虚拟网卡,以及为pod分配pod ip,为该宿主机划分pod网段    cID := kubecontainer.BuildContainerID(runtimeName, createResp.ID)    networkOptions := make(map[string]string)    if dnsConfig := config.GetDnsConfig(); dnsConfig != nil {        // Build DNS options.        dnsOption, err := json.Marshal(dnsConfig)        if err != nil {            return nil, fmt.Errorf("failed to marshal dns config for pod %q: %v", config.Metadata.Name, err)        }        networkOptions["dns"] = string(dnsOption)    }    // 这一步调用网络插件来setup sandbox pod    // 由于我们网络插件都是cni(container network interface),所以代码在 pkg/kubelet/dockershim/network/cni/cni.go    err = ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations, networkOptions)    // ...    return resp, nil}
复制代码

由于我们网络插件都是 cni(container network interface),代码 ds.network.SetUpPod 继续追下去发现实际调用的是 cniNetworkPlugin.SetUpPod(),代码在 pkg/kubelet/dockershim/network/cni/cni.go(https://github.com/kubernetes/kubernetes/blob/release-1.17/pkg/kubelet/dockershim/network/cni/cni.go#L300-L321) :

func (plugin *cniNetworkPlugin) SetUpPod(namespace string, name string, id kubecontainer.ContainerID, annotations, options map[string]string) error {    // ...    netnsPath, err := plugin.host.GetNetNS(id.ID)    // ...    // Windows doesn't have loNetwork. It comes only with Linux    if plugin.loNetwork != nil {        // 添加loopback        if _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.loNetwork, name, namespace, id, netnsPath, annotations, options); err != nil {            return err        }    }    // 调用网络插件创建网络相关资源    _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.getDefaultNetwork(), name, namespace, id, netnsPath, annotations, options)    return err}func (plugin *cniNetworkPlugin) addToNetwork(ctx context.Context, network *cniNetwork, podName string, podNamespace string, podSandboxID kubecontainer.ContainerID, podNetnsPath string, annotations, options map[string]string) (cnitypes.Result, error) {    // 这一步准备网络插件所需相关参数,这些参数最后会被calico插件使用    rt, err := plugin.buildCNIRuntimeConf(podName, podNamespace, podSandboxID, podNetnsPath, annotations, options)    // ...    // 这里会调用调用cni标准库里的AddNetworkList函数,最后会调用calico二进制命令    res, err := cniNet.AddNetworkList(ctx, netConf, rt)    // ...    return res, nil}// 这些参数主要包括container id,pod等相关参数func (plugin *cniNetworkPlugin) buildCNIRuntimeConf(podName string, podNs string, podSandboxID kubecontainer.ContainerID, podNetnsPath string, annotations, options map[string]string) (*libcni.RuntimeConf, error) {    rt := &libcni.RuntimeConf{        ContainerID: podSandboxID.ID,        NetNS:       podNetnsPath,        IfName:      network.DefaultInterfaceName,        CacheDir:    plugin.cacheDir,        Args: [][2]string{            {"IgnoreUnknown", "1"},            {"K8S_POD_NAMESPACE", podNs},            {"K8S_POD_NAME", podName},            {"K8S_POD_INFRA_CONTAINER_ID", podSandboxID.ID},        },    }    // port mappings相关参数    // ...       // dns 相关参数    // ...    return rt, nil}  
复制代码

addToNetwork()函数会调用 cni 标准库里的 AddNetworkList(https://github.com/containernetworking/cni/blob/master/libcni/api.go#L400-L440)函数。CNI 是容器网络标准接口 Container Network Interface,这个代码仓库提供了 CNI 标准接口的相关实现,所有 K8s 网络插件都必须实现该 CNI 代码仓库中的接口,K8s 网络插件如何实现规范可见 SPEC.md(https://github.com/containernetworking/cni/blob/master/SPEC.md) ,我们也可实现遵循该标准规范实现一个简单的网络插件。所以 kubelet、cni 和 calico 的三者关系就是:kubelet 调用 cni 标准规范代码包,cni 调用 calico 插件二进制文件。cni 代码包中的 AddNetworkList 相关代码如下 AddNetworkList(https://github.com/containernetworking/cni/blob/master/libcni/api.go#L400-L440)

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)    // ...    // pluginPath就是calico二进制文件路径,这里其实就是调用 calico ADD命令,并传递相关参数,参数也是上文描述的已经准备好了的    // 参数传递也是写入了环境变量,calico二进制文件可以从环境变量里取值    return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)}// AddNetworkList executes a sequence of plugins with the ADD commandfunc (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {    // ...    for _, net := range list.Plugins {        result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)        // ...    }    // ...    return result, nil}
复制代码

以上 pluginPath 就是 calico 二进制文件路径,这里 calico 二进制文件路径参数是在启动 kubelet 时通过参数 --cni-bin-dir 传进来的,可见官网 kubelet command-line-tools-reference(https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/) ,并且启动参数 --cni-conf-dir 包含 cni 配置文件路径,该路径包含 cni 配置文件内容类似如下:

{  "name": "k8s-pod-network",  "cniVersion": "0.3.1",  "plugins": [    {      "type": "calico",      "log_level": "debug",      "log_file_path": "/var/log/calico/cni/cni.log",      "datastore_type": "kubernetes",      "nodename": "minikube",      "mtu": 1440,      "ipam": {          "type": "calico-ipam"      },      "policy": {          "type": "k8s"      },      "kubernetes": {          "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"      }    },    {      "type": "portmap",      "snat": true,      "capabilities": {"portMappings": true}    },    {      "type": "bandwidth",      "capabilities": {"bandwidth": true}    }  ]}
复制代码

cni 相关代码是个标准骨架,核心还是需要调用第三方网络插件来实现为 sandbox 创建网络资源。cni 也提供了一些示例 plugins,代码仓库见 containernetworking/plugins(https://github.com/containernetworking/plugins),并配有文档说明见 plugins docs(https://www.cni.dev/plugins/) ,比如可以参考学习官网提供的 static IP address management plugin(https://www.cni.dev/plugins/ipam/static/) 。

总结

总之,kubelet 在创建 sandbox container 时候,会先调用 cni 插件命令,如 calico ADD 命令并通过环境变量传递相关命令参数,来给 sandbox container 创建 network 相关资源对象,比如 calico 会创建 route 和 virtual interface,以及为 pod 分配 ip 地址,和从集群网段 cluster cidr 中为当前 worker 节点分配 pod cidr 网段,并且会把这些数据写入到 calico datastore 数据库里。所以,关键问题,还是得看 calico 插件代码是如何做的。

参考链接

  • https://docs.projectcalico.org/networking/use-specific-ip

  • https://mp.weixin.qq.com/s/lyfeZh6VWWjXuLY8fl3ciw

  • https://www.yuque.com/baxiaoshi/tyado3/lvfa0b

  • https://github.com/containernetworking/cni

  • https://github.com/projectcalico/cni-plugin

本文转载自:360 技术(ID:qihoo_tech)

原文链接:Kubernetes学习笔记之Calico CNI Plugin源码解析(一)