天天看點

Kubernetes + CRI + Kata + Firecracker

Kata

源自希臘文Καταπίστευμα(ka-ta-PI-stev-ma),原意是值得信任的人,kata container正是解容器安全的問題而誕生的。傳統的容器是基于namespace和cgroup進行隔離,在帶來輕量簡潔的同時,也帶來了安全的隐患。事實上容器雖然提供一個與系統中的其它程序資源相隔離的執行環境,但是與主控端系統是共享核心的,一旦容器裡的應用逃逸到核心,後果不堪設想,尤其是在多租戶的場景下。Kata就是在這樣的背景下應運而生,kata很好的權衡了傳統虛拟機的隔離性、安全性與容器的簡潔、輕量。這一點和firecracker很相似,都是輕量的虛拟機。但是他們的本質的差別在于:kata雖然是基于虛機,但是其表現的卻跟容器是一樣的,可以像使用容器一樣使用kata;而firecracker雖然具備容器的輕量、極簡性,但是其依然是虛機,一種比QEMU更輕量的VMM,暫時不能相容容器生态。

Kata的基本原理是,為每一個容器單獨開一個虛機(如果是k8s下作為runtime,則是一個pod對應一個虛機而不是容器),具有獨立的核心,這樣傳遞的容器就具備了虛機級别的隔離和安全性。kata的原理圖如下所示:

Kubernetes + CRI + Kata + Firecracker

Kata container作為

OCI

标準的成員之一,其kata-runtime也是相容OCI标準,和runc處在同一個層級,對安全和隔離性要求高的場景,可以從Docker或者k8s預設的runtime(比如runc)切到kata-runtime。

CRI

CRI基本原理

早期的k8s使用docker作為預設的runtime,後來又加入rkt,每加入一種新運作時,k8s都要修改接口來關聯新的容器運作時。随着越來越多的容器運作時想加入k8s運作時,而且不同的容器的實作和功能差異很大,比如docker已經再不是一個單純的容器運作時,這時候亟需一套标準來定義k8s支援的運作時。這套标準就是

(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工作的流程:

Kubernetes + CRI + Kata + Firecracker

CRI分類

CRI的第一個實作就是k8s自己提供的針對Docker運作時的dockerShim,也是目前k8s使用docker的标準方式,已經內建在k8s的源碼中。如今CRI的衆多實作中除了dockershim外,比較具有代表性的還有有

CRI-containerd

,

CRI-O

以及

frakti

rkt

作為k8s最早支援的運作時之一,現在也開始轉向标準的CRI實作

rktlet

,這是k8s未來使用rkt的标準方式。該項目的目标就是像現在的dockerShim一樣,而社群貌似并不活躍。目前主流的幾種CRI實作的生産容器的流程圖如下所示:

Kubernetes + CRI + Kata + Firecracker

其中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的實作。

Kubernetes + CRI + Kata + Firecracker

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的流程,

和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。

本節重點分析

的實作。如果說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。

Kubernetes + CRI + Kata + Firecracker

Kata + Firecracker

Kubernetes + CRI + Kata + Firecracker

在沒有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就沒有差異了。

Kubernetes + CRI + Kata + Firecracker

但是,限于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

繼續閱讀