云原生引入了不少新的观点和思维办法,也影响了运用所采取的实现技能。容器是云原生运用的根本性技能,其颠覆了运用的开拓、交付和运行模式,在云打算、互联网等领域得到了广泛运用。
本文选自《Harbor威信指南》一书,下面我们一同理解下容器的发展进程及基本事理。
容器技能的发展背景

在电子打算机刚涌现时,由于硬件本钱高昂,人们试图探求能够多用户共享打算资源的办法,以提高资源利用率和降落本钱。在20世纪60年代,基于硬件技能的主机虚拟化技能涌现了。一台物理主机可以被划分为多少个小的机器,每个机器的硬件互不共享,并可以安装各自的操作系统来利用。20世纪90年代后期,X86架构的硬件虚拟化技能逐渐兴起,可在同一台物理机上隔离多个操作系统实例,带来了很多的优点,目前绝大多数的数据中央都采取了硬件虚拟化技能。
虽然硬件虚拟化供应了分隔资源的能力,但是采取虚拟机办法隔离运用程序时,效率每每较低,毕竟还要在每个虚拟机中安装或复制一个操作系统实例,然后把运用支配到个中。因此人们探索出一种更轻量的方案——操作系统虚拟化,使面向运用的管理更便捷。所谓操作系统虚拟化,便是由操作系统创建虚拟的系统环境,使运用感知不到其他运用的存在,仿佛在独自霸占全部的系统资源,从而实现运用隔离的目的。在这种办法中不须要虚拟机,也能够实现运用彼此隔离,由于运用是共享同一个操作系统实例的,因此比虚拟机更节省资源,性能更好。操作系统虚拟化在不少系统里面也被称为容器(Container),下面也会以容器来指代操作系统虚拟化。
操作系统虚拟化最早涌如今2000年,FreeBSD 4.0推出了Jail。Jail加强和改进了用于文件系统隔离的chroot环境。到了2004年,Sun公司发布了Solaris 10的Containers,包括Zones和Resource management两部分。Zones实现了命名空间隔离和安全访问掌握,Resource management实现了资源分配掌握。2007年,Control Groups(简称cgroups)进入Linux内核,可以限定和隔离一组进程所利用的资源(包括CPU、内存、I/O和网络等)。
2013年,Docker公司发布Docker开源项目,供应了一系列简便的工具链来利用容器。绝不夸年夜地说,Docker公司率先点燃了容器技能的火焰,拉开了云原生运用变革的帷幕,促进容器生态圈提高神速地发展。截至2020年,Docker Hub中的镜像累计下载了1300亿次,用户创建了约600万个容器镜像库。从这些数据可以看到,用户正在以惊人的速率从传统模式切换到基于容器的运用发布和运维模式。
2015年,OCI(Open Container Initiative)作为Linux基金会项目成立,旨在推动开源技能社区制订容器镜像和运行时规范,使不同厂家的容器办理方案具备互操作能力。同年还成立了CNCF,目的是促进容器技能在云原生领域的运用,降落用户开拓云原生运用的门槛。创始会员包括谷歌、红帽、Docker、VMware等多家公司和组织。
CNCF成立之初只有一个开源项目,便是后来大名鼎鼎的Kubernetes。Kubernetes是一个容器运用的编排工具,最早由谷歌的团队研发,后来开源并捐赠给了CNCF成为种子项目。由于Kubernetes是厂家中立的开源项目,开源后得到了社区用户和开拓者的广泛参与和支持。到了2018年,Kubernetes已成为容器编排领域事实上的标准,并成为首个CNCF的毕业(graduated)项目。2020年8月,CNCF旗下的开源项目增加到了63个,包括原创于中国的Harbor等项目。
从容器的发展进程可以看到,容器在涌现的早期并没有得到人们的广泛关注,紧张缘故原由是当时开放的云打算环境还没涌现或者未成为主流。2010年之后,随着IaaS、PaaS和SaaS等云平台逐渐成熟,用户对云端运用开拓、支配和运维的效率不断重视,重新发掘了容器的代价,终极匆匆成了容器技能的盛行。
容器的基本事理
下面我们以Linux容器为例,讲解容器的实现事理,紧张包括命名空间(Namespace)和掌握组(cgroups)。
▊ 命名空间
命名空间是Linux操作系统内核的一种资源隔离办法,使不同的进程具有不同的系统视图。系统视图便是进程能够感知到的系统环境,如主机名、文件系统、网络协议栈、其他用户和进程等。利用命名空间后,每个进程都具备独立的系统环境,进程间彼此觉得不到对方的存在,进程之间相互隔离。目前,Linux中的命名空间共有6种,可以嵌套利用。
◎ Mount
隔离了文件系统的挂载点(mount points),处于不同“mount”命名空间中的进程可以看到不同的文件系统。
◎ Network
隔离进程网络方面的系统资源,包括网络设备、IPv4和IPv6的协议栈、路由表、防火墙等。
◎ IPC
进程间相互通信的命名空间,不同命名空间中的进程不能通信。
◎ PID
进程号在不同的命名空间中是独立编号的,不同的命名空间中的进程可以有相同的编号。当然,这些进程在操作系统中的全局(命名空间外)编号是唯一的。
◎ UTS
系统标识符命名空间,在每个命名空间中都可以有不同的主机名和NIS域名。
◎ User
命名空间中的用户可以有不同于全局的用户ID和组ID,从而具有不同的特权。
命名空间实现了在同一操作系统中隔离进程的方法,险些没有额外的系统开销,所以是非常轻量的隔离办法,进程启动和运行的过程在命名空间中和表面险些没有差别。
▊ 掌握组
命名空间实现了进程隔离功能,但由于各个命名空间中的进程仍旧共享同样的系统资源,如CPU、磁盘I/O、内存等,以是如果某个进程永劫光占用某些资源,其他命名空间里的进程就会受到影响,这便是“吵闹的邻居(noisy neighbors)”征象。因此,命名空间并没有完备达到进程隔离的目的。为此,Linux内核供应了掌握组(Control Groups,cgroups)功能来处理这个问题。
Linux把进程分成掌握组,给每组里的进程都设定资源利用规则和限定。在发生资源竞争时,系统会根据每个组的定义,按照比例在掌握组之间分配资源。掌握组可设定规则的资源包括CPU、内存、磁盘I/O和网络等。通过这种办法,就不会涌现某些进程无限度抢占其他进程资源的情形。
Linux系统通过命名空间设置进程的可见且可用资源,通过掌握组规定进程对资源的利用量,这样隔离进程的虚拟环境(即容器)就建立起来了。
容器运行时
Linux 供应了命名空间和掌握组两大系统功能,它们是容器的根本。但是,要把进程运行在容器中,还须要有便捷的SDK或命令来调用Linux的系统功能,从而创建出容器。容器的运行时(runtime)便是容器进程运行和管理的工具。
容器运行时分为低层运行时和高层运行时,功能各有侧重。低层运行时紧张卖力运行容器,可在给定的容器文件系统上运行容器的进程;高层运行时则紧张为容器准备必要的运行环境,如容器镜像下载和解压并转化为容器所需的文件系统、创建容器的网络等,然后调用低层运行时启动容器。紧张的容器运行时的关系如下。
▊ OCI运行时规范
成立于2015年的OCI是Linux基金会旗下的互助项目,以开放管理的办法制订操作系统虚拟化(特殊是Linux容器)的开放工业标准,紧张包括容器镜像格式和容器运行时(runtime)。初始成员包括Docker、亚马逊、CoreOS、谷歌、微软和VMware等公司。OCI成立之初,Docker公司为其捐赠了容器镜像格式和运行时的草案及相应的实当代码。原来属于Docker的libcontainer项目被捐赠给OCI,成为独立的容器运行时项目runC。
OCI运行时规范定义了容器配置、运行时和生命周期的标准,主流的容器运行时都遵照OCI运行时的规范,从而提高系统的可移植性和互操作性,用户可根据须要进行选择。
首先,容器启动前须要在文件系统中按一定格式存放所需的文件。OCI运行时规范定义了容器文件系统包(filesystem bundle)的标准,在OCI运行时的实现中常日由高层运行时下载OCI镜像,并将OCI镜像解压成OCI运行时文件系统包,然后OCI运行时读取配置信息和启动容器里的进程。OCI运行时文件系统包紧张包括以下两部分。
◎ config.json
这是必需的配置文件,存放于文件系统包的根目录下。OCI运行时规范对Linux、Windows、Solaris和虚拟机4种平台的运行时做了相应的配置规范。
◎ 容器的根文件系统
容器启动后进程所利用的根文件系统,由 config.json 中的root.path属性确定该文件系统的路径,常日是“rootfs/”。
然后,在定义文件系统包的根本上,OCI运行时规范制订了运行时和生命周期管理规范。生命周期定义了容器从创建到删除的全过程,可用以下三条命令解释。
◎“create”命令
在调用该命令时须要用到文件系统包的目录位置和容器的唯一标识。在创建运行环境时须要利用config.json里面的配置。在创建的过程中,用户可加入某些事宜钩子(hook)来触发一些定制化处理,这些事宜钩子包括prestart、createRuntime和createContainer。
◎“start”命令
在调用该命令时须要运行容器的唯一标识。用户可在 config.json 的process 属性中指明运行程序的详细信息。“start”命令包括两个事宜钩子:startContainer和poststart。
◎“delete”命令
在调用该命令时须要运行容器的唯一标识。在用户的程序终止后(包括正常和非常退出),容器运行时实行“delete”命令以打消容器的运行环境。“delete”命令有一个事宜钩子:poststop。
-----
除了上述生命周期命令,OCI运行时还必须支持其余两条命令。
(1)“state”命令
在调用该命令时须要运行容器的唯一标识。该命令查询某个容器的状态,必须包括的状态属性有ociVersion、id、status、pid和bundle,可选属性有annotation。不同的运行时实现可能会有一些差异。下面是一个容器状态的例子:
1{ 2 "ociVersion": "1.0.1", 3 "id": "oci-container001", 4 "status": "running", 5 "pid": 8080, 6 "bundle": "/containers/nginx", 7 "annotations": { 8 "key1": "value1" 9 }10}
(2)“kill”命令
在调用该命令时须要运行容器的唯一标识和旗子暗记(signal)编号。该命令给容器进程发送旗子暗记,如Linux操作系统的旗子暗记9表示立即终止进程。
▊ runC
runC是OCI运行时规范的参考实现,也是最常用的容器运行时,被其他多个项目利用,如containerd和CRI-O等。runC也是低层容器运行时,开拓职员可通过runC实现容器的生命周期管理,避免繁琐的操作系统调用。根据OCI运行时规范,runC不包括容器镜像的管理功能,它假定容器的文件包已经从镜像里解压出来并存放于文件系统中。runC创建的容器须要手动配置网络才能与其他容器或者网络节点连通,为此可在容器启动之前通过OCI定义的事宜钩子来设置网络。
由于runC供应的功能比较单一,繁芜的环境须要更高层的容器运行时来天生,以是runC常常成为其他高层容器运行时的底层实现根本。
▊ containerd
在OCI成立时,Docker公司把其Docker项目拆分为runC的低层运行时及高层运行时功能。2017年,Docker公司把这部分高层容器运行时的功能集中到containerd项目里,捐赠给云原生打算基金会。
containerd 已经成为多个项目共同利用的高层容器运行时,供应了容器镜像的下载和解压等镜像管理功能,在运行容器时,containerd先把镜像解压成OCI的文件系统包,然后调用runC运行容器。containerd供应了API,其他运用程序可以通过API与containerd交互。“ctr”是containerd的命令行工具,和“docker”命令很相像。但作为容器运行时,containerd只看重在容器运行等方面,因而不包含开拓者利用的镜像构建和镜像上传镜像仓库等功能。
▊ Docker
Docker引擎是最早盛行也是最广泛利用的容器运行时之一,是一个容器管理工具,架构如下图。
Docker的客户端(命令行CLI工具)通过API调用容器引擎Docker Daemon(dockerd)的功能,完成各种容器管理任务。
Docker引擎在发布时是一个单体运用,所有功能都集中在一个可实行文件里,后来按功能分拆成runC和containerd两个不同层次的运行时,分别捐献给了OCI和CNCF。上面两节已经分别先容了runC和containerd的紧张特点,剩下的dockerd便是Docker公司掩护的容器运行时。
dockerd同时供应了面向开拓者和面向运维职员的功能。个中,面向开拓者的命令紧张供应镜像管理功能。容器镜像一样平常可由Dockerfile构建(build)而来。Dockerfile是一个文本文件,通过一组命令关键字定义了容器镜像所包含的根本镜像(base image)、所需的软件包及有关运用程序。在Dockerfile编写完成往后,就可以用“docker build”命令构建镜像了。下面是一个Dockerfile的大略例子:
1FROM ubuntu:18.042EXPOSE 80803CMD ["nginx", "-g", "daemon off;"]
容器的镜像在构建之后被存放在本地镜像库里,当须要与其他节点共享镜像时,可上传镜像到镜像仓库(Registry)以供其他节点下载。
Docker还供应了容器存储和网络映射到宿主机的功能,大部分由containerd实现。运用的数据可以被保存在容器的私有文件系统里面,这部分数据会随着容器一起被删除。对须要数据持久化的有状态运用来说,可用数据卷Volume的办法导入宿主机上的文件目录到容器中,对该目录的所有写操作都将被保存到宿主机的文件系统中。Docker可以把容器内的网络映射到宿主机的网络上,并且可以连接外部网络。
▊ CRI和CRI-O
Kubernetes是当今主流的容器编排平台,为了适应不同场景的需求,Kubernetes须要有利用不同容器运行时的能力。为此,Kubernetes从1.5版本开始,在kubelet中增加了一个容器运行时接口CRI(Container Runtime Interface),须要接入Kubernetes的容器运行时必须实现CRI接口。由于kubelet的任务是管理本节点的事情负载,须要有镜像管理和运行容器的能力,因此只有高层容器运行时才适宜接入CRI。CRI和容器运行时的关系如下图。
CRI和容器运行时之间须要有个接口层,常日称之为shim(垫片),用以匹配相应的容器运行时。CRI接口由shim实现,定义如下,分为RuntimeService和ImageServiceManager(代码拜会GitHub上kubernetes/cri-api的项目文件“pkg/apis/services.go”):
1// RuntimeService接口必须由容器运行时实现 2// 以下方法必须是线程安全的 3type RuntimeService interface { 4RuntimeVersioner 5ContainerManager 6PodSandboxManager 7ContainerStatsManager 8 9// UpdateRuntimeConfig更新运行时配置10UpdateRuntimeConfig(runtimeConfig runtimeapi.RuntimeConfig) error1112// Status返回运行时的状态13Status() (runtimeapi.RuntimeStatus, error)14}1516// ImageManagerService接口必须由容器管理器实现17// 以下方法必须是线程安全的18type ImageManagerService interface {19// ListImages列涌现有镜像20ListImages(filter runtimeapi.ImageFilter) ([]runtimeapi.Image, error)2122// ImageStatus返回镜像状态23ImageStatus(image runtimeapi.ImageSpec) (runtimeapi.Image, error)2425// PullImage用认证配置拉取镜像26PullImage(image runtimeapi.ImageSpec, auth runtimeapi.AuthConfig, podSandboxConfig runtimeapi.PodSandboxConfig) (string, error)2728// RemoveImage删除镜像29RemoveImage(image runtimeapi.ImageSpec) error3031// ImageFsInfo返回存储镜像的文件系统信息32ImageFsInfo() ([]runtimeapi.FilesystemUsage, error)33}
Docker运行时被普遍利用,它的CRI shim被称为dockershim,内置在Kubernetes的kubelet中,由Kubernetes项目组开拓和掩护。其他运行时则须要供应外置的shim。containerd从1.1版本开始内置了CRI plugin,不再须要外置shim来转发要求,因此效率更高。在安装Docker的最新版本时,会自动安装containerd,以是在一些系统中,Docker和Kubernetes可以同时利用containerd来运行容器,但是二者的镜像用了命名空间隔离,彼此是独立的,即镜像不可以共用。由于Docker和containerd常常同时存在,因此在不须要利用Docker的系统中只安装containerd即可。
containerd最早是为Docker设计的代码,包含一些用户干系的功能。比较之下,CRI-O是替代Docker或者containerd的高效且轻量级的容器运行时方案,是CRI的一个实现,能够运行符合OCI规范的容器,以是被称为CRI-O。CRI-O是原生为生产系统运行容器设计的,有个大略的命令行工具供测试用,但并不能进行容器管理。CRI-O支持OCI的容器镜像格式,可以从容器镜像仓库中下载镜像。CRI-O支持runC和Kata Containers这两种低层容器运行时。
新书上市
▊《Harbor威信指南:容器镜像、Helm Chart等云原天生品的管理与实践》
张海宁 等 著
在云原生生态中,容器镜像和其他云原天生品的管理与分发是至关主要的一环。本书对开源云原天生品仓库Harbor展开全面讲解,由Harbor开源项目掩护者和贡献者倾力撰写,内容涵盖Harbor的架构、事理、功能、支配与配置、运维、定制化开拓、API、项目管理和成功案例等,还有很多未公开拓表的内容在本书中都有详尽讲解!