58

为Kubernetes选择合适的容器运行时

 4 years ago
source link: https://www.tuicool.com/articles/imUjmmf
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【编者的话】作为后台支撑,Kubernetes优势明显,具有自动化部署、服务伸缩、故障自我修复、负载均衡等特性。咪付的蓝牙过闸系统和全态识别AI系统的后台支撑采用了Kubernetes,经过线上的长期运行,其状态良好运行平稳。

蓝牙过闸系统和全态识别AI系统有着不同的数据特性,对数据的安全要求及运行效率也各不一样,因此如何选择容器的运行时成为了一个重点考虑的因素。

CRI的由来

CRI(Container Runtime Interface)是Kubernetes提出的容器运行时接口规范。

y6b6Fzr.jpg!web

在Kubernetes体系中,是由Kubelet组件负责与容器运行时交互的。Kubelet调用容器运行时的流程如上图所示。CRI shim是实现CRI接口的gRPC server服务,负责连接Kubelet和Container runtime,Container runtime是容器运行时工具,它为用户进程隔离出一个独立的运行环境;具体的流程是Kubelet调用CRI shim接口,CRI shim响应请求,然后调用底层的Container runtime工具运行容器。Kubelet、CRI shim和Container runtime都部署在一个Kubernetes worker节点上,前两者是以独立的守护进程的方式启动的,而Container runtime不是守护进程,它通常是一个命令行工具。 如果你想和更多Kubernetes技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

Kubernetes在v1.5版本之前是没有CRI接口的,那时Kubelet源码内部只集成了两个容器运行时(Docker和rkt)的相关代码。这两种容器运行时并不能满足所有用户的使用需求,在某些业务场景,用户对容器的安全隔离性有着更高的需求,用户希望Kubernetes能支持更多种类的容器运行时。因此,Kubernetes在1.5版本推出了CRI接口,各个容器运行时只要实现了CRI接口规范,就可以接入到Kubernetes平台为用户提供容器服务。

CRI接口带来的好处,首先它很好的将Kubernetes和容器运行时解耦,容器运行时的每次更新迭代,都不必对Kubelet工程源码进行编译发布;其次解放了容器运行时更新迭代的步伐,也能保证Kubernetes的代码质量和平台的稳定。

OCI是什么

OCI规范(Open Container Initiative 开放容器标准),该规范包含两部分内容:容器运行时标准(runtime spec)、容器镜像标准(image spec)。其具体内容的定义如下图:

2qAZzqz.jpg!web

容器运行时标准

runtime spec包含配置文件、运行环境、生命周期三部分内容。

配置文件

config.json,包含容器的配置信息(mounts、process、hostname、hooks等)。

运行环境

定义运行环境,是为了确保应用程序在多个运行时之间,能具有一致的环境 。

容器生命周期

定义了运行时的相关指令及其行为:state、create、start、kill、delete、prestart hooks、poststart hooks、poststop hooks。

容器镜像标准

通常我们根据Dockerfile定义的内容制作镜像,目的是构建一个符合OCI标准的镜像文件,那么OCI标准镜像文件的内容都有哪些呢?在OCI规范里的image spec对容器镜像格式做了定义,它主要包括以下几块内容:

文件系统

描述了如何以layer的方式叠加成一个完整的文件系统,以及如何用layer去表示对文件作出的改动(增加、删除、修改)。

config文件

保存了文件系统的层级信息(每个层级的 hash 值、历史信息),以及容器运行时需要的一些信息(比如环境变量、工作目录、命令参数、mount 列表等)。

manifest文件

记录镜像元信息,包括Image Config和Image Layers。

index文件

可选的文件,指向不同平台的manifest文件,相当于整个镜像的入口,从这个文件可以获取整个镜像依赖的所有文件信息。

OCI项目

下表是兼容OCI规范的容器运行时项目:

nyAzQnR.jpg!web

容器运行时对比

按照底层容器运行环境依托的技术分类,我们将容器运行时分为以下三类:

  • OSContainerRuntime(基于进程隔离技术)
  • HyperRuntime(基于Hypervisor技术)
  • UnikernelRuntime(基于unikernel)

通过下图对比它们之间的区别:

juyiauv.jpg!web

OSContainerRuntime下的Linux Container共享Linux内核,使用namespace、cgroup等技术隔离进程资源。namespace只包含了六项隔离(UTS、IPC、PID、Network、Mount、User),并非所有Linux资源都可以通过这些机制控制,比如时间和Keyring,另外,容器内的应用程序和常规应用程序使用相同的方式访问系统资源,直接对主机内核进行系统调用。因此即使有了很多限制,内核仍然向恶意程序暴露过多的攻击面。

HyperRuntime下的VM Container容器各自拥有独立Linux内核,资源隔离比Linux Container更彻底。但并不是说使用VM容器用户就可以高枕无忧,只是VM容器的攻击面比Linux容器小了很多,黑客要逃逸到宿主机就只剩下Hypervisor这个入口,所以说没有绝对的安全,相对来说VM容器更安全。

