互联网技术 / 互联网资讯 · 2023年11月6日 0

容器技术演进与基础原理

容器技术的发展背景

近年来,容器技术迅速崛起,改变了应用的开发、交付和运行方式,广泛应用于云计算和互联网等领域。实际上,容器技术在大约二十年前便已问世,但直到2013年Docker推出后才开始广泛流行,这其中既有偶然因素,也有时代背景的必然性。接下来,我们来回顾容器的产生背景和发展历程。

在电子计算机刚出现的时代,由于硬件成本高昂,人们寻求多用户共享计算资源的方式,以提高资源利用率并降低成本。在20世纪60年代,基于硬件的主机虚拟化技术应运而生。通过这种技术,一台物理主机可以分割成多个小型机器,每台机器可以独立安装各自的操作系统。进入20世纪90年代后期,基于X86架构的硬件虚拟化技术逐渐兴起,可以在同一物理机上隔离多个操作系统实例,这种方式带来了许多优点,至今大多数数据中心依赖于硬件虚拟化技术。

尽管硬件虚拟化提供了资源隔离的能力,但使用虚拟机来隔离应用程序时,效率通常较低,因为每个虚拟机都需要安装或复制一个操作系统实例,然后再在其中部署应用程序。因此,人们探索出一种更轻量的方案——操作系统虚拟化,使应用管理更为便捷。操作系统虚拟化是指由操作系统创建虚拟的系统环境,使得应用无法感知到其他应用的存在,仿佛独占了全部系统资源,从而实现应用的隔离。在这种方式下,不需要虚拟机,也能实现应用之间的隔离,由于应用共享同一个操作系统实例,因此比虚拟机更为节省资源,性能更佳。操作系统虚拟化在许多系统中被称为容器(Container),以下将以容器来指代操作系统虚拟化。

操作系统虚拟化最早出现在2000年,FreeBSD 4.0推出了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的毕业项目。到2020年8月,CNCF旗下的开源项目数量已增至63个,包括源自中国的Harbor等项目。

从容器的发展历程可以看出,早期容器并未受到广泛关注,主要因为当时开放的云计算环境尚未出现或未成为主流。2010年后,随着IaaS、PaaS和SaaS等云平台逐渐成熟,用户对云端应用开发、部署和运维的效率愈发重视,重新发现了容器的价值,最终促成了容器技术的盛行。

容器的基本原理

本节将以Linux容器为例,讲解容器的实现原理,主要包括命名空间(Namespace)和控制组(cgroups)。

命名空间

命名空间是Linux操作系统内核的一种资源隔离机制,使得不同的进程拥有不同的系统视图。系统视图是指进程能够感知的系统环境,例如主机名、文件系统、网络协议栈、其他用户和进程等。使用命名空间后,每个进程都具备独立的系统环境,进程间彼此无法感知对方的存在,实现了相互隔离。目前,Linux中的命名空间共有六种,可以嵌套使用。

1. Mount:隔离文件系统的挂载点,处于不同“Mount”命名空间中的进程可以看到不同的文件系统。

2. Network:隔离进程网络相关的系统资源,包括网络设备、IPv4和IPv6的协议栈、路由表、防火墙等。

3. IPC:进程间相互通信的命名空间,不同命名空间中的进程无法通信。

4. PID:进程号在不同命名空间中独立编号,不同命名空间中的进程可以有相同的编号,但在操作系统的全局编号中是唯一的。

5. UTS:系统标识符命名空间,每个命名空间可以拥有不同的主机名和NIS域名。

6. 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运行时文件系统包主要由以下两部分组成:

1. config.json:这是必需的配置文件,存放于文件系统包的根目录下。OCI运行时规范对Linux、Windows、Solaris和虚拟机四种平台的运行时制定了相应的配置规范。

2. 容器的根文件系统:容器启动后进程所使用的根文件系统,由config.json中的Root.path属性确定该文件系统的路径,通常为“Rootfs/”。

在定义文件系统包的基础上,OCI运行时规范还制定了运行时和生命周期管理规范。生命周期定义了容器从创建到删除的全过程,可通过以下三条命令进行说明。

“create”命令:调用该命令时需要提供文件系统包的目录位置和容器的唯一标识。在创建运行环境时需要使用config.json中的配置。在创建过程中,用户可以加入某些事件钩子(hook)以触发定制化处理,这些事件钩子包括restart、createRuntime和createContainer。

“start”命令:调用该命令时需要容器的唯一标识。用户可以在config.json的Process属性中指明运行程序的详细信息。“start”命令包括两个事件钩子:startContainer和postStart。

“delete”命令:调用该命令时需要容器的唯一标识。在用户程序终止后(包括正常或异常退出),容器运行时执行“delete”命令以清除容器的运行环境。“delete”命令有一个事件钩子:poststop。

除了上述生命周期命令,OCI运行时还必须支持另外两条命令。

“state”命令:调用该命令时需要容器的唯一标识。该命令查询某个容器的状态,必须包括的状态属性有ociversion、id、status、pid和bundle,可选属性有annotations。不同运行时实现可能存在一些差异。以下是一个容器状态的示例:

{ "ociversion": "1.0.1", "id": "oci-container001", "status": "Running", "pid": 8080, "bundle": "/containers/Nginx", "annotations": { "key1": "value1" } }

“kill”命令:调用该命令时需要容器的唯一标识和信号(signal)编号。该命令用于向容器进程发送信号,例如Linux操作系统中的信号9表示立即终止进程。

RunC

RunC是OCI运行时规范的参考实现,也是最常用的容器运行时,被多个项目所使用,如containerd和CRI-O等。RunC作为低层容器运行时,开发人员可以通过RunC实现容器的生命周期管理,避免繁琐的操作系统调用。根据OCI运行时规范,RunC不包括容器镜像的管理功能,它假设容器的文件包已经从镜像中解压并存放于文件系统中。通过RunC创建的容器需要手动配置网络以便与其他容器或网络节点连接,因此可以在容器启动之前通过OCI定义的事件钩子来设置网络。

由于RunC提供的功能较为单一,复杂环境下需要更高层的容器运行时来生成,因此RunC常常成为其他高层运行时的基础。

[[[IMG_1]]]

[[[IMG_2]]]

[[[IMG_3]]]