容器运行时

2022-11-28 17:28:54 浏览数 (1)

要把进程运行在容器中,还需要有便捷的SDK或命令来调用Linux的系统功能,从而创建出容器。容器的运行时(runtime)就是运行和管理容器进程、镜像的工具。

作者:赵慧慧, 中国移动云能力中心软件开发工程师,专注于云原生、Istio、微服务等领域。

01

容器运行时分类

Docker属于容器技术早期的发展项目,也是目前最广泛的容器引擎技术。当然,随着容器生态圈的日益繁荣,业界慢慢也出现了其他各种运行时工具,如containerd、rkt、Kata Container、CRI-O等。这些工具提供的功能不尽相同,有些只有容器运行的功能,有些除运行容器外还提供了容器镜像的管理功能。根据容器运行时提供功能,可以讲容器运行时分为低层运行时高层运行时

低层运行时主要负责与宿主机操作系统打交道,根据指定的容器镜像在宿主机上运行容器的进程,并对容器的整个生命周期进行管理。而这个低层运行时,正是负责执行我们前面讲解过的设置容器 Namespace、Cgroups等基础操作的组件。常见的低层运行时种类有:

Ø runc:传统的运行时,基于Linux Namespace和Cgroups技术实现,代表实现Docker

Ø runv:基于虚拟机管理程序的运行时,通过虚拟化 guest kernel,将容器和主机隔离开来,使得其边界更加清晰,代表实现是Kata Container和Firecracker

Ø runsc:runc safety ,通过拦截应用程序的所有系统调用,提供安全隔离的轻量级容器运行时沙箱,代表实现是谷歌的gVisor

高层运行时主要负责镜像的管理、转化等工作,为容器的运行做前提准备。主流的高层运行时主要containerd和CRI-O。

高层运行时与低层运行时各司其职,容器运行时一般先由高层运行时将容器镜像下载下来,并解压转换为容器运行需要的操作系统文件,再由低层运行时启动和管理容器。

两者之间的关系如下:

02

Kubernetes容器运行时

前面的两部分,我们介绍了容器运行的原理及常见的容器运行时工具,Kubernetes作为容器编排工具会对容器进行调度和管理。那Kubernetes如何启动容器的?

Kubernetes早期是利用Docker作为容器运行时管理工具的,在1.6版本之前Kubernetes将Docker默认为自己的运行时工具,通过直接调用Docker的API来创建和管理容器。在Docker项目盛行不久,CoreOS推出了rkt运行时工具,Kubernetes又添加了对rkt的支持。但随着容器技术的蓬勃发展,越来越多的运行时工具出现,提供对所有运行时工具的支持,显然是一项庞大的工程;而且直接将运行时的集成内置于Kubernetes,两者紧密结合,对Kubernetes代码本身也是一种负担,每更新一次重要的功能,Kubernetes都需要考虑对所有容器运行时的兼容适配。

为了打破这种尴尬的局面,Kubernetes将对容器的操作抽象为一个接口,将接口作为kubelet与运行时工具之间的桥梁,kubelet通过发送接口请求对容器进行启动和管理,各个容器工具通过实现这个接口即可接入Kubernetes。这个统一的容器操作接口,就是容器运行时接口(Container Runtime Interface, CRI)。

我们来具体看下CRI的工作流程,上图可以看到,CRI主要有gRPC client、gRPC Server和具体的容器运行时工具三个组件。其中kubelet作为gRPC 的客户端来调用 CRI 接口;CRI shim作为gRPC服务端来响应CRI请求,负责将CRI请求的内容转换为具体的容器运行时API,在kubelet和运行时之间充当翻译的角色。具体的容器创建逻辑是,Kubernetes在通过调度指定一个具体的节点运行pod,该节点的Kubelet在接到pod创建请求后,调用一个叫作 GenericRuntime 的通用组件来发起创建 Pod 的 CRI 请求给CRI shim;CRI shim监听一个端口来响应kubelet, 在收到CRI请求后,将其转化为具体的容器运行时指令,并调用相应的容器运行时来创建pod。

因此,任何容器运行时如果想接入Kubernetes,都需要实现一个自己的CRI shim,来实现CRI接口规范。那么CRI有哪些接口需要实现呢?查看Kubernetes代码可以发现,它定义了下图所示两类接口:RuntimeService和ImageService。RuntimeService定义了跟容器相关的操作,如创建、启动、删除容器等。ImageService主要定义了容器镜像相关的操作,如拉取镜像、删除镜像等。

ImageService的操作比较简单,就是拉取、删除、查看镜像状态及获取镜像列表这几个操作。下面着重介绍下RuntimeService。

从上图可以看到,RuntimeService除了有container的管理接口外,还包含PodSandbox相关的管理接口和exec、attach等与容器交互的接口。

