containerd系统分析(六)-CRI接口

containerd系列文章:

containerd系统分析(一)-系统组成-CSDN博客

containerd系统分析(二)-镜像管理-CSDN博客

containerd系统分析(三)-容器创建流程分析-CSDN博客

containerd系统分析(四)-容器启动流程分析-CSDN博客

containerd系统分析(五)-网络分析-CSDN博客

containerd系统分析(六)-CRI接口-CSDN博客

containerd实现了CRI接口,上层应用通过CRI接口调用containerd的容器服务。

1 CRI创建sandbox分析

cri创建sandbox总结如下:containerd通过内置CRI插件,实现了gprc的接口/runtime.v1.RuntimeService/RunPodSandbox。

这个接口实现了sandbox容器的创建和启动。

RunPodSandbox负责sandbox创建,最终是通过类似于ctr调用containerd的container接口进行容器创建,通过task接口创建任务启动、启动任务,创建网络等操作,在一系列操作完成后,sandbox就可以用了。

具体流程如下:

  1. 先生成唯一的sandbox的容器id
  2. 拼接唯一的名称作为sandbox的名称podname_podnamespace_poduid_attemp num
  3. 看看这个id或sandbox的名称是不是存在的,若存在就返回错误,报告已存在
  4. 构建sandbox实体
    //构建sandbox内存实体
    sandbox := sandboxstore.NewSandbox(
       sandboxstore.Metadata{
          ID:             id,
          Name:           name,
          Config:         config,
          //这个runtimeHandler是由k8s创建pod时用户指定的runtimeclass,也有可能是空的
          RuntimeHandler: r.GetRuntimeHandler(),
       },
       sandboxstore.Status{
          State: sandboxstore.StateUnknown,
       },
    )

  5. 检查镜像是不是存在,若不存在就去下载镜像,确保这个镜像是存在的,如果镜像不存在,那就去下载镜像.这个sandboximage是由containerd自己配置的,所以在kubelet1.24版本中,已经不需要配置infra的镜像了
    image, err := c.ensureImageExists(ctx, c.config.SandboxImage, config)

  6. 获得镜像的描述信息
    sandbox.NetNS, err = netns.NewNetNS(netnsMountDir)

  7. 获得runtime config的描述信息.这里的runtimeHandler,实际指的是用什么runtime,默认的为runc
  8. 获得到所要用的runtime的runtime的配置信息
  9. 看看pod所需要的网络模式是否是主机模式,如果是主机模式,就不要创建网络了,否则标识需要创建网络
  10. 若是标识了需要创建网络,那么要设置的nsMountDir的地址,默认地址为:/var/run/netns,如果是设置绑定到State目录下,那么地址就是/run/containerd/netns
    sandbox.NetNS, err = netns.NewNetNS(netnsMountDir)

  11. 创建一个新的net namespace,指向的地址是nsPath={nsMountDir}/cni-{rand}
  12. 安装sandbox的net,安装过程和第5节一样,这里的path是上述的nsPath。在NewNetNS时,会将/proc/{pid}/ns/net挂载到nsPath,实际上nsPath就是/proc/{pid}/ns/net
  13. 根据传入的podsandbox的参数和rumtime的config、镜像信息、nsPath生成container spec
  14. 当rumtime是kata时,要设置pec.Process.SelinuxLabel为kvm的
  15. 后面创建sandbox容器的流程和上面的容器的创建的流程基本一致,有所区别的是上述的nsPath会添加到namespace中,类型为network
    specOpts = append(specOpts, oci.WithLinuxNamespace(
       runtimespec.LinuxNamespace{
          Type: runtimespec.NetworkNamespace,
          Path: nsPath,
       }))
    opts := []containerd.NewContainerOpts{
       //指定snapshotter
       containerd.WithSnapshotter(c.runtimeSnapshotter(ctx, ociRuntime)),
       //创建一个新的snapshot
       customopts.WithNewSnapshot(id, containerdImage, snapshotterOpt),
       //设定container.spec
       containerd.WithSpec(spec, specOpts...),
       //设定container.label
       containerd.WithContainerLabels(sandboxLabels),
       containerd.WithContainerExtension(sandboxMetadataExtension, &sandbox.Metadata),
       //设定运行时的信息
       containerd.WithRuntime(ociRuntime.Type, runtimeOpts)}

  16. 获得sandbox rootdir,路径是/var/lib/containerd/io.containerd.grpc.v1.cri/{id},并创建这个路径
    //root dir在/var/lib/containerd/io.containerd.grpc.v1.cri/{id}下
    sandboxRootDir := c.getSandboxRootDir(id)

  17. 获得sandbox的statedir,路径是/run/containerd/io.containerd.grpc.v1.cri/sandboxes/{id},并创建这个路径
    //stateDir, /run/containerd/io.containerd.grpc.v1.cri/sandboxes/{id}
    //config.StateDir的生成规则和rootDir的一样的,都是在插件注册的时候生成,由{stateRoot}/{plugintype=io.containerd.grpc.v1}.{ID=cri}
    //stateDir放的是运行时的信息
    volatileSandboxRootDir := c.getVolatileSandboxRootDir(id)

  18. 在sandbox root目录下写hosts, resolv.conf, shm,以及hostname等文件,这些文件的内容和挂载到sandbox容器的内容是完全是一至和的,后面创建普通容器的时候,会从sandbox root下直接mount到相应的路径
  19. 准备启动容器了
  20. 在启动容器之前,根据runtime的类型,设置容器运行时所需要的参数
    taskOpts := c.taskOpts(ociRuntime.Type)

  21. 执行启动容器的任务,这里标识了,不需要IO:
    task, err := container.NewTask(ctx, containerdio.NullIO, taskOpts...)

  22. 在任务创建成功之后,创建nri接口,去invoke相应的配置文件中的插件,对task进行处理
    nric, err := nri.New()
    if err != nil {
       return nil, fmt.Errorf("unable to create nri client: %w", err)
    }
    if nric != nil {
       nriSB := &nri.Sandbox{
          ID:     id,
          Labels: config.Labels,
       }
    //处理处于创建状态的sandbox task
       if _, err := nric.InvokeWithSandbox(ctx, task, v1.Create, nriSB); err != nil {
          return nil, fmt.Errorf("nri invoke: %w", err)
       }
    }

  23. 启动task,在task启动完成后,标识状态为ready

