1.2 Docker的结构与特性
通过上一小节的介绍,大家对Docker有一个初步的了解。这一节,再来聊一下Docker的组织结构。
1.2.1 Docker构成
如果把Docker当作一个独立的软件来看,它就是用Golang写的开源程序,采用C/S架构,包含Docker Server和Docker Client, 源代码托管在https://github.com/docker/docker上。
如果把Docker看作一个生态的话,它主要由两部分组成:Docker仓库和Docker自身程序。拿iPhone做类比的话,Docker仓库相当于iPhone的Appstore(应用商店),Docker相当于iPhone的iOS手机操作系统。
1.Docker仓库
官方Docker仓库地址为https://hub.docker.com, 上面的应用非常丰富,既有各大公司打包的应用,也有大量个人开发者提供的应用,如图1-1所示。
2.Docker自身程序
Docker本身是一个单机版的程序,它运行在Linux操作系统之上,属于用户态程序,通过一些接口和内核交互。它在机器上的位置如图1-2所示。
由于Docker需要用到Linux的cgroups、namespaces等特性,所以目前只能运行在Linux环境下,当然,通过虚拟机,也可以在Windows和Mac上使用Docker。
Docker是一个C/S的架构,它的Docker Daemon作为Server端,在宿主机上以后台守护进程的形式运行。Docker Client使用比较灵活,既可以在本机上以bin命令的形式(如Docker info、Docker start)发送指令,也可以在远端通过RESTful API的形式发送指令;Docker的Server端接收指令并把指令分解为一系列任务去执行。
3.工作流程
我们知道了Docker的构成,那么该如何使用Docker呢?
首先,要在Linux服务器上安装Docker软件包,并启动Docker Daemon守护进程。然后,就可以通过Docker Client端发送各种指令,Docker Daemon守护进程执行完指令,向Client端返回结果。
假如要启动一个新的Docker应用app1(名字是随便起的),它的工作流程大致如图1-3所示。
1)Docker Client向Daemon发送启动app1指令。
2)因为我们的Linux服务器只装有Docker软件包,根本没有app1相关软件或服务,Docker Daemon就发请求给Docker的官方仓库,在仓库中搜索app1。
3)如果找到app1这个应用,就把它下载到我们的服务器上。
4)Docker Daemon启动app1这个应用。
5)把启动app1应用是否成功的结果返回给Docker Client。
Docker的其他操作,比如停止或删除Docker应用和启动的流程差不多,这里就不再一一介绍了。
1.2.2 Docker化应用的存在形式
我们知道,经过20多年的发展,Linux下应用软件已经不计其数,不但种类繁多,而且安装部署方式也千奇百怪、不一而足,如有些软件依赖特定操作系统、有些依赖特定内核版本、有些依赖一些第三方软件和共享库等。另外,不同操作系统,不同的系统版本软件的配置和启动方式也存在很大差异。
既然软件安装部署方式没有一个统一的标准,那么Docker的官方仓库该如何做呢?总不能针对每个软件,写一个安装说明书吧。
换个角度想一下,用户的需求是什么——把软件运行起来,至于怎么安装软件、软件运行在什么操作系统下用户不太关心。那么,就把软件和它依赖的环境(包括操作系统和共享库等)、依赖的配置文件打包在一起,以虚拟机的形式放到官方仓库,供大家使用。只要有虚拟机的运行环境,就可以不做任何修改把软件轻松地运行起来。这种方式甚至不需要大家重复安装和配置软件,只要有一个人把软件安装和配置好,提交到官方仓库,其他人下载后就可以直接以虚拟机的形式运行起来。我们以这种方式解决了软件安装部署方式没有一个统一标准的问题,如图1-4所示。
但这种软件部署方式却存在很多问题,一般一个软件包大小也就几兆到几十兆不等,但一个操作系统却有好几个G。如果每个软件都带上它依赖的操作系统,那么每个软件都有几个G,不要说运行,仅仅下载1个软件都要数小时,是不是有“捡了芝麻丢了西瓜”的感觉?
Docker为了解决这个问题,引入分层的概念。把一个应用分为任意多个层,比如操作系统是第一层,依赖的库和第三方软件是第二层,应用的软件包和配置文件是第三层。如果两个应用有相同的底层,就可以共享这些层。
以图1-4为例,假如应用A和应用B操作系统版本是一样的,它们就可以使用共享这一层,安装应用A时需要下载操作系统层,安装B应用就不用下载操作系统层,只需要下载它的依赖包和自身的软件包。因为主流的操作系统也就那么几个,最差情况下,也就把常用的操作系统都安装一遍,然后,包含操作系统的软件包就和传统的软件包一样大小了,如图1-5所示。
但这种共享层存在冲突问题,比如,应用A需要修改操作系统的某个配置,应用B不需要修改。如何解决这个冲突呢?我们规定层次是有优先级的,上层和下层有相同的文件和配置时,上层覆盖下层,数据以上层的数据为准。我们给每个应用一个优先级最高的空白层,如果需要修改下层的文件,就把这个文件拷贝到这个优先级最高的空白层进行修改,保证下层的文件不做任何改变。这样,从应用A的角度来看,文件已经修改成功了,而从应用B的角度来看,文件没发生任何改变,如图1-6所示。
Docker的分层和写时拷贝策略,解决了包含操作系统的应用程序比较大的问题。但我们知道,主流的虚拟机(KVM、Xen、VMWare、VirtualBox等)一般比较笨重,除了虚拟机本身运行要消耗大量的系统资源(CPU、内存等)外,启动一个虚拟机也需要花费数分钟,如何把虚拟机做到轻量化呢?
以OpenVZ、VServer、LXC为代表的容器类虚拟机,是一种内核虚拟化技术,与宿主机运行在相同Linux内核,不需要指令级模拟,性能消耗非常小,是非常轻量级的虚拟化容器,虚拟容器的系统资源消耗和一个普通的进程差不多。Docker就是使用LXC(后来又推出libcontainer)让虚拟机变得轻量化。
在Docker的官方仓库里,只需它有完整的文件系统和程序包,没有动态生成新文件的需求;当把它下载到宿主机上运行对外提供服务时,有可能修改文件(比如输出新日志到日志文件中),需要有空白层用于写时拷贝。Docker把这两种不同状态做了区分,分别叫作镜像(image)和容器(container),如图1-7所示。
在仓库中的应用都是以镜像的形式存在的,把镜像从Docker仓库中下拉到本机,以这个镜像为模板启动应用,就叫容器。
综上所述,镜像指的是以分层的、可以被LXC/libcontainer理解的文件存储格式。Docker的应用都是以这种格式发布到Docker仓库中,供大家使用。把应用镜像从Docker仓库下载到本地机器上,以镜像为模板,在一个容器类虚拟机中把这个应用启动,这个虚拟机叫作容器。
在Docker的世界里,镜像和容器是它的两大核心概念,几乎所有的指令和文档都是围绕这两个概念展开的。
1.2.3 Docker对变更的管理
对于软件开发来说,版本迭代、版本回退是常态,Docker对变更管理又有什么特别之处呢?
假若有一个应用的Docker镜像,它的V1.0版本有三层,每层文件的大小如图1-8所示。
接下来,我们需要对它做如下修改:
修改位于第一层的文件A。
删除位于第二层的文件B。
添加一个新文件C。
Docker会新增一个第四层,针对上面的修改需求,它处理方法如下:
把第一层的文件A拷贝到第四层,修改文件A的内容。
在第四层,把名称为B的文件设置为不存在。
在第四层,创建一个新文件C。
通过增加一个第四层,我们的版本变更为V1.1,如图1-9所示。
我们想把应用的V1.1版本发布到Docker仓库,供其他宿主机使用。Docker的仓库已经存在这个应用镜像的V1.0版本,也就存储有这个应用的第一层、第二层和第三层,我们上传V1.1版本时,不需要重复上传前三层,只需要把第四层(只有3M大小)上传到Docker仓库就可以了。
有一台远程服务器,正在运行这个应用的V1.0版,它想升级到V1.1版。因为它本机已经有这个应用的前三层,所以只需要从Docker仓库把第四层下载下来,就可以运行V1.1版,如图1-10所示。
综上所述,Docker不仅具有版本控制功能,并且还能够利用分层特性做到增量更新。