Kubernetes + CRI + Kata + Firecracker

简介: 在最近的AWS re:invent 2018上,AWS又发布了一系列新的产品,在这些产品中,最受关注的无疑就是面向serverless的Firecracker。Firecracker是针对目前现有的虚拟化技术在serverless应用场景中的各种不足,而专门为serverless量身打造的一项新的虚拟化技术。

Kata

Kata源自希腊文Καταπίστευμα(ka-ta-PI-stev-ma),原意是值得信任的人,kata container正是解容器安全的问题而诞生的。传统的容器是基于namespace和cgroup进行隔离,在带来轻量简洁的同时,也带来了安全的隐患。事实上容器虽然提供一个与系统中的其它进程资源相隔离的执行环境,但是与宿主机系统是共享内核的,一旦容器里的应用逃逸到内核,后果不堪设想,尤其是在多租户的场景下。Kata就是在这样的背景下应运而生,kata很好的权衡了传统虚拟机的隔离性、安全性与容器的简洁、轻量。这一点和firecracker很相似,都是轻量的虚拟机。但是他们的本质的区别在于:kata虽然是基于虚机,但是其表现的却跟容器是一样的,可以像使用容器一样使用kata;而firecracker虽然具备容器的轻量、极简性,但是其依然是虚机,一种比QEMU更轻量的VMM,暂时不能兼容容器生态。
Kata的基本原理是,为每一个容器单独开一个虚机(如果是k8s下作为runtime,则是一个pod对应一个虚机而不是容器),具有独立的内核,这样交付的容器就具备了虚机级别的隔离和安全性。kata的原理图如下所示:
13ae35bbc77e9aebe9b864783ce81262

Kata container作为OCI标准的成员之一,其kata-runtime也是兼容OCI标准,和runc处在同一个层级,对安全和隔离性要求高的场景,可以从Docker或者k8s默认的runtime(比如runc)切到kata-runtime。

CRI

CRI基本原理

早期的k8s使用docker作为默认的runtime,后来又加入rkt,每加入一种新运行时,k8s都要修改接口来关联新的容器运行时。随着越来越多的容器运行时想加入k8s运行时,而且不同的容器的实现和功能差异很大,比如docker已经再不是一个单纯的容器运行时,这时候亟需一套标准来定义k8s支持的运行时。这套标准就是CRI(Container RunTime Interface)。k8s(甚至k8s用户)不再关心底层的容器运行时,kubelet只感知到CRI server,而CRI server只需遵循CRI标准实现对应的runtime的标准化的接口。

CRI接口具体的定义细节在k8s的kubelet/apis/cri/runtime/v1alpha2/api.proto中:

