写点什么

认识容器,我们从它的历史开始聊起

发布于: 1 小时前

摘要:Docker 为什么火,靠的就是 Docker 镜像。他打包了应用程序的所有依赖,彻底解决了环境的一致性问题,重新定义了软件的交付方式,提高了生产效率。


本文分享自华为云社区《认识容器,我们从它的历史开始聊起》,作者:技术火炬手。

 

关于容器的历史、发展以及技术本质,在互联网上已经有非常多的文章了。这里旨在结合自身的工作经验和理解,通过一系列的文章,讲清楚这项技术。

容器的历史和发展

1、前世


讲到容器,就不得不提 LXC(Linux Container),他是 Docker 的前生,或者说 Docker 是 LXC 的使用者。完整的 LXC 能力在 2008 年合入 Linux 主线,所以容器的概念在 2008 年就基本定型了,并不是后面 Docker 造出来的。关于 LXC 的介绍很多,大体都会说“LXC 是 Linux 内核提供的容器技术,能提供轻量级的虚拟化能力,能隔离进程和资源”,但总结起来,无外乎就两大知识点 Cgroups(Linux Control Group)和 Linux Namespace。搞清楚他俩,容器技术就基本掌握了。


  • Cgroups:重点在“限制”。限制资源的使用,包括 CPU、内存、磁盘的使用,体现出对资源的管理能力。


  • Namespace:重点在“隔离”。隔离进程看到的 Linux 视图。说大白话就是,容器和容器之间不要相互影响,容器和宿主机之间不要相互影响。

2、少年期起步艰难


2009 年,Cloud Foundry 基于 LXC 实现了对容器的操作,该项目取名为 Warden。2010 年,dotCloud 公司同样基于 LXC 技术,使用 Go 语言实现了一款容器引擎,也就是现在的 Docker。那时,dotCloud 公司还是个小公司,出生卑微的 Docker 没什么热度,活得相当艰难。

3、 成长为巨无霸


2013 年,dotCloud 公司决定将 Docker 开源。开源后,项目突然就火了。从大的说,火的原因就是 Docker 的这句口号“Build once,Run AnyWhere”。呵呵,是不是似曾相识?对的,和 Java 的 Write Once,Run AnyWhere 一个道理。对于一个程序员来说,程序写完后打包成镜像就可以随处部署和运行,开发、测试和生产环境完全一致,这是多么大一个诱惑。程序员再也不用去定位因环境差异导致的各种坑爹问题。


Docker 开源项目的异常火爆,直接驱动 dotCloud 公司在 2013 年更名为 Docker 公司。Docker 也快速成长,干掉了 CoreOS 公司的 rkt 容器和 Google 的 lmctfy 容器,直接变成了容器的事实标准。也就有了后来人一提到容器就认为是 Docker。


总结起来,Docker 为什么火,靠的就是 Docker 镜像。他打包了应用程序的所有依赖,彻底解决了环境的一致性问题,重新定义了软件的交付方式,提高了生产效率。

4、 被列强蚕食


Docker 在容器领域快速成长,野心自然也变大了。2014 年推出了容器云产品 Swarm(K8s 的同类产品),想扩张事业版图。同时 Docker 在开源社区拥有绝对话语权,相当强势。这种走自己的路,让别人无路可走的行为,让容器领域的其他大厂玩家很是不爽,为了不让 Docker 一家独大,决定要干他。


2015 年 6 月,在 Google、Redhat 等大厂的“运作”下,Linux 基金会成立了 OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准,也就是我们常说的 OCI 标准。同时,Docker 公司将 Libcontainer 模块捐给 CNCF 社区,作为 OCI 标准的实现,这就是现在的 RunC 项目。说白了,就是现在这块儿有个标准了,大家一起玩儿,不被某个特定项目的绑定。


讲到 Docker,就得说说 Google 家的 Kubernetes,他作为容器云平台的事实标准,如今已被广泛使用,俨然已成为大厂标配。Kubernetes 原生支持 Docker,让 Docker 的市场占有率一直居高不下。如图是 2019 年容器运行时的市场占有率。



