前言
preface
Docker 包含三个基本概念,分别是镜像(Image)、容器(Container)和仓库(Repository)。镜像是 Docker 运行容器的前提,仓库是存放镜像的场所,可见镜像更是Docker的核心。
Docker镜像相关操作
Docker
当运行容器时,使用的镜像如果在本地中不存在,Docker就会自动从Docker镜像仓库中下载,默认是从 Docker Hub公共镜像源下载,也可以通过其他的公共仓库中下载,或者自己创建镜像,下面学习如果获取镜像与镜像的相关操作。
列出镜像列表
我们可以使用docker images来列出本地主机上的镜像。
各个选项说明:
REPOSITORY:表示镜像的仓库源
TAG:镜像的标签
IMAGE ID:镜像ID
CREATED:镜像创建时间
SIZE:镜像大小
同一仓库源可以有多个TAG,代表这个仓库源的不同个版本,如nginx仓库源里,有1.17.8、1.17.9等多个不同的版本,我们使用REPOSITORY:TAG 来定义不同的镜像。
搜索镜像
可以从Docker Hub网站来搜索镜像,Docker Hub网址为:https://hub.docker.com/。也可以使用 docker search命令来搜索镜像。比如我们需要一个httpd的镜像。我们可以通过docker search命令搜索 httpd来寻找适合我们的镜像。
NAME: 镜像仓库源的名称
DESCRIPTION: 镜像的描述
OFFICIAL: 是否docker官方发布
STARS: 类似Github 里面的star,表示点赞、喜欢的意思
AUTOMATED: 自动构建
获取镜像
当我们在本地主机上使用一个不存在的镜像时,Docker就会自动下载这个镜像。如果我们想预先下载这个镜像,我们可以使用docker pull命令来下载它。而镜像可以通过Docker Hub官方获取,或者通过国内的阿里云、DaoCloud等厂商所提供的镜像仓库下载。
使用tag命令添加镜像标签
为了方便在后续工作中使用特定镜像,可以使用docker tag命令来作为本地镜像任意添加新的标签。例如:添加一个myhttpd:latest镜像标签。
之后就可以使用myhttpd:latest来表示这个镜像了,通过查看我们可以发现,myhttpd:latest镜像的ID与httpd:latest是完全一致的,他们实际上指向了同一个镜像文件,只是别名不同而已。docker tag命令添加的标签实际上起到了类似链接的作用。
删除镜像
可以使用docker rmi或者docker image rm将我们用不到的镜像删除,如果镜像被使用,那么无法删除镜像。删除的镜像可以是镜像名称标签或者是镜像ID。
清理镜像
使用docker一段时间后,系统可能会遗留一些临时的镜像文件,以及一些没有被使用的镜像,可以通过docker image prune命令来进行清理。支持的选项包括:
-a,-all:删除所有无用的镜像,不仅仅是临时镜像;
-f,-force:强制删除镜像,而不进行提示确认。
例如,如下命令会自动清理临时的遗留镜像文件层,最后会提示释放的存储空间:
导出镜像
可以使用docker image save将一个镜像导出到一个压缩包中,便于在其他主机上运行。
导入镜像
可以使用docker image load导入我们需要的镜像压缩包(为了实验效果,首先我们把之前的httpd镜像删除)。
镜像历史
可以使用docker history查看镜像的构建历史信息,因为在使用docker时经常会自己构建一些我们所需要的镜像,所以要会查看镜像的构建信息。
Docker镜像结构
Docker
Docker镜像揭秘
从整体的角度来讲,一个完整的Docker镜像可以支撑一个Docker容器的运行,在Docker容器运行过程中主要提供文件系统。例如一个centos:7的镜像,提供了一个基本的centos:7的发行版,当然此镜像是不包含操作系统Linux内核的。
从根本上讲,一个容器不过是一个正在运行的进程,并对其应用了一些附加的封装功能,以使其与主机和其他容器隔离。容器隔离的最重要方面之一是每个容器都与自己的专用文件系统进行交互。该文件系统由Docker镜像提供。镜像包括运行应用程序所需的一切代码或二进制文件,运行时,依赖项以及所需的任何其他文件系统对象。
Docker镜像与Docker容器的关系
Docker镜像是Docker容器运行的基础,没有Docker镜像,就不可能有Docker容器,这也是Docker的设计原则之一。
可以理解的是Docker镜像毕竟是镜像,属于静态的内容。而Docker容器就不一样了,容器属于动态的内容。动态的内容,大家很容易联想到进程,内存,CPU等之类的东西。的确,Docker容器作为动态的内容,都会包含这些。
为了便于理解,大家可以把Docker容器,理解为一个或多个运行进程,而这些运行进程将占有相应的内存,相应的CPU计算资源,相应的虚拟网络设备以及相应的文件系统资源。而Docker容器所占用的文件系统资源,则通过Docker镜像的镜像层文件来提供。
那么作为静态的镜像,如何才有能力转化为一个动态的Docker容器呢?其实,转化的依据是每个镜像的json文件,Docker可以通过解析Docker镜像的json的文件,获知应该在这个镜像之上运行什么样的进程,应该为进程配置怎么样的环境变量,此时也就实现了静态向动态的转变。
是由Docker守护进程来完成这个转化工作的。在上个小节中我们也提到:Docker容器实质上就是一个或者多个进程,而容器的父进程就是Docker守护进程。这样的,转化工作的执行就不难理解了。Docker守护进程 手握Docker镜像的json文件,为容器配置相应的环境,并真正运行Docker镜像所指定的进程,完成Docker容器的真正创建。
Docker容器运行起来之后,Docker镜像json文件就失去作用了。此时Docker镜像的绝大部分作用就是为Docker容器提供一个文件系统,供容器内部的进程访问文件资源。
最小Linux发行版镜像
我们先使用docker pull下载一个centos:7镜像,用docker images查看镜像列表,发现centos:7镜像才200多兆。
那么这个centos7镜像中包含了哪些内容?可以通过Dockerfile查看镜像构建过程:
只有短短的三条指令,第一行FROM scratch表示此镜像是从白手起家,从0开始构建。第二行ADD表示添加到镜像的tar包就是CentOS7的rootfs文件。在制作镜像时,这个tar包会自动解压到/目录下,生成/dev,/porc, /bin 等目录,相当于提供了一个基本的操作系统环境,用户可以根据需要安装和配置软件。这样的镜像我们称作base镜像。
CentOS7的rootfs可以在github上下载:https://github.com/CentOS/sig-cloud-instance-images。
base镜像
base镜像有两层含义:
1、不依赖其他镜像,从scratch构建。
2、以此为基础镜像,进行扩展。
base镜像通常都是各种Linux发行版的Docker镜像,比如Ubuntu,Debian,CentOS 等。使用docker pull centos:7下载最新版本的Centos镜像也就203M左右,而我们平时下载一个原生的centos7镜像都是4G,这是因为Linux 操作系统由内核空间和用户空间组成,除此之外还包含了一些其他的软件包或工具包,所以我们常用的ISO镜像才会很大。
如图1.1所示,内核空间是kernel,Linux刚启动时会加载bootfs文件系统,之后bootfs会被卸载掉。用户空间的文件系统是rootfs,包含我们熟悉的/dev,/proc,/bin等目录。对于base镜像来说,底层直接用Docker host的kernel,自己只需要提供rootfs就行了。
而对于一个精简的OS,rootfs可以很小,只需要包括最基本的命令、工具和程序库就可以了。相比其他 Linux发行版,CentOS的rootfs已经算臃肿的了,alpine还不到10MB。我们平时安装的CentOS除了rootfs还会选装很多软件、服务、图形桌面等,需要好几个GB就不足为奇了。
不同Linux发行版的区别主要就是rootfs。比如Ubuntu 14.04使用upstart管理服务,apt管理软件包;而 CentOS7使用systemd和yum。这些都是用户空间上的区别,Linux kernel差别不大。所以Docker可以同时支持多种Linux镜像,模拟出多种操作系统环境。
容器只能使用Docker host的kernel,并且不能修改。所有容器都共用Docker host的kernel,在容器中没办法对kernel升级。如果容器对kernel版本有要求(比如应用只能在某个kernel版本下运行),则不建议用容器,这种场景虚拟机可能更合适。我们可以通过创建一个容器,对比容器所使用的内核和Docker host的内核,发现版本是一样的。
分层结构
Docker里的镜像绝大部分都是在别的镜像的基础上去进行创建的,也就是使用镜像的分层结构。实际上,Docker Hub中99%的镜像都是通过在base镜像中安装和配置需要的软件构建出来的。比如我们现在构建一个新的镜像,具体操作如下:
Docker镜像构建过程,如图1.2所示,通过docker history可以看到镜像的构建历史信息,新镜像是从base镜像一层一层叠加生成的。每执行一个任务,就在现有镜像的基础上增加一层。
Docker镜像要采用这种分层结构最大的一个好处就是共享资源。比如:有多个镜像都从相同的base镜像构建而来,那么Docker host只需在硬盘上保存一份base镜像,同时内存中也只需加载一份base镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享。
如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如/etc下的文件,这时其他容器的/etc是不会被修改,因为修改会被限制在单个容器内。
容器层
当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。如图1.3所示
所有对容器的改动,无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。
镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如/galaxy,上层的/galaxy会覆盖下层的/galaxy,也就是说用户只能访问到上层中的文件 /galaxy。在容器层中,用户看到的是一个叠加之后的文件系统。
1、添加文件:在容器中创建文件时,新文件被添加到容器层中。
2、读取文件:在容器中读取某个文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。
3、修改文件:在容器中修改已存在的文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改。
4、删除文件:在容器中删除文件时,Docker也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作,并不是真正的删除。
只有当需要修改时才复制一份数据,这种特性被称作Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。