另一方面,VM容器的性能比不上Linux容器,因为Hypervisor这一层带来的性能损耗,在Linux容器这边是不存在的。

UnikernelRuntime下的容器同VM Container一样有着安全级别很高的运行环境,同样是使用Hypervisor技术进行容器隔离。

简单来说Unikernel是一个运行在Hypervisor之上的libOS系统,而libOS是由应用程序和libraries一起构建出的操作系统。

unikernel的特点如下:

性能好,应用程序和内核在同一地址空间,消除了用户态和内核态转换以及数据复制带来的开销。

更精简的内核,去掉了多余的驱动、依赖包、服务等,最终打包镜像更小,启动速度更快。

完全不可调试,在生产环境中如果遇到问题,只能依赖于收集到的日志进行排查,要不就是重启容器,原先熟悉的Linux排查方法和工具完全派不上用场。

Kubelet CRI架构

Kubernetes在引入CRI之后,Kubelet的架构如下图所示:

7nyuiqM.jpg!web

每一个容器引擎只需要自己实现一个CRI shim,对CRI请求进行处理,就可以接入Kubelet当中去。

我们所说的容器运行时,准确来说包含两部分,一部分是上层容器运行时CRI shim(即容器运行时管理程序,如Containerd、CRI-O),另一部分是下层容器运行时Container runtime(即容器运行时命令工具,如runc)。

CRI接口定义

CRI接口分为两部分,一个是容器运行时服务RuntimeService,负责管理pod和容器的生命周期;一个是镜像服务ImageService,负责管理镜像的生命周期。

A7FNnqe.jpg!web

当前CRI格局

目前实现了CRI的主流项目有:Docker、containerd、CRI-O、Frakti、Pouch,它们衔接Kubelet与运行时方式对比如下:

I3iYZ3R.jpg!web

PS:由于rkt容器引擎目前未能完全兼容OCI规范,所以图中未将其包含进来。

Docker

Docker容器引擎安装后包含有这些组件:dockerd、Containerd、runC。

dockershim是内置在Kubelet的CRI gRPC server服务,它接收CRI请求后,通过unix本地套接字调用dockerd的API接口,再由dockerd请求下游的运行时组件Containerd和runc。调用的链路:dockershim => dockerd => Containerd => runc。

在当前CRI的请求链路上,dockerd只是简单接收CRI请求,在转换之后调用Containerd,dockerd还集成有其他功能,如network、swarm、volume等在Kubernetes平台下没有用武之地,可以说在CRI场景下docker显得笨重。

Containerd

Containerd项目是从早期的Docker源码中提炼出来的,它使用CRI插件来向kubelet提供CRI接口服务。

CRI插件是一个独立的项目,在Containerd编译时,如果go build命令没有显示设置参数-tags=no_cri,那么CRI插件将自动编译集成到Containerd的二进制文件中,然后在配置文件/etc/containerd/config.toml中声明启用CRI插件,就可以在Containerd中启动CRI shim服务了。

Containerd能支持多运行时,目前它内置了runc运行时,其他运行时如果要接入Containerd,则需要实现Containerd shim v2 gRPC接口,这样Containerd就可以通过shim v2调用其他运行时。他们的调用关系如下:Containerd --> shim v2 --> runtimes

CRI-O

CRI-O完整实现CRI接口功能,并且严格兼容OCI标准,CRI-O比Containerd更专注,它只服务于Kubernetes(而Containerd除支持Kubernetes CRI,还可用于Docker Swarm),从官网上我们可以了解到CRI-O项目的功能边界:

  • 支持多种image格式
  • 支持多种image下载方式
  • 容器镜像管理
  • 容器生命周期管理
  • 提供CRI要求的监控、日志功能
  • 提供CRI要求的资源隔离功能

CRI-O通过命令行调用默认运行时runC,所以runC二进制文件必须部署在目录/usr/bin/runc。CRI-O和Containerd调用runtime的方式不同,前者是通过Linux命令调用,后者是通过gRPC服务调用,所以只要符合OCI规范的runtime,都能直接接入CRI-O提供运行时服务,而除runC外的其他运行时要接入Containerd,只能走shim v2接口,因此我们看到像kata-runtime这样的运行时项目就是通过shim v2接口来适配Containerd的。

Frakti

Frakti是基于Hypervisor虚拟机管理程序的容器运行时,它相比其他的容器运行时具有如下这些功能特性:

YV7nUvF.jpg!web

PouchContainer

