1、rootfs的基础知识

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

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

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

  1. 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,将多个不同位置的目录联合挂载到同一个目录下)实现的。

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

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

  1. "RootFS": {
  2. "Type": "layers",
  3. "Layers": [
  4. "sha256:a30b835850bfd4c7e9495edf7085cedfad918219227c7157ff71e8afe2661f63",
  5. "sha256:6267b420796f78004358a36a2dd7ea24640e0d2cd9bbfdba43bb0c140ce73567",
  6. "sha256:f73b2816c52ac5f8c1f64a1b309b70ff4318d11adff253da4320eee4b3236373",
  7. "sha256:6a061ee02432e1472146296de3f6dab653f57c109316fa178b40a5052e695e41",
  8. "sha256:8d7ea83e3c626d5ef1e6a05de454c3fe8b7a567db96293cb094e71930dba387d"
  9. ]
  10. },

  可以看出这个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。       

 

4、如何制作镜像

  假设要用docker部署一个用python编写的Web应用,这个应用的代码如下

  1. ##用Flask框架启动了一个Web服务器,它唯一的功能是:
  2. ##如果当前环境中有“NAME”这个环境变量,就把它打印在“Hello”之后
  3. ##否则就打印“Hello World”,最后在打印出当前环境的hostname
  4. from flask import Flask
  5. import socket
  6. import os
  7.  
  8. app = Flask(__name__)
  9.  
  10. @app.route('/')
  11. def hello():
  12. html = "<h3>Hello {name}! </h3>"\
  13. "<b>Hostename:</b>{hostname}<br/>"
  14. return html.format(name=os.getenv("NAME","world"),hostname=socket.gethostname())
  15.  
  16. if __name__=="__main__":
  17. app.run(host="0.0.0.0", port=80)

  这个应用的依赖,则被定义在了同目录下的requirements.txt文件里,内容如下所示

  1. $cat requriements.txt
  2. Flask

  DockerFile用于制作容器镜像

  1. #使用官方提供的Python开发镜像作为基础镜像
  2. FROM python:2.7-slim
  3.  
  4. #将工作目录切换为 /app
  5. WORKDIR /app
  6.  
  7. #将当前目录下的所有内容复制到 /app下
  8. ADD . /app
  9.  
  10. #使用pip命令安装这个应用所需要的依赖
  11. #RUN原语就是在容器里执行shell命令
  12. RUN pip install --trusted-host pypi.python.org -r requirements.txt
  13.  
  14. #允许外界访问容器的80端口
  15. EXPOSE 80
  16.  
  17. #设置环境变量
  18. ENV NAME World
  19.  
  20. #设置容器进程为: python app.py
  21. CMD ["python","app.py"]
  22. ###上面这一句等价于“docker run python aap.py”
  23. ##另外在使用Dcokerfile时,你可能还会看到叫ENTRYPOINT的原语
  24. ##实际上它和 CMD都是Docker容器进程启动所必须的参数
  25. ##完整执行格式是:“ENTRYPOINT CMD”
  26. ##在默认情况下Docker会提供一个隐含的ENTRYPOINT,即:/bin/sh -c
  27. ##所以在这个例子里,实际运行在容器里的完整进程是:/bin/sh -c "python app.py"

  此时目录下有三个文件 app.py  Dockerfile  requirements.txt

