❤️ 专栏简介 :本专栏我们会从最基础的内容开始学习Docker的相关内容,循序渐进的掌握Docker知识并进行实战。

☀️ 专栏适用人群 :适用于具备基础 Linux 知识的 Docker 初学者,当然希望各位有经验的docker开发者不吝赐教。

🌴 专栏说明 :如果文章知识点有错误的地方,欢迎大家随时在文章下面评论,我会第一时间改正。让我们一起学习,一起进步。


k8s支持的docker版本 k8s docker实战(长篇)_linux

本节详细讲解了docker镜像原理;主要内容包括:docker镜像原理自定义docker镜像的实现原理、docker镜像分层的原因、可写容器层等。


文章目录

  • 一、在开发中,docker具体解决了什么问题
  • 二、详解docker镜像原理
  • 三、自定义docker镜像的实现原理
  • 四、Docker为什么要进行镜像分层
  • 五、可写容器层


一、在开发中,docker具体解决了什么问题

在我们日常开发过程中,肯定要用到各种开发软件,比如mysql、vscode、kafka、nginx等等;假设我们的开发环境是Windows,如下图所示:

k8s支持的docker版本 k8s docker实战(长篇)_k8s支持的docker版本_02


我们所有开发所需要的软件环境都安装在了宿主机上,那么我们可能会遇到以下麻烦:

  • 1、环境不兼容,比如有些软件需要运行在linux下,但是咱们现在的环境是Windows,所以只能去安装一个vmware虚拟机,或者购买一个云服务器,用来安装所需软件;
  • 2、会讲我们当前系统的环境,搞个一团糟;比如全装在了C盘,C盘又满了,岂不是非常头疼;
  • 3、再有,万一我们想卸载这些工具,万一不会卸载,更麻烦了。
  • 4、软件环境不方便迁移;比如我们当前是在Windows开发了mysql,并且进行了一系列的配置;但是有需求需要将mysql里面的数据以及配置等完整的迁移到linux上使用,那这从Windows迁移到linux就相当麻烦了。

使用docker就彻底解决了以上的问题

使用docker后,我们的开发环境如下图所示:

k8s支持的docker版本 k8s docker实战(长篇)_docker_03

我们只需要在Windows开发环境中装上docker,我们可以将每个开发软件运行在单独的容器中,比如mysql运行在容器1,kafka运行在容器2,elk运行在容器3,redis运行在容器4;这样的好处是啥呢?

  • 1 解决了环境的兼容问题,在容器中运行linux发行版,以及各种软件,即【Windows+docker+容器1(centos)+容器2(ubuntu)】;
  • 2 环境很干净,我们安装的所有内容,都在容器里,如果不想要了,直接删除容器即可,不影响我们宿主机的环境;
  • 3 方便进行迁移;比如我们想要把mysql容器内的数据,配置,全部迁移到服务器上,只需要提交该容器,生成镜像;再将该镜像pull到我们需要迁移到的目标服务器上,docker run,就可以恢复我们在Windows上的开发环境了,且数据配置全都在;是不是很方便。

二、详解docker镜像原理

我们知道,一个完整的docker镜像可以创建出docker容器的运行,例如一个centos:7.8.2003镜像文件,可以通过 docker run + 镜像id运行出一个centos7.8.2003的容器;下面我们来看一下这个镜像:

k8s支持的docker版本 k8s docker实战(长篇)_centos_04

可以看到,该镜像的大小只有203MB,那么问题来了,为什么之前我们在装centos系统的时候,使用的iso镜像文件有好几个G,而docker里centos的镜像只有203M呢?原因就是docker里获取的centos镜像并不是一个完整的操作系统,docker镜像里只包括centos发行版,而不包括linux内核,所以只有203M(完成的镜像由linux内核+centos发行版组成),内核使用宿主机的内核;

docker的架构,镜像就是一个【发行版】的作用,所以我们需要准备好一个linux内核,然后上层使用不同的【发行版】就好了;这样我们就可以共用一个linux内核,自由的的使用各种不同的版本系操作统,实现兼容多种环境。

下面我们看一下docker镜像原理图(分层原理),以tomcat镜像为例,原理图如下图所示:

k8s支持的docker版本 k8s docker实战(长篇)_k8s支持的docker版本_05

下面详细解释一下图中各个分层:

  • 1 首先我们看最底层的bootfs:Bootfs全名boot-file system,即引导文件系统;主要包含bootloader(系统加载)和kernel(内核);bootloader主要用于引导加载kernel,kernel内核主要是宿主机提供的linux内核。Linux刚启动时会加载bootfs文件系统,也就是加载宿主机的linux内核。所以,我们的docker容器在运行时,第一步就是加载宿主机的linux内核。linux内核加载完成后,就会启动第二层。
  • 2 第二层是叫Rootfs,即root-file system;Rootfs在bootfs之上,包含的就是典型的linux系统中/dev、/proc、/etc等标准目录和文件;rootfs就是各种不同操作系统的发行版,比如ubuntu、centos等;所以,第二步就是由Rootfs,负责docker获取基础镜像;即进行完第二步,我们就获取到了基础的linux发行版(比如是centos还是ubuntu等),例如我们本例中获取的就是centos发行版。
  • 3、4 第三层和第四层是依赖层,我们在依赖层就可以定制化安装我们所需要的各种依赖环境了;此容器是用来运行什么的,我们就得安装什么依赖了。比如我们的镜像是用来运行tomcat的,而tomcat想要运行,就必须得先有jdk环境(java环境),即第三层安装jdk,安装完jdk后才能去第四层安装tomcat;tomcat也装好了后,必须得放入文件才能运行,这个文件时需要我们自己写的;所以最后一层就是容器层(container层)。
  • 5 第五层即最后一层是容器层(container层),它的作用是就是本容器所要实现的具体功能了;比如在本例中就是运行具体的tomcat程序。

注意
第1、2、3、4层是只读的,是只读镜像,是不能修改的;而第5层容器层是可读可写的

如上所提到的这5层就构成了tomcat镜像,那如何确定该镜像都包括哪几层呢?该镜像的这几层是怎么关联起来的呢,或者说是怎么一步一步,一层一层的构建出来的呢?那就是dockerfiledockerfile的作用就是自定义docker镜像每一层

现在我们知道了,我们pull下来的centos镜像其实包含多层镜像文件的,但是在我们pull的时候,从外面看起来像是只下载了一个镜像文件,这是怎么实现的呢?其实是通过Union File System(联合文件系统)实现的,docker通过联合文件系统,将上图中的每一层,整合为一个文件系统,为我们用户隐藏了多层的视角;所以最后在我们看起来只是用了一个镜像,即docker images看到的镜像,然后我们用docker run 镜像id就可以运行此容器了。

总结:
1、当通过一个image启动容器时,docker会在该image的最顶层(也就是容器层)添加一个读写文件系统作为容器,然后运行该容器。
2、docker镜像的本质是基于UnionFS管理的分层文件系统。
3、dockerfile的作用:dockerfile就是用于自定义docker镜像的每一层。
4、docker镜像为什么才几百兆?
答案:因为docker镜像只有rootfs和其他镜像层,不包含linux内核bootfs;而是共用宿主机的linux内核bootfs,因此docker镜像很小。
5、为什么下载一个docker的nginx镜像,需要141MB?nginx安装包不是才几兆大小吗?
答案:因为我们需要基于nginx镜像运行出一个nginx容器;nginx是一个软件,它必须依赖于操作系统才能运行,而一个操作系统是由(linux内核+发行版)两部分组成的;首先,在Linux宿主机上,提供了linux内核(当然如果是在Windows上跑的docker,那这里的内核就是Windows内核);然后利用docker下载镜像(镜像提供了发行版:centos);最后该nginx就可以运行在该centos发行版环境中了。所以整体结构是Linux(或者Windows)+【docker+centos+nginx】
docker的nginx镜像是分层的,nginx安装包的确就几兆大小,但是一个用于运行nginx的镜像文件,依赖于父镜像(图中的3、4依赖环境)和基础镜像(发行版),所以下载的nginx镜像有一百多兆。

三、自定义docker镜像的实现原理

经过上面的学习我们已经知道了,docker镜像不包含linux内核,和宿主机共用;所以如果想要自定义一个镜像,还得安装需要发行版;那么我们来看一下,我们已有的容器,他们所依赖的发行版都是什么呢?以nginx为例看一下吧:

我们使用docker images查看一下我们当前系统上的所有镜像文件:

k8s支持的docker版本 k8s docker实战(长篇)_linux_06


可以看到,上面每个镜像文件都是一个完整的docker镜像文件;我们运行一下nginx

docker run -d -p 80:80 nginx
[root@localhost acl]# docker run -d -p 80:80 nginx
d9f8b942ef91b5c7993362787d5bd4ad4d34a5fe4ad69028949f9fd89982853f
docker: Error response from daemon: driver failed programming external connectivity on endpoint affectionate_villani (3c099ee78f0738fdb0e806ca38fcd4d31c4e1253c359880891e07982295b214a): Bind for 0.0.0.0:80 failed: port is already allocated.

发现报错80端口已经被占用了;哦对,之前我们已经运行过nginx了,那就docker ps看一下:

k8s支持的docker版本 k8s docker实战(长篇)_docker_07


确实是nginx容器在运行。

nginx是一个软件,它要想运行,必须运行在一个发行版上,即必须依赖于一个发行版;那咱们现在运行的nginx到底运行在什么样的系统环境中呢?或者说它哪个发行版上的?我们进入到正在运行的nginx容器看一下:

# 进入到正在运行的容器内,命令使docker exec
# 2f5cdfebdd90是我nginx容器id
docker exec -it 2f5cdfebdd90 bash

k8s支持的docker版本 k8s docker实战(长篇)_centos_08

可以发现已经进入2f5cdfebdd90(nginx容器)内部了;然后查看一下该容器所依赖的发行版:

