Docker镜像原理与镜像的构建
转自:https://yq.aliyun.com/articles/68477
镜像
传统企业是以交付应用的方式进行发布的,交付应用相当于可执行性程序,其整个应用与环境是分开维护的。随着容器技术的兴起,提出了交付环境的概念。交付环境与交付应用相比,交付的不仅是可执行程序,还交付可执行程序依赖的配置文件、类库甚至是整个文件系统。在Docker语境里面,环境(文件系统和其中的可执行程序以及程序的依赖)就是镜像。从上图左下角镜像示例图可以看出,镜像本身的组织结构是分层的。其优点是,虽然它包含了所有的依赖,但是发布部署的时候不会显著增加信息的传输量。
镜像的表示分为四部分:红色的部分是镜像中心域名,黄色的部分是镜像命名空间,我们可以根据命名空间进行权限控制等操作,绿色是镜像的名称,每个镜像有一个版本(即标签)。Docker官方的镜像不需要镜像中心的域名,有一些镜像可以省略命名空间。
镜像基本操作
镜像制作——Dockerfile
第一行是一个FROM指令,使用了一个叫alpine的基础镜像。所有的镜像都可以用来做基础镜像,我们通常不需要关心最基础的镜像是怎么来的,只需要在现有镜像的基础上,构建新的镜像即可。我们在构建的时候,Docker依赖这个基础镜像,在这个基础镜像之上我们再做一些改动,生产新的镜像。FROM指令必须要有,而且只能有一个,通常是放在整个Dockerfile的最前面。
RUN指令做的是在镜像里安装一些软件,或者做一些需要的操作。Docker RUN之后执行了Shell指令,它对镜像里面的内容做了一些改动,最后再执行Docker COMMIT,把当前容器里面的改动持久化到镜像里面。RUN命令可以执行多次。但是通常来说建议把需要的指令都写在一条RUN命令里面,用&&符号连接起来,好处是镜像只增加了一层,可以加快镜像构建的速度,减少镜像的层次。
在讨论ADD指令前,我们先看看构建命令的输入。构建命令的输入内容包括两项,第一项是Dockerfile,另一项是Dockerfile依赖的上下文目录。第一条ADD命令就是相当于把www目录从上下文目录拷贝到镜像目录下面,目录名是相同的。第二条ADD的命令是添加文件到镜像,还有一个类似的命令是COPY,ADD命令与COPY命令做的事情是一样的。但是ADD命令能做的更多,比如,你的源不一定是Dockerfile上下文目录里面的内容,可以是一个网络路径。ADD命令还能将可识别的压缩文件进行解压,然后再放到镜像里面。
EXPOSE命令,相当于告诉Docker的守护进程,当前这个镜像在运行变成一个容器的时候,其监听的端口是什么,上图中其监听的是80端口。这样声明出去,其他与当前容器在同一个网络里面的容器可以通过这个端口来访问这个当前容器。这个地方其实并没有指定主机的端口,主机端口需要在启动容器的时候,再具体指定。
CMD命令会做两件事情,但需要依赖一些前提条件,比如上图中做的事情是指定了镜像运行时候的首进程。
CMD指定首进程的方式有两种。一种是shell方式,以上图这种方式来指定可执行文件及其参数,实际上它的首进程会先启动一个shell进程,然后再将可执行文件及它的参数作为shell进程的参数传进去再启动。另外一种方式叫exec方式,它会把这些参数用方括号括起来,看上去是一个列表。这种启动方式的首进程就不是shell,而是可执行文件本身。
CMD命令与ENTRYPOINT命令有什么区别?如果CMD命令和ENTRYPOINT命令中有一个没有指定任何命令的话,实际上剩下的那个就是具体指定容器首进程的命令。但是如果两个命令同时存在,整个容器起来的首进程又是怎么指定呢?比如,现在指定了一个ENTRYPOINT命令,它有一个可执行文件,然后指定了可执行文件的参数。它生成这个首进程的话,就是刚刚提到的是以shell方式起来的,所以它解释出来的话,前面会加一个shell命令,然后再紧跟可执行文件,然后再跟上可执行文件的参数。如果在指定了ENTRYPOINT命令的同时又指定了这个CMD命令, CMD命令也有一个可执行文件及其参数。最后其效果是,在指定了ENTRYPOINT的情况下,如果有CMD命令,那么直接加在ENTRYPOINT命令后面,当作它的参数。整个这一串是作为首进程的指令起动起来的。
镜像优化
减小镜像大小
比如很常见的一个需求——镜像太大了。我们之前交付的是应用,通常一个可执行文件,它本身是很小。但是整个镜像如果太大的话,它整个传输过程中会增加部署的时间。减小镜像大小有几种方式:
使用轻量发行版的基础镜像。然后这个地方典型代表就是alpine的发行版。目前Docker的官方镜像基本都有一个基于alpine发行版制作的版本。其实我们在选择某一个基础镜像的时候会考量很多;
清理不必要的安装包和临时文件。在传输过程中,不同镜像大小的传输速度差别很明显。
加快镜像构建速度
现在应用的发布需求可能越来越多,比如说一天要发布很多次。我们在做镜像构建的时候,最常用的一个操作就是下载软件包去安装。所以我们推荐用国内的镜像软件源下载,比如使用阿里云的软件源进行下载。
构建使用缓存的条件包括:父层信息没有发生变化;当前构建指令没有变化;当前指令依赖的本地上下文没有发生变化。
镜像常见问题
唯一识别某个镜像
比如,一个Dockerfile构建了,推到了仓库,但是从仓库拉下来的时候,怎么知道拉到本地去部署的就是推上去的那个呢?所以说需要有一个标识来唯一识别某一个镜像。可以使用Docker镜像ID,其摘要信息是本地镜像配置文件的摘要。也可以使用manifest的摘要,这是另外一个唯一的标识符。比如说我们在使用Docker push命令的时候,会发现它的标准输出里面最后一行会输出一个摘要字符串,这个字符串就是manifest摘要。这两个摘要信息是不一样的。但是有依赖关系,比如manifest文件里面其实就保存了那个镜像ID,所以在给定一个manifest文件的基础上只可能有一个镜像ID是与之对应,但是反过来就不一定了。因为manifest还有一些别的信息。
上图里面有两个镜像中心,即两个registry,多个节点上都有Docker。在高版本的Docker基础上,从镜像中心拉镜像A到上面这个Docker,然后查看它的镜像ID。另外一个Docker也从相同registry拉镜像ID,然后查看镜像ID,它能保证这两个镜像ID是一样的。如果把这个镜像推到别的仓库,换一个名字,用Docker tag重新打一个标签,又把这个镜像推到了另外一个镜像中心,然后又有第三个Docker的节点把它拉下来,这些操作都能保证不管这个镜像怎么传输,其镜像ID是一致的,即内容可定位的。
唯一识别某个镜像:通过镜像版本来管理镜像,好处是镜像版本带有语义信息,缺点是镜像版本可以被覆盖;通过manifest摘要来管理镜像,好处是镜像唯一确定,缺点是镜像版本不带有语义信息。
正确管理容器内的进程
镜像要求指定容器的首进程,即完成三个工作:给这个容器里面其他进程正确传递信号,正确回收僵尸进程,等待子进程的退出。
编译型语言和解释型语言
对编译性语言和解释性语言,其构建是有区别的。编译性语言比如说JAVA,它本身是有源代码,需要经过一个编译打包的过程,生成一个war包,最后再去执行构建,这是比较推荐的方式。所以,建议对于编译性的语言,首先对它做一个打包动作,打包生成的war包放到Dockerfile上下文目录,然后通过拷贝的方式添加到镜像中,而不是在Dockerfile远程拉下来。这里推荐使用阿里云的持续构建平台——CRP平台。这个平台做的事情就是,拿到源代码之后,首先打包,打成war包作为一个Dockerfile构建上下文的一个输入,然后再拿到镜像中心构建服务进行构建。
但是像PHP这种不需要编译的解释性语言来说,可以考虑直接进行构建。