在开始谈docker容器之前,先需要清楚什么是虚拟化,什么是容器
如果要⽤简单的语句来阐述虚拟化技术的话,那么可以这么解释:
虚拟化技术是⼀种将计算机物理资源进⾏抽象、转换为虚拟的计算机资源提供给程序使⽤的技术。
这⾥所指的计算机资源,就包括了 CPU 提供的运算控制资源,硬盘提供的数据存储资源,⽹卡提供的⽹络传输资源等。
计算机发展早期,各类计算机平台,计算资源所提供的接口都不一样,调用十分混乱,没有像今天一样有相对统一的标准。由于为兼容不同的平台写各种各样的兼容代码,于是虚拟技术运应而生。虚拟化技术通过本身适配不同平台的硬件,抽象成统一的接口,从而实现程序的跨平台。
在虚拟化技术发展的过程中,人们又发现了虚拟化技术的另外一个用途:资源管理。
因为虚拟化技术本来就是对计算机物理资源的抽象转换成虚拟的计算机资源,这样就很容易在这里对计算机资源进行修改,比如可以告诉程序这台计算机只有4G内存,而无论计算机是有16G还是32G,程序都会按照虚拟机告诉它的4G内存来进行使用。
通过虚拟化技术管理计算机资源的方式,不当能让我们对计算机资源的控制更加灵活,而且还能极大的提交计算机资源的使用率。
看到这可能会有些迷惑,虚拟化技术本身就要耗费部分的计算机资源,怎么还能产生1+1>2的效果?
其实这里指的是计算机的使用率,而非计算机的占用率,这两者看似很相近,其实并非一个概念。虚拟化技术能够提高计算机资源的使用率,是指利用虚拟化技术,将原本程序使用不到的资源分配给其他程序使用,从而提升计算机资源的整体利用率。
例如,这⾥我们有⼀台运⾏ Nginx 的机器,由于 Nginx 运⾏对系统资源的消耗并不⾼,这就让系统⼏乎 95% 以上的资源处于闲置状态。这时候我们通过虚拟化技术,把其他的⼀些程序放到这台机器上来 运⾏,它们就能够充分利⽤闲置的资源。这带来的好处就是我们不需要再为这些程序单独部署机器,从⽽节约不少的成本。
部分读者读到这⾥就会产⽣疑惑了,我本⾝就可以在操作系统⾥安装这些程序并且同时运⾏,为什么还要把它们分别装到不同的虚拟环境中去呢? 其实道理很简单,虽然我们能够在操作系统⾥同时运⾏多个程序,但前提得是这些程序本⾝不存在冲突。这⾥的冲突体现在很多的⽅⾯,例如不同的程序同时使⽤了同⼀个端⼜;不同程序依赖于同⼀个 ⼯具库的不同版本;程序本⾝限制了同时开启的进程数等。虚拟化技术通过资源隔离的⽅式,⽆形地也可以把这些程序隔离在不同的虚拟环境中,既然虚拟环境不同,⾃然运⾏在不同环境中的程序就不 会互相⼲扰或争抢资源了。
所谓硬件虚拟化,指的是物理硬件本⾝就提供虚拟化的⽀持。举个例⼦来说,某个平台的 CPU,能够将另外⼀个平台的指令集转换为⾃⾝的指令集执⾏,并给程序完全运⾏在那个平台上的感觉。又或者 说,CPU 能够⾃⾝模拟裂变,让程序或者操作系统认为存在多个 CPU,进⽽能够同时运⾏多个程序或者操作系统。这些都是硬件虚拟化的体现。
⽽软件虚拟化则指的是通过软件的⽅式来实现虚拟化中关键的指令转换部分。依然⽤ CPU 的例⼦来说话,在软件虚拟化实现中,通过⼀层夹杂在应⽤程序和硬件平台上的虚拟化实现软件来进⾏指令的转换。也就是说,虽然应⽤程序向操作系统或者物理硬件发出的指令不是当前硬件平台所⽀持的指令,这个实现虚拟化的软件也会将之转换为当前硬件平台所能识别的
在实际场景中,虚拟化还能进⾏更加细化的分类,例如:
虚拟机 ( Virtual Machine )。所谓虚拟机,通常来说就是通过⼀个虚拟机监视器 ( Virtual Machine Monitor ) 的设施来隔离操作系统与硬件或者应⽤程序与操作系统,以此达到虚拟化的⽬的。这个夹在其中的虚拟机监视器,常常被称为 Hypervisor。
从我们习惯⽤来搭建虚拟操作系统环境的 VMware Workstation、Xen 等软件,到 Java 虚拟机 JVM,PHP 虚拟 机 HHVM 等等,都充活跃在我们程序开发到程序运⾏的过程中。
这个时候可能聪明的你们会发现,发现原来 JVM、HHVM 等特定语⾔运⾏环境中的核⼼部分,也是虚拟化的⼀种实实在在的实现。没错,只要⼤家仔细分析和思考⼀下就会发现,它们正是基于虚拟化的思想来实现的。它们通过隔离程序和操作系统,将程序的指令转换为当前所在操作系统平台所能执⾏的指令,达到了不⽤对程序进⾏任何修改即可执⾏的⽬的。也正是这个原因,这些语⾔的程序都具有⾮常强的跨平台性。
虽然虚拟机技术得益于 Hypervisor 的加持,使得应⽤程序或者操作系统可以在⽆任何修改的情况下运⾏在另⼀平台上,但⼤家很容易发现,其有⼀个致命的缺陷,就是所有的指令都必须经过虚拟机监视器的处理。这也就意味着,虚拟机的性能是低下的,例如运⾏在 ZendVM 或者 HHVM 中的 PHP 程序,所有代码虽然编译成了 Opcode 码,但其依然是通过虚拟机才最终转换为机器所能识别的机器码去执⾏。
这种效率的低下有时候是⽆法容忍的,为了解决这个问题,真实的虚拟机程序常常不完全遵循 Hypervisor 的设计结构,⽽是引⼊⼀些其他技术来解决效率问题。
例如,在 VMware Workstation、Xen 中我们能够看到硬件辅助虚拟化的使⽤,通过让指令直达⽀持虚拟化的硬件,以此避开了效率低下的 Hypervisor。⽽如 JRE、HPHP 中,除了基于 Hypervisor 实现的解 释执⾏机制外,还有即时编译 ( Just In Time ) 运⾏机制,让程序代码在运⾏前编译成符合当前硬件平台的机器码。
容器技术是⼀种全新意义上的虚拟化技术,按分类或者实现⽅式来说,其应该属于操作系统虚拟化的范畴,也就是在由操作系统提供虚拟化的⽀持。
所谓容器技术,指的是操作系统⾃⾝⽀持⼀些接⼜,能够让应⽤程序间可以互不⼲扰的独⽴运⾏,并且能够对其在运⾏中所使⽤的资源进⾏⼲预。
由于应⽤程序的运⾏被隔离在了⼀个独⽴的运⾏环境之中,这个独⽴的运⾏环境就好似⼀个容器,包裹住了应⽤程序,这就是容器技术名字的由来。
虚拟机VS容器
由于没有了虚拟操作系统和虚拟机监视器这两个层次,从而省略了指令转换这一操作,⼤幅减少了应⽤程序运⾏带来的额外消耗。 更准确的来说,所有在容器中的应⽤程序其实完全运⾏在了宿主操作系统中,与其他真实运⾏在其中的应⽤程序在指令运⾏层⾯是完全没有任何区别的。
Docker是由 dotCloud 在2013年开源的一个由Go实现的容器引擎。
利⽤它的全⾯性和易⽤性带来的提升我们的⼯作效率,可以将开发人员或者运维人员从重复且容易出错的服务搭建中解放出来,特别是在微服务的浪潮下,项目多模块化和服务化,一个完整的项目由很多小服务组成,这对于搭建来说也是一个不小的挑战。
docker技术实现
Docker 的实现,主要归结于三⼤技术:命名空间 ( Namespaces ) 、控制组 ( Control Groups ) 和联合⽂件系统 ( Union File System ) 。
命名空间是 Linux 核⼼在 2.4 版本后逐渐引⼊的⼀项⽤于运⾏隔离的模块。Linux 内核的命名空间,就是能够将计算机资源进⾏切割划分,形成各⾃独⽴的空间。 就实现⽽⾔,Linux Namespaces 可以分为很多具体的⼦系统,如 User Namespace、Net Namespace、PID Namespace、Mount Namespace 等等。
这⾥我们以进程为例,通过 PID Namespace,我们可以造就⼀个独⽴的进程运⾏空间,在其中进程的编号又会从 1 开始。在这个空间中运⾏的进程,完全感知不到外界系统中的其他进程或是其他进程命名空间中运⾏的进程。
利⽤ PID Namespace,Docker 就实现了容器中隔离程序运⾏中进程隔离这⼀⽬标。
资源控制组 ( 常缩写为 CGroups ) 是 Linux 内核在 2.6 版本后逐渐引⼊的⼀项对计算机资源控制的模块。 顾名思义,资源控制组的作⽤就是控制计算机资源的。与隔离进程、⽹络、⽂件系统等虚拟资源为⽬的 Namespace 不同,CGroups 主要做的是硬件资源的隔离。 之前我们提到了,虚拟化除了制造出虚拟的环境隔离同⼀物理平台运⾏的不同程序之外,另⼀⼤作⽤就是控制硬件资源的分配,CGroups 的使⽤正是为了这样的⽬的。
需要再强调⼀次的是,CGroups 除了资源的隔离,还有资源分配这个关键性的作⽤。通过 CGroups,我们可以指定任意⼀个隔离环境对任意资源的占⽤值或占⽤率,这对于很多分布式使⽤场景来说是⾮ 常有⽤的功能。
联合⽂件系统 ( Union File System ) 是⼀种能够同时挂载不同实际⽂件或⽂件夹到同⼀⽬录,形成⼀种联合⽂件结构的⽂件系统。联合⽂件系统本⾝与虚拟化并⽆太⼤的关系,但 Docker 却创新的将其引⼊ 到容器实现中,⽤它解决虚拟环境对⽂件系统占⽤过量,实现虚拟环境快速启停等问题。 在 Docker 中,提供了⼀种对 UnionFS 的改进实现,也就是 AUFS ( Advanced Union File System )。
AUFS 将⽂件的更新挂载到⽼的⽂件之上,⽽不去修改那些不更新的内容,这就意味着即使虚拟的⽂件系统被反复修改,也能保证对真实⽂件系统的空间占⽤保持⼀个较低⽔平。 也许这个表述还不够形象,那么我们来⽤ Git 进⾏⽐较,会让⼤家会更容易理解。⼤家知道,我们在 Git 中每进⾏⼀次提交,Git 并不是将我们所有的内容打包成⼀个版本,⽽只是将修改的部分进⾏记 录,这样即使我们提交很多次后,代码库的空间占⽤也不会倍数增加。 同样的,通过 AUFS,Docker ⼤幅减少了虚拟⽂件系统对物理存储空间的占⽤。由此,Docker 也开创出了虚拟化领域很多新的轻量级解决⽅案,这在之后的⼩节⾥我们会提到。
Docker 的理念
在对 Docker 及其背后的⼀些技术有了⼀个初步了解之后,我们还要着重说⼀下 Docker 本⾝的⼀些设计理念。如果说熟悉 Docker 背后的技术能够更好的帮助你正确使⽤ Docker,那么理解 Docker 的理念 将更好的指导你如何搭配 Docker 容器间的关系。 让我们先来从⼀张 Docker 官⽅提供的架构图来看看 Docker 对容器结构的设计。
与其他虚拟化实现甚⾄其他容器引擎不同的是,Docker 推崇⼀种轻量级容器的结构,即⼀个应⽤⼀个容器。
举个具体的例⼦,在常见的虚拟机实现中,我们要搭建⼀套 LAMP 结构的服务,我们通常会建⽴⼀个虚拟机,在虚拟机中安装上 Linux 系统,之后分别安装 Apache、MySQL 和 PHP。⽽在 Docker ⾥,最佳的实践是分别基于 Apache、MySQL 和 PHP 的镜像建⽴三个容器,分别运⾏ Apache、MySQL 和 PHP ,⽽它们所在的虚拟操作系统也直接共享于宿主机的操作系统。
如果我们将 Docker 的轻量级容器实现和虚拟机的⼀些参数进⾏对⽐,更容易得到结果。
属性 | Docker | 虚拟机 |
---|---|---|
启动速度 | 秒级 | 分钟级 |
硬盘使⽤ | MB 级 | GB 级 |
性能 | 接近原⽣ | 较低 |
普通机器⽀撑量 | 数百个 | ⼏个 |
从理论上我们已经知道 Docker 能够为我们的⼯作带来巨⼤的便利,那么将其放于实践中,我们应该如何正确的使⽤它呢?这⾥我摘录整理了⼀段来⾃ Docker 官⽅⽂档的指导意见,希望能够对⼤家的实 践提供参考。
使⽤ Docker 后,开发者能够在本地容器中得到⼀套标准的应⽤或服务的运⾏环境,由此可以简化开发的⽣命周期 ( 减少在不同环境间进⾏适配、调整所造成的额外消耗 )。对于整个应⽤迭代来说,加⼊ Docker 的⼯作流程将更加适合持续集成 ( Continuous Integration ) 和持续交付 ( Continuous Delivery )。
举个具体的例⼦:
开发者能够使⽤ Docker 在本地编写代码并通过容器与其他同事共享他们的⼯作。 他们能够使⽤ Docker 将编写好的程序推送⾄测试环境进⾏⾃动化测试或是⼈⼯测试。 当出现 Bugs 时,开发者可以在开发环境中修复它们,并很快的重新部署到测试环境中。 在测试完成后,部署装有应⽤程序的镜像就能完成⽣产环境的发布。
基于容器技术的 Docker 拥有很⾼的跨平台性,Docker 的容器能够很轻松的运⾏在开发者本地的电脑,数据中⼼的物理机或虚拟机,云服务商提供的云服务器,甚⾄是混合环境中。
同时,Docker 的轻量性和⾼可移植性能够很好的帮助我们完成应⽤的动态伸缩,我们可以通过⼀些⼿段近实时的对基于 Docker 运⾏的应⽤进⾏弹性伸缩,这能够⼤幅提⾼应⽤的健壮性。
Docker 的⾼效和轻量等特征,为替代基于 Hypervisor 的虚拟机提供了⼀个经济、⾼效、可⾏的⽅案。在 Docker 下,你能节约出更多的资源投⼊到业务中去,让应⽤程序产⽣更⾼的效益。同时,如此低的 资源消耗也说明了 Docker ⾮常适合在⾼密度的中⼩型部署场景中使⽤。
在 Docker 体系⾥,有四个对象 ( Object ) 是我们不得不进⾏介绍的,因为⼏乎所有 Docker 以及周边⽣态的功能,都是围绕着它们所展开的。它们分别是:镜像 ( Image )、容器 ( Container )、⽹络 ( Network )、数据卷 ( Volume )。
镜像,可以理解为⼀个只读的⽂件包,其中包含了虚拟环境运⾏最原始⽂件系统的 内容。
当然,Docker 的镜像与虚拟机中的镜像还是有⼀定区别的。⾸先,之前我们谈到了 Docker 中的⼀个创新是利⽤了 AUFS 作为底层⽂件系统实现,通过这种⽅式,Docker 实现了⼀种增量式的镜像结构。
每次对镜像内容的修改,Docker 都会将这些修改铸造成⼀个镜像层,⽽⼀个镜像其实就是由其下层所有的镜像层所组成的。当然,每⼀个镜像层单独拿出来,与它之下的镜像层都可以组成⼀个镜像。
另外,由于这种结构,Docker 的镜像实质上是⽆法被修改的,因为所有对镜像的修改只会产⽣新的镜像,⽽不是更新原有的镜像。
容器 ( Container ) 就更好理解了,在容器技术中,容器就是⽤来隔离虚拟环境的基础设施,⽽在 Docker ⾥,它也被引申为隔离出来的虚拟环境。
如果把镜像理解为编程中的类,那么容器就可以理解为类的实例。镜像内存放的是不可变化的东西,当以它们为基础的容器启动后,容器内也就成为了⼀个“活”的空间。 ⽤更官⽅的定义,Docker 的容器应该有三项内容组成:
在 Docker 中,实现了强⼤的⽹络功能,我们不但能够⼗分轻松的对每个容器的⽹络进⾏配置,还能在容器间建⽴虚拟⽹络,将数个容器包裹其中,同时与其他⽹络环境隔离。
另外,利⽤⼀些技术,Docker 能够在容器中营造独⽴的域名解析环境,这使得我们可以在不修改代码和配置的前提下直接迁移容器,Docker 会为我们完成新环境的⽹络适配。对于这个功能,我们甚⾄能 够在不同的物理服务器间实现,让处在两台物理机上的两个 Docker 所提供的容器,加⼊到同⼀个虚拟⽹络中,形成完全屏蔽硬件的效果。
除了⽹络之外,⽂件也是重要的进⾏数据交互的资源。在以往的虚拟机中,我们通常直接采⽤虚拟机的⽂件系统作为应⽤数据等⽂件的存储位置。然⽽这种⽅式其实并⾮完全安全的,当虚拟机或者容器 出现问题导致⽂件系统⽆法使⽤时,虽然我们可以很快的通过镜像重置⽂件系统使得应⽤快速恢复运⾏,但是之前存放的数据也就消失了。
为了保证数据的独⽴性,我们通常会单独挂载⼀个⽂件系统来存放数据。这种操作在虚拟机中是繁琐的,因为我们不但要搞定挂载在不同宿主机中实现的⽅法,还要考虑挂载⽂件系统兼容性,虚拟操作 系统配置等问题。值得庆幸的是,这些在 Docker ⾥都已经为我们轻松的实现了,我们只需要简单的⼀两个命令或参数,就能完成⽂件系统⽬录的挂载。
能够这么简单的实现挂载,主要还是得益于 Docker 底层的 Union File System 技术。在 UnionFS 的加持下,除了能够从宿主操作系统中挂载⽬录外,还能够建⽴独⽴的⽬录持久存放数据,或者在容器间共 享。
在 Docker 中,通过这⼏种⽅式进⾏数据共享或持久化的⽂件或⽬录,我们都称为数据卷 ( Volume )。
时⾄今⽇,Docker ⽣态已经远⽐它诞⽣之初要庞⼤许多,虽然我们仍然习惯使⽤ Docker 这个名字去指代实现容器技术⽀持的软件,但显然更加容易与其他的概念产⽣混淆。这⾥我们很有必要对这个 Docker 中最核⼼的软件进⾏介绍,不仅因为它在 Docker ⽣态中扮演着中⼼的地位,也因为它是我们在开发中实实在在接触最多的东西。 ⽬前这款实现容器化的⼯具是由 Docker 官⽅进⾏维护的,Docker 官⽅将其命名为 Docker Engine,同时定义其为⼯业级的容器引擎 ( Industry-standard Container Engine )。在 Docker Engine 中,实现了 Docker 技术中最核⼼的部分,也就是容器引擎这⼀部分。
虽然我们说 Docker Engine 是⼀款软件,但实实在在去深究的话,它其实算是由多个独⽴软件所组成的软件包。在这些程序中,最核⼼的就是 docker daemon 和 docker CLI 这俩了。 所有我们通常认为的 Docker 所能提供的容器管理、应⽤编排、镜像分发等功能,都集中在了 docker daemon 中,⽽我们之前所提到的镜像模块、容器模块、数据卷模块和⽹络模块也都实现在其中。在操 作系统⾥,docker daemon 通常以服务的形式运⾏以便静默的提供这些功能,所以我们也通常称之为 Docker 服务。
在 docker daemon 管理容器等相关资源的同时,它也向外暴露了⼀套 RESTful API,我们能够通过这套接⼜对 docker daemon 进⾏操作。或者更确切的说,是通过这套 RESTful API 对 docker daemon 中运 ⾏的容器和其他资源进⾏管理。 通常来说,我们是采⽤在控制台或者命令⾏输⼊命令来控制 docker daemon 的,因为这样很酷也更容易适应那些有或者没有图形界⾯的操作系统。
如果我们在控制台中编写⼀个 HTTP 请求以借助 docker daemon 提供的 RESTful API 来操控它,那显然是个费脑、费⼿又费时间的活⼉。所以在 Docker Engine ⾥还直接附带了 docker CLI 这个控制台程序。
熟悉程序结构的朋友们⽐较容易看出来,docker daemon 和 docker CLI 所组成的,正是⼀个标准 C/S ( Client-Server ) 结构的应⽤程序。衔接这两者的,正是 docker daemon 所提供的这套 RESTful API。