设想一下,在我们的日常项目开发过程中,存在一个应用服务,其使用一些基础库函数并具有某些依赖项。如果我们在不支持这些依赖项的环境平台上运行此应用程序,那么,我们可能会遇到意外错误。随着 DevOps 及云原生理念的注入,我们希望我们所开发的应用程序能够可以跨多个操作系统及平台正常运行。
为了解决这个问题,基于传统方式,我们可以在虚拟机上运行应用程序。这样做的问题是,为了运行应用程序而启动整个操作系统会产生开销,并且操作系统的许多组件将不会被使用,容易浪费资源。因此,我们急需要寻求一种折中的解决方案以平衡资源使用率与跨平台特性,Container 技术,便是一种解决方案。
什么是 Container ?
Container 是一种打包软件的方式,因为它不依赖于环境。应用程序的所有代码、库和依赖配置项都打包到容器中。与虚拟机不同,容器不是创建一个完整的虚拟操作系统,而是允许应用程序使用与它们正在运行的系统相同的 Linux 内核,并且只需要应用程序与尚未在主机上运行的东西一起交付。现在我们没有启动整个操作系统,而且我们拥有独立于机器的代码包。故此,我们可以认为:Container 是一个包含要执行的程序及其所有依赖项(例如代码、运行时、系统库等)的软件包。如下为基于 Kubernetes 平台下 Container 所在的位置。
OCI 概念解析
在解析 Container 之前,我们先了解一下 OCI 概念。OCI 全称为“Open Container Initiative” ,是一个开放的治理结构,于 2015 年 6 月 22 日由 Docker、CoreOS 和其他容器行业的领导者推出,旨在制定围绕容器格式和运行时创建开放行业标准。除此之外,开放容器计划 (OCI) 是一个轻量级的项目,在 Linux 基金会的支持下成立,其明确目的是围绕容器格式和运行时创建开放的行业标准。
OCI 目前包含两个规范:运行时规范(runtime-spec)和镜像规范(image-spec)。运行时规范概述了如何运行在磁盘上解压缩的“文件系统包”。在高层次上,OCI 实现将下载一个 OCI 映像,然后将该镜像解压到一个 OCI 运行时文件系统包中。此时,OCI Runtime Bundle 将由 OCI Runtime 运行。整个工作流程应该支持用户对 Docker 和 rkt 等容器引擎所期望的 UX:主要是无需额外参数即可运行镜像的能力。
换句话来讲,OCI 是为操作系统进程和应用程序容器的标准制定一系列规范。
每个 OCI 运行时规范,具体内容如下所示:
- 容器是运行进程的隔离和受限的盒子
- 容器将应用程序及其所有依赖项(包括操作系统库)打包在一起
- 容器是为了可移植性——任何兼容的运行时都可以运行标准容器
- 容器可以使用 Linux、Windows 和其他操作系统来实现
- 虚拟机也可以用作标准容器
其实,从本质上而言,通常往往有多种实现方法可以用来创建容器,尤其是在 Linux 操作系统上。除了目前广泛使用的 Docker 实现之外,我们可能还听说过 LXC、systemd-nspawn,甚至 OpenVZ。在 2015 年,Docker 已经相当受欢迎,再加上 Kubernetes 的正式问世,使得在此期间催生了其他竞争项目实现了自己的容器引擎,如 rkt 和 lmctfy。显然,OCI 的建立是为了标准化做容器的方式。事实上,它使 Docker 的容器实现成为一个标准的实现,但也包含了一些非 Docker 部分。接下来,我们来看一下 Container 的体系架构示意图,如下所示:
基于前面章节所述以及结合 Container 体系架构示意图,我们可以看到,仅从 Container 角度,其主要包括以下核心要素:
1、应用程序,基于不同语言所编写的业务代码,例如,Java 、Go 、Python 以及其他开发语言等。
2、运行进程,主要涉及容器运行过程中所提供的依赖环境资源信息,例如,守护进程等等。
3、依赖组件,此处为应用程序运行所依赖的相关配置项及库函数,例如,代码所调用的第三方组件以及其运行过程中所需要依赖的操作系统底层库函数等。
但若从其所处的关联环境角度解读,其同时也涉及“运行时”和“环境”等方面。基于 OCI 规范,Container 被定义为是一种用于执行具有可配置隔离和资源限制的进程的环境。那么,既然具备“隔离”和“资源限制”等机制,那么意味着 Container 可以以自描述和可移植的格式封装软件组件及其所有依赖项,以便兼容任何的运行时从而使得运行过程中无需额外的依赖项,无关乎其底层机器和容器的内容。
除此之外,依据 OCI 规范所定义,基于 Container ,我们可以实现如下操作:
1、使用标准的容器工具来进行创建、启动及停止。
2、使用标准文件系统工具进行复制和快照建立。
3、使用标准网络工具实现文件的下载和上传。
4、OCI 运行时应该支持的容器操作涉及创建、启动、终止、删除和查询等多种事件状态。
基于容器,在日常的软件项目开发过程中,开发人员能够将应用程序及其所有必需的部分内容打包,并将其作为一个标准的、轻量级的、安全的及整体的包交付出去。基于此种场景,在一定程度上能够让 DevOps 团队高枕无忧,因为他们知道他们正在构建和支持的应用程序将在任何环境中正常运行——无论是虚拟机、裸机还是云平台。毕竟,容器从根本上消除了单体应用程序固有的“在特定的环境上工作”的问题。
VM VS Container
在解析虚拟机与 Container 之前,我们先了解下 Linux Container 体系相关概念。除了 Container 的操作和生命周期之外,OCI Runtime Spec 还指定了 Container 的配置和执行环境。
根据 OCI 运行时规范,要创建 Container,需要为运行时提供所谓的文件系统包,该包由一个强制性的 config.json 文件和一个包含未来容器根文件系统的可选文件夹组成。
“题外话:bundle 通常是通过解压容器镜像获得的,但镜像不是运行时规范的一部分。相反,它们受专用 OCI 镜像规范的约束。”
config.json 包含对容器实施标准操作(创建、启动、查询状态、终止和删除)所需的数据。但是当我们谈到 config.json 文件的实际结构时,事情开始变得非常有趣。配置由公共部分和特定于平台的部分组成。公共部分包括 ociVersion、包中的根文件系统路径、根之外的附加挂载、要在容器中启动的进程、用户和主机名。嗯...但是著名的命名空间和 Cgroup 在哪里?
在撰写本文时,OCI 运行时规范为以下平台定义了容器:Linux、Solaris、Windows、z/OS 及其他常用平台。
我们都知道,目前最广泛使用的 OCI 运行时应该是 runc 和 crun。不出所料,两者都实现了 Linux 容器。但正如我们刚刚看到的,OCI 运行时规范提到了 Windows、Solaris 和其他容器。对我们来说更有趣的是,它定义了 VM 容器。
容器不是要取代虚拟机作为相同执行环境抽象的更轻量级的实现吗?
无论怎样,让我们仔细了解下 VM 容器。显然,它们不受 Linux 命名空间和 Cgroup 的支持。相反,特定于虚拟机的容器配置提到了管理程序、内核和 VM 镜像。 因此,隔离是通过虚拟化一些硬件(管理程序)然后在其上启动成熟的操作系统(内核 镜像)来实现的。由此产生的环境便是我们所认知的黑匣子,即一个容器。我们来看一下 Linux Container 与 VM 基础架构,具体如下图所示:
不过我们需要注意的是,OCI Runtime Spec 中所提到的 VM 镜像与用于创建包的传统容器镜像无任何相关联性,捆绑根文件的系统单独挂载到 VM 容器中。
总而言之,要在计算机上可靠且可重复地部署软件,我们需要一个类似“盒子”的装置来存放应用程序的代码。此盒子应包含整个系统,以便开发人员完全了解他们要交付的内容,而无需关注盒子所包含环境的相关具体信息及依赖组件,毕竟。此盒子也不应该受到虚拟机性能成本的影响。最后,也是非常至关重要的一点:此盒子必须有一个通用型标准接口,无论里面存储什么应用程序,它都不应因环境的变化而发生变动。
基于上述所讲,容器不仅仅是稍微孤立和受限的 Linux 进程。相反,它们是标准化的执行环境,可提高工作负载的可移植性。Linux 容器是当今最普遍的容器形式,但对更安全的容器的需求正在增长。OCI 运行时规范定义了 VM 支持的容器,而 Kata 项目使它们成为现实。所以,此刻是探索容器世界的激动人心的最佳时间~
针对 Kubernetes Container 的其他方面内容特性,大家可阅读之前的文章,具体链接为:Kubernetes Container 解析。以上为本文基于底层原理对 Container 的相关简要解析,若文章中所述有问题,欢迎大家随时沟通、指导及交流。
# 参考资料 https://opencontainers.org