zoukankan      html  css  js  c++  java
  • [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。       

     

    4、如何制作镜像

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

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

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

    $cat requriements.txt
    Flask

      DockerFile用于制作容器镜像

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

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

           在当前目录执行:

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

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

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

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

    docker run -p 4000:80 helloworld
    ##因为在Dockerfile中已经制定了CMD,否则就的把进程启动命令加载后面:

    docker run -p 4000:80 helloworld python app.py

    ##-p 4000:80是把容器内的80端口隐式在宿主机的4000端口上

    ##这样做得目的是只要访问宿主机的4000端口就可以看到容器里应用返回的结果
    ##否则就要先用docker inspect命令查看容器的IP地址,然后访问“http://<容器IP地址 >:80”才可以看到容器内应用的返回

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

    root@R740-2-1:/usr/test# docker login
    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.
    Username: yuxiaoba
    Password: 
    Login Succeeded
    root@R740-2-1:/usr/test# docker tag helloworld yuxiaoba/helloworld:v1   //yuxiaoba是我的docker hub上的用户名,helloworld是镜像的名字,v1是镜像的版本号
    root@R740-2-1:/usr/test# docker push yuxiaoba/helloworld:v1
    The push refers to repository [docker.io/yuxiaoba/helloworld]
    6e37ff1f1c7c: Pushed 
    fa111037b1b5: Pushed 
    f27d3835da60: Pushed 
    d509372bacf0: Pushed 
    18cc3d97f405: Pushed 
    80db77e224a0: Pushed 
    8b15606a9e3e: Pushed 
    v1: digest: sha256:ab7ba9b1707a8cf3a88de6c2bcee107a1a0b07076c8c6529fa4c151049aa72fa size: 1788

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

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

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

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

       

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

  • 相关阅读:
    字符串的问题(strstr和strncpy 水题)
    数一数(KMP+思维)
    栗酱的数列(KMP+思维)
    D. Almost All Divisors(思维)
    E. Two Arrays and Sum of Functions(贪心)
    好位置(思维)
    Just A String(kmp)
    Dubbo简介
    lambda表达式快速创建
    分布式RPC系统框架Dubbo
  • 原文地址:https://www.cnblogs.com/yuxiaoba/p/9613209.html
Copyright © 2011-2022 走看看