顾名思义,PodSandbox这个概念对应的是Kubernetes里的Pod,它描述了Kubernetes里的Pod与容器运行相关的属性或者信息,如HostName、CgroupParent等。设计这个的初衷是因为Pod里所有容器的资源和环境信息是共享的,但是不同的容器运行时实现共享的机制不同,如Docker中Pod会是一个Linux命名空间,各容器网络信息的共享通过创建pause 容器的方法来实现,而Kata Containers则直接将pod具化为一个轻量级的虚拟机。将这个逻辑抽象为PodSandbox接口,可以让不同的容器运行时在pod实现上自由发挥,自己解释和实现pod的的逻辑。

Exec、Attach 和 PortForward 是三个和容器进行数据交互的接口,由于交互数据需要长链接来传输,称这些接口为 Streaming API。CRI shim依赖一套独立的Streaming Server机制来实现客户端与容器的交互需求。长连接比较消耗网络资源,为了避免因长连接给kubelet节点带来网络流量瓶颈,CRI要求容器运行时启动一个对应请求的单独的流服务器,让客户端直接与流服务器进行连同交互。

上图所示,kubectl exec命令实现过程如下:

1. 客户端发送 kubectl exec命令给apiserver;

2. apiserver 调用 kubelet 的 Exec API;

3. kubelet 调用CRI 的Exec接口(具体的执行者为实现该接口的 CRI Shim );

4. CRI Shim 向 kubelet 返回Streaming Server 的地址和端口;

5. kubelet 以 redirect 的方式返回给apiserver

6. apiserver 通过重定向来向 Streaming Server 发起真正的 /exec 请求,与它建立长连接,完成 Exec 的请求和响应。

以上是CRI的设计及工作原理。

概括来讲,kubelet在引入CRI之后,主要的架构如下图所示。其中Generic Runtime Manager负责发送容器创建、删除等CRI请求,Container Runtime Interface(CRI)负责定义CRI接口规范,具体的CRI实现可分为两种:kubelet内置的dockershim和远端的CRI shim。其中dockershim是Kubernetes自己实现的适配Docker接口的CRI接口实现,主要用来将CRI 请求里的内容组装成 Docker API 请求发给 Docker Daemon;远端的CRI shim主要是用来匹配其他的容器运行时工具到kubelet。CRI shim主要负责响应kubelect发送的CRI请求,并将请求转化为具体的运行时命令发送给具体的运行时(如runc、kata等);Stream Server用来响应客户端与容器的交互,除此之外,CRI还提供接入CNI的能力以实现pod网络的共享。常用的远端CRI的实现有CRI-Containerd、CRI-O等。

从上图可以看出,Kubernetes把docker shim内置在了官方的代码库中,将Docker设计为Kubernetes默认的容器运行时工具。但是官方在Kubernetes 1.20版本的更新日志中声明已经废用对Docker的支持,并将在未来的版本中将其删除。在Kubernetes 1.24版本中,dockershim代码也如期被删除,替换为containerd作为其默认运行时。

那Kubernetes为何要抛弃Docker转而使用containerd,其中的缘由是什么?

这话要从头说起,Docker最初是一个单体引擎,主要负责容器镜像的制作、上传、拉取及容器的运行及管理。随着容器技术的繁荣发展,为了促进容器技术相关的规范生成和Docker自身项目的发展,Docker将单体引擎拆分为三部分,分别为runC、containerd和dockerd,其中runC主要负责容器的运行和生命周期的管理(即前述的低层运行时)、containerd主要负责容器镜像的下载和解压等镜像管理功能(即前述的高层运行时)、dockerd主要负责提供镜像制作、上传等功能同时提供容器存储和网络的映射功能,同时也是Docker服务器端的守护进程,用来响应Docker客户端(命令行CLI工具)发来的各种容器、镜像管理的任务。Docker公司将runC捐献给了OCI,将containerd捐献给了CNCF,剩下的dockerd作为Docker运行时由Docker公司自己维护。

如前所述,Kubernetes在引入CRI之后,kubelet需要通过CRI shim去调用具体的容器运行时工具,由于早期Kubernetes对Docker的支持是内置的,因此官方自己实现了dockershim,通过dockershim去访问dockerd。

由于dockershim的维护出现了问题,官方废弃了对Docker的支持,使用containerd为默认运行时。那我们知道,kubelet需要一个CRI shim作为中间件去调用运行时,那kubelet在抛弃了dockershim之后又是怎么访问containerd的呢?答案是containerd自己集成了CRI shim,提供了一个CRI插件来实现shim的功能,这样kubelet就可以直接访问containerd。

由此可以看到,废弃dockershim之前kubelet其实也是使用containerd作为高层运行时,只是中间通过了dockershim和dockerd两步转发;在将dockershim移除之后,kubelet越过docker门户直接访问了containerd,这明显的轻量化了调用过程,大大加快了kubelet调用运行时的速度。

