42

容器发展简史

 5 years ago
source link: http://dockone.io/article/8832?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【编者的话】这篇文章图文并茂的讲述了容器化发展的历史。

在过去四年中(2015-2019),云以及分布式计算成为最受欢迎的技术之一,它们从小众技能逐渐变成更被雇主看重的突出技能。容器化技术是云经济和IT生态系统中最新潮的技术之一。这篇文章可能会帮助您理解有关Docker和容器的一些令人困惑的概念。我们还将看到容器化生态系统在2019年的现状以及演变方向。

“不是我们造就了历史,而是历史造就了我们。” -- 马丁·路德·金

Docker是当今最知名的容器平台之一,它于2013年发布。但是在此之前,隔离和容器化已经被使用。让我们回到1979年,当时我们刚开始使用Chroot Jail,之后便出现了最著名的容器化技术。了解这段历史不但有助于我们理解相关的新概念,也有利于我们理解这项技术。

7NrIRj7.png!web

时间退回到1979年, Unix版本7 在开发过程中引入 Chroot Jail 以及Chroot系统调用。Chroot jail被用于“Change Root”,它被认为是最早的容器化技术之一。它允许您将进程及其子进程与操作系统的其余部分隔离开来。这种隔离的唯一问题是根进程(root process)可以轻松地退出chroot。它从未考虑实现安全机制。 FreeBSD Jail 于2000年在FreeBSD OS中引入,旨在为简单的Chroot文件隔离带来更多安全性。与Chroot不同,FreeBSD还实现了将进程及其活动隔离到文件系统的特定视图中。

RBJBfeN.gif

A Chroot Jail. 来源: https://linuxhill.wordpress.co ... heezy

FZnYnyu.png!web

当Linux内核具有操作系统级的虚拟化的功能以后, Linux VServer 于2001年被推出,它使用了类似chroot的机制与“安全上下文”(“security context”)以及操作系统虚拟化(容器化)相结合来提供虚拟化解决方案。它比简单的chroot更先进,允许您在单个Linux发行版(VPS)上运行多个Linux发行版。

2004年2月,Oracle发布了 Oracle Solaris Containers ,这是一个用于X86和SPARC处理器的Linux-Vserver版本。

SPARC是由Sun Microsystems开发的RISC(精简指令集计算)架构。

Solaris Container是由系统资源控制和“区域”(zone)提供的边界隔离组合而成。

6BJBz26.png!web

Oracle Solaris 11.3

与Solaris Containers类似, OpenVZ 的第一个版本于2005年推出。OpenVZ与Linux-VServer一样,使用操作系统级虚拟化,许多托管公司采用它来隔离和销售VPS。操作系统级虚拟化有一些限制,因为容器共享相同的体系结构和内核版本,当客户需要不同于主机的内核版本的情况下这种缺点就会显现出来。

Linux-VServer和OpenVZ需要为内核打补丁以添加一些用于创建隔离容器的控制机制。 OpenVZ的补丁未集成到内核中。

yQFJZju.png!web

2007年,谷歌发布了 CGroups ,这是一种机制,这种机制能限制和隔离一系列进程的资源使用(CPU,内存,磁盘I / O,网络等)。与OpenVZ 内核相反,CGroups在2007年集成进了Linux内核。

2008年, LXC(Linux containers, Linux容器) 的第一个版本发布。 LXC与OpenVZ,Solaris Containers和Linux-VServer类似,但是它使用的是已经在Linux内核中实现的CGroup。然后,CloudFoundry于2013年创建了 Warden ,这是一个管理隔离,短暂存在和被资源控制的环境的API。在其第一个版本中, Warden 使用了LXC。

BfI3Y3M.png!web

2013年, Docker 推出了的第一个版本。它像OpenVZ和Solaris Containers一样,实现操作系统级虚拟化。

2014年,谷歌推出了LMCTFY(Let me contain that for you),谷歌容器栈的开源版本,提供Linux应用程序容器。谷歌工程师一直在与Docker合作libcontainer,并将核心概念和抽象移植到libcontainer。因此没有积极开发LMCTFY项目,未来LMCTFY项目的核心可能会被libcontainer取代。

LMCTFY在同一内核上的隔离环境中运行应用程序,并且无需补丁,因为它使用CGroup,命名空间和其他Linux内核功能。