service RuntimeService { // Version returns the runtime name, runtime version, and runtime API version. rpc Version(VersionRequest) returns (VersionResponse) {} // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure // the sandbox is in the ready state on success. rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {} // CreateContainer creates a new container in specified PodSandbox rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {} // Exec prepares a streaming endpoint to execute a command in the container. rpc Exec(ExecRequest) returns (ExecResponse) {} // ContainerStats returns stats of the container. If the container does not // exist, the call returns an error. rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {} // ListContainerStats returns stats of all running containers // Status returns the status of the runtime. rpc Status(StatusRequest) returns (StatusResponse) {} ... } // ImageService defines the public APIs for managing images. service ImageService { // ListImages lists existing images. rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {} // ImageStatus returns the status of the image. If the image is not // present, returns a response with ImageStatusResponse.Image set to // nil. rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {} // PullImage pulls an image with authentication config. rpc PullImage(PullImageRequest) returns (PullImageResponse) {} // RemoveImage removes the image. // This call is idempotent, and must not return an error if the image has // already been removed. rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {} // ImageFSInfo returns information of the filesystem that is used to store images. rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {} }

可以看到CRI server包括 RuntimeService 和 ImageService 两个服务,均为gRPC server。ImageService负责镜像的管理,比如查询、拉取、删除镜像等;RuntimeService负责四大块:PodSandbox(Pause容器或者一台虚拟机,取决于具体的runtime实现),Container,Streaming API(exec),状态查询接口等。

下面分别以k8s create pod和stream API exec来分析CRI工作的流程:
341e4fae6f5c48d54ac9627fc649c5c2

CRI分类

CRI的第一个实现就是k8s自己提供的针对Docker运行时的dockerShim,也是目前k8s使用docker的标准方式,已经集成在k8s的源码中。如今CRI的众多实现中除了dockershim外,比较具有代表性的还有有CRI-containerd, CRI-O以及fraktirkt作为k8s最早支持的运行时之一,现在也开始转向标准的CRI实现rktlet,这是k8s未来使用rkt的标准方式。该项目的目标就是像现在的dockerShim一样,而社区貌似并不活跃。目前主流的几种CRI实现的生产容器的流程图如下所示:
bde54cf7fdb6c3f4a3d941e092155c72
其中dockershim、CRI-containerd、CRI-O属于基于OCI的CRI,dockershim目前不支持kata runtime,其他的两种CRI-containerd、CRI-O均支持runc和kata runtime。frakti是一种特殊的CRI实现,它不依赖于任何runtime,而是可以直接使用kata提供的硬件虚拟化API库来实现CRI的标准接口,即直接开VM然后runv启动pod和容器。frakti虽然相比于其他的CRI实现复杂、接口偏重,但是实现的灵活性更强。

Dockershim

k8s开始支持CRI之后,k8s便不再以之前的方式依赖docker,而是采用标准的CRI的方式来使用docker运行时。Dockershim便是k8s对CRI的一个标准实现,已经集成在了k8s的源码中,这里重点看下k8s中dockershim的实现。


4d1f61f50a2f32545fa11147bf8959f269fdca57.png

RuntimeService client实现:

首先,dockershim的gRPC client的实现是在kubelet/remote/remote_runtime.go中:

// RemoteRuntimeService is a gRPC implementation of internalapi.RuntimeService. type RemoteRuntimeService struct { timeout time.Duration runtimeClient runtimeapi.RuntimeServiceClient // Cache last per-container error message to reduce log spam lastError map[string]string // Time last per-container error message was printed errorPrinted map[string]time.Time errorMapLock sync.Mutex }

runtimeapi.RuntimeServiceClient是一个连接到了gRPC server的client端,主要接收来自kubelet的请求。RuntimeServiceClient的所有调用都是gRPC的方式。以RunPodSandbox为例:

func (c *runtimeServiceClient) RunPodSandbox(ctx context.Context, in *RunPodSandboxRequest, opts ...grpc.CallOption) (*RunPodSandboxResponse, error) { out := new(RunPodSandboxResponse) err := grpc.Invoke(ctx, "/runtime.v1alpha2.RuntimeService/RunPodSandbox", in, out, c.cc, opts...) if err != nil { return nil, err } return out, nil }

RuntimeService server实现:

服务端实现的核心部分在kubelet/dockershim/docker_service.go中

// CRIService includes all methods necessary for a CRI server. type CRIService interface { runtimeapi.RuntimeServiceServer runtimeapi.ImageServiceServer Start() error } // DockerService is an interface that embeds the new RuntimeService and // ImageService interfaces. type DockerService interface { CRIService // For serving streaming calls. http.Handler // For supporting legacy features. DockerLegacyService }

可以看到DockerService接口包装了RuntimeServiceServer和ImageServiceServer两个必须的接口。DockerService的实现类是dockerService(go duck type):

type dockerService struct { client libdocker.Interface os kubecontainer.OSInterface podSandboxImage string streamingRuntime *streamingRuntime streamingServer streaming.Server network *network.PluginManager // Map of podSandboxID :: network-is-ready networkReady map[string]bool networkReadyLock sync.Mutex containerManager cm.ContainerManager // cgroup driver used by Docker runtime. cgroupDriver string checkpointManager checkpointmanager.CheckpointManager // caches the version of the runtime. versionCache *cache.ObjectCache // startLocalStreamingServer indicates whether dockershim should start a // streaming server on localhost. startLocalStreamingServer bool }

其中最核心的就是client,它包含了dockerService必须要实现的所有方法,其本质上是一个docker client,即DockerService接口的所有方法的实现最终都是通过直接调用docker client实现的。其中dockerEndpoint值为kubelet初始化时通过--docker-endpoint传入。

// ConnectToDockerOrDie creates docker client connecting to docker daemon. func ConnectToDockerOrDie(dockerEndpoint string, requestTimeout, imagePullProgressDeadline time.Duration, withTraceDisabled bool, enableSleep bool) Interface { if dockerEndpoint == FakeDockerEndpoint { fakeClient := NewFakeDockerClient() if withTraceDisabled { fakeClient = fakeClient.WithTraceDisabled() } if enableSleep { fakeClient.EnableSleep = true } return fakeClient } //最核心的一行代码,创建docker client client, err := getDockerClient(dockerEndpoint) if err != nil { klog.Fatalf("Couldn't connect to docker: %v", err) } klog.Infof("Start docker client with request timeout=%v", requestTimeout) return newKubeDockerClient(client, requestTimeout, imagePullProgressDeadline) }

dockerService只是完成了标准接口的实现,还不能对外提供服务,需要注册到gRPC中,即DockerServer:

// DockerServer is the grpc server of dockershim. type DockerServer struct { // endpoint is the endpoint to serve on. endpoint string // service is the docker service which implements runtime and image services. service dockershim.CRIService // server is the grpc server. server *grpc.Server }
runtimeapi.RegisterRuntimeServiceServer(s.server, s.service) runtimeapi.RegisterImageServiceServer(s.server, s.service)

虽然RuntimeService和ImageService均为gRPC服务,但是实现上可以共用一个gRPC也可以分别启用一个gRPC,dockerShim采用的是第一种方式。

再回到最上层,在kubelet初始化的时候,会判断container runtime的类型,如果是docker,就会进入dockershim的初始化,即完成以上的流程:dockershim.NewDockerService()—>dockerremote.NewDockerServer(),并启动gRPC server:

 case kubetypes.DockerContainerRuntime: // Create and start the CRI shim running as a grpc server. streamingConfig := getStreamingConfig(kubeCfg, kubeDeps, crOptions) ds, err := dockershim.NewDockerService(kubeDeps.DockerClientConfig, crOptions.PodSandboxImage, streamingConfig, &pluginSettings, runtimeCgroups, kubeCfg.CgroupDriver, crOptions.DockershimRootDirectory, !crOptions.RedirectContainerStreaming) if err != nil { return nil, err } if crOptions.RedirectContainerStreaming { klet.criHandler = ds } // The unix socket for kubelet <-> dockershim communication. klog.V(5).Infof("RemoteRuntimeEndpoint: %q, RemoteImageEndpoint: %q", remoteRuntimeEndpoint, remoteImageEndpoint) klog.V(2).Infof("Starting the GRPC server for the docker CRI shim.") server := dockerremote.NewDockerServer(remoteRuntimeEndpoint, ds) if err := server.Start(); err != nil { return nil, err } // Create dockerLegacyService when the logging driver is not supported. supported, err := ds.IsCRISupportedLogDriver() if err != nil { return nil, err } if !supported { klet.dockerLegacyService = ds legacyLogProvider = ds } case kubetypes.RemoteContainerRuntime: // No-op. break default: return nil, fmt.Errorf("unsupported CRI runtime: %q", containerRuntime)

这是dockerShim的流程,rktlet和dockerShim的实现非常类似。由于是集成在k8s中,所以kubelet需要负责CRI server的这些初始化工作,如果是其他的CRI实现,就需要在节点上启动runtimeService和imageService,kubelet只用关心runtimeService和imageService的endpoint,即启动kubelet时通过设置参数--container-runtime-endpoint、image-service-endpoint(imageService的endpoint缺省值默认使用runtimeService的endpoint的值)来告诉kubelet CRI gRPC server的endpoint。Anyway,即便把dockerShim独立出来,这个流程依然是一样的,在kubelet看来依然是一个接口一样的gRPC server。

CRI-O

本节重点分析CRI-O的实现。如果说dockerShim只是针对docker runtime的标准实现,那CRI-O就是真正的兼容OCI标准的实现:CRI-O全名即为CRI-OCI。CRI-O默认会使用runc,但是能够识别k8s pod的注解annotations:io.kubernetes.cri-o.TrustedSandbox,用户可以使用这个注解来通过CRI-O选择合适的runtime。比如,对安全级别要求较高,可以将注解的值设置为false,CRI-O就会使用kata-runtime,即每个pod对应一个虚机(这和docker使用kata-runtime稍有区别,docker中默认每个kata container对应一个虚机隔离,而在k8s pod中的一组容器往往是业务上相互协作的应用,他们之间的安全隔离性不需要很高,所以pod内的一组容器和runc的类似,只是在pod层需要达到虚机级别的隔离)。还需要说明的是,CRI-O支持一个节点上同时运行两种不同类型runtime的pod。

kata-4.png

Kata + Firecracker

46911be6ad828b9a148e62b514d49e3f

在没有firecracker之前,CRI-O就可以通过k8s注解或者配置文件的方式将默认的runtime替换为kata runtime,进而为容器(pod)开独立的虚机。firecracker本质上是虚拟化技术,和qemu在一个层面,只是它更加轻量、精简。所以很自然的会想到,kata能否直接开出firecracker虚机,在虚机里运行容器?答案是肯定的。

在最新发布的kata1.5中,已经开始支持firecracker。通过k8s pod的runtimeClass字段来设定pod内容器的运行时。runtimeClass也是k8s的新增的字段,属于beta版。目前支持的最小粒度为pod,即一个node上可以运行多种runtime的pod,但是每个pod内部的容器的运行时必须相同。因为在一个pod内,容器和虚机的数据共享和网络通信是非常的麻烦,超出了k8s目前的能力。但是抽象到pod层以上,在k8s看来,虚机和pod就没有差异了。
ff0273c0467789e1c173b38f20fe76c3

但是,限于firecracker本身功能过于简单,因为其设计之初就是追求最少的设备、最简洁的功能,firecracker目前很多k8s的功能还不支持,比如volume、secret、configmap等[1]。如果应用比较复杂,对运行环境的要求比较高,就只能使用qemu vm。

更多关于kata对firecracker hypervisor的支持实现方面的细节可以参考:

1、Firecracker hypervisor接口的实现:firecracker: VMM API support

2、添加firecracker作为qemu之后的新的一种hypervisor选项:virtcontainers: Add firecracker as a supported hypervisor

3、目前kata+fc的限制:Firecracker limitations

3、官方Demo演示

总结

从firecracker去年开始问世至今,将firecracker融入如今的容器生态一直是AWS和开源社区在致力推进。从最开始初见雏形的containerd+firecracker到如今已经接近成熟的kata+firecracker,未来firecracker在容器生态中将处于什么样的地位,是通过containerd和kata成为qemu一样的runtime选项,还是作为serverless容器底层沙箱的的标准(类似如今AWS的Lambda和 Fargat),现在还不能有定论。但是serverless领域对极简极快的追求和firecracker这种极简的VMM设计是完全契合的。qemu是没有做到最精简,依然有很多不必要的模块。用firecracker替换qemu(或者借鉴firecracker的思路构建自己的面向severless的轻量级容器底层沙箱)是未来可以尝试的方向。

引用资料

[1] https://github.com/kata-containers/documentation/issues/351

[2] https://github.com/kata-containers/runtime/releases/tag/1.5.0

[3] https://github.com/kata-containers/runtime/commit/c1d3f1a98b0f8be6a2353cf288cf94b6f27cc57c

[4] https://github.com/kata-containers/runtime/commit/e65bafa79371704090b81e89e145807b35dfd648

[5] https://github.com/kubernetes/kubernetes

[6] https://github.com/kubernetes-sigs/cri-o

[7] https://github.com/containerd/cri

[8] https://github.com/kubernetes/frakti

[9] https://github.com/kubernetes-incubator/rktlet

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
Kubernetes Linux API
[没接触过kubevirt?]15分钟快速入门kubevirt
什么是kubevirt? kubevirt是一个容器方式运行虚拟机的项目。`kubevirt`是附加`kubernetes`集群上的,它是通过 `CustomResourceDefinition(CRD)`部署到`Kubernetes API`变成资源对象。使用方式类似创建`deploy、pod`......这些资源清单。
5403 0
[没接触过kubevirt?]15分钟快速入门kubevirt
|
存储 Dragonfly 缓存
Nydus:开源的下一代容器镜像加速服务
让更多的容器用户能够体验到容器快速启动和安全加载方面的能力。
7889 0
Nydus:开源的下一代容器镜像加速服务
|
存储 Kubernetes 搜索推荐
使用容器方式创建firecracker虚拟机
使用容器方式创建firecracker虚拟机
527 1
|
运维 安全 Linux
计算机架构“寒武纪爆发”,操作系统进化迸发中国浪潮
计算机架构“寒武纪爆发”,操作系统进化迸发中国浪潮
|
Kubernetes Docker 容器
k8s学习-CKS真题-Runtime设置gVisor
k8s学习-CKS真题-Runtime设置gVisor
248 0
|
Rust 运维 安全
Kata3.0.0 x LifseaOS x 龙蜥内核三管齐下!带你体验最新的安全容器之旅
袋鼠RunD正式成为安全容器上游社区最新3.0.0标准,龙蜥也已推出最新体验包,带给大家更完整的安全容器体验。
Kata3.0.0 x LifseaOS x 龙蜥内核三管齐下!带你体验最新的安全容器之旅
|
存储 资源调度 安全
ATC'22顶会论文RunD:高密高并发的轻量级 Serverless 安全容器运行时 | 龙蜥技术
RunD可以在88毫秒内启动,并且在104核384GB内存的单节点上每秒启动超过200个安全容器。
ATC'22顶会论文RunD:高密高并发的轻量级 Serverless 安全容器运行时 | 龙蜥技术
|
Rust 安全 Go
如何在Anolis OS上轻松使用 Kata 安全容器
本篇文章我们将详细介绍怎么轻松在 Anolis OS 上使用 Kata Containers 安全容器
如何在Anolis OS上轻松使用 Kata 安全容器
下一篇