摘要:Docker为何火,靠的就是Docker镜像。他打包了应用程序的全部依赖,完全解决了环境的一致性问题,从新定义了软件的交付方式,提升了生产效率。
本文分享自华为云社区《认识容器,咱们从它的历史开始聊起》,做者:技术火炬手。程序员
关于容器的历史、发展以及技术本质,在互联网上已经有很是多的文章了。这里旨在结合自身的工做经验和理解,经过一系列的文章,讲清楚这项技术。docker
容器的历史和发展
一、前世
讲到容器,就不得不提LXC(Linux Container),他是Docker的前生,或者说Docker是LXC的使用者。完整的LXC能力在2008年合入Linux主线,因此容器的概念在2008年就基本定型了,并非后面Docker造出来的。关于LXC的介绍不少,大致都会说“LXC是Linux内核提供的容器技术,能提供轻量级的虚拟化能力,能隔离进程和资源”,但总结起来,无外乎就两大知识点Cgroups(Linux Control Group)和Linux Namespace。搞清楚他俩,容器技术就基本掌握了。shell
- Cgroups:重点在“限制”。限制资源的使用,包括CPU、内存、磁盘的使用,体现出对资源的管理能力。
- Namespace:重点在“隔离”。隔离进程看到的Linux视图。说大白话就是,容器和容器之间不要相互影响,容器和宿主机之间不要相互影响。
二、少年期起步艰难
2009年,Cloud Foundry基于LXC实现了对容器的操做,该项目取名为Warden。2010年,dotCloud公司一样基于LXC技术,使用Go语言实现了一款容器引擎,也就是如今的Docker。那时,dotCloud公司仍是个小公司,出生卑微的Docker没什么热度,活得至关艰难。bash
三、 成长为巨无霸
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镜像。他打包了应用程序的全部依赖,完全解决了环境的一致性问题,从新定义了软件的交付方式,提升了生产效率。ui
四、 被列强蚕食
Docker在容器领域快速成长,野心天然也变大了。2014年推出了容器云产品Swarm(K8s的同类产品),想扩张事业版图。同时Docker在开源社区拥有绝对话语权,至关强势。这种走本身的路,让别人无路可走的行为,让容器领域的其余大厂玩家非常不爽,为了避免让Docker一家独大,决定要干他。url
2015年6月,在Google、Redhat等大厂的“运做”下,Linux基金会成立了OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准,也就是咱们常说的OCI标准。同时,Docker公司将Libcontainer模块捐给CNCF社区,做为OCI标准的实现,这就是如今的RunC项目。说白了,就是如今这块儿有个标准了,你们一块儿玩儿,不被某个特定项目的绑定。spa
讲到Docker,就得说说Google家的Kubernetes,他做为容器云平台的事实标准,现在已被普遍使用,俨然已成为大厂标配。Kubernetes原生支持Docker,让Docker的市场占有率一直居高不下。如图是2019年容器运行时的市场占有率。.net
但在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。
因此,咱们能够获得一个结论。从宿主机的视角看,容器就是进程。
二、接下来,咱们进入这个容器。
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查看,你会发现无论是直接启动容器,仍是在容器中启动新的进程,从宿主机的角度看,他们都是进程。
2、容器视角看容器:
前面咱们已经进入容器里面,并启动了新的进程。可是咱们并无在容器里查看进程的状况。在容器中执行ps,会发现获得的结果和宿主机上执行ps的结果彻底不同。下图是容器中的执行结果。
在Container1容器中只能看见刚起启动的shell进程(container1和container1-embed),看不到宿主机上的其余进程,也看不到Container2和Container3里面的进程。这些进程像被关进了一个盒子里面,彻底感知不到外界,甚至认为咱们执行的container1是1号进程(1号进程也叫init进程,是系统中全部其余用户进程的祖先进程)。因此,从容器的视角,容器以为“我就是天,我就是地,欢迎来到个人世界”。
但尴尬的是,在宿主机上,他们倒是普通得不能再普通的进程。注意,相同的进程,在容器里看到的进程ID和在宿主机上看到的进程ID是不同的。容器中的进程ID分别是1和1859,宿主机上对应的进程ID分别是12280和9775(见上图)。
3、总结
经过上面的实验,对容器的定义就须要再加上一个定语。容器就是进程=>容器是与系统其余部分隔离开的进程。这个时候咱们再看下图就更容易理解,容器是跑在宿主机OS(虚机容器的宿主机OS就是Guest OS)上的进程,容器间以及容器和宿主机间存在隔离性,例如:进程号的隔离。
在容器内和宿主机上,同一个进程的进程ID不一样。例如:Container1在容器内PID是1,在宿主机上是12280。那么该进程真正的PID是什么呢?固然是12280!那为何会形成在容器内看到的PID是1呢,形成这种幻象的,正是Linux Namespace。
Linux Namespace是Linux内核用来隔离资源的方式。每一个Namespace下的资源对于其余Namespace都是不透明,不可见的。
Namespace按隔离的资源进行分类:
前面提到的容器内外,看到的进程ID不一样,正是使用了PID Namespace。那么这个Namespace在哪呢?在Linux上一切皆文件。是的,这个Namespace就在文件里。在宿主机上的proc文件中(/proc/进程号/ns)变记录了某个进程对应的Namespace信息。以下图,其中的数字(例如:pid:[ 4026534312])则表示一个Namespace。
对于Container一、Container二、Container3这3个容器,咱们能够看到,他们的PID Namespace是不同的。说明他们3个容器中的PID相互隔离,也就是说,这3个容器里面能够同时拥有PID号相同的进程,例如:都有PID=1的进程。
在一个命名空间中,那这俩进程就相互可见,只是PID与宿主机上看到的不一样而已。
至此,咱们能够对容器的定义再细化一层。容器是与系统其余部分隔离开的进程=》容器是使用Linux Namespace实现与系统其余部分隔离开的进程。