03

安全容器运行时

Kubernetes目前作为企业级容器平台,企业生产最重要的是安全。前面我们讲过,Docker 容器通过Linux Namespace和Cgroups实现容器之间的资源限制和隔离,在实际运行中各容器资源(网络、存储、计算)仍由宿主机直接提供,这就可能出现某个容器进程夺取整个宿主机控制权的问题,在安全问题上存在一定的隐患。于是,Kata Container和gVisor等安全容器运行时应用而生。

Kata Container 来源于 Intel Clear Containers 和 Hyper runV 项目的合并,它使用传统的虚拟化技术,通过虚拟硬件模拟出了一台“小虚拟机”,然后再这台小虚拟机中安装了一个裁剪后的Linux内核来实现容器建的隔离。gVisor由谷歌公司发布,它通过为容器进程配置一个用Go语言实现的、在用户态实现的、极小的“独立内核”,通过这个内核控制容器进程向宿主机发起有限可控的系统调用。

那Kubernetes如何集成这些安全运行时呢?下面以Kata Container为例,介绍安全容器运行时如何集成到Kubernetes中对各种资源进行管控。

Kata Container支持OCI运行时规范,可以以插件形式嵌入到 Docker 中作为低层的容器运行时;也支持 Kubernetes 的 CRI 接口,可以与 CRI-O 和 containerd 集成。由于目前Kubernetes默认的运行时是containerd,下面主要讲解Kata Container如何与containerd集成以及集成后Kubernetes如何使用Kata Container创建负载。

由于Kata Container使用虚拟化技术实现,首先需要集成的Kubernetes环境支持Intel VT-x technology、ARM Hyp mode、IBM Power Systems或IBM Z manframes四种中的任意一种CPU虚拟化技术

集成及使用过程如下:

1、安装Kata Container(以centos为例)

代码语言:javascript复制

source /etc/os-release
sudo yum -y install yum-utils
export BRANCH='stable-1.10'
ARCH=$(arch)
BRANCH="${BRANCH:-master}"
sudo -E yum-config-manager --add-repo "http://download.opensuse.org/repositories/home:/katacontainers:/releases:/${ARCH}:/${BRANCH}/CentOS_${VERSION_ID}/home:katacontainers:releases:${ARCH}:${BRANCH}.repo"
sudo -E yum -y install kata-runtime kata-proxy kata-shim

安装完成之后,执行命令 kata-runtime kata-check 检查系统是否支持运行 kata runtime,下面的输出表示运行环境支持 Kata Containers 。

代码语言:javascript复制
1. [root@node1 ~]# kata-runtime kata-check
2.System is capable of running Kata Containers
3.System can currently create Kata Containers

2、修改Kubernetes 启动参数

1)修改 kubelet 启动参数

代码语言:javascript复制
1. mkdir -p  /etc/systemd/system/kubelet.service.d/
2.cat << EOF | sudo tee  /etc/systemd/system/kubelet.service.d/0-containerd.conf
3.[Service]                                                 
4.Environment="KUBELET_EXTRA_ARGS=--container-runtime=remote --runtime-request-timeout=15m --container-runtime-endpoint=unix:///run/containerd/containerd.sock"
5.EOF

2)重启 containerd 和 kubelet

代码语言:javascript复制
1. systemctl daemon-reload
2.systemctl start containerd
3.systemctl restart kubelet

3、Kubernetes 配置使用Kata Container

配置了Kata Container之后,我们就可以在Kubernetes集群中使用Kata Container进行容器管理了。如上所述,目前containerd中存在两个低层运行时,分别是默认的runC和新接入的Kata Container。那我们该如何告诉 Kubernetes 哪些负载需要使用 Kata Container呢?根据不同的版本,Kata 提供了不同的方式:

Ø 使用 Kubernetes 的 RuntimeClass(推荐)

Ø 使用 Kubernetes的untrusted_workload_runtime

1)使用RuntimClass

这种方式对相关组件版本有要求:

代码语言:javascript复制
Kata Containers v1.5.0 or above (including 1.5.0-rc)
Containerd v1.2.0 or above
Kubernetes v1.12.0 or above

a)、修改containerd的配置文件

在/etc/containerd/config.toml配置文件中[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]下增加kata运行时配置:

代码语言:javascript复制
 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata] #这里最后的这个kata 将被作为 RuntimeClass handler 关键字
  runtime_type = "io.containerd.kata.v2"
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.katacli]
    runtime_type = "io.containerd.runc.v1"
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.katacli.options]
      NoPivotRoot = false
      NoNewKeyring = false
      ShimCgroup = ""
      IoUid = 0
      IoGid = 0
      BinaryName = "/usr/bin/kata-runtime"
      Root = ""
      CriuPath = ""
      SystemdCgroup = false

