导读
谈到OCI,会想到docker,先有docker后有OCI,说到docker就说到容器技术, docker不是最早的容器技术,比如早期的chroot Jails,Solaris Containers等,但是是docker把容器技术推向了巅峰,让更多人关注并使用了容器技术
docker自2013发布以来,github中docker的代码活跃度居高不下,更多的个人或企业使用docker,容器就是docker也逐渐被大家认可,如果docker就是容器的标准,那其他容器怎么办(比如coreOS推出的rkt),
其次容器上层的编排工具(容器集群调度,比如kubernetes,mesos等)和docker紧密耦合,docker 接口的变化导致上层编排的稳定性甚至服务异常.
如果容器以docker作为标准,那么docker接口的变化可能导致社区中所有相关工具都要更新,不然就无法使用,如果没有标准,这将导致容器实现的碎片化,出现大量的冲突和冗余,
这两种情况都是社区不愿意看到的事情,于是OCI出现了
它的核心目标围绕容器的格式和运行时制定一个开放的工业化标准,并推动这个标准,保持容器的灵活性和开放性,容器能运行在任何的硬件和系统上.
容器不应该绑定到特定的客户机或编排堆栈,不应该与任何特定的供应商紧密关联,并且可以跨多种操作系统
官网上对 OCI 的介绍如下:
OCI由docker以及其他容器行业领导者创建于2015年,目前主要有两个标准:容器运行时标准(runtime-spec)和容器镜像标准(image-spec)
这两个标准通过OCI runtime filesytem bundle的标准格式连接在一起,OCI镜像可以通过工具转换成bundle,然后 OCI 容器引擎能够识别这个bundle来运行容器
文档主要做了两个事情:
- 创建镜像的规则
- 运行镜像的规则
-
容器镜像标准(image-spec)
- 文件系统: 以layer保存的文件系统,每个layer保存了和上层之间变化的部分,layer应该保存哪些文件,怎么表示增加、修改和删除的文件等
- config文件: 保存了文件系统的层级信息(每个层级的hash值,以及历史信息)以及容器运行时需要的一些信息(比如环境变量、工作目录、命令参数、mount 列表)
- manifest文件: 镜像的config文件索引,有哪些layer,额外的annotation信息,manifest文件中保存了很多和当前平台有关的信息
- index文件: 可选的文件,指向不同平台的manifest文件,这个文件能保证一个镜像可以跨平台使用,每个平台拥有不同的manifest文件,使用index作为索引
-
容器运行时标准(runtime spec)
容器的状态包括如下属性
- ociVersion: OCI版本
- id: 容器的ID,在宿主机唯一
-
status: 容器运行时状态,生命周期
- creating: 使用 create 命令创建容器,这个过程称为创建中,创建包括文件系统、namespaces、cgroups、用户权限在内的各项内容
- created: 容器创建出来,但是还没有运行,表示镜像和配置没有错误,容器能够运行在当前平台
- running: 容器的运行状态,里面的进程处于up状态,正在执行用户设定的任务
- stopped: 容器运行完成,或者运行出错或者stop命令之后,容器处于暂停状态,这个状态,容器还有很多信息保存在平台中,并没有完全被删除
- pid: 容器进程在宿主机的进程ID
- bundle: 容器文件目录,存放容器rootfs及相应配置的目录
- annotations: 与容器相关的注释
了解了OCI标准, 我们再看一下docker架构的演变,早期docker所有功能都在docker daemon里面的,但是之后功能越来越多,越来越重,同时响应社区兼容OCI标准,docker做了架构调整
调整后将容器运行时相关的程序从docker daemon剥离出来,形成了containerd, Containerd向docker提供运行容器的API,二者通过gRPC进行交互, containerd最后会通过runC来实际运行容器,
调整完后的docker架构图如下(docker 1.11版本以后)
runC的前身是docker的libcontainer项目,在libcontainer的基础上做了封装,捐赠给OCI的一个符合标准的runtime实现,上图可以看出docker引擎内部也是基于runC构建的
关于libcontainer和runC stackoverflow 的一个回答:
runC只做一件事情就是运行容器,提供创建和运行容器的CLI(command-line interface)工具, runC直接与容器所依赖的cgroup/namespace linux kernel等进行交互,
负责为容器配置cgroup/namespace等启动容器所需的环境,创建启动容器的相关进程
关于containerd-shim 看一下Michael Crosby (runC,containerd作者)的解释
containerd-shim进程由containerd进程拉起,即containerd进程是containerd-shim的父进程, 容器进程由containerd-shim进程拉起, 这样的优点比如升级,重启docker或者containerd 不会影响已经running的容器进程, 而假如这个父进程就是containerd,那每次containerd挂掉或升级,整个宿主机上所有的容器都得退出了. 而引入了 containerd-shim 就规避了这个问题(当 containerd 退出或重启时, shim 会 re-parent 到 systemd 这样的 1 号进程上)
containerd是一个简单的守护进程,它可以使用runC管理容器,使用gRPC暴露容器的其他功能. 相比较Docker引擎使用gRPC, containerd暴露出针对容器的增删改查的接口,然而Docker引擎只是使用 full-blown HTTP API接口对Images,Volumes,network,builds等暴露出这些方法
了解了OCI,以及docker在兼容OCI标准架构的调整后, 迎来我们的重点 CRI
CRI是kubernetes推出的一个标准, 推出标准可见其在容器编排领域的地位
讲CRI之前我们先简单了解一下kubelet拉起一个容器的过程,如下:
- Kubelet通过CRI接口(gRPC)调用docker-shim,请求创建一个容器这一步中,kubelet可以视作一个简单的CRI Client,而docker-shim就是接收请求的Server,
注意的是docker-shim是内嵌在Kubelet中的 - docker-shim收到请求后,转化成Docker Daemon能听懂的请求,发到Docker Daemon上请求创建一个容器
- Docker Daemon请求containerd创建一个容器
- containerd收到请求后创建一个containerd-shim进程,通过containerd-shim操作容器,容器进程需要一个父进程来做诸如收集状态, 维持stdin等fd打开等工作
- containerd-shim在调用runC来启动容器
- runC 启动完容器后本身会直接退出,containerd-shim则会成为容器进程的父进程,负责收集容器进程的状态,上报给containerd
通过上面kubelet创建容器的流程, 我们可以看到kubelet通过CRI的标准来与外部容器运行时进行交互
kubernetes 早期版本1.5之前内置了docker和rkt,也就是支持两种运行时, 这个时候如果用户想自定义运行时就比较痛苦了,需要修改kubelet源码
同时不同的容器运行时各有所长,随着k8s在容器编排领域里面老大的地位,许多用户希望kubernetes支持更多的容器运行时,满足不同用户,不同环境的使用
于是从kubernetes1.5开始增加了CRI接口, 有了CRI接口无需修改kubelet源码就可以支持更多的容器运行时
于此同时内置的docker和rtk逐渐从kubernetes源码中移除,到kubernetes1.11版本Kubelet内置的rkt代码删除,CNI的实现迁移到dockers-shim之内,
除了docker之外,其他的容器运行时都通过CRI接入.
外部的容器运行时一般称为CRI shim,它除了实现CRI接口外,也要负责为容器配置网络,即CNI,有了CNI可以支持社区内的众多网络插件.
CRI主要定义两个接口, ImageService和RuntimeService,如下图
- ImageService:负责镜像的生命管理周期
- 查询镜像列表
- 拉取镜像到本地
- 查询镜像状态
- 删除本地镜像
- 查询镜像占用空间
- RuntimeService:负责管理Pod和容器的生命周期
- PodSandbox 的管理接口
PodSandbox是对kubernete Pod的抽象,用来给容器提供一个隔离的环境(比如挂载到相同的cgroup下面)并提供网络等共享的命名空间.PodSandbox通常对应到一个Pause容器或者一台虚拟机 - Container 的管理接口
在指定的 PodSandbox 中创建、启动、停止和删除容器。 - Streaming API接口
包括Exec、Attach和PortForward 等三个和容器进行数据交互的接口,这三个接口返回的是运行时Streaming Server的URL,而不是直接跟容器交互 - 状态接口
包括查询API版本和查询运行时状态
- PodSandbox 的管理接口
- cri-o:同时兼容OCI和CRI的容器运行时
- cri-containerd:基于Containerd的Kubernetes CRI 实现
- rkt:由CoreOS主推的用来跟docker抗衡的容器运行时
- docker:kuberentes最初就开始支持的容器运行时,目前还没完全从kubelet中解耦,docker公司同时推广了OCI标准
- Kata Containers:符合OCI规范同时兼容CRI
- gVisor:由谷歌推出的容器运行时沙箱(Experimental)
容器生态可以下面的三层抽象:
- Orchestration API: kubernetes API标准就是这层的标准,无可非议
- Container API: 标准就是CRI
- Kernel API: 标准就是OCI
参考资料
- https://www.opencontainers.org/
- https://blog.docker.com/2017/07/demystifying-open-container-initiative-oci-specifications/
- https://github.com/kubernetes/community/blob/master/contributors/devel/sig-node/container-runtime-interface.md
- https://searchitoperations.techtarget.com/definition/Open-Container-Initiative
- https://github.com/opencontainers/
- https://github.com/containerd/containerd
- https://stackoverflow.com/questions/31213126/libcontainer-vs-docker-vs-ocf-vs-runc
- https://aleiwu.com/post/cncf-runtime-landscape/
- https://groups.google.com/forum/#!topic/docker-dev/zaZFlvIx1_k
- https://feisky.xyz/posts/kubernetes-container-runtime/