# Kubernetes 中的容器运行时接口

原文：[Introducing Container Runtime Interface (CRI) in Kubernetes](https://kubernetes.io/blog/2016/12/container-runtime-interface-cri-in-kubernetes)

> 文中多次出现了个单词 `shim`，胡翻成代理了，虽然垫片还是比鲁棒啥的好听。。

归根结底，Kubernetes Node 的最底层就是启动和停止容器的组件了，这一部分我们称之为容器运行时（ Container Runtim ），这其中最知名的也就是 Docker 了，这一领域正在快速成长，他并不孤独。为了让 Kubernetes 更具扩展性，我们投入了不少精力，在 Kubernetes 中加入了容器运行时插件 API，我们称之为 “CRI”。

## CRI 是什么？Kubernetes 需要他么？

每一种容器运行时都有其长处，因此不少用户希望 Kubernetes 能够支持更多的运行时。在 Kubernetes 1.5 中，我们引入了 [CRI](https://github.com/kubernetes/kubernetes/blob/242a97307b34076d5d8f5bbeb154fa4d97c9ef1d/docs/devel/container-runtime-interface.md)，这一插件接口让 Kubernetes 无需重新编译就可以使用更多的容器运行时。CRI 包含 [Protocol Buffers](https://developers.google.com/protocol-buffers/)、[gRPC API](http://www.grpc.io/)、以及[运行库](https://github.com/kubernetes/kubernetes/blob/release-1.5/pkg/kubelet/server/streaming/server.go)支持，还有尚在开发的标准规范和工具。 CRI 在 [Kubernetes 1.5](http://blog.kubernetes.io/2016/12/kubernetes-1.5-supporting-production-workloads.html) 中发布了 Alpha 版本。

可替代的容器运行时支持是 Kubernetes 中的新概念。在 1.3 时，我们发布了 [rktnetes](http://blog.kubernetes.io/2016/07/rktnetes-brings-rkt-container-engine-to-Kubernetes.html) 项目，让 [rkt 容器引擎](https://github.com/coreos/rkt) 成为 Docker 之外的又一选择。然而不管是 Docker 还是 rkt，都是用的 Kubelet 的内部接口，同 Kubelet 源码纠缠不清。这种程度的集成，需要对 Kubelet 内部机制有非常深入的了解，还会给社区带来管理压力。这样就给新生代容器运行时造成了难于跨越的集成壁垒。我们用清晰定义的抽象层清除了这一壁垒，让开发者能够专注于容器运行时本身。在通向插件式容器支持以及建设健康生态环境的路上，这是一小步，也是重要的一步。

## CRI 概览

Kubelet 使用 gRPC 框架利用 Unix socket 同容器运行时（或者是 CRI 代理）进行通信，这一过程中 Kubelet 是客户端，CRI 代理是服务端。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745852143703/82450671-eb3e-40d1-915b-388fbb4f313d.png align="center")

Protocol Buffers [API](https://github.com/kubernetes/kubernetes/blob/release-1.5/pkg/kubelet/api/v1alpha1/runtime/api.proto) 包含两个 gRPC 服务，`ImageService` 和 `RuntimeService`。`ImageService` 提供从仓库拉取镜像、查看和移除镜像的功能。`RuntimeService` 包含了对 Pod 和容器的生命周期管理、和容器的交互（ exec/attach/port-forward ）。rtk 和 Docker 这样的容器运行时可以利用一个 Socket 同时提供两个服务。在 Kubelet 中可以用 `--container-runtime-endpoint` 和 `--image-service-endpoint` 参数设置这个 socket。

## Pod 和容器的生命周期管理

```go
service RuntimeService {
    // Sandbox operations.
    rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
    rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
    rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
    rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
    rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
    // Container operations.
    rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
    rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
    rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
    rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
    rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
    rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
    ...
}
```

Pod 由一组应用容器组成，其中包含了共有的环境和资源约束。在 CRI 里，这个环境被称为 `PodSandbox`。我们有意的给容器运行时留下了一些发挥空间，他们可以根据自己的内部实现来解释 `PodSandbox`。对于 Hypervisor 类的运行时，`PodSandbox` 会具体化为一个虚拟机。其他的例如 Docker，会是一个 Linux 命名空间。在 v1alpha1 API 中，Kubelet 会创建 Pod 级别的 cgroup 传递给容器运行时，并以此运行所有进程来保障 PodSandbox 对 Pod 的资源保障。

在启动 Pod 之前，Kubelet 调用 `RuntimeService.RunPodSandbox` 来创建环境。这一过程包括为 Pod 设置网络（分配 IP）。`PodSandbox` 激活之后，就可以独立的创建、启动、停止和删除不同的容器了。Kubelet 会在停止和删除 `PodSandbox` 之前首先停止和删除其中的容器。

Kubelet 的职责在于通过 RPC 管理容器的生命周期，实现容器生命周期的钩子，以及存活和健康监测，执行 Pod 的重启策略等。

## 为什么 CRI 是围绕容器进行的？

Kubernetes 有一个 Pod 资源的接口。我们曾经可能采用的一个 CRI 的设计就是抽象复用 Pod 对象，容器运行时就可以自行实现自己的控制逻辑和状态转换，这样一来，就能极大地简化 API，让 CRI 能够更广泛的适用于多种容器运行时。但是经过深入讨论之后，我们放弃了这一想法。

首先，Kubelet 有很多的 Pod 级功能和机制（例如循环崩溃的处理），交给容器运行时实现的话，会造成很重的负担；第二，更重要的是，Pod 标准还在高速前进。很多的新功能（例如容器初始化）是由 Kubelet 直接管理容器的，而无需容器运行时进行变更。

CRI 选择了围绕容器进行实现，这样容器运行时能够共享这些通用特性，获得更好的开发进度。这并不意味着我们设计哲学的改变 —— Kubelet 要负责保证实际状态和声明状态的一致性。

### Exec/attach/port-forward 请求

```go
service RuntimeService {
    ...
    // ExecSync runs a command in a container synchronously.
    rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
    // Exec prepares a streaming endpoint to execute a command in the container.
    rpc Exec(ExecRequest) returns (ExecResponse) {}
    // Attach prepares a streaming endpoint to attach to a running container.
    rpc Attach(AttachRequest) returns (AttachResponse) {}
    // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
    rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}
    ...
}
```

Kubernetes 为用户提供了和 Pod 以及其中的容器进行交互的能力（`kubectl exec/attach/port-forward`）。Kubelet 目前支持两种方式来支持这些功能：调用容器的本地方法，或者使用 Node 上的工具（例如 `nsenter` 以及 `socat`）。因为多数工具假设 Pod 利用 Linux namespace 做了隔离，因此使用 Node 上的工具并不是一个可移植的方案。在 CRI 中，我们显式的定义这些调用，让运行时可以做特定实现。

当下还有一个潜在问题是，Kubelet 处理所有的请求连接，所以他有成为 Node 通信瓶颈的可能。在设计 CRI 的时候，我们采纳了一些反馈，让运行时能够排除中间人。容器运行时可以启动一个单独的流服务器处理请求（还能为 Pod 的资源使用进行记录），并把服务器地址返回给 Kubelet。这样 Kubelet 就能反馈信息给 API Server，使之可以直接连接到容器运行时的服务，并连接到客户端。

CRI 还有很多本文没有提及的内容，可以参考[设计文档](https://github.com/kubernetes/community/blob/master/contributors/devel/container-runtime-interface.md#design-docs-and-proposals)来获得更多内容。

## 当前状态

虽然 CRI 还比较初级，但也已经有了很多项目在尝试把各种容器运行时纳入 CRI：

* [cri-o](https://github.com/kubernetes-incubator/cri-o)：OCI 兼容运行时
    
* [rktlet](https://github.com/kubernetes-incubator/rktlet)：rkt 容器运行时。
    
* [frakti](https://github.com/kubernetes/frakti)：基于 hypervisor 的容器运行时。
    
* [Docker CRI 代理](https://github.com/kubernetes/kubernetes/tree/release-1.5/pkg/kubelet/dockershim)
    

如果对这些运行时感兴趣，可以浏览一下他们的仓库，获取最新的进展情况和相关资料。

开发者如果有兴趣集成新的容器运行时，需要阅读[开发者指南](https://github.com/kubernetes/community/blob/master/contributors/devel/container-runtime-interface.md)，会在这里得到 API 中已知的限制和问题。我们乐于从早期开发者这里获取反馈，加以改进来促进项目成长。开发者需要有对突发故障的心理准备，谁让我们还在 Alpha 呢。

## 尝试新的 CRI Docker

Kubelet 还没有把 CRI 作为缺省选项，我们正在积极促成这一转变。第一步就是用 CRI 的方式重新对 Docker 进行 Kubelet 的集成。在 1.5 中，我们让 Kubelet 开始支持 CRI，还给 Kubelet 加入了内嵌的 Docker CRI 代理。这样 Kubelet 就可以启动 Docker 的 gRPC 服务了。要尝试新的 Kubelet-CRI-Docker 集成，只需要简单的给 API-Server 参数加上 `--feature-gates=StreamingProxyRedirects=true` 开关，就启用了新的请求重定向特性，然后用 `--experimental-cri=true` 开关来启动 Kubelet。

虽说现在的实现还有少量[功能缺失](https://github.com/kubernetes/community/blob/master/contributors/devel/container-runtime-interface.md#docker-cri-integration-known-issues)，不过已经通过了主要的端到端测试，我们计划扩展测试覆盖范围，也再次邀请社区多多提供反馈来促进我们的工作。

## Minikube 和 CRI

如果想要测试新功能，却没时间部署新的测试集群。[Minikube](https://github.com/kubernetes/minikube) 让你能够快速的启动一个本地集群。

* 检查可用的 Kubernetes 版本，选择最新的 1.5.x，这里使用的是 v1.5.9-beta.1：`minikube get-k8s-versions`
    
* 启动一个带有内置 Docker CRI 集成的 Minikube 集群：
    
    $ minikube start --kubernetes-version=v1.5.0-beta.1  
    \--extra-config=kubelet.EnableCRI=true  
    \--network-plugin=kubenet  
    \--extra-config=kubelet.PodCIDR=10.180.1.0/24  
    \--iso-url=http://storage.googleapis.com/minikube/iso/buildroot/minikube-v0.0.6.iso
    

`--extra-config=kubelet.EnableCRI=true` 启用 Kubelet 的 CRI 实现，`--network-plugin=kubenet` 和 `--extra-config=kubelet.PodCIDR=10.180.1.0/24` 为网络插件提供了网络设置，分配 PodCIDR 给 Node。这里也可以使用 `cni` 插件，就无需依赖 PodCIDR 了。`--iso-url` 给 Minikube 指定一个例子中使用的 ISO 镜像。

* 检查 Minikube 日志，确认 CRI 的启用
    
    $ minikube logs | grep EnableCRI I1209 01:48:51.150789 3226 localkube.go:116\] Setting EnableCRI to true on kubelet.
    
* 创建一个 Pod 并检查状态，应该会看到 "SandboxReceived" 事件，这表明 Kubelet 正在使用 CRI
    
    $ kubectl run foo --image=gcr.io/google\_containers/pause-amd64:3.0 deployment "foo" created $ kubectl describe pod foo ... ... From Type Reason Message ... ----------------- ----- --------------- ----------------------------- ...{default-scheduler } Normal Scheduled Successfully assigned foo-141968229-v1op9 to minikube ...{kubelet minikube} Normal SandboxReceived Pod sandbox received, it will be created. ...
    

> 注意 `kubectl attach/exec/port-forward` 目前还不能对启用 CRI 模式的 Miniqube 生效，[新版本](https://github.com/kubernetes/minikube/issues/896)将会加入支持。

## 相关引用

* **CRI**：`https://github.com/kubernetes/kubernetes/blob/242a97307b34076d5d8f5bbeb154fa4d97c9ef1d/docs/devel/container-runtime-interface.md`
    
* **cri-o**：`https://github.com/kubernetes-incubator/cri-o`
    
* **rktlet**：`https://github.com/kubernetes-incubator/rktlet`
    
* **frakti**：`https://github.com/kubernetes/frakti`
    
* **Docker CRI 代理**：`https://github.com/kubernetes/kubernetes/tree/release-1.5/pkg/kubelet/dockershim`
