扩展 Kubernetes 之 CRI
简介
CRI 是什么
- 容器运行时插件(Container Runtime Interface,简称 CRI)是 Kubernetes v1.5 引入的容器运行时接口,它将 Kubelet 与容器运行时解耦,将原来完全面向 Pod 级别的内部接口拆分成面向 Sandbox 和 Container 的 gRPC 接口,并将镜像管理和容器管理分离到不同的服务。
- CRI 主要定义了两个 grpc interface.
RuntimeService
:容器(container) 和 (Pod)Sandbox 运行时管理ImageService
:拉取、查看、和移除镜像
- OCI (开放容器标准): 定义了 ImageSpec(镜像格式, 比如文件夹结构,压缩方式)和 RuntimeSpec(如何运行,比如支持 create, start, stop, delete)
- 代表实现有:runC,Kata(以及它的前身 runV 和 Clear Containers),gVisor
- CRI 区别于 OCI,CRI的定义比较简单直接,只是定义了一套协议(grpc 接口)。
- 代表实现有 kubernetest 内置的 dockershim, CRI-containerd(或者 containerd with CRI plugin), cri-o
CRI 位于什么位置
在 kubernetes 中:
在和OCI,调度层的角度看:
graph LR
OrchestrationAPI --> ContainerAPI-criRuntime
ContainerAPI-criRuntime --> KernelAPI-ociRuntime
CRI/CRI Runtime
CRI Runtime 的执行流程
经典的 kubernetes runtime 执行流程
- 执行流程里面核心组件是 kubelet/KubeGenericRuntimeManager 他调用很多其他组件,比如 cm (ContainerManager/podContainerManager/cgroupManager/cpuManager/deviceManager, pod 级别的资源管理), RuntimeService (grpc 调用 CRI 的客户端) 共同完成 SyncPod 的操作。
经典的 dockershim -> containerd 的流程 (称为 docker cri)
- Kubelet 通过 CRI 接口(gRPC)调用 dockershim,请求创建一个容器。CRI 即容器运行时接口(Container Runtime Interface),这一步中,Kubelet 可以视作一个简单的 CRI Client,而 dockershim 就是接收请求的 Server。目前 dockershim 的代码其实是内嵌在 Kubelet 中的,所以接收调用的凑巧就是 Kubelet 进程
- dockershim 收到请求后,转化成 Docker Daemon 能听懂的请求,发到 Docker Daemon 上请求创建一个容器。
- Docker Daemon 早在 1.12 版本中就已经将针对容器的操作移到另一个守护进程——containerd 中了,因此 Docker Daemon 仍然不能帮我们创建容器,而是要请求 containerd 创建一个容器;
- containerd 收到请求后,并不会自己直接去操作容器,而是创建一个叫做 containerd-shim 的进程,让 containerd-shim 去操作容器。这是因为容器进程需要一个父进程来做诸如收集状态,维持 stdin 等 fd 打开等工作。而假如这个父进程就是 containerd,那每次 containerd 挂掉或升级,整个宿主机上所有的容器都得退出了。而引入了 containerd-shim 就规避了这个问题(containerd 和 shim 并不是父子进程关系);
- 我们知道创建容器需要做一些设置 namespaces 和 cgroups,挂载 root filesystem 等等操作,而这些事该怎么做已经有了公开的规范了,那就是 OCI(Open Container Initiative,开放容器标准)。它的一个参考实现叫做 runC。于是,containerd-shim 在这一步需要调用 runC 这个命令行工具,来启动容器;
- runC 启动完容器后本身会直接退出,containerd-shim 则会成为容器进程的父进程,负责收集容器进程的状态,上报给 containerd,并在容器中 pid 为 1 的进程退出后接管容器中的子进程进行清理,确保不会出现僵尸进程。
直接对接 cri-containerd/cri-o 的运行时
使用 cri-containerd 的调用流程更为简洁, 省去了上面的调用流程的 1,2 两步
常见 CRI runtime 实现
Cri-containerd
执行流程为:
- Kubelet 通过 CRI runtime service API 调用 cri plugin 创建 pod
- cri 通过 CNI 创建 pod 的网络配置和 namespace
- cri 使用 containerd 创建并启动 pause container (sandbox container) 并且把这个 container 置于 pod 的 cgroups/namespace
- Kubelet 接着通过 CRI image service API 调用 cri plugin, 获取容器镜像
- cri 通过 containerd 获取容器镜像
- Kubelet 通过 CRI runtime service API 调用 cri, 在 pod 的空间使用拉取的镜像启动容器
- cri 通过 containerd 创建/启动 应用容器, 并且把 container 置于 pod 的 cgroups/namespace. Pod 完成启动.
实践
准备集群
使用 terraform 在腾讯云上创建 tke 测试集群
# Configure the TencentCloud Provider
provider "tencentcloud" {
secret_id = var.secret_id
secret_key = var.secret_key
region = var.region
}
# test cluster
resource "tencentcloud_kubernetes_cluster" "managed_cluster" {
vpc_id = var.vpc
cluster_cidr = "10.4.0.0/16"
cluster_max_pod_num = 32
cluster_name = "test"
cluster_desc = "test cluster desc"
cluster_max_service_num = 32
container_runtime = "containerd"
cluster_version = "1.14.3"
worker_config {
count = 2
availability_zone = var.availability_zone
instance_type = var.default_instance_type
system_disk_size = 50
security_group_ids = [var.sg]
internet_charge_type = "TRAFFIC_POSTPAID_BY_HOUR"
internet_max_bandwidth_out = 100
public_ip_assigned = true
subnet_id = var.subnet
enhanced_security_service = false
enhanced_monitor_service = false
key_ids = [var.key_id]
}
cluster_deploy_type = "MANAGED_CLUSTER"
}
用 bash 实现一个 CRI runtime
CRI runtime 的实现需要实现大量 API,这里我们做一个简单的 shell 脚本,将请求转发给 runc,同时打印出调用的参数。把这个脚本命名为 runb
$ cat /usr/local/bin/runb
#!/bin/bash -e
echo "["`date --iso-8601=seconds`"] call runb: $@" >> /var/log/runb.log
exec runc $@
使用 ctr/crictl 测试
ctr 是调用containerd 的命令行工具,而 crictl 是调用 cri 相关 api 的工具,一个测试的例子 参考这里
配置 containerd, 使用 k8s yaml 测试
在 /etc/containerd/config.toml 下面添加如下的配置,配置 runtime 为 runb,二进制程序为 runb. containerd 的配置因版本变化有所不同,具体可以参考这里。重启 containerd
[plugins.cri.containerd.runtimes.runb]
runtime_type = "io.containerd.runc.v1"
[plugins.cri.containerd.runtimes.runb.options]
NoPivotRoot = false
NoNewKeyring = false
ShimCgroup = ""
IoUid = 0
IoGid = 0
BinaryName = "runb"
Root = ""
CriuPath = ""
SystemdCgroup = false
用 kubectl 把名为 runb 的 runtimeclass 创建出来
apiVersion: node.k8s.io/v1beta1 # RuntimeClass is defined in the node.k8s.io API group
kind: RuntimeClass
metadata:
name: runb # The name the RuntimeClass will be referenced by
# RuntimeClass is a non-namespaced resource
handler: runb # The name of the corresponding CRI configuration
创建一个使用 runb 为runtime 的pod
root@VM-8-12-ubuntu:~# cat nginx.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
runtimeClassName: runb
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
name: http
观察创建结果和 runb 的输出日志
root@VM-8-12-ubuntu:~# crictl ps
CONTAINER ID IMAGE CREATED STATE NAME ATTEMPT POD ID
d0e454f31ad06 2073e0bcb60ee 11 minutes ago Running nginx 0 f640723d57cc2
root@VM-8-12-ubuntu:~# head /var/log/runb.log
starting with runb -namespace default -address /run/containerd/containerd.sock -publish-binary /usr/local/bin/containerd -id hello -debug start
starting with runb: --root /run/containerd/runc/k8s.io --log /run/containerd/io.containerd.runtime.v2.task/k8s.io/5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac/log.json --log-format json create --bundle /run/containerd/io.containerd.runtime.v2.task/k8s.io/5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac --pid-file /run/containerd/io.containerd.runtime.v2.task/k8s.io/5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac/init.pid 5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac
starting with runb: --root /run/containerd/runc/k8s.io --log /run/containerd/io.containerd.runtime.v2.task/k8s.io/5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac/log.json --log-format json state 5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac
starting with runb: --root /run/containerd/runc/k8s.io --log /run/containerd/io.containerd.runtime.v2.task/k8s.io/5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac/log.json --log-format json start 5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac
starting with runb: --root /run/containerd/runc/k8s.io --log /run/containerd/io.containerd.runtime.v2.task/k8s.io/5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac/log.json --log-format json state 5dd814edd58826c3ebcfa87ae96bb972b1541d6007f7f7ed5c01a4da639b0fac