Docker诞生于2013年,并普及了容器的概念,以至于大多数人仍然将容器的概念等同于”DockeR容器”。
作为第一个吃螃蟹的人,Docker设置了新加入者必须遵守的标准。例如,Docker有一个大型系统镜像库。所有的替代方案都必须使用相同的镜像格式,同时试图改变Docker所基于的整个堆栈的一个或多个部分。
在此期间,出现了新的容器标准,容器生态系统朝着不同方向发展。现在除了Docker之外,还有很多方法可以使用容器。
将ChRoot、cgRoups和命名空间作为容器的技术基础。定义Docker所基于的软件堆栈。说明Docker和Kubernetes需要坚持和遵守的标准。介绍替代解决方案,这些解决方案尝试使用具有更好更安全的组件来替换原始Docker容器。容器的软件堆栈。
像ChRoot调用、cgRoups和命名空间等Linux特性帮助容器在与所有其他进程隔离的情况下运行,从而保证运行时的安全性。
ChRoot。所有类似Docker的技术都起源于类似Unix操作系统(OS)的根目录。在根目录上方是根文件系统和其他目录。
从长远来看,这是很危险的,因为根目录中任何不需要的删除都会影响整个操作系统。这就是为什么存在一个系统调用chRoot()。它创建了额外的根目录,例如一个用于运行遗留软件,另一个用于包含数据库等等。
对于所有这些环境,chRoot似乎是一个真正的根目录,而是实际上,它只是将路径名添加到任何以/开头的名字上。真正的根目录仍然存在,并且任何进程都可以引用指定根目录以外的任何位置。
Linux cgRoups。自2008年2.6.24版本以来,Control Groups (cgRoups)一直是Linux内核的一项功能。CgRoup将同时限制、隔离和测量多个进程的系统资源使用情况。
假设我们想阻止用户从服务器发送大量电子邮件。我们创建了一个内存限制为1GB、CPU占用率为50%的cgRoup,并将应用程序的Process id添加到该组中。当达到这些限制时,系统将限制电子邮件发送过程。它甚至可能终止进程,这取决于托管策略。
Namespaces。Linux命名空间是另一个有用的抽象层。命名空间允许我们拥有许多进程层次,每个层次都有自己的嵌套”子树(subtree)”。命名空间可以使用全局资源,并将其呈现给其成员,就像它是自己的资源一样。
具体来看,Linux系统开始时的进程标识符(PID)为1,并且所有其他进程将包含在其树中。PID命名空间允许我们跨越一棵新树,它拥有自己的PID 1进程。现在有两个值为1的PID,每个命名空间可以产生自己的命名空间,并且相同的过程可以附加了几个PID。
子命名空间中的一个进程将不知道父级的进程存在,而父命名空间将可以访问整个子命名空间。
有七种类型的名称空间:cgRoup、IPC、网络、Mount、PID、用户和UTS。
Network Namespace。一些资源是稀缺的。按照惯例,有些端口具有预定义的角色,不应用于其他任何用途:端口80仅服务于HTTP调用,端口443仅服务于https调用等等。在共享主机环境中,两个或多个站点可以监听来自端口80的HTTP请求。第一个获得该端口的站点不允许任何其他应用程序访问该端口上的数据。第一个应用程序在互联网上是可见的,而其他所有应用程序将不可见。
解决方案是使用网络命名空间,通过网络命名空间,内部进程将看到不同的网络接口。
在一个网络命名空间中,同一端口可以是开放的,而在另一个网络命名空间中,可以关闭该端口。为此,我们必须采用额外的”虚拟”网络接口,这些接口同时属于多个命名空间。中间还必须有一个路由器进程,将到达物理设备的请求连接到相应的名称空间和其中的进程。
复杂吗?这就是为什么Docker和类似工具如此受欢迎。现在让我们来介绍一下Docker,以及它的替代方案。
Docker: 人人可用的容器
在容器统治云计算世界之前,虚拟机非常流行。如果你有一台Windows机器,但想为iOS开发移动应用程序,你可以购买一台新的Mac,或者将其虚拟机安装到Windows硬件上。虚拟机也可能是笨重的,它们经常吞噬不需要的资源,而且启动速度通常很慢(长达一分钟)。
容器是标准软件单元,具有运行程序所需的一切:操作系统、数据库、镜像、图标,软件库、代码和所需的其他组件。容器的运行也与所有其他容器,甚至与操作系统本身隔离。与虚拟机相比,容器是轻量级的,所以它们可以快速启动,并且容易被替换。
要运行隔离和保护,容器需要基于ChRoot、cgRoups和命名空间。
容器的镜像是在实际机器上形成应用程序的模板,能够根据单个镜像创建尽可能多的容器,一个名为Dockerfile的文本文件包含了组装镜像所需的所有信息。
Docker带来的真正革命是创建了Docker镜像仓库和开发了Docker引擎,这些镜像以相同的方式在各地运行,作为第一个被广泛采用的容器镜像,形成了一个不成文的世界标准,所有后来的入局者都必须关注它。
CRI and OCI
OCI 全称为Open Container Initiative,它发布镜像和容器的规范。它于2015年由Docker发起,并被微软、Facebook、Intel、VMware、Oracle和许多其他行业巨头接受。
OCI还提供了规范的一个实现,被称为Runc,它可以直接使用容器,创建并运行它们等。
容器运行时接口(Container Runtime Interface,简称CRI)是一个Kubernetes API,它定义了Kubernetes如何与容器运行时交互。它也是标准化的,所以我们可以选择采用哪个CRI实现。
用于CRI和OCI的容器的软件堆栈
Linux是运行容器的软件堆栈中最基本的部分:
请注意,Containerd和CRI-O都坚持CRI和OCI规范。对于Kubernetes而言,这意味着它可以使用Containerd或CRI-O,而用户不会注意到其中的区别。它还可以使用我们现在要提到的任何其他替代方案,这正是创建和采用了OCI和CRI等软件标准的目标。
Docker软件堆栈
Docker的软件堆栈包括:
Docker-cli,面向开发者的Docker命令行界面 Containerd,最初由Docker编写,后来作为一个独立的项目启动; 它实现了CRI规范 Runc,它实现了OCI规范 容器(使用ChRoot、cgRoups、命名空间等)
Kubernetes的软件堆栈几乎是相同的;Kubernetes使用CRI-O,而不是Containerd,这是由Red Hat / IBM和其他人创建的CRI实现。
Containerd
Containerd作为一个守护程序在Linux和Windows上运行。它加载镜像,将其作为容器执行,监督底层存储,并负责整个容器的运行时间和生命周期。
Containerd诞生于2014年,一开始作为Docker的一部分,2017年成为云原生计算基金会(CNCF)中的一个项目,并于2019年年初毕业。如果你想了解一些Containerd的使用技巧,欢迎查看下方的文章:
配置Containerd镜像仓库完全攻略
Runc
Runc是OCI规范的参考实现。它创建并运行容器以及其中的进程。它使用较低级别的Linux特性,比如cgRoup和命名空间。
Runc的替代方案包括Kata-RunTime、GVisor和CRI-O。
Kata-RunTime使用硬件虚拟化作为单独的轻量级VM实现OCI规范。它的运行时与OCI、CRI-O和Containerd兼容,因此它可以与Docker和Kubernetes无缝工作。
Google的GVisor创建包含自己内核的容器。它通过名为Runsc的项目实现OCI,该项目与Docker和Kubernetes集成。有自己内核的容器比没有内核的容器更安全,但它不是万能的,而且这种方法在资源使用上要付出代价。
CRI-O是一个纯粹为Kubernetes设计的容器堆栈,是CRI标准的第一个实现。它从任何容器镜像仓库中提取镜像,可以作为使用Docker的轻量级替代方案。
今天它支持Runc和Kata Containers作为容器运行时,但也可以插入任何其他OCI兼容的运行时(至少在理论上)。
它是一个CNCF孵化项目。
PodMan
PodMan是一个没有守护进程的Docker替代品。它的命令有意与Docker尽可能兼容,以至于您可以在CLI界面中创建一个别名并开始使用单词”Docker”而不是”PodMan”。
PodMan的目标是取代Docker,因此坚持使用相同的命令集是有意义的。PodMan试图改进Docker中的两个问题。
首先,Docker总是使用内部守护进程执行。守护进程是在后台运行的单进程。如果它失败了,整个系统就会失败。
第二,Docker作为后台进程运行,具有Root权限,所以当你给一个新的用户访问权时,你实际上是给了整个服务器的访问权。
PodMan是一个远程Linux客户端,可直接从操作系统运行容器。你也可以以Rootless模式运行它们。它从DockerHub下载镜像,并以与Docker完全相同的方式运行它们,具有完全相同的命令。
PodMan以Root以外的用户身份运行命令和镜像,所以它比Docker更安全。另一方面,有许多为Docker开发的工具在PodMan上是不可用的,如Portainer和Watchtower。摆脱Docker意味着放弃你之前建立的工作流程。
PodMan的目录结构与Buildah、Skopeo和CRI-I类似。它的Pod也非常类似于Kubernetes Pod。
Linux容器:LXC和LXD
LXC(linux Containers)于2008年推出,是Linux上第一个上游内核的容器。Docker的第一个版本使用了LXC,但在后来的发展中,由于已经实现了Runc,所以LXC被移除了。
LXC的目标是使用一个Linux内核在一个控制主机上运行多个隔离的Linux虚拟环境。为此,它使用了Cgroups功能,而不需要启动任何虚拟机;它还使用命名空间,将应用程序与底层系统完全隔离。
LXC旨在创建系统容器,几乎就像你在虚拟机中一样,但硬件开销很小,因为这些硬件是被虚拟化的。
LXC不模拟硬件和软件包,只包含需要的应用程序,所以它几乎以裸机速度执行。相反,虚拟机包含整个操作系统,然后模拟硬件,如硬盘、虚拟处理器和网络接口。
所以,LXC是小而快的,而虚拟机是大而慢的。另一方面,虚拟环境不能被打包成现成的、可快速部署的机器,而且很难通过GUI管理控制台进行管理。LXC要求技术人员有很高的技术水平,并且优化后的机器可能与其他环境不兼容。
LXC VS Docker
LXC就像Linux上的一个增压Chroot,它产生的”小”服务器启动更快,需要更少的RAM。然而,Docker提供了更多特性:
跨机器的可移植部署:使用一个版本的Docker创建的对象可以传输并安装到任何其他支持Docker的Linux主