Kubernetes权威指南:从Docker到Kubernetes实践全接触(第4版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.9 CRI(容器运行时接口)详解

归根结底,Kubernetes Node(kubelet)的主要功能就是启动和停止容器的组件,我们称之为容器运行时(Container Runtime),其中最知名的就是Docker了。为了更具扩展性,Kubernetes从1.5版本开始就加入了容器运行时插件API,即Container Runtime Interface,简称CRI。

2.9.1 CRI概述

每个容器运行时都有特点,因此不少用户希望Kubernetes能够支持更多的容器运行时。Kubernetes从1.5版本开始引入了CRI接口规范,通过插件接口模式,Kubernetes无须重新编译就可以使用更多的容器运行时。CRI包含Protocol Buffers、gRPC API、运行库支持及开发中的标准规范和工具。Docker的CRI实现在Kubernetes 1.6中被更新为Beta版本,并在kubelet启动时默认启动。

可替代的容器运行时支持是Kubernetes中的新概念。在Kubernetes 1.3发布时,rktnetes项目同时发布,让rkt容器引擎成为除Docker外的又一选择。然而,不管是Docker还是rkt,都用到了kubelet的内部接口,同kubelet源码纠缠不清。这种程度的集成需要对kubelet的内部机制有非常深入的了解,还会给社区带来管理压力,这就给新生代容器运行时造成了难于跨越的集成壁垒。CRI接口规范试图用定义清晰的抽象层清除这一壁垒,让开发者能够专注于容器运行时本身。在通向插件式容器支持及建设健康生态环境的路上,这是一小步,也是很重要的一步。

2.9.2 CRI的主要组件

kubelet使用gRPC框架通过UNIX Socket与容器运行时(或CRI代理)进行通信。在这个过程中kubelet是客户端,CRI代理(shim)是服务端,如图2.3所示。

图2.3 CRI的主要组件

Protocol Buffers API包含两个gRPC服务:ImageService和RuntimeService。

ImageService提供了从仓库拉取镜像、查看和移除镜像的功能。

RuntimeService负责Pod和容器的生命周期管理,以及与容器的交互(exec/attach/port-forward)。rkt和Docker这样的容器运行时可以使用一个Socket同时提供两个服务,在kubelet中可以用--container-runtime-endpoint和--image-service-endpoint参数设置这个Socket。

2.9.3 Pod和容器的生命周期管理

Pod由一组应用容器组成,其中包含共有的环境和资源约束。在CRI里,这个环境被称为PodSandbox。Kubernetes有意为容器运行时留下一些发挥空间,它们可以根据自己的内部实现来解释PodSandbox。对于Hypervisor类的运行时,PodSandbox会具体化为一个虚拟机。其他例如Docker,会是一个Linux命名空间。在v1alpha1 API中,kubelet会创建Pod级别的cgroup传递给容器运行时,并以此运行所有进程来满足PodSandbox对Pod的资源保障。

在启动Pod之前,kubelet调用RuntimeService.RunPodSandbox来创建环境。这一过程包括为Pod设置网络资源(分配IP等操作)。PodSandbox被激活之后,就可以独立地创建、启动、停止和删除不同的容器了。kubelet会在停止和删除PodSandbox之前首先停止和删除其中的容器。

kubelet的职责在于通过RPC管理容器的生命周期,实现容器生命周期的钩子,存活和健康监测,以及执行Pod的重启策略等。

RuntimeService服务包括对Sandbox和Container操作的方法,下面的伪代码展示了主要的RPC方法:

        service RuntimeService {
            // 沙箱操作
            rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
            rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
            rpc RemovePodSandbox(RemovePodSandboxRequest) returns
    (RemovePodSandboxResponse) {}
            rpc PodSandboxStatus(PodSandboxStatusRequest) returns
    (PodSandboxStatusResponse) {}
            rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
            // 容器操作
            rpc CreateContainer(CreateContainerRequest) returns
    (CreateContainerResponse) {}
            rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
            rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
            rpc RemoveContainer(RemoveContainerRequest) returns
    (RemoveContainerResponse) {}
            rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
            rpc ContainerStatus(ContainerStatusRequest) returns
    (ContainerStatusResponse) {}
            ......
        }

2.9.4 面向容器级别的设计思路

众所周知,Kubernetes的最小调度单元是Pod,它曾经可能采用的一个CRI设计就是复用Pod对象,使得容器运行时可以自行实现控制逻辑和状态转换,这样一来,就能极大地简化API,让CRI能够更广泛地适用于多种容器运行时。但是经过深入讨论之后,Kubernetes放弃了这一想法。

首先,kubelet有很多Pod级别的功能和机制(例如crash-loop backoff机制),如果交给容器运行时去实现,则会造成很重的负担;其次且更重要的是,Pod标准还在快速演进中。很多新功能(如初始化容器)都是由kubelet完成管理的,无须交给容器运行时实现。

CRI选择了在容器级别进行实现,使得容器运行时能够共享这些通用特性,以获得更快的开发速度。这并不意味着设计哲学的改变——kubelet要负责、保证容器应用的实际状态和声明状态的一致性。

Kubernetes为用户提供了与Pod及其中的容器进行交互的功能(kubectl exec/attach/port- forward)。kubelet目前提供了两种方式来支持这些功能。

(1)调用容器的本地方法。

(2)使用Node上的工具(例如nsenter及socat)。

因为多数工具都假设Pod用Linux namespace做了隔离,因此使用Node上的工具并不是一种容易移植的方案。在CRI中显式定义了这些调用方法,让容器运行时进行具体实现。下面的伪代码显示了Exec、Attach、PortForward这几个调用需要实现的RuntimeService方法:

        service RuntimeService {
            ......
            // ExecSync在容器中同步执行一个命令。
            rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
            // Exec在容器中执行命令
            rpc Exec(ExecRequest) returns (ExecResponse) {}
            // Attach附着在容器上
            rpc Attach(AttachRequest) returns (AttachResponse) {}
            // PortForward从Pod沙箱中进行端口转发
            rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}
            ......
        }

目前还有一个潜在问题是,kubelet处理所有的请求连接,使其有成为Node通信瓶颈的可能。在设计CRI时,要让容器运行时能够跳过中间过程。容器运行时可以启动一个单独的流式服务来处理请求(还能对Pod的资源使用情况进行记录),并将服务地址返回给kubelet。这样kubelet就能反馈信息给API Server,使之可以直接连接到容器运行时提供的服务,并连接到客户端。

2.9.5 尝试使用新的Docker-CRI来创建容器

要尝试新的Kubelet-CRI-Docker集成,只需为kubelet启动参数加上--enable-cri=true开关来启动CRI。这个选项从Kubernetes 1.6开始已经作为kubelet的默认选项了。如果不希望使用CRI,则可以设置--enable-cri=false来关闭这个功能。

查看kubelet的日志,可以看到启用CRI和创建gRPC Server的日志:

        I0603 15:08:28.953332    3442 container_manager_linux.go:250] Creating
    Container Manager object based on Node Config: {RuntimeCgroupsName: SystemCgroupsName:
    KubeletCgroupsName: ContainerRuntime:docker CgroupsPerQOS:true CgroupRoot:/
    CgroupDriver:cgroupfs ProtectKernelDefaults:false EnableCRI:true
    NodeAllocatableConfig:{KubeReservedCgroupName: SystemReservedCgroupName:
    EnforceNodeAllocatable:map[pods:{}] KubeReserved:map[] SystemReserved:map[]
    HardEvictionThresholds:[{Signal:memory.available Operator:LessThan
    Value:{Quantity:100Mi Percentage:0} GracePeriod:0s MinReclaim:<nil>}]}
    ExperimentalQOSReserved:map[]}
        ......
        I0603 15:08:29.060283    3442 kubelet.go:573] Starting the GRPC server for the
    docker CRI shim.

创建一个Deployment:

        $ kubectl run nginx --image=nginx
        deployment "nginx " created

查看Pod的详细信息,可以看到将会创建沙箱(Sandbox)的Event:

        $ kubectl describe pod nginx
        ......
        Events:
        ...From                   Type    Reason               Message
        ...-----------------   -----   ---------------
    -----------------------------
          ...default-scheduler    Normal  Scheduled          Successfully assigned nginx
      to k8s-node-1
          ...kubelet, k8s-node-1  Normal  SandboxReceived   Pod sandbox received, it will
      be created.
          ......

这表明kubelet使用了CRI接口来创建容器。

2.9.6 CRI的进展

目前已经有多款开源CRI项目可用于Kubernetes:Docker、CRI-O、Containerd、frakti(基于Hypervisor的容器运行时),各CRI运行时的安装手册可参考官网https://kubernetes.io/docs/setup/cri/的说明。