容器(Container)是一种轻量级虚拟化技术,“轻量”主要是因为容器与传统虚拟机比较,是内核共享的,所以启动快、资源占用小。随着虚拟化技术发展,Docker与Kubernets逐步成为应用打包与部署的标准,以及公有云租赁模式推广,出现了“虚拟机容器”这种形态。
虚拟机容器首先是一台虚拟机,由于Docker镜像打包与分发方面的优势(分层构建、Build Once Run Anywhere),虚拟机容器也兼容Docker镜像;并提供了OCI规范的 runtime ,且适配了Kubernetes调度管理API,能够被Kubernetes调度,因此被称为虚拟机容器。
虚拟机容器主要使用场景是在公有云模式下,云服务厂商提供无服务器计算服务(函数服务,无服务器容器),这种模式下租户只租一个容器或进程,不同租户的容器/进程可能运行到同一台机器上,传统的内核共享模式在这种场景下安全风险太高,虚拟机容器能够做到内核独占,适合这种场景。
Kata 项目介绍
Kata 源自 Intel 的开源项目 Hyper.sh ,当前在 Openstack 软件基金会(OSF)下治理。
Kata南向虚拟化实现技术可插拔,当前支持QEMU/NEMU,在最新的1.5版本里支持Firecrack;北向支持与Docker的Containerd对接,并可以被Kubernetes编排,接入Docker与Kubernetes生态。
Kata 架构概览 (基于1.5版本)
Kata container runtime and shimv2
Kata容器项目主要由容器运行时( kata container runtime )与一个兼容CRI接口的 shim 部件组成。
Kata container runtime 符合 OCI 运行时规范 因此能够被Docker引擎管理,作为Docker引擎的一个runtime插件。 Kata container runtime 还基于Containerd的CRI插件与CRI-O实现了Kubernetes的CRI规范,因此,使用者可以在Docker默认的runtime runc 与 kata container runtime (runv) 之间平滑切换,上层组件不感知差异。
Kata容器的另外一个组成部分是 containerd-shim-kata-v2 ,简称为 shimv2 , shimv2 提供了了 Containerd Runtime V2 (Shim API) 的 Kata 实现,从而使得 Kubernetes 场景下能够实现每个 Pod 一个 shim 进程 – shimv2 ;而在此之前,一个Pod需要一个2个shim( containrd-shim, kata-shim ),如果 Pod Sandbox没有暴露 VSOCK 则还需要一个 kata-proxy 。
agent 与 kata-proxy
Kata容器运行在虚拟机沙箱内,每个虚拟机内运行一个Agent,Agent负责运行container。Agent同时提供gRPC接口,通过QEMU的VIRTIO serial或VSOCK接口向HOST主机暴露接口,主机上的 kata-runtime 使用gRPC协议与Agent通信,向虚拟机内的容器发送指令,I/O流也通过此通道管理。 如果是使用 VIRTIO serial 的方式暴露接口到Host主机,那么还需要在主机上部署一个 kata-proxy 负责转发指令到Agent。
容器进程管理
在Host主机上,每个容器进程的清理是由更上层的进程管理器完成的,在Docker containerd的实现里,进程管理器是 containerd-shim ;在CRI-O的实现里是common。
在Kata容器场景下,容器进程运行在虚拟机内,Host主机上的进程管理器不能直接管理到容器进程,Kata容器项目通过 kata-shim 来解决此问题。kata-shim 运行在Host主机上,介于容器进程管理器与kata-proxy之间,kata-shim 将来自Host主机的信号量、stdin 转发到虚拟机内的容器进程上,并将虚拟机容器内的stdout与stderr转发到Host主机上的容器进程管理器。
kata-runtime 为每个容器进程创建一个 kata-shim 守护进程,为每个通过OCI命令连接到容器进程内部执行用户命令的操作创建一个 kata-shim 守护进程(如docker exec)。
在 Kata1.5 版本,shimv2 收编 kata-runtime, kata-shim, kata-proxy 到 shimv2 进程中。
虚拟化
Kata 架构上能够支持多种虚拟化实现,在 Kata1.0 版本,支持 QEMU/KVM 虚拟化。在 Kata1.5 版本,支持 AWS Firecracker 极轻量的虚拟机。
QEMU/KVM
根据Host主机架构,Kata容器支持多种主机类型,比如 x86 上的 pc
与 q35
,ARM 上的 virt
, IBM Power System 上的 pseries
。默认的Kata容器主机类型是 pc
,默认主机类型可以通过配置修改。
Kata容器使用下面的 QEMU 特性来管理资源配额、缩短启动时间、减少内存占用:
- 机器加速器
- 热插拔设备
机器加速器
机器加速器是与特定服务器架构相关的,机器加速器能够提升性能并开启某些特性。下面这些机器加速器在Kata容器中使用。
- NVDIMM: x86平台的机器加速器,仅支持
pc
与q35
机器类型。nvdimm 用来以持久化内存(persistent memory)方式提供虚拟机的根文件系统。
虽然Kata容器能够支持大多数QEMU发行版本,但是考虑到Kata容器的启动时间、内存占用、IO性能因素,Kata容器使用一个针对这些因素专门优化过的QEMU版本 qemu-lite ,并增加了一些自定义的机器加速器,这些自定义加速器在 QEMU Upstream 版本中不可用。
- nofw: x86平台的机器加速器,仅支持
pc
与q35
机器类型。nofw
用来启动 ELF 格式的系统内核,但是可以跳过 BIOS 与固件自检 (BIOS/firmware) ,这个加速器可以显著提升虚拟机启动速度。 - static-prt: x86平台的机器加速器,仅支持
pc
与q35
机器类型。static-prt
用来减少虚拟机ACPI(Advanced Configuration and Power Management Interface)的解释负担。
热插拔设备
Kata容器虚拟机初始以最小的资源启动,为了提升启动速度,在启动过程中,设备可以热插到虚拟机上。如,容器定义了cpu资源,可以通过热插的方式加到虚拟机上。Kata容器虚拟机支持如下热插设备:
- Virtio block
- Virtio SCSI
- VFIO
- CPU
Kernel 与 Image
虚拟机内核
虚拟机内核在虚拟机启动时候被加载,Kata容器提供的虚拟机内核针对启动时间与内存占用做了优化。
虚拟机镜像
Kata 容器支持 initrd
与 root filesystem
两种虚拟机镜像。
root filesystem
Kata 容器提供的默认打包好的 root filesystem 镜像,这种镜像也被称为 “mini O/S”,是基于 Clear Linux 优化的,提供最小的运行环境与高度优化的启动路径。 镜像中只有Kata Agent与systemd两个进程,用户的工作负载被打包到docker镜像,在虚拟机内通过libcontainer库,以runc方式运行起来。
举例,当用户执行 docker run -it ubuntu date
命令时,流程如下:
- 虚拟化层加载虚拟机内核,虚拟机内核加载虚拟机镜像。
systemd
启动虚拟机运行环境(mini-OS Context),并启动 kata-agent 进程(在同一个Context)- kata-agent 创建一个独立的context,运行用户指定的命令(例子中是 date )
- kata-agent 准备 ubuntu 的运行环境并运行 date 命令
initrd
待补充。
Agent
kata-agent 是一个运行在虚拟机中的进程管理虚拟机中的容器进程。
kata-agent 的最小运行单元是沙箱,一个 kata-agent 沙箱是一个由一些列namespace(NS, UTS, IPC, PID)隔离出来的。 kata-runtime 能够在一个虚拟机内运行多个容器进程以支持POD内多个container模式。
kata-agent 使用gRPC协议与Kata其他组件通信,在gRPC同一个URL上还运行了一个 yamux 服务。
kata-agent 使用 libcontainer 管理容器生命周期,复用了 runc 的大部分代码。
Runtime
kata-runtime 是一个符合OCI规范的容器运行时,负责处理OCI运行时规范中的所有命令,并启动 kata-shim 进程。
关键的OCI命令实现
create
kata-runtime 处理 OCI create 命令步骤:
- 创建虚拟机与shim进程的network namespace。
- 执行 pre-start hook ,回调中负责创建
veth
网络设备,用于连接主机网络与新创建的network namespace。 - 扫描新创建的network namespace,在其中的veth设备上创建一个macvtab设备。
- 在新的network namespace中创建虚拟机,并将tab设备传递给虚拟机。
- 等待虚拟机启动完成。
- 启动kata-proxy,kata-proxy负责代理所有发送给虚拟机的请求,每个虚拟机一个kata-proxy进程。
- 调用kata-agent接口配置虚拟机内的沙箱。
- 调用kata-agent接口创建容器,使用kata-runtime提供的默认的OCI配置文件
config.json
。 - 启动kata-shim进程,kata-shim连接到kata-agent的gRPC socket端口。kata-shim会创建几个Go routine阻塞式调用
ReadStdout()
,ReadStderr()
,WaitProcess()
。ReadStdout()
,ReadStderr()
以死循环方式执行直到虚拟机内的容器进程中止。WaitProcess()
返回虚拟机内容器进程的 exit code。 kata-shim运行在虚拟机的network namespace中,通过kata-shim进程可以找到创建了哪些namespace。启动 kata-shim 进程还会创建一个新的PID namespace,对应到同一个container的所有kata-shim进程都在同一个PID namespace,这样当容器进程终止时候很容器将所有kata-shim进程终止掉。
此时容器进程在虚拟机内部运行起来了,在Host主机上对应到kata-shim进程。
start
传统容器的 start 会在容器namespace中启动容器进程。Kata容器中, start 会在虚拟机内启动容器的工作负载,步骤如下;
- 调用kata-agent接口在虚拟机内启动容器负载命令。如,容器内负载命令为
top
,kata-shim 进程的ReadStdout()
会读取到 top 的输出,WaitProcess()
会一直等待到 top 命令结束。 - 执行 post-start 回调,当前 post-start 实现为空。
exec
OCI 的 exec 命令允许在已有的容器中执行命令。在Kata容器中, exec 执行步骤如下:
- 调用kata-agent接口在已有容器中执行命令。
- 一个新的kata-shim进程会创建出来,被放置到已有容器对应的kata-shim所在的PID namespace中。
此时通过 exec 命令启动的新的容器负载已经在虚拟机内运行,共享已有容器的namespace (uts, pid, mnt, ipc) 。
kill
OCI kill 命令通过发送 UNIX 信号,如 SIGTERM
, SIGKILL
,来终止容器进程。在Kata容器中, kill 命令会终止虚拟机内的容器进程与虚拟机。
- 调用kata-agent接口请求kill容器进程。
- 等待 kata-shim 进程退出。
- 调用kata-agent接口请求强制kill容器进程(发送 KILL 信号量给容器进程),如果 kata-shim 进程在超时时间内未退出。
- 等待 kata-shim 进程退出,如果等待超时则报错。
- 调用kata-agent接口删除虚拟机内的容器配置。
- 调用kata-agent接口删除虚拟机内的沙箱配置。
- 停止虚拟机。
- 删除network namespace中的网络配置,删除network namespace。
- 执行 post-stop 回调。
delete
delete 指令删除容器所有相关的资源,正在运行中的容器不能不删除,除非通过 --force
指令强制删除。
如果虚拟机内的沙箱未停止,但是沙箱内的容器进程已经推出,kata-runtime会先执行一次kill流程,之后如果沙箱已经停止,kata-runtime执行如下动作:
- 删除容器相关资源:目录
/var/{lib,run}/virtcontainers/sandboxes/<sandboxID>/<containerID>
下的所有文件。 - 删除沙箱:目录
/var/{lib,run}/virtcontainers/sandboxes/<sandboxID>
下的所有文件。
此时,所有容器相关内容都已经在Host主机上被删除,没有任何相关进程在运行。
state
state 返回容器的运行状态。在Kata容器中,state 需要检测容器进程是否在运行,通过检查对应容器进程的 kata-shim 进程的状态。
- 通过存储在磁盘上的信息获得容器状态(需要澄清)。
- 检查kata-shim进程。
- 如果kata-shim进程不存在,但是磁盘上的容器状态文件还是ready或running,那么意味着在得到容器返回状态之前,容器进程已经被正常停止了。
Proxy
Host主机与虚拟机通信可以通过 virtio-serial
或 virtio-socket
, virtio-socket
需要内核版本 4.8 以上。默认使用 virtio-serial
。
虚拟机内可能运行多个容器进程,在使用 virtio-serial
场景下,需要Host主机上运行 multiplexed 与 demultiplexed 进程;使用 virtio-socket
则不需要。
kata-proxy
进程提供代理访问虚拟机内的 kata-agent
,对应到多个 kata-shim 进程与 kata-runtime 客户端。 kata-proxy
主要功能是负责代理IO流与信号量到 kata-agent
。 kata-proxy
通过Unix domain socket方式连接 kata-agent
。
Shim
容器进程回收器(reaper),如Docker的 containerd-shim
或 CRI-O 的 common
,其设计假设是基于能够监控并回收实际的容器进程。在Kata容器中,由于容器进程运行在虚拟机中,Host主机上的容器进程回收器不能直接监控到虚拟机内的容器进程,至多能看到QEMU进程,这个是远远不够的。Kata容器中的kata-shim进程是主机上对应到虚拟机内容器进程的映射,因此kata-shim进程需要处理容器进程的I/O流并负责转发信号到容器进程。
- kata-shim 通过Unix domain socket连接到kata-proxy,socket URL是由kata-runtime在启动kata-shim进程时候传递给kata-shim的,一并传递的参数还有containerID与execID,containerID与execID用来标识虚拟机内的容器进程。
- 转发来自容器进程回收器的标准输入流,通过kata-proxy的gRPC的
WriteStdin
API。 - 从容器进程读取标准输出与错误输出。
- 转发来自容器进程回收器的信号,通过kata-proxy的
SignalProcessRequest
API。 - 监控终端的变化并转发,通过kata-proxy的
TtyWinResize
API。
Networking
容器进程一般被放置在独立的network namespace中,在容器生命周期的某个点,容器引擎会创建network namespace并将容器进程加入到network namespace中。容器进程网络与主机Host网络隔离。 在实现技术上,通常使用veth技术,veth的两端分别放置到容器的network namespace与主机Host的network namespace中。这种方式是以namespace为中心的,而一些虚拟化技场景下不支持veth,特别是QEMU,这种情况下使用TAP技术替代。
为了消除虚拟化场景下网络隔离与容器场景下网络隔离的不兼容,kata-runtime使用veth+tab(MACVLAN)方式实现网络隔离与互通。
- 主机Host网络与容器网络通过network namespace隔离,使用veth互通。
- 容器网络network namespace内的veth网络设备上创建MACVLAN(实际上为MACVTAP)设备。
- 在创建虚拟机(QEMU)时候,将TAP网络设备作为虚拟机网卡。
- 虚拟机内的容器进程使用虚拟机网卡作为主网卡与外部通信,虚拟机内部的多个POD共享虚拟机网络,不再隔离。
CNM
CNM lifecycle
- RequestPool
- CreateNetwork
- RequestAddress
- CreateEndPoint
- CreateContainer
- Create config.json
- Create PID and network namespace
- ProcessExternalKey
- JoinEndPoint
- LaunchContainer
- Launch
- Run container
CNM 网络配置过程
- 读取
config.json
- 创建network namespace: netns
- 回调
prestart
钩子(在netns内) - 扫描netns命名空间下的,由
prestart
回调创建的网络接口 - 创建bridge/tap并通过veth与主机连接
网络热插拔
Kata容器开发了一套命令与api支持添加/删除/查看 guest网络。下面流程图展示了Kata容器热插拔的流程。
存储
Kata容器的虚拟机与Host通过9pfs共享文件,对于虚拟机内的容器,不建议使用主流的overlay2存储驱动,overlay2存储驱动是基于文件系统的。
Kubernetes 集成
Kubernetes目前是容器编排的事实标准,Kubernetes为了解耦Kubelet与各种容器runtime,抽象了CRI(Container Runtime Interface)接口规范,Kubelet相当于是一个CRI客户端,不同的CRI实现提供gRPC接口与Kubelet对接,接入到Kubernetes生态。当前基于OCI标准容器提供的CRI接口实现有CRI-O与Containerd CRI Plugin。
Kata容器的runtime是CRI-O与Containerd CRI Plugin的官方runtime之一,因此Kata容器可以很容易的集成到Kubernetes生态中。
但是由于Kubernetes最小调度单元是Pod而非容器进程,一个Pod可以有多个容器进程,而Kata容器又是将容器进程跑在虚拟机内的,因此Kata容器需要Kubernetes在创建容器进程传递更多信息,用来告知Kata runtime是要创建一个新的虚拟机,还是在已有虚拟机内启动容器进程。
Containerd CRI Plugin 集成kata-runtime
在Kata1.5版本,对应containerd1.2.0,通过shimv2
实现了Kata runtime与Kubernetes集成,具体指导参考链接。CRI-O的实现也正在开发中,跟踪此Issue。
CRI-O 集成Kata-runtime
OCI annotations
为了让kata-runtime(或者任何虚拟机容器的runtime)区分是要创建一个虚拟机还是仅在虚拟机内创建容器进程,CRI-O在OCI配置文件(config.json)中增加了一个annotation来告知这个区分给kata-runtime。
在执行runtime之前,CRI-O会增加一个io.kubernetes.cri-o.ContainerType
的annotation,这个注解由Kubelet生成,取值范围是sandbox
, container
,kata-runtime将sandbox
对应到创建虚拟机(新Pod),container
对应到在已有Pod中创建容器进程。
containerType, err := ociSpec.ContainerType()
if err != nil {
return err
}
handleFactory(ctx, runtimeConfig)
disableOutput := noNeedForOutput(detach, ociSpec.Process.Terminal)
var process vc.Process
switch containerType {
case vc.PodSandbox:
process, err = createSandbox(ctx, ociSpec, runtimeConfig, containerID, bundlePath, console, disableOutput, systemdCgroup)
if err != nil {
return err
}
case vc.PodContainer:
process, err = createContainer(ctx, ociSpec, containerID, bundlePath, console, disableOutput)
if err != nil {
return err
}
}
虚拟机容器与namespace隔离容器混合管理
一个有趣的演进是在一个Kubernetes集群中混合管理虚拟机容器与namespace隔离的容器。 现在Kubernetes集群运维人员可以对工作负载打trusted
, untrusted
标签,trusted
标签表示工作负载是安全的,untrusted
表示工作负载存在潜在风险,在支持kata容器的Kubernetes集群中,会自动根据标签,将trusted
工作负载以runc
方式运行,untrusted
工作负载以runv(kata-runtime)方式运行。
CRI-O默认行为是认为所有的工作负载在都是trusted
,除非设置了注解io.kubernetes.cri-o.TrustedSandbox=false
,CRI-O默认的trust配置在configuration.toml
中。
综合来看,CRI-O是选择runc
还是runv
,由Pod的Privileged
参数,CRI-O trust配置trusted/untrusted
,io.kubernetes.cri-o.TrustedSandbox
注解三个值确定。 如果Pod是Privileged
,那么只能是runc
。如果Pod不是Privileged
,那么runtime的选择方式如下:
io.kubernetes.cri-o.TrustedSandbox 未设置 | io.kubernetes.cri-o.TrustedSandbox=true | io.kubernetes.cri-o.TrustedSandbox=false | |
---|---|---|---|
默认的CRI-O turst设置: trusted | runc | runc | runv(kata-runtime) |
默认的CRI-O turst设置: untrusted | runv(kata-runtime) | runv(kata-runtime) | runv(kata-runtime) |
原文链接
本文链接:http://www.yunweipai.com/28856.html