但在 2020 年,Kubernetes 突然宣布在 1.20 版本以后,也就是 2021 年以后,不再支持 Docker 作为默认的容器运行时,将在代码主干中去除 dockershim。



如图所示,K8s 自身定义了标准的容器运行时接口 CRI(Container Runtime Interface),目的是能对接任何实现了 CRI 接口的容器运行时。在初期,Docker 是容器运行时不容置疑的王者,K8s 便内置了对 Docker 的支持,通过 dockershim 来实现标准 CRI 接口到 Docker 接口的适配,以此获得更多的用户。随着开源的容器运行时 Containerd(实现了 CRI 接口,同样由 Docker 捐给 CNCF)的成熟,K8s 不再维护 dockershim,仅负责维护标准的 CRI,解除与某特定容器运行时的绑定。当然,也不是 K8s 不支持 Docker 了,只是 dockershim 谁维护的问题。 随着 K8s 态度的变化,预计将会有越来越多的开发者选择直接与开源的 Containerd 对接,Docker 公司和 Docker 开源项目(现已改名为 moby)未来将会发生什么样的变化,谁也说不好。



讲到这里,不知道大家有没有注意到,Docker 公司其实是捐献了 Containerd 和 runC。这俩到底是啥东西。简单的说,runC 是 OCI 标准的实现,也叫 OCI 运行时,是真正负责操作容器的。Containerd 对外提供接口,管理、控制着 runC。所以上面的图,真正应该长这样。



Docker 公司是一个典型的小公司因一个爆款项目火起来的案例,不管是技术层面、公司经营层面以及如何跟大厂缠斗,不管是好的方面还是坏的方面,都值得我们去学习和了解其背后的故事。

什么是容器


按国际惯例,在介绍一个新概念的时候,都得从大家熟悉的东西说起。幸好容器这个概念还算好理解,喝水的杯子,洗脚的桶,养鱼的缸都是容器。容器技术里面的“容器”也是类似概念,只是装的东西不同罢了,他装的是应用软件本身以及软件运行起来需要的依赖。用鱼缸来类比,鱼缸这个容器里面装的应用软件就是鱼,装的依赖就是鱼食和水。这样大家就能理解 docker 的 logo 了。大海就是宿主机,docker 就是那条鲸鱼,鲸鱼背上的集装箱就是容器,我们的应用程序就装在集装箱里面。



在讲容器的时候一定绕不开容器镜像,这里先简单的把容器镜像理解为是一个压缩包。压缩包里包含应用的可执行程序以及程序依赖的文件(例如:配置文件和需要调用的动态库等),接下来通过实际操作来看看容器到底是个啥。

一、宿主机视角看容器


1、首先,我们启动容器。


docker run -d --name="aimar-1-container" euleros_arm:2.0SP8SPC306 /bin/sh -c "while true; do echo aimar-1-container; sleep 1; done"
复制代码


这是 Docker 的标准命令。意思是使用 euleros_arm:2.0SP8SPC306 镜像(镜像名:版本号)创建一个新的名字为"aimar-1-container"的容器,并在容器中执行 shell 命令:每秒打印一次“aimar-1-container”。


参数说明:

-d:使用后台运行模式启动容器,并返回容器 ID。

--name:为容器指定一个名字。


docker run -d --name="aimar-1-container" euleros_arm:2.0SP8SPC306 /bin/sh -c "while true; do echo aimar-1-container; sleep 1; done"207b7c0cbd811791f7006cd56e17033eb430ec656f05b6cd172c77cf45ad093c
复制代码


从输出中,我们看到一串长字符 207b7c0cbd811791f7006cd56e17033eb430ec656f05b6cd172c77cf45ad093c。他就是容器 ID,能唯一标识一个容器。当然在使用的时候,不需要使用全 id,直接使用缩写 id 即可(全 id 的前几位)。例如下图中,通过 docker ps 查询到的容器 id 为 207b7c0cbd81



aimar-1-container 容器启动成功后,我们在宿主机上使用 ps 进行查看。这时可以发现刚才启动的容器就是个进程,PID 为 12280。



我们尝试着再启动 2 个容器,并再次在宿主机进行查看,你会发现又新增了 2 个进程,PID 分别为 20049 和 21097。