谷歌是容器化行业的领导者。谷歌的一切都在容器上运行。每周有 超过20亿个容器 在Google基础架构上运行。

2014年12月,CoreOS发布并开始支持rkt(最初作为Rocket发布)作为Docker的替代品。

zaYvIzb.jpg!web

Jails,虚拟专用服务器(VPS),区域(Zones),容器和VM ##

使用 Jails,Zones,VPS,VM容器 都是为了隔离和资源控制,但每种技术是通过不同的方式实现它,每种方式都有其局限性和优势。

到目前为止,我们已经简要介绍了Jail如何工作,并介绍了Linux-VServer如何允许运行隔离的用户空间,其中计算机程序直接在主机操作系统的内核上运行,但程序只能访问其资源的受限子集。

Linux-VServer允许运行 “虚拟专用服务器”(“Virtual Private Servers”) ,但必须为主机内核打补丁才能使用它。 (将VPS视为商业名称。)

Solaris容器被称为区域(Zone)。

“虚拟机”是在“真实硬件机器”之上模拟虚拟机的通用术语。该术语最初由Popek和Goldberg定义为真实计算机的有效,孤立副本。

虚拟机可以是“系统虚拟机”或“过程虚拟机”。在我们日常使用VM这个词时,我们通常指的是“系统虚拟机”,它是模拟的是主机硬件来模拟整个操作系统。但是,“进程虚拟机”(“Process Virtual Machine”,有时称为应用虚拟机)是用于模拟执行单个进程的编程环境:Java Virtual Machine(jvm,java虚拟机)就是一个例子。

操作系统级虚拟化也称为容器化。 Linux-VServer和OpenVZ等技术可以运行多个操作系统,同时共享相同的体系架构和内核版本。

在guest需要的虚拟机于主机的内核版本的情况时,共享相同的体系结构和内核会有一些限制和缺点。