2 CRI创建普通容器分析

cri实现了/runtime.v1.RuntimeService/CreateContainer接口

这里实现创建容器,与普通创建容器不同的,是这里创建容器在设置spec时,要共享sandbox的网络配置、host信息。

流程如下:

  1. 获得sandbox的id
  2. 从请求中获得sandbox的配置信息
  3. 获得sandbox的task信息,从中获得sandbox的pid信息
  4. 生成容器的id
  5. 从容器中的取出名称,构建containerd存储的容器的名称:name_pod name _ namespace _ uid _ attemp time
  6. 检验容器的名称和id是否重了
  7. 检查镜像是否存在,镜像须提前下载好了
  8. 获得sandbox的容器信息
  9. 获得容器的root路径,/var/lib/containerd/io.containerd.grpc.v1.cri/containers/{id}
  10. 获得容器的state路径,/run/containerd/io.containerd.grpc.v1.cri/containers/{id}
  11. 从sandbox生成容器要mounts信息:hosts, dev,hostname,resolve.conf,/dev/shim
  12. 获得ociruntime信息
  13. 生成容器的spec信息,这里要特别的注意,ipc,net,uts,pid的namespace和pod的用同一个
  14. 设定用户名,以及seccomp, cdi的,以及apparmor等信息
  15. 根据上述信息去 创建容器

3 CRI启动容器分析

cri实现/runtime.v1.RuntimeService/StartContainer,执行动作是调用了containerd的newTask和startTask

详细分析如下:

  1. 根据容器的id,获得容器的信息,这里的是cri自己存的
  2. 从containerd获得的容器的描述信息
  3. 更下cri自己存储的容器的信息为启动状态
  4. 获得sandbox的信息,检查下状态是否为startReady,如果不是这个状态,那就退出了
  5. 创建ioCreation,这里作用和上面ctr创建的ioCreator的作用是一样的
  6. 获得runtime的配置描述信息,这里和runsandbox的处理是一样的
  7. 然后执行创建任务newTask,然后startTask,这里的流程没有特别的了这里执行的代码和上面分析ctr的createTask和startTask没有区别
  8. 有一点不同的是,在newTask之后,要执行nri,这个nri的执行在runSandbox已分析过
  9. 最后更新task的状态,并存储

containerd在创建任务时,与普通的容器创建任务不同的是,在创建shim时,要用前面sandbox的shim socket去创建容器,其它的都相同,也就是得复用了之前sandbox的shim,毕竟那个shim已经运行在那了,直接用就行了。构建创建容器的报文,指定好bundle和相关的参数。

// This container belongs to sandbox which supposed to be already started via sandbox API.
if opts.SandboxID != "" {
   process, err := m.Get(ctx, opts.SandboxID)
   if err != nil {
      return nil, fmt.Errorf("can't find sandbox %s", opts.SandboxID)
   }

   // Write sandbox ID this task belongs to.
   if err := os.WriteFile(filepath.Join(bundle.Path, "sandbox"), []byte(opts.SandboxID), 0600); err != nil {
      return nil, err
   }

   address, err := shimbinary.ReadAddress(filepath.Join(m.state, process.Namespace(), opts.SandboxID, "address"))
   if err != nil {
      return nil, fmt.Errorf("failed to get socket address for sandbox %q: %w", opts.SandboxID, err)
   }

   // Use sandbox's socket address to handle task requests for this container.
   if err := shimbinary.WriteAddress(filepath.Join(bundle.Path, "address"), address); err != nil {
      return nil, err
   }

   shim, err := loadShim(ctx, bundle, func() {})
   if err != nil {
      return nil, fmt.Errorf("failed to load sandbox task %q: %w", opts.SandboxID, err)
   }

   if err := m.shims.Add(ctx, shim); err != nil {
      return nil, err
   }

   return shim, nil
}

4 CRI镜像下载及删除服务

containerd实现的cri插件实现了接口/runtime.v1.ImageService/PullImage进行地镜像的下载

/runtime.v1.ImageService/RemoveImage进行地镜像的下载

具体的实现方式与ctr image pull /ctr image rm

镜像操作是一样的

5 总结

containerd的CRI接口实现的内容大体上和ctr实现的内容差不多,但在普通容器的启动与ctr的实现有所区别。containerd遵循CRI实现时,实际上是按pod的维度进行实现,每个pod有一个infra容器,也就是sandbox,在启动普通容器时,实际上是每个pod有一个shim,这一点和ctr创建的容器有明显的区别。

你可能感兴趣的:(容器,技术分析,kubernetes,容器,云原生,linux)