[Docker]容器镜像

 1、rootfs的基础知识

       Mount namespaces 隔离的是文件系统挂接点,它使每个容器能看到不同的文件系统层次结构,即每当创建一个新容器时,希望容器进程看到的文件系统时一个独立的隔离环境,而不是继承自宿主机的文件系统。  

  Mount Namespace修改的是容器进程对文件系统挂载点的认知。这意味着只有在挂载操作(mount)发生之后,进程的视图才会发生改变,而在此之前,新创建的容器会直接继承宿主机的各个挂载点。因而在创建新进程时,除了声明要启用的Mount  Namespace之外,还可以告诉容器进程有哪些目录需要重新挂载。因此在容器启动之前重新挂载它的整个根目录“/”,这时由于Mount Namespace的存在,这个挂载对宿主机不可见。

  在Linux系统里,chroot(change root file system,改变进程的根目录到指定位置)命令可以完成这个工作

chroot $HOME/test /bin/bash    //将使用$HOME/test目录作为/bin/bash进程的根目录

  为了能够让容器的这个根目录看起来更真实,一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如Ubuntu16.04的ISO。这样在容器启动之后,在容器里执行:“ls /”查看根目录下的内容,就是Ubuntu16.04的所有目录和文件

  而挂载在容器根目录上,用来为容器进程提供隔离后执行环境的文件系统,就是容器镜像,也叫rootfs(根文件系统)。需要注意的是rootfs只是一个操作系统所包含的文件、配置和目录,并不包括操作系统的内核。在Linux操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。即只包括了操作系统的躯壳,并不包括操作的系统的灵魂。

   实际上同一台机器上的所有容器都共享宿主机操作系统的内核。因此在配置内核参数、加载额外的内核模块以及跟内核进行直接的交互时的操作和依赖的对象都是宿主机操作系统的内核,它对于该机器上的所有容器来说都是一个全局变量。这是容器的主要缺陷之一。

     

2、因此Docker项目最核心的原理实际上是为待创建的用户进程:

    1)、启用Linux Namespace

    2)、设置指定的Cgroups参数

    3)、切换进程的根目录(change root)

3、容器的一致性

  由于rootfs里打包的不只是应用,而是整个操作系统的文件和目录,即应用以及它运行所需要的所以依赖都被封装在了一起。(对一个应用来说,操作系统本身是他运行所需要的最完整的依赖库)

  容器镜像“打包操作系统”的能力赋予了容器的一致性:无论在本地、云端还是在一台任何地方的机器,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现处理了。

  但是这里有一个问题,难度每开发一个应用或升级现有的应用都需要重复制作一次rootfs吗?

  答案肯定不是的,可以采用增量的方式去做这些修改。Docker在镜像设计中,引入了层(layer)的概念,也就是说用户制作镜像每一步操作都会生成一个层,即一个增量的rootfs

  它是使用联合文件系统(Union File System,将多个不同位置的目录联合挂载到同一个目录下)实现的。

docker run -d ubuntu:latest sleep 3600   //启动一个容器

   Docker会从DockerHub上拉取一个Ubuntu镜像到本地,这个镜像就是Ubuntu操作系统的rootfs,它的内容是Ubuntu操作系统的所有文件和目录。Docker镜像使用的rootfs往往由多层组成

        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:a30b835850bfd4c7e9495edf7085cedfad918219227c7157ff71e8afe2661f63",
                "sha256:6267b420796f78004358a36a2dd7ea24640e0d2cd9bbfdba43bb0c140ce73567",
                "sha256:f73b2816c52ac5f8c1f64a1b309b70ff4318d11adff253da4320eee4b3236373",
                "sha256:6a061ee02432e1472146296de3f6dab653f57c109316fa178b40a5052e695e41",
                "sha256:8d7ea83e3c626d5ef1e6a05de454c3fe8b7a567db96293cb094e71930dba387d"
            ]
        },

  可以看出这个Ubuntu镜像实际上由五层组成,这五层就是五个增量rootfs,每一层都是Ubuntu操作系统文件与目录的一部分,而是用镜像时,Docker会把这些增量联合挂载在一个统一的挂载点上

  这个容器的rootfs如下图所示的三部分组成:

    

    

      1)只读层

      它是这个容器的rootfs最下面的五层,对应的是Ubuntu:latest镜像的五层,它们的挂载方式都是只读(ro+wh, readonly+whiteout),这些层以增量的方式分别包含了Ubuntu的一部分

      2)可读写层

      它是这个容器的rootfs最上面的一层,它的挂载方式是:rw(read write)。在没有写入文件之前,这个目录是空的,而一旦在容器里做了写操作,修改产生的内容会以增量的方式出现在这个层中。

      那要是删除只读层的文件呢?AuFS会在读写层创建一个whiteout文件,把只读层里的文件“遮挡起来”

      所以可读写层的作用,就是专门用来存放修改rootfs后产生的增量,增、删、改都发生在这里,当使用完这个被修改得容器之后,还可以使用docker commit和push指令,保存这个被修改过的可读写层并上传到Docker Hub上。而与此同时,原先的只读层里的内容不会有任何变化

      3)Init层

      以-init层结尾的层,夹在只读层和读写层之间。Init层是Docker项目单独生成的一个内部层,专门用来存放/etc/hosts,/etc/resolv.conf等信息。需要这样一层的原因是这些文件本来属于只读的Ubuntu镜像的一部分,但是用户需要在容器启动时写入一些指定的值(如hostname),所以就需要在可读写层对它们进行修改,可是这些修改往往只对当前容器有效,并不需要在执行docker commit时把这些信息连同可读写层一起提交。因此Docker在修改这些文件以后,以一个单独的层挂载了出来。

      4)既然容器的rootfs是以只读的方式挂载的?那么如何在容器里修改镜像的内容呢?

       上面读写层通常也称为容器层,下面的只读层称为镜像层,所有的增删查改操作都只会作用在容器层,相同的文件上层会覆盖掉下层。知道这一点,就不难理解镜像文件的修改,比如修改一个文件的时候,首先会从上到下查找有没有这个文件,找到,就复制到容器层中,修改,修改的结果就会作用到下层的文件,这种方式也被称为copy-on-write。       

猜你喜欢

转载自www.cnblogs.com/yuxiaoba/p/9613209.html