所以,我们可以得到一个结论。从宿主机的视角看,容器就是进程


2、接下来,我们进入这个容器。


docker exec -it 207b7c0cbd81 /bin/bash
复制代码


docker exec 也是 Docker 的标准命令,用于进入某个容器。意思是进入容器 id 为 207b7c0cbd81 的容器,进入后执行/bin/bash 命令,开启命令交互。


参数说明:

-it 其实是-i 和-t 两个参数,意思是容器启动后,要分配一个输入/输出终端,方便我们跟容器进行交互,实现跟容器的“对话”能力。



hostname 从 kwephispra09909 变化为 207b7c0cbd81,说明我们已经进入到容器里面了。在容器中,我们尝试着启动一个新的进程。


[root@207b7c0cbd81 /]# /bin/sh -c "while true; do echo aimar-1-container-embed; sleep 1; done" &
复制代码



再次回到宿主机进行 ps 查看,你会发现不管是直接启动容器,还是在容器中启动新的进程,从宿主机的角度看,他们都是进程

二、容器视角看容器


前面我们已经进入容器里面,并启动了新的进程。但是我们并没有在容器里查看进程的情况。在容器中执行 ps,会发现得到的结果和宿主机上执行 ps 的结果完全不一样。下图是容器中的执行结果。



在 Container1 容器中只能看见刚起启动的 shell 进程(container1 和 container1-embed),看不到宿主机上的其他进程,也看不到 Container2 和 Container3 里面的进程。这些进程像被关进了一个盒子里面,完全感知不到外界,甚至认为我们执行的 container1 是 1 号进程(1 号进程也叫 init 进程,是系统中所有其他用户进程的祖先进程)。所以,从容器的视角,容器觉得“我就是天,我就是地,欢迎来到我的世界”



但尴尬的是,在宿主机上,他们却是普通得不能再普通的进程。注意,相同的进程,在容器里看到的进程 ID 和在宿主机上看到的进程 ID 是不一样的。容器中的进程 ID 分别是 1 和 1859,宿主机上对应的进程 ID 分别是 12280 和 9775(见上图)。

三、总结


通过上面的实验,对容器的定义就需要再加上一个定语。容器就是进程=>容器是与系统其他部分隔离开的进程。这个时候我们再看下图就更容易理解,容器是跑在宿主机 OS(虚机容器的宿主机 OS 就是 Guest OS)上的进程,容器间以及容器和宿主机间存在隔离性,例如:进程号的隔离。



在容器内和宿主机上,同一个进程的进程 ID 不同。例如:Container1 在容器内 PID 是 1,在宿主机上是 12280。那么该进程真正的 PID 是什么呢?当然是 12280!那为什么会造成在容器内看到的 PID 是 1 呢,造成这种幻象的,正是 LinuxNamespace。


Linux Namespace 是 Linux 内核用来隔离资源的方式。每个 Namespace 下的资源对于其他 Namespace 都是不透明,不可见的。



Namespace 按隔离的资源进行分类:



前面提到的容器内外,看到的进程 ID 不同,正是使用了 PID Namespace。那么这个 Namespace 在哪呢?在 Linux 上一切皆文件。是的,这个 Namespace 就在文件里。在宿主机上的 proc 文件中(/proc/进程号/ns)变记录了某个进程对应的 Namespace 信息。如下图,其中的数字(例如:pid:[4026534312])则表示一个 Namespace。



对于 Container1、Container2、Container3 这 3 个容器,我们可以看到,他们的 PIDNamespace 是不一样的。说明他们 3 个容器中的 PID 相互隔离,也就是说,这 3 个容器里面可以同时拥有 PID 号相同的进程,例如:都有 PID=1 的进程。



在一个命名空间中,那这俩进程就相互可见,只是 PID 与宿主机上看到的不同而已。



至此,我们可以对容器的定义再细化一层。容器是与系统其他部分隔离开的进程=》容器是使用 LinuxNamespace 实现与系统其他部分隔离开的进程


点击关注,第一时间了解华为云新鲜技术~

发布于: 1 小时前阅读数: 4
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
认识容器,我们从它的历史开始聊起