PouchContainer是阿里开源的容器引擎,它内部有一个CRI协议层和cri-manager模块,用于实现CRI shim功能。它的技术优势包括:

  1. 强隔离,包括的安全特性:基于Hypervisor的容器技术、lxcfs、目录磁盘配额、补丁Linux内核等。
  2. 基于P2P镜像分发,利用P2P技术在各节点间互传镜像,减小镜像仓库的下载压力,加快镜像下载速度。
  3. 富容器技术,PouchContainer的容器中除了运行业务应用本身之外,还有运维套件、系统服务、systemd进程管家等。

CRI命令工具

cri-tools是由kubernetes-sigs 开发维护的CRI命令工具(后简称crictl),它是为了在Kubernetes node节点上管理镜像、管理pod/container、与容器进行交互而设计的,crictl调用CRI接口获取相关信息、或者通过CRI的exec接口与容器交互。

crictl并不是docker cli的完全继任者,它仅提供了在Kubernetes node节点上常用的一些运维功能,具备一个较小的功能子集,而docker cli的命令更强大,事实上很多docker命令在生产环境并没有必要使用,所以crictl相对来说更安全,它避免运维人员在生产节点非法使用docker cli(rename、rm、rmi等子命令)造成一些人为的故障。所有实现了CRI接口的容器运行时,都可以通过crictl工具对其进行操作。

Kubernetes支持多运行时

为什么要支持多运行时呢?举个例子,有一个开放的云平台向外部用户提供容器服务,平台上运行有两种容器,一种是云平台管理用的容器(可信的),一种是用户部署的业务容器(不可信)。在这种场景下,我们希望使用runc运行可信容器(弱隔离但性能好),用runv运行不可信容器(强隔离安全性好)。面对这种需求,Kubernetes也给出了解决方案(使用API对象RuntimeClass支持多运行时)。

多运行时工作原理

RzqM7vn.jpg!web

Kubelet从apiserver接收到的Pod Spec,如果Pod Spec中使用runtimeClassName指定了容器运行时,则在调用CRI接口的RunPodSandbox()函数时,会将runtimeClassName信息传递给CRI shim,然后CRI shim根据runtimeClassName去调用对应的容器运行时,为Pod创建一个隔离的运行环境。

RuntimeClass配置

Kubernetes在v1.12中增加了RuntimeClass这个新API对象来支持多运行时(目的就是在一个woker节点上运行多种运行时)。

在Kubernetes中启用RuntimeClass时需要注意,尽量保持当前Kubernetes集群中的节点在容器运行时方面的配置都是同构的,如果是异构的,那么需要借助node Affinity等功能来调度Pod到已部署有匹配容器运行时的节点。

接下来只需要三步配置就可以为pod指定运行时:

  1. 在Kubernetes worker节点配置CRI shim;
  2. 创建RuntimeClass资源对象;
  3. 在pod中指定RuntimeClass。

配置CRI shim

例如CRI-O运行时的配置,需要在文件/etc/crio/crio.conf定义runtime的handler_name:

[crio.runtime.runtimes.${HANDLER_NAME}]

runtime_path = "${PATH_TO_BINARY}"

创建RuntimeClass

RuntimeClass yaml定义如下:

apiVersion: node.k8s.io/v1beta1  # RuntimeClass is defined in the node.k8s.io API group

kind: RuntimeClass

metadata:

name: myclass  # The name the RuntimeClass will be referenced by

# RuntimeClass is a non-namespaced resource

handler: myconfiguration  # The name of the corresponding CRI configuration

配置Pod

在pod yaml中指定RuntimeClassName:

apiVersion: v1

kind: Pod

metadata:

name: mypod

spec:

runtimeClassName: myclass

# ...

如何选择合适的容器运行时

在生产环境中,我们并不需要Docker的镜像打包、容器网络、文件挂载、swarm等这些能力,只需要部署Containerd + runC就可以在Node节点上运行Pod。因此在生产环境中我们可以不安装Docker,而是安装CRI shim组件和运行时工具来运行pod。在多个CRI shim和OCI工具之间,我们该如何选择呢?

首先对比Containerd和CRI-O调用runC的方式,runC代码内置在Containerd内部,通过函数调用;CRI-O是通过Linux命令方式调用runC二进制文件,显然前者属于进程内的函数调用,在性能上Containerd更具优势。其次对比runC和runV,这是两种完全不同的容器技术,runC创建的容器进程直接运行在宿主机内核上,而runV是运行在由Hypervisor虚拟出来的虚拟机上,后者占用的资源更多、启动速度慢,而且runV容器在调用底层硬件时(如CPU),中间多了一层虚拟硬件层,计算效率上不如runC容器。

eAza6bu.jpg!web

因此建议结合自身业务特点、以及使用场景选择合适的容器运行时。在对用户的隔离没有很高诉求的情况下,可以优先考虑使用性能更好更轻量的Containerd + runc;在隔离性要求较高的业务场景下,推荐使用基于Hypervisor 的虚拟化容器运行时Frakti + runv。

原文链接: https://mp.weixin.qq.com/s/YooQQh7bXbM58kvSOMoQiw


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK