镜像类似程序文件是静态的,容器相当于进程是动态的。

一、Docker 镜像

通俗的讲,可以将Docker镜像理解为包含应用程序以及其相关依赖的一个基础文件系统,在Docker容器启动的过程中,它以只读的方式被用于创建容器的运行环境。

Docker镜像其实是由基于UnionFS文件系统的一组镜像层依次挂载而得,而每个镜像层包含的其实是对上一镜像层的修改,这些修改其实是发生在容器运行的过程中的。所以,也可以反过来理解,镜像是对容器运行环境进行持久化存储的结果。

A、镜像

与其他虚拟机镜像管理不同,Docker将镜像管理纳入到了自身设计之中,也即所有的Docker镜像都是按照Docker所设定的逻辑打包的,也是受到Docker Engine所控制的。

常见的虚拟机镜像,通常是由提供者打包成镜像文件,安装者从网上下载或是其他方式获得,恢复到虚拟机中的文件系统里;而Docker的镜像必须通过Docker打包,也必须通过Docker下载或导入后使用,不能单独直接恢复成容器中的文件系统。

虽然失去了灵活性,但固定的格式意味着可以很轻松的在不同的服务器间传递Docker镜像,配合Docker自身对镜像的管理功能,使得在不同的机器中传递和共享Docker变得非常方便,这也是Docker能够提升工作效率的一处体现。

B、镜像实现

对于每一个记录文件系统修改的镜像层来说,Docker都会根据它们的信息生成了一个Hash码,足以保证全球唯一性,这种编码(64长度的字符串)的形式在Docker很多地方都有体现。

由于镜像每层都有唯一的编码,就能够区分不同的镜像层并能保证它们的内容与编码是一致的,这带来了另一项好处,允许在镜像之间共享镜像层。




docker指定容器的保存位置 docker 容器保存_生命周期


由Docker官方提供的两个镜像ElasticSearch镜像和Jenkins镜像都是在OpenJDK镜像之上修改而得,实际使用的时候,这两个镜像是可以共用OpenJDK镜像内部的镜像层的。

这带来的一项好处就是让镜像可以共用存储空间,达到1+1<2的效果,为在同一台机器里存放众多镜像提供了可能。

一个虚拟机镜像的占用空间往往用GB来衡量,而Docker管理之下的镜像,占用空间是以MB为单位进行衡量的,加之镜像之间还能够共享部分的镜像层,也就是共享存储空间,所以在常见的硬盘里放下几十、数百个镜像也不是什么难事。

C、镜像命名

镜像层的ID既可以识别每个镜像层,也可以用来直接识别镜像(因为根据最上层镜像能够找出所有依赖的下层镜像,所以最上层进行的镜像层ID就能表示镜像的ID),但是使用这种无意义的超长哈希码显然是违背人性的,所以需要引入镜像命名,通过镜像名能够更容易的识别镜像。

docker images命令打印出的内容中,有两个与镜像命名有关的数据:repository和tag,这两者组成了docker对镜像的命名规则。


docker指定容器的保存位置 docker 容器保存_docker 容器保存为镜像_02


完整看,镜像的命名可以分成三个部分:usernamerepositorytag

  • username: 上传镜像的用户,与GitHub中的用户空间类似
  • repository:镜像内容,形成对镜像的表意描述
  • tag:表示镜像版本,方便区分内容细节

对于username来说,有的镜像有username部分,而有的镜像是没有的;没有username表示镜像是由Docker官方所维护和提供的。

Docker中镜像的repository部分通常采用的是软件名。之所以镜像通常直接采用软件名,还要回归到Docker对容器的轻量化设计中。Docker对容器的设计和定义是微型容器而不是庞大臃肿的完整环境,使得通常只在一个容器中运行一个应用程序,这样的好处自然是能够大幅降低程序之间相互的影响,也有利于利用容器技术控制每个程序所使用的资源

一个容器运行一个程序,那么容器的镜像也会仅包含程序以及与它运行有关的一些依赖包,所以使用程序的名字直接套用在镜像之上,既去除了镜像取名的麻烦,又能直接表达镜像中的内容。

镜像的标签(tag)是对同一种镜像进行更细层次区分的方法,也是最终识别镜像的关键部分。通常来说,镜像的标签主要是为了区分同类镜像不同构建过程所产生的不同结果的。由于时间、空间等因素的不同,Docker每次构建镜像的内容也就有所不同,具体体现就是镜像层以及它们的ID都会产生变化。而标签就是在镜像命名这个层面上区分这些镜像的方法。

与镜像的repository类似,镜像tag的命名方法也通常参考镜像所关联的应用程序。更确切的来说,通常会采用镜像内应用程序的版本号以及一些环境、构建方式等信息来作为镜像的tag。

二、容器的生命周期


docker指定容器的保存位置 docker 容器保存_怎么修改docker镜像里的内容_03


Docker封装了对容器底层的管理,只提供简单的操作接口,也就意味着Docker里对容器的一些运行细节会被更加严格的定义,其中就包括了容器的生命周期,也即图中色块表示的:CreatedRunningPausedStoppedDeleted

A、主进程

在诸多状态中,Running是最为关键的状态。如果单纯去看容器的生命周期会有一些难理解的地方,而Docker中对容器生命周期的定义其实并不是独立存在的。

在Docker的设计中,容器的生命周期其实与容器中PID为1这个进程有着密切的关系。更确切的说,它们其实是共患难,同生死的兄弟。容器的启动,本质上可以理解为这个进程的启动,而容器的停止也就意味着这个进程的停止,反过来理解亦然。

启动容器时,Docker会按照镜像中的定义,启动对应的程序,并将这个程序的主进程作为容器的主进程(PID为1的进程)。控制容器停止时,Docker会向主进程发送结束信号,通知程序退出。而当容器中的主进程主动关闭时(正常结束或出错停止),也会让容器随之停止。

Docker不仅是从设计上推崇轻量化的容器,也是许多机制上是以此为原则去实现的。所以,最佳的Docker实践方法是遵循着它的逻辑,逐渐习惯这种容器即应用,应用即容器的虚拟化方式。虽然在Docker中也能够实现在同一个容器中运行多个不同类型的程序,但这么做的话,Docker就无法跟踪不同应用的生命周期,有可能造成应用的非正常关闭,进而影响系统、数据的稳定性。


参考:

http://blog.poetries.top/2018/11/20/docker-base/

Docker Internals