![whyContainers( https://cdn-images-1.medium.com/max/800/0 *ReM7IT-89RvS5d6X.png)

来源: https://fntlnz.wtf/post/why-containers

系统容器(例如LXC)提供的环境非常接近您从虚拟机获得的环境,与此同时又省去了运行单独内核和模拟所有硬件所带来的开销。

6BNvI3M.png!web

VM与容器。来源:Docker博客

操作系统容器与应用容器

操作系统级虚拟化有助于我们创建容器。 LXC和Docker等技术使用这种隔离方式。我们这里有两种类型的容器:

  • OS容器会打包整个应用程序栈的操作系统(示例LEMP技术栈,LEMP是指一组一起使用来运行动态网站或者服务器的开源软件,软件名称的首字母缩写:L代表Linux,E代表Nginx(engine x),M代表MariaDB或MySQL,P代表Php)。
  • 应用容器通常每个容器会运行一个进程

而对于app容器来说,它会创建3个容器来构成LEMP技术栈

- PHP服务器(或者是PHP FPM).

- Web服务器(Nginx)

- Mysql

bMFJZnM.png!web

系统(OS)容器与应用(App)容器

Docker: 是容器还是平台

简短回答:都是

ZvuuU3u.png!web

详细答案:

当Docker启动时,它使用LXC作为容器运行时,其想法是创建一个API来管理容器运行时,隔离运行应用程序的单个进程,并监督容器的生命周期及其使用的资源。

在2013年初,Docker项目是建立一个“标准容器”,我们可以在这个 宣言 中看到。

m2q2e2U.png!web

现在标准容器宣言已删除。

3YnaqmQ.png!web

Docker开始构建一个具有多种功能的单体应用程序,这些功能包括启动云服务器以及构建和运行镜像/容器等等。

Docker使用“ libcontainer ”与Linux内核功能(如 控制组命名空间 )进行交互。

6BZzmyb.jpg!web

Docker,Libcontainer和Linux内核功能

让我们使用命名空间和Cgroup(控制组)创建一个容器

我在这个例子中使用的是Ubuntu,但对于大多数发行版来说操作应该类似。首先安装CGroup工具和压力测试工具程序(因为我们将进行一些压力测试)。

sudo apt install cgroup-tools

sudo apt install stress

此命令将创建一个新的执行上下文:

sudo unshare --fork --pid --mount-proc bash
fuUbAzB.gif

unshare 命令解除了进程的部分执行上下文的关联

unshare()允许进程(或线程)与当前和其他进程(或线程)共享的执行上下文解除关联。但是部分执行上下文(例如mount

namespace),当使用fork(2)或vfork(2)去创建新进程时,执行上下文,与此同时,其他部分(例如虚拟内存),可能在使用clone(2)创建进程或线程,通过显式请求共享虚拟内存。

现在,使用 cgcreate 我们来创建一个控制组并定义两个控制器,一个在内存上,另一个在CPU上。

VJ7nMjI.gif

下一步是定义内存限制并使之生效:

echo 3000000 > /sys/fs/cgroup/memory/mygroup/memory.kmem.limit_in_bytes cgexec -g memory:mygroup bash
YZjqqan.gif

现在让我们压力测试一下我们创建的独立命名空间(包含内存限制)。

stress --vm 1 --vm-bytes 1G --timeout 10s
ryQJn2a.gif

可以看到执行失败了,于是我们知道内存限制正常工作。

如果我们在主机上做同样的事情(不要在16G RAM上模仿),测试永远不会失败,除非你真的没有足够的可用内存:

U73Yjm3.gif

遵循这些步骤有利于你理解linux工具(如 CGroup (控制组)和其他资源管理功能)是如何在linux系统中创建和管理隔离的环境。

libcontainer与这些工具交互以管理和运行Docker容器。

runC:在不使用Docker的情况下使用libcontainer

2015年,Docker发布 runC :一个轻量级,可移植的容器运行时。

77Jz6jy.png!web

runC实际上是一个直接利用libcontainer的命令行工具,无需通过Docker引擎。

runC的目标是使标准容器随处可用。

这个项目被捐赠了 Open Container Initiative,OCI

libcontainer存储库现已被存档。

7Rruqab.png!web

实际上,libcontainer并没有被抛弃,而是被转移到了runC仓库。

V7V3yeB.png!web

让我们回到实践部分并使用runC创建一个容器。

首先安装runC运行时:

MZZjIbU.gif

让我们创建一个目录(/mycontainer),我们将导出镜像 Busybox 的内容。

BusyBox的大小介于1到5 Mb之间(取决于变体),在制作高效利用空间发行版的时候,是一个非常好的组成部分。

BusyBox将许多常见UNIX实用程序的微小版本组合到一个小的可执行文件中。它提供了你通常在GNU

fileutils,shellutils等中找到的大多数实用程序的替代工具。BusyBox中的工具程序通常比它们包含所有功能的GNU版本有更少的选项;但包含的选项提供了大部分你所需要的功能,与GNU的对应版本非常相似。

BusyBox为任何小型或嵌入式系统提供了相当完善的环境。

来源:Docker Hub。

Q7jYVve.gif

使用runC命令,我们可以用提取的镜像和spec文件(config.json)来运行busybox容器。

runc spec 命令一开始会创建以下JSON文件:

{

"ociVersion": "1.0.1-dev",

"process": {

"terminal": true,

"user": {

"uid": 0,

"gid": 0

},

"args": [

"sh"

],

"env": [

"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",

"TERM=xterm"

],

"cwd": "/",

"capabilities": {

"bounding": [

"CAP_AUDIT_WRITE",

"CAP_KILL",

"CAP_NET_BIND_SERVICE"

],

"effective": [

"CAP_AUDIT_WRITE",

"CAP_KILL",

"CAP_NET_BIND_SERVICE"

],

"inheritable": [

"CAP_AUDIT_WRITE",

"CAP_KILL",

"CAP_NET_BIND_SERVICE"

],

"permitted": [

"CAP_AUDIT_WRITE",

"CAP_KILL",

"CAP_NET_BIND_SERVICE"

],

"ambient": [

"CAP_AUDIT_WRITE",

"CAP_KILL",

"CAP_NET_BIND_SERVICE"

]

},

"rlimits": [

{

"type": "RLIMIT_NOFILE",

"hard": 1024,

"soft": 1024

}

],

"noNewPrivileges": true

},

"root": {

"path": "rootfs",

"readonly": true

},

"hostname": "runc",

"mounts": [

{

"destination": "/proc",

"type": "proc",

"source": "proc"

},

{

"destination": "/dev",

"type": "tmpfs",

"source": "tmpfs",

"options": [

"nosuid",

"strictatime",

"mode=755",

"size=65536k"

]

},

{

"destination": "/dev/pts",

"type": "devpts",

"source": "devpts",

"options": [

"nosuid",

"noexec",

"newinstance",

"ptmxmode=0666",

"mode=0620",

"gid=5"

]

},

{

"destination": "/dev/shm",

"type": "tmpfs",

"source": "shm",

"options": [

"nosuid",

"noexec",

"nodev",

"mode=1777",

"size=65536k"

]

},

{

"destination": "/dev/mqueue",

"type": "mqueue",

"source": "mqueue",

"options": [

"nosuid",

"noexec",

"nodev"

]

},

{

"destination": "/sys",

"type": "sysfs",

"source": "sysfs",

"options": [

"nosuid",

"noexec",

"nodev",

"ro"

]

},

{

"destination": "/sys/fs/cgroup",

"type": "cgroup",

"source": "cgroup",

"options": [

"nosuid",

"noexec",

"nodev",

"relatime",

"ro"

]

}

],

"linux": {

"resources": {

"devices": [

{

 "allow": false,

 "access": "rwm"

}

]

},

"namespaces": [

{

"type": "pid"

},

{

"type": "network"

},

{

"type": "ipc"

},

{

"type": "uts"

},

{

"type": "mount"

}

],

"maskedPaths": [

"/proc/kcore",

"/proc/latency_stats",

"/proc/timer_list",

"/proc/timer_stats",

"/proc/sched_debug",

"/sys/firmware",

"/proc/scsi"

],

"readonlyPaths": [

"/proc/asound",

"/proc/bus",

"/proc/fs",

"/proc/irq",

"/proc/sys",

"/proc/sysrq-trigger"

]

}

}
B73qU3a.gif

另一个替代方案是使用“oci-runtime-tool”的子命令“oci-runtime-tool generate”,它包含很多选项帮助你做更多自定义配置。

更多信息,请参阅 runtime-tools

使用生成的规范JSON文件,您可以自定义容器的运行时。例如,我们可以更改要执行的应用程序的参数。

MFrQRvu.png!web

让我们来看看原始config.json文件和新文件之间的差别:

VB7Rbuj.png!web

现在让我们再次运行容器并观察到它在退出前休眠了10秒。

行业标准的容器运行时

鉴于容器逐渐成为主流,容器生态系统中的不同参与者一直致力于标准化。 标准化是自动化和总结最佳实践的关键。

在将runC项目提供给OCI的同时,Docker在2016年开始使用 containerd 作为容器运行时,containerd可以与更底层运行时runC进行交互。

JrqqQ3z.gif

Cotnainerd完全支持启动OCI下的软件并管理这些软件的生命周期。 Containerd(以及其他像cri-o这样的运行时)都是使用runC来运行容器,但Containerd实现了其他更高级功能,如镜像管理和更高级API。

BnaEjae.png!web

containerd与Docker和OCI运行时集成

Containerd,Shim和RunC,这一切是如何一起工作的

runC建立在libcontainer之上,libcontainer是以前为Docker引擎提供动力的容器库。

在1.11版之前,Docker引擎被用于管理卷,网络,容器,图像等。

现在,Docker架构被拆分成四个部分:

  • Docker引擎
  • containerd运行时
  • containerd-shim
  • 和runC运行时。

二进制文件分别称为 dockerdocker-containerddocker-containerd-shimdocker-runc

让我们用docker的新架构来列举运行容器的步骤:

  1. Docker引擎创建容器(来自镜像)并将其传递给containerd。
  2. Containerd调用containerd-shim
  3. Containerd-shim使用runC来运行容器
  4. Containerd-shim可以让运行时(此处为runC)在启动容器后退出 ,

使用新架构,我们可以运行“无守护容器”(“daemon-less containers” ),它有两个优点:

  1. runC可以在启动容器后退出,我们不必运行整个运行时进程。
  2. 即使Docker和/或容器死亡,containerd-shim也会保证stdin,stdout和stderr这些文件描述符为打开状态。
    qURRVfA.png!web

“既然runC和Containerd都是运行时,为什么运行单个容器时需要两个运行时?”

这可能是被问的最多的问题之一。在之前讲解为什么Docker将其架构拆分为runC和Containerd时,您会发现这两个都是运行时。

如果您是从头开始阅读的,那么您一定注意到有高层级和低层级的运行时。这就是runC和Containerd两者之间的实际差异。 两者都可以称为运行时,但每个运行时都有不同的用途和功能。为了使容器生态系统保持标准化,低层级的容器运行时只允许运行容器。

低级运行时(如runC)应该轻巧,快速,并且不会与其他更高层级的容器管理发生冲突。

当你创建一个容器时,这个容器实际上同时被containerd和 runC两个运行时同时管理。 您会发现有很多容器运行时,其中一些是遵循OCI标准的而另一些不是,一些是低层级的运行时,而另一些不仅仅是运行时,它们同时实现了工具层来管理容器的生命周期等等:

  • 镜像传输和存储,
  • 容器运行和监控,
  • 底层存储
  • 网络附件
  • 等等..
yYFFZje.png!web

来源: https://www.ianlewis.org/en/co ... ner-r

我们可以为docker添加新的运行时,只需运行:

sudo dockerd --add-runtime=<runtime-name>=<runtime-path> 

示例:

sudo apt-get install nvidia-container-runtime 

sudo dockerd --add-runtime=nvidia=/usr/bin/nvidia-container-runtime 

容器运行时接口

Kubernetes是当下最流行的容器编排系统之一。随着容器运行时数量的不断增加,kubernetes目标是变得更加灵活,并且与更多的容器运行时(不仅仅是docker)进行交互。

最初,Kubernetes使用Docker运行时来运行容器,并且现在它仍然是默认的运行时。

但是,CoreOS希望将kubernetes与RKT运行时一起使用,并为kubernetes提供补丁,以便将来此运行时可以用来替代Docker运行时。

在添加新的容器运行时,Kubernetes不希望改变自己的代码库,于是它决定创建容器运行时接口(CRI或Container Runtime Interface),这是一组API和库,允许在Kubernetes中运行不同的容器运行时。

kubernetes通过CRI API与它支持的运行时进行交互。

Yfyauq6.png!web

以下是一些CRI插件:

CRI-O:

是为kubernetes CRI接口创建的第一个容器运行时。 cri-o不是为了取代Docker,而是可以在Kubernetes的特定上下文中使用它而不是Docker运行时。

F3myaaF.png!web

Containerd CRI:

使用cri-containerd,用户可以使用containerd作为底层运行时运行Kubernetes集群,而无需安装Docker。

V3AbYfY.png!web

gVisor CRI:

gVisor是由Google开发的项目,它在用户空间中实现了大约200个Linux系统调用,与直接在Linux内核上运行的Docker容器(使用命名空间隔离)相比,具有更高的安全性。

Google Cloud App Engine使用gVisor CRI实现客户之间的隔离。

rUJFjme.png!web

gVisro运行时与Docker和Kubernetes集成,使得运行沙盒容器变得简单。

CRI-O Kata容器

Kata Containers是一个开源项目,用于构建轻量级虚拟机,可插入容器生态系统。 CRI-O Kata 容器允许在Kubernetes上运行Kata Containers来替代默认的Docker运行时。

RJ3ma26.jpg!webMoby项目

建立一个单体的Docker平台的项目在某种程度上已被抛弃,并催生了Moby项目,在这个项目中,Docker被拆分成多个组件,例如RunC。

RVzQFni.png!web

来源:Solomon Hykes Twitter

Moby是一个将Docker开发进行组织和模块化的项目。

它是一个开发和生产的生态系统。 Docker的常规用户很难意识到变化。

vmMFf2U.png!web

资料来源:Solomon Hykes Twitter

Moby帮助开发和运行Docker CE和EE(Moby是Docker上游)以及为其他运行时和平台创建开发和生产环境。

开放容器计划(The Open Containers Initiative)

正如我们所看到的,Docker将RunC捐赠给Open Container Initiative(OCI),但这项计划是什么?

OCI是一个轻量级,开放型治理架构的组织,由Docker,CoreOS和容器行业的其他领导者于2015年发起。

开放容器计划(OCI)旨在建立软件容器的通用标准,以避免容器生态系统内部可能出现的分裂和分化。

Yv6jey7.png!web

它包含两个规范:

  • runtime-spec:运行时规范
  • image-spec:镜像规范

使用不同运行时的容器可以与Docker API一起使用。使用Docker创建的容器可以能在任何其他引擎上运行。

【原文链接】 The Missing Introduction To Containerization

译者介绍

Grace,程序员,研究生毕业于SUNY at Stony Brook,目前供职于Linktime Cloud Company,对大数据技术以及数据可视化技术感兴趣。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK