容器开放接口规范(OCI、CRI、CNI)
- 云容器
- 2022-08-09
- 13热度
- 0评论
OCI(Open Container Initiative)
Linux基金会于2015年6月成立OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准,目前主要有两个标准文档:
- 容器运行时标准 (runtime spec)
- 容器镜像标准(image spec)
制定容器格式标准的宗旨概括来说就是不受上层结构的绑定,如特定的客户端、编排栈等,同时也不受特定的供应商或项目的绑定,即不限于某种特定操作系统、硬件、CPU架构、公有云等。
这两个协议通过 OCI runtime filesytem bundle 的标准格式连接在一起,OCI 镜像可以通过工具转换成 bundle,然后 OCI 容器引擎能够识别这个 bundle 来运行容器。
image spec(容器标准包)
OCI 容器镜像主要包括几块内容:
-
文件系统
- 以 layer 保存的文件系统,每个 layer 保存了和上层之间变化的部分,layer 应该保存哪些文件,怎么表示增加、修改和删除的文件等。
-
config 文件
- 保存了文件系统的层级信息(每个层级的 hash 值,以及历史信息),以及容器运行时需要的一些信息(比如环境变量、工作目录、命令参数、mount 列表),指定了镜像在某个特定平台和系统的配置。比较接近我们使用 docker inspect \<image_id> 看到的内容。
-
manifest 文件
- 镜像的 config 文件索引,有哪些 layer,额外的 annotation 信息,manifest 文件中保存了很多和当前平台有关的信息。
-
index 文件
- 可选的文件,指向不同平台的 manifest 文件,这个文件能保证一个镜像可以跨平台使用,每个平台拥有不同的 manifest 文件,使用 index 作为索引
runtime spec(容器运行时和生命周期)
容器标准格式也要求容器把自身运行时的状态持久化到磁盘中,这样便于外部的其它工具对此信息使用和演绎。该运行时状态以JSON格式编码存储。推荐把运行时状态的JSON文件存储在临时文件系统中以便系统重启后会自动移除。基于Linux内核的操作系统,该信息应该统一地存储在/run/opencontainer/containers
目录,该目录结构下以容器ID命名的文件夹(/run/opencontainer/containers/<containerID>/state.json
)中存放容器的状态信息并实时更新。有了这样默认的容器状态信息存储位置以后,外部的应用程序就可以在系统上简便地找到所有运行着的容器了。
state.json 包含信息如下:
- 版本信息
- 存放OCI标准的具体版本号。
- 容器ID
- 通常是一个哈希值,也可以是一个易读的字符串。在state.json文件中加入容器ID是为了便于之前提到的运行时hooks只需载入state.json就- - 可以定位到容器,然后检测state.json,发现文件不见了就认为容器关停,再执行相应预定义的脚本操作。
- PID
- 容器中运行的首个进程在宿主机上的进程号。
- 容器文件目录
- 存放容器rootfs及相应配置的目录。外部程序只需读取state.json就可以定位到宿主机上的容器文件目录。
- 容器创建
- 创建包括文件系统、namespaces、cgroups、用户权限在内的各项内容。
- 容器进程的启动
- 运行容器进程,进程的可执行文件定义在的config.json中,args项。
- 容器暂停
- 容器实际上作为进程可以被外部程序关停(kill),然后容器标准规范应该包含对容器暂停信号的捕获,并做相应资源回收的处理,避免孤儿进程的出现。
容器生命周期
- init 状态
- 笔者自己添加的状态,表示没有容器存在的初始状态,标准中并无此状态
- creating 状态
- 使用
create
命令创建容器,此过程称为创建中。创建包括文件系统、namespaces、cgroups、用户权限在内的各项内容
- 使用
- running 状态
- 容器的运行状态,里面的进程处于 up 状态,正在执行用户设定的任务
- stopped 状态
- 容器运行完成,或者运行出错,或者 stop 命令之后,容器处于暂停状态。这个状态,容器还有很多信息保存在平台中,并没有完全删除
CRI(Container Runtime Interface)
介绍
CRI 是 kubernetes 推出的一个标准,容器运行时接口简称 CRI ,CRI中定义了容器和镜像的服务接口,因为容器运行时与镜像的生命周期是彼此隔离的,因此要定义两个服务,该接口使用 Protocol Buffer(序列化),基于 gRPC
架构
Container Runtime实现了CRI gRPC Server,包括RuntimeService和ImageService。该gRPC Server需要监听本地的Unix socket,而kubelet则作为gRPC Client运行
Kubelet与容器运行时通信(或者是CRI插件填充了容器运行时)时,Kubelet就像是客户端,而CRI插件就像对应的服务器。它们之间可以通过Unix 套接字或者gRPC框架进行通信。
CRI 接口
两个gRPC服务
- RuntimeService:容器和Sandbox运行时管理,负责管理Pod和容器的生命周期
- PodSandbox 的管理接口
- PodSandbox是对kubernete Pod的抽象,用来给容器提供一个隔离的环境(比如挂载到相同的cgroup下面)并提供网络等共享的命名空间。PodSandbox通常对应到一个Pause容器或者一台虚拟机。
- Container 的管理接口
- 在指定的 PodSandbox 中创建、启动、停止和删除容器。
- Streaming API接口
- 包括Exec、Attach和PortForward 等三个和容器进行数据交互的接口,这三个接口返回的是运行时Streaming Server的URL,而不是直接跟容器交互。
- 状态接口
- 包括查询API版本和查询运行时状态。
- PodSandbox 的管理接口
- ImageService:提供了从镜像仓库拉取、查看、和移除镜像的RPC,负责镜像的生命管理周期
- 查询镜像列表、拉取镜像到本地、查询镜像状态、删除本地镜像、查询镜像占用空间
容器生态三层抽象:
- Orchestration API: kubernetes API标准就是这层的标准,无可非议 >>
- Container API: 标准就是CRI >>
- Kernel API: 标准就是OCI
CRI 启用
除非集成了 rktnetes,否则 CRI 都是被默认启用了,从 Kubernetes 1.7 版本开始,旧的预集成的 docker CRI 已经被移除。
要想启用 CRI 只需要在 kubelet 的启动参数重传入此参数:--container-runtime-endpoint 远程运行时服务的端点。当前 Linux 上支持 unix socket,windows 上支持 tcp。例如:unix:///var/run/dockershim.sock、 tcp://localhost:373,默认是 unix:///var/run/dockershim.sock,即默认使用本地的 docker 作为容器运行时。
当前支持的 CRI 后端
我们最初在使用 Kubernetes 时通常会默认使用 Docker 作为容器运行时,其实从 Kubernetes 1.5 开始已经支持 CRI,通过 CRI 接口可以指定使用其它容器运行时作为 Pod 的后端,目前支持 CRI 的后端有:
- cri-o:cri-o 是 Kubernetes 的 CRI 标准的实现,并且允许 Kubernetes 间接使用 OCI 兼容的容器运行时,可以把 cri-o 看成 Kubernetes 使用 OCI 兼容的容器运行时的中间层。
- cri-containerd:基于 Containerd 的 Kubernetes CRI 实现
- rkt:由 CoreOS 主推的用来跟 docker 抗衡的容器运行时
- frakti:基于 hypervisor 的 CRI
- docker:Kuberentes 最初就开始支持的容器运行时,目前还没完全从 kubelet 中解耦,Docker 公司同时推广了 OCI 标准
CNI(Container Network Interface)
容器网络接口,是 google 和 CoreOS 主导制定的容器网络标准,是 CNCF 旗下的一个项目,由一组用于配置 Linux 容器的网络接口的规范和库组成,同时还包含了一些插件。CNI 仅关心容器创建时的网络分配,和当容器被删除时释放网络资源。它本身并不是实现或者代码,可以理解成一个协议。这个标准是在 rkt 网络提议的基础上发展起来的,综合考虑了灵活性、扩展性、ip 分配、多网卡等因素。Kubernetes 内置 CNI
CNI 出现的原因
不管是 Docker 还是 Kubernetes,在网络方面目前都没有一个完美的、终极的、普适性的解决方案,不同的用户和企业因为各种原因会使用不同的网络方案。
目前存在网络方案 flannel、calico、openvswitch、weave、ipvlan等,而且以后一定会有其他的网络方案,这些方案接口和使用方法都不相同,而不同的容器平台都需要网络功能,它们之间的适配如果没有统一的标准,会有很大的工作量和重复劳动。
CNI 就是这样一个标准,它旨在为容器平台提供网络的标准化。不同的容器平台(比如目前的 kubernetes、mesos 和 rkt)能够通过相同的接口调用不同的网络组件。
架构
CNI 协议连接了两个组件:容器管理系统和网络插件。它们之间通过 JSON 格式的文件进行通信,实现容器的网络功能。具体的事情都是插件来实现的,包括:创建容器网络空间(network namespace)、把网络接口(interface)放到对应的网络空间、给网络接口分配 IP 等等。
官方网络插件
官方提供的插件目前分成三类:main、meta 和 ipam。main 是主要的实现了某种特定网络功能的插件;meta 本身并不会提供具体的网络功能,它会调用其他插件,或者单纯是为了测试;ipam 是分配 IP 地址的插件。ipam 并不提供某种网络功能,只是为了灵活性把它单独抽象出来,这样不同的网络插件可以根据需求选择 ipam,或者实现自己的 ipam。
-
main
- loopback:创建一个回环接口,这个插件很简单,负责生成 lo 网卡,并配置上 127.0.0.1/8 地址
- bridge:创建网桥,并添加主机和容器到该网桥,和 docker 默认的网络模型很像,把所有的容器连接到虚拟交换机上
- macvlan:创建一个新的 MAC 地址,将所有的流量转发到容器,使用 macvlan 技术,从某个物理网卡虚拟出多个虚拟网卡,它们有独立的 ip 和 mac 地址macvlan:使用 macvlan 技术,从某个物理网卡虚拟出多个虚拟网卡,它们有独立的 ip 和 mac 地址
- ipvlan:在容器中添加一个 ipvlan 接口,和 macvlan 类似,区别是虚拟网卡有着相同的 mac 地址
- ptp:创建 veth 对,通过 veth pair 在容器和主机之间建立通道
- vlan:分配一个 vlan 设备
-
meta
- flannel:根据 flannel 的配置文件创建接口,结合 bridge 插件使用,根据 flannel 分配的网段信息,调用 bridge 插件,保证多主机情况下容器
- tuning:调整现有接口的 sysctl 参数
- portmap:一个基于 iptables 的 portmapping 插件。将端口从主机的地址空间映射到容器。
-
ipam
- host-local:维护分配 IP 的本地数据库,基于本地文件的 ip 分配和管理,把分配的 IP 地址保存在文件中
- dhcp:在主机上运行守护程序,代表容器发出 DHCP 请求,从已经运行的 DHCP 服务器中获取 ip 地址
CRI 和 OCI 的区别
Open Container Initiative,也就是常说的OCI,是由多家公司共同成立的项目,并由linux基金会进行管理,致力于container runtime的标准的制定和runc的开发等工作。所谓container runtime,主要负责的是容器的生命周期的管理。oci的runtime spec标准中对于容器的状态描述,以及对于容器的创建、删除、查看等操作进行了定义。在k8s 1.5版本之后,kubernetes推出了自己的运行时接口api–CRI(container runtime interface)。cri接口的推出,隔离了各个容器引擎之间的差异,而通过统一的接口与各个容器引擎之间进行互动。与oci不同,cri与kubernetes的概念更加贴合,并紧密绑定。cri不仅定义了容器的生命周期的管理,还引入了k8s中pod的概念,并定义了管理pod的生命周期。在kubernetes中,pod是由一组进行了资源限制的,在隔离环境中的容器组成。而这个隔离环境,称之为PodSandbox。在cri开始之初,主要是支持docker和rkt两种。其中kubelet是通过cri接口,调用docker-shim,并进一步调用docker api实现的。后来,docker独立出来了containerd,kubernetes也顺应潮流,孵化了cri-containerd项目,用以将containerd接入到cri的标准中。为了进一步与oci进行兼容,kubernetes还孵化了cri-o,成为了架设在cri和oci之间的一座桥梁。
通过这种方式,可以方便更多符合oci标准的容器运行时,接入kubernetes进行集成使用。可以预见到,通过cri-o,kubernetes在使用的兼容性和广泛性上将会得到进一步加强。
CSI(Container Storage Interface)
背景
Kubernetes原生支持一些存储类型的 PV,如 iSCSI、NFS、CephFS 等等,这些 in-tree 类型的存储代码放在 Kubernetes 代码仓库中。这里带来的问题是 Kubernetes 代码与三方存储厂商的代码强耦合:
- 更改 in-tree 类型的存储代码,用户必须更新 Kubernetes组件,成本较高
- in-tree 存储代码中的 bug 会引发 Kubernetes组件不稳定
- Kubernetes社区需要负责维护及测试 in-tree 类型的存储功能
- in-tree 存储插件享有与 Kubernetes核心组件同等的特权,存在安全隐患
- 三方存储开发者必须遵循 KKubernetes8s 社区的规则开发 in-tree 类型存储代码
CSI 容器存储接口标准的出现解决了上述问题,将三方存储代码与 Kubernetes代码解耦,使得三方存储厂商研发人员只需实现 CSI 接口(无需关注容器平台是 Kubernetes还是 Swarm 等)。
介绍
容器存储接口(Container Storage Interface),简称 CSI,CSI 试图建立一个行业标准接口的规范,借助 CSI 容器编排系统(CO)可以将任意存储系统暴露给自己的容器工作负载。
csi 卷类型是一种 out-tree(即跟其它存储插件在同一个代码路径下,随 Kubernetes 的代码同时编译的) 的 CSI 卷插件,用于 Pod 与在同一节点上运行的外部 CSI 卷驱动程序交互。部署 CSI 兼容卷驱动后,用户可以使用 csi 作为卷类型来挂载驱动提供的存储。
CSI 持久化卷支持是在 Kubernetes v1.9 中引入的,作为一个 alpha 特性,必须由集群管理员明确启用。换句话说,集群管理员需要在 apiserver、controller-manager 和 kubelet 组件的 “--feature-gates =” 标志中加上 “CSIPersistentVolume = true”。
CSI 持久化卷具有以下字段可供用户指定:
- driver:一个字符串值,指定要使用的卷驱动程序的名称。必须少于 63 个字符,并以一个字符开头。驱动程序名称可以包含 “。”、“ - ”、“_” 或数字。
- volumeHandle:一个字符串值,唯一标识从 CSI 卷插件的 CreateVolume 调用返回的卷名。随后在卷驱动程序的所有后续调用中使用卷句柄来引用该卷。
- readOnly:一个可选的布尔值,指示卷是否被发布为只读。默认是 false。