需要注意的是,[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata] 中的 kata 将被作为 RuntimeClass handler 关键字。

b)、重启 containerd

代码语言:javascript复制
systemctl daemon-reload
systemctl restart containerd

c)、使用Kata Container

ü 准备RuntimeClass,并用该RuntimeClass创建Pod声明文件

代码语言:javascript复制
runtime.yaml
apiVersion: node.k8s.io/v1beta1  # RuntimeClass is defined in the node.k8s.io API group
kind: RuntimeClass
metadata:
  name: kata  
handler: kata  # 这里与containerd配置文件中的 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.{handler}] 匹配
pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: kata-nginx
spec:
  runtimeClassName: kata
  containers:
    - name: nginx
      image: nginx
      ports:
      - containerPort: 80

ü执行yaml,创建RuntimeClass和Pod

代码语言:javascript复制
[root@node1 zhh]# kubectl apply -f runtime.yaml
runtimeclass.node.k8s.io/kata created
[root@node1 zhh]# kubectl get runtimeclass
NAME   HANDLER   AGE
kata   kata      7s
[root@node1 zhh]# kubectl apply -f pod.yaml
7pod/kata-nginx created

ü验证是否正确创建:通过kata-runtime list 可以查看创建出来的 container

代码语言:javascript复制
[root@node1 zhh]# kata-runtime list
ID                                                                 PID         STATUS      BUNDLE                                                                                                                  CREATED                          OWNER
a5abf7227bf3e1868ff590e207d4f755ff6c879d821274a6ae25ae33839f5933   -1          running     /run/containerd/io.containerd.runtime.v2.task/k8s.io/a5abf7227bf3e1868ff590e207d4f755ff6c879d821274a6ae25ae33839f5933   2022-10-12T11:30:44.156957259Z   #0
[root@node1 zhh]# kubectl get pod
NAME                                READY   STATUS              RESTARTS   AGE
kata-nginx                          0/1     ContainerCreating   0          33s
nginx-deployment-746fbb99df-lmjcv   1/1     Running             1          18h

2) 使用untrusted_workload_runtime

对于不符合上述版本要求的环境,可以使用untrusted_workload_runtime的方式,该方式对版本无要求。

a)、修改containerd的配置文件

在/etc/containerd/config.toml配置文件中新增untrusted_workload_runtime运行时配置:

代码语言:javascript复制
 [plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime]
 runtime_type = "io.containerd.kata.v2"

对于不支持Runtime V2 (Shim API)的早期版本的Kata Containers和containd,可以使用以下替代配置:

代码语言:javascript复制
[plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime]
  # runtime_type is the runtime type to use in containerd e.g. io.containerd.runtime.v1.linux
  runtime_type = "io.containerd.runtime.v1.linux"
  # runtime_engine is the name of the runtime engine used by containerd.
  runtime_engine = "/usr/bin/kata-runtime"

b)、重启 containerd

代码语言:javascript复制
systemctl daemon-reload & systemctl restart containerd

c)、使用Kata Container

untrusted_workload_runtime 使用 annotations 告诉 Kubernetes 集群哪些负载需要使用 kata-runtime。因此需要使用kata-runtime的资源,只需要在annotations中声明即可。如:

代码语言:javascript复制
apiVersion: v1
kind: Pod
metadata:
  name: nginx-untrusted
  annotations:
    io.kubernetes.cri.untrusted-workload: "true"
spec:
  nodeName: k8s3
  containers:
  - name: nginx
    image: nginx

验证方式Pod是否被正确创建方法同RuntimClass。

当然,如果想直接把Kata Container作为Kubernetes默认的运行时也是可以的。我们知道Kubernetes默认的低层运行时是runC,如果想把Kata Container设置为默认的低层运行时,在containerd的配置文件中设置default_runtime,重启containerd(systemctl daemon-reload & systemctl restart containerd)后,创建的负载就自动使用Kata Container来进行管理了。

代码语言:javascript复制
  [plugins."io.containerd.grpc.v1.cri".containerd]
    [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime]
      runtime_type = "io.containerd.kata.v2"

对于不支持Runtime V2 (Shim API)的早期版本的Kata Containers和containerd,可以使用以下替代配置:

代码语言:javascript复制
 [plugins."io.containerd.grpc.v1.cri".containerd]
    [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/bin/kata-runtime"

参考文献:

1. https://www.bilibili.com/video/BV15Q4y1B7yk?spm_id_from=333.337.search-card.all.click

2.https://github.com/kata-containers/documentation/blob/master/how-to/containerd-kata.md

3. https://blog.csdn.net/m2l0zgssvc7r69efdtj/article/details/110297314

4. https://zhuanlan.zhihu.com/p/102171749

5. https://blog.csdn.net/u013533380/article/details/115682900

6. https://zhuanlan.zhihu.com/p/438351320

0 人点赞