k8s支持的docker版本 k8s docker实战(长篇)_学习_09


可以看到,运行着的nginx软件的基础系统环境(基础发行版)是Debian发行版系统;所以nginx镜像的生成过程大致如下:

  • 获取基础镜像,选择Debian发行版平台;
  • 在Debian镜像中安装nginx软件。

好了,那我们知道如何自定义一个自己的镜像,首先内核我们已经有了(和宿主机共用),还需要选择一个发行版平台;所以自定义镜像的步骤如下:

我们以自定义一个centos 的mysql5.6镜像为例:

  • 获取基础镜像,选择一个发行版平台(centos)
  • 在centos镜像中安装mysql5.6软件
  • 导出镜像,我们将其命名为mysql:5.6镜像文件。

从这个过程中,我们可以感到出这是一层一层的添加的,docker镜像的层级概念就出来了,底层是centos镜像,上层是mysql镜像,centos镜像属于mysql的父镜像。

docker的层级概念图如下图所示:

k8s支持的docker版本 k8s docker实战(长篇)_docker_10


首先第一张图是我们nginx镜像的第一层:最下面的是bootfs,用于加载内核Kernel(我们这里的例子是nginx内核,当然如果你是在Windows上跑的docker,那这里内核就是Windows内核);在bootfs之上的是基础镜像Debian(当然如果我们自定义一个镜像时也可以将其定义为centos或ubuntu),该基础镜像使用的是rootfs文件系统;

然后到了第二张图,就是在第一层的基础上又添加了一层,用来安装所需要的依赖镜像,比如add emacs

最后在第三张图,就是在第二层的基础上又添加了一层,用来安装具体要运行的软件,比如Apache软件;

因此,docker镜像是在指定了基础的发行版镜像层之后,再逐层的进行添加,比如安装软件的层,配置软件的层等,最后构建出来一个完整的镜像;在后续我们使用Dockerfile自定义镜像时,我们的每一个指令都会单独的构建出一个docker层。

这种层级的现象在学习dockerfile构建的时候,将更加的清晰;

四、Docker为什么要进行镜像分层

镜像分层的一大好处就是共享资源,例如有多个镜像都来自于同一个base镜像,那么docker host只需要存储一份base镜像即可。

共享资源怎么理解呢?

假设我们现在有3个容器,它们都需要基于centos镜像,如果我们没有分层的结构,那么这三个容器,都必须要包含一个独立的centos镜像,如下图所示:

k8s支持的docker版本 k8s docker实战(长篇)_centos_11

那这样,如果运行三个容器,每个包含一个独立的centos镜像,即每个容器的进程都要单独去开辟100M空间去存放centos镜像;就占用了三百兆空间;而这三个centos镜像是一模一样的,就相当于内存里有一个资源重复占用了三次,这样显然是不合理的;

所以如果有分层的结构,一个基础镜像层可以给三个容器去使用,即内存里只需要加载一份centos镜像,只占用100M空间;如下图

k8s支持的docker版本 k8s docker实战(长篇)_centos_12

所以镜像分层可以共享资源;即内存里也只需要加载一份host,就可以为多个容器服务。

另外,即使多个容器共享一个base镜像,某个容器修改了base镜像的内容,例如修改了/etc下配置文件,其他容器下的/etc/下的内容是不会被修改的,修改动作只限制在单个容器内,这就是容器的写入时复制特性(Copy-on-write),如下图所示:

k8s支持的docker版本 k8s docker实战(长篇)_docker_13


比如此时容器1修改了/ect/passwd文件,就相当于修改了centos基础镜像里的文件;但是我们前面讲过,除了docker容器层之外,其他层都是只读的,不允许被修改;所以,容器1修改/ect/passwd文件,是不会修改base image基础镜像层的,其实改的是最顶层的可写层的容器层,这也就是我们下面要讲的内容;

五、可写容器层

当基于一个镜像启动了一个容器后,一个新的可写层被加载到镜像的顶部,这一层被称为容器层容器层下的层都称为镜像层。

k8s支持的docker版本 k8s docker实战(长篇)_k8s支持的docker版本_14


如上图所示,只有最顶层的容器层(container)是可写的,即可写的容器层;容器层以下的所有层都是只读的镜像层,均只读,不可修改。所有对容器的修改动作,都只会发生在容器层里。

具体的操作过程如下表所示:

k8s支持的docker版本 k8s docker实战(长篇)_centos_15


从表中可以看出,只要当需要修改某一个文件时,才会将该文件从镜像层复制一份到容器层进行修改,所以我们修改的其实是容器层从镜像层复制上来的备份文件,真正的系统层的该文件是不会被修改的;这种特性被称作写时复制特性(Copy-on-Write)。可见,容器层保存的是镜像变化的部分,不会对镜像层本身进行任何修改。当然,如果我们要删除一个镜像层已有的文件时,真正镜像层的该文件也不会被删除,只是在容器层记录一下对该文件的删除操作

这就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。