在当前目录执行:

  1. docker bulid -t helloworld .
  2. #-t的作用是给这个镜像加一个Tag,即起名字
  3. #docker build会自动加载当前目录下的Dockerfile文件,然后按照顺序执行文件中的原语。
  4. #Dockerfile中的每个原语执行后,都会生成一个对应的镜像层,即使原语本身并没有明显修改文件的操作(如ENV原语),它对应的层也会存在,只不过在外界看来是空的

  在build操作完成之后,就可以通过docker images查看结果

  1. root@R740-2-1:/usr/test# docker image ls
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. helloworld latest 482458a88f79 2 hours ago 131MB

  接下来通过docker run 命令启动容器

  1. docker run -p 4000:80 helloworld
    ##因为在Dockerfile中已经制定了CMD,否则就的把进程启动命令加载后面:
  2.  
  3. docker run -p 4000:80 helloworld python app.py
  4.  
  5. ##-p 4000:80是把容器内的80端口隐式在宿主机的4000端口上
  6.  
  7. ##这样做得目的是只要访问宿主机的4000端口就可以看到容器里应用返回的结果
  1. ##否则就要先用docker inspect命令查看容器的IP地址,然后访问“http://<容器IP地址 >:80”才可以看到容器内应用的返回

  镜像制作完成了,那该如何上传到DockerHub呢?首先需要注册一个Docker Hub账号

  1. root@R740-2-1:/usr/test# docker login
  2. Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
  3. Username: yuxiaoba
  4. Password:
  5. Login Succeeded
  6. root@R740-2-1:/usr/test# docker tag helloworld yuxiaoba/helloworld:v1 //yuxiaoba是我的docker hub上的用户名,helloworld是镜像的名字,v1是镜像的版本号
  7. root@R740-2-1:/usr/test# docker push yuxiaoba/helloworld:v1
  8. The push refers to repository [docker.io/yuxiaoba/helloworld]
  9. 6e37ff1f1c7c: Pushed
  10. fa111037b1b5: Pushed
  11. f27d3835da60: Pushed
  12. d509372bacf0: Pushed
  13. 18cc3d97f405: Pushed
  14. 80db77e224a0: Pushed
  15. 8b15606a9e3e: Pushed
  16. v1: digest: sha256:ab7ba9b1707a8cf3a88de6c2bcee107a1a0b07076c8c6529fa4c151049aa72fa size: 1788

·  此外还可以使用docker cmmit指令,把一个正在运行的容器,直接提交一个镜像

docker commit实际上是容器运行起来后,把最上层的可读写层,加上原先容器镜像的只读层(在宿主机上共享,不会占用额外的空间),打包组成了一个新的镜像。

    而由于使用联合文件系统,你在容器里对镜像rootfs所做的任何修改,都会被操作系统新复制到这个可读写层,然后再修改(Copy-on-write),正如前面所说,Init层的存在就是为了避免你执行docker commit的时候,把Docker 自己对/etc/hosts等文件做得修改也一起提交掉

  1. root@R740-2-1:~# docker exec -it 675a52fd08aa /bin/sh
  2. # touch test.txt
  3. # exit
  4. root@R740-2-1:~# docker commit 675a52fd08aa yuxiaoba/helloworld:v2
  5. sha256:140af7554a45281f85695f22aeb99d2c789c4496d19b8645725bf5f550e25bf4
  6. root@R740-2-1:~# docker push yuxiaoba/helloworld:v2
  7. The push refers to repository [docker.io/yuxiaoba/helloworld]
  8. 435c128b063e: Pushed
  9. 6e37ff1f1c7c: Layer already exists
  10. fa111037b1b5: Layer already exists
  11. f27d3835da60: Layer already exists
  12. d509372bacf0: Layer already exists
  13. 18cc3d97f405: Layer already exists
  14. 80db77e224a0: Layer already exists
  15. 8b15606a9e3e: Layer already exists
  16. v2: digest: sha256:644e07f6d39326d3e7bc66a98f825c409ab88f410b024b62ece62afacf2b8e44 size: 1997

  

最后的最后,放出一个全景图有益于理解以上所有内容

[Docker]容器镜像的更多相关文章

  1. 运行docker容器镜像

    docker容器可以理解为在盒中运行的进程. 这个盒包含了该进程运行所必须的资源,包括文件系统.系统类库.shell 环境等等. 但这个盒默认是不会运行任何程序的. 1.运行镜像之前,可以先查看本地有 ...

  2. Docker容器镜像瘦身的三个小窍门(转)

    [转自:http://dockone.io/article/8174] 在构建Docker容器时,我们应尽可能减小镜像的大小.使用共享层的镜像尺寸越小,其传输和部署速度越快. 不过在每个RUN语句都会 ...

  3. HyperLedger/Fabric SDK使用Docker容器镜像快速部署上线

    HyperLedger/Fabric SDK Docker Image 该项目在github上的地址是:https://github.com/aberic/fabric-sdk-container ( ...

  4. 使用Aliyun Docker 容器镜像/注册表服务

    1.前往阿里云容器镜像服务创建相关资源. 2.登录你的仓库,账户名+公共地址 docker login --username=xxxxxxxxx@aliyun.com registry.cn-hang ...

  5. 运行docker容器镜像2(指定容器启动时启动的脚本)

    docker中启动容器有以下两种情况. 第一种是通过 # docker run containerid 启动一个容器. 第二种是重新启动已经关闭的容器. # docker start containe ...

  6. 企业级Docker容器镜像仓库Harbor的搭建

    Harbor简述 Habor是由VMWare公司开源的容器镜像仓库.事实上,Habor是在Docker Registry上进行了相应的企业级扩展,从而获得了更加广泛的应用,这些新的企业级特性包括:管理 ...

  7. Docker容器镜像删除

    好吧,本来认为删除镜像是一件很容易的事情,但刚开始上手,还是有点百思不得其解.删着删着,发现果然很容易.分享下本人的心得: 分两种情况:那么要删除镜像,首先得删除容器,删除容器时,确保容器已停止运行: ...

  8. Docker容器/镜像查看及删除操作

    列出所有正在运行的容器 docker ps 暂停容器 docker stop <name> 删除容器 docker rm <name> 停止所有container docker ...

  9. Docker 容器镜像操作

    1.停止所有的container,这样才能够删除其中的images:docker stop $(docker ps -a -q)如果想要删除所有container的话再加一个指令: docker rm ...

随机推荐

  1. mysql replication错误常见处理

    大部分的错误,都是日志错误 日志本身的错误 主日志和中继日志都可能出错,可以使用mysqlbinlog来读一下mysqlbinlog mysql-bin.000007>/dev/null ##只 ...

  2. android开发学习 ------- Retrofit+Rxjava+MVP网络请求的实例

    http://www.jianshu.com/p/7b839b7c5884   推荐 ,照着这个敲完 , 测试成功 , 推荐大家都去看一下 . 下面贴一下我照着这个敲完的代码: Book实体类 - 用 ...

  3. AJPFX总结string类和简单问题

    String表示字符串,所谓字符串,就是一连串的字符;String是不可变类,一旦String对象被创建,包含在对象中的字符序列(内容)是不可变的,直到对象被销毁://一个String对象的内容不能变 ...

  4. iOS9 开发新特性 Spotlight使用

    1.Spotloight是什么? Spotlight在iOS9上做了一些新的改进, 也就是开放了一些新的API, 通过Core Spotlight Framework你可以在你的app中集成Spotl ...

  5. 程序员必须知道FTP命令

                                             程序员必须知道FTP命令 文件传输软件的使用格式为:FTP<FTP地址>,若连 接成功,系统将提示用户输入 ...

  6. webapi参数处理get过个参数

    // GET api/values/5 [HttpGet("{logInName}/{pwd}/{orgId}")] public LogInOutPut Get(string l ...

  7. Mybatis(一)入门

    mybatis使用的三个部分数据查询主体 : SqlSession查询映射层 : Mapper接口数据维护层 : Bean 设计一.添加maven依赖<!-- mybatis依赖 -->& ...

  8. mybatis 存储过程的写法

    (注意事项: 在使用游标的时候,不能在游标声明之前,使用crud) 存储过程示例 CREATE DEFINER=`root`@`::` PROCEDURE `earnings_proceduce`() ...

  9. Python3简明教程(九)—— 文件处理

    文件是保存在计算机存储设备上的一些信息或数据.你已经知道了一些不同的文件类型,比如你的音乐文件,视频文件,文本文件.Linux 有一个思想是“一切皆文件”,这在实验最后的 lscpu 的实现中得到了体 ...

  10. svn批处理语句

    sc create SVNService binpath="O:\ProgramingSoftware\SuiVersion\bin\svnserve.exe --service -r E: ...