zoukankan      html  css  js  c++  java
  • 容器基础4:重识docker容器

    docker容器实际案例

    1.使用docker部署python写的web应用

    from flask import Flask
    import socket
    import os
    
    app = Flask(__name__)
    
    @app.route('/')
    def hello():
        html = "<h3>Hello {name} </h3><br>Hostname:</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)
    
    $ cat requirements.txt
    Flask
    

    2.制作容器镜像

    使用Dockerfile制作docker镜像,也就是rootfs

    # 使用官方提供的Python开发镜像作为基础镜像
    FROM python:2.7-slim
    
    # 将工作目录切换为/app
    WORKDIR /app
    
    # 将当前目录下的所有内容复制到/app下
    ADD ./app
    
    # 使用pip命令安装这个应用所需要的依赖
    RUN pip install --trusted-host pypi.python.org -r requirements.txt
    
    # 允许外接访问容器的80端口
    EXPOSE 80
    
    # 设置环境变量
    ENV NAME world
    
    # 设置容器进程为:python app.py 即:这个Python应用程序的启动命令
    CMD ["python","app.py"]
    
    

    3.Dockerfile设计思想

    使用标准原语,(大写高亮的词语),描述我们要构建的Docker镜像。并且这些原语,都是按顺序处理的

    FROM原语:指定"python:2.7-slim"这个官方维护的镜像,从而免去安装Python等语言环境的操作

    RUN原语: 容器里执行shell命令的意思

    WORKDIR:dockerfile后面的操作都以这一句指定的/app目录作为当前目录

    CMD: dockerfile指定python app.py为这个容器的进程,这里app.py的实际路径是/app/app.py
    所以CMD["python","app.py"]等价于 docker run python app.py

    ENTRYPOINT:它和CMD都是docker容器里进程启动所必须的参数,完整执行格式"ENTRYPOINT CMD"
    (不写,默认是/bin/sh -c,所以实际执行的是/bin/sh -c "python ap.py",cmd的内容就是ENTRYPOINT 参数)

    4.dockerfile 存放位置

    Dockerfile app.py requirements.txt
    

    5.制作docker镜像

    $ docker build -t helloworld .
    

    -t:镜像加Tag,docker build 会自动加载当前目录下的Dockerfile文件,按顺序指定原语
    过程:docker使用基础镜像启动了一个容器,在容器中依次执行Dockerfile中的原语

    注意事项:
    docker每个原语执行后,都会生成一个对应的镜像层,即使原语本身没有明显的修改文件操作(env),对应的层也会存在,只不过外界看这个层是空的

    6.查看docker镜像

    $ docker image ls
    RESPOSITORY      TAG        IMAGE  ID
    helloworld            latest      653287cdf998
    

    7.启动容器

    docker run -p 4000:80 helloworld
    

    -p 4000:80告诉docker,把容器内的80端口映射在宿主机的4000端口上
    镜像名helloworld,什么都没写,dockerfile中已经指定了CMDB。否组就需要些进程的启动命令

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

    8.查看容器

    $ docker ps
    CONTAINER ID        IMAGE               COMMAND             CREATED
    4ddf4638572d        helloworld       "python app.py"     10 seconds ago
    
    

    发现容器启动了
    验证一下

    
    $ curl http://localhost:4000
    <h3>Hello World!</h3><b>Hostname:</b> 4ddf4638572d<br/>
    

    9.镜像上传DockerHub

    注册Docker Hub账号,docker login登录
    给镜像起一个完整的名字geektime/helloworld:v1
    geektime是账户名

    
    $ docker tag helloworld geektime/helloworld:v1
    $ docker push geektime/helloworld:v1
    

    10.将正在运行的容器,直接转为一个镜像

    这个容器运行起来后,我又在里面做了一些操作,并且要把操作结果保存到镜像里

    将容器4ddf4638572d 提交为镜像geektime/helloworld:v2
    把最上层的"可读写层"加上原先容器镜像的只读层,打包成新镜像。

    $ docker commit 4ddf4638572d geektime/helloworld:v2
    

    11.docker exec 怎么进入容器里的呢?

    进程的namespace信息在宿主机上是真实存在的,以文件方式存在

    如下命令,可以看到docker容器在宿主机上的进程号是25686

    
    $ docker inspect --format '{{ .State.Pid }}'  4ddf4638572d
    25686
    

    可以查看宿主机的proc文件,看到进程25686的所有namespace对应文件(ns是namespace的简写)

    
    $ ls -l  /proc/25686/ns
    total 0
    lrwxrwxrwx 1 root root 0 Aug 13 14:05 cgroup -> cgroup:[4026531835]
    lrwxrwxrwx 1 root root 0 Aug 13 14:05 ipc -> ipc:[4026532278]
    lrwxrwxrwx 1 root root 0 Aug 13 14:05 mnt -> mnt:[4026532276]
    lrwxrwxrwx 1 root root 0 Aug 13 14:05 net -> net:[4026532281]
    lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid -> pid:[4026532279]
    lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid_for_children -> pid:[4026532279]
    lrwxrwxrwx 1 root root 0 Aug 13 14:05 user -> user:[4026531837]
    lrwxrwxrwx 1 root root 0 Aug 13 14:05 uts -> uts:[4026532277]
    

    每种namespace,在ns目录下都对应一个虚拟文件,并连接到真实的namespace文件上

    这样就可以实现,将一个进程加入到一个已经存在的namespace中。(就实现了进入到进程所在容器的目的,这就是docker exec原理)

    操作依赖的就是linux的一个系统调用setns

    
    #define _GNU_SOURCE
    #include <fcntl.h>
    #include <sched.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)
    
    int main(int argc, char *argv[]) {
        int fd;
        
        fd = open(argv[1], O_RDONLY);
        if (setns(fd, 0) == -1) {
            errExit("setns");
        }
        execvp(argv[2], &argv[2]); 
        errExit("execvp");
    }
    

    方法传2个参数,参数1是要进入的namespace的文件路径/proc/25686/net/ns,第二个参数是进入namespace后执行的命令/bin/bash

    测试,可看到网卡只有2个了

    
    $ gcc -o set_ns set_ns.c 
    $ ./set_ns /proc/25686/ns/net /bin/bash 
    $ ifconfig
    eth0      Link encap:Ethernet  HWaddr 02:42:ac:11:00:02  
              inet addr:172.17.0.2  Bcast:0.0.0.0  Mask:255.255.0.0
              inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:12 errors:0 dropped:0 overruns:0 frame:0
              TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
         collisions:0 txqueuelen:0 
              RX bytes:976 (976.0 B)  TX bytes:796 (796.0 B)
    
    lo        Link encap:Local Loopback  
              inet addr:127.0.0.1  Mask:255.0.0.0
              inet6 addr: ::1/128 Scope:Host
              UP LOOPBACK RUNNING  MTU:65536  Metric:1
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0
              TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:1000 
              RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
    

    可以看看新的bash,对应的网卡的namespace文件是否与之前docker容器的net的namespace是否是同一个文件

    
    $ ls -l /proc/28499/ns/net
    lrwxrwxrwx 1 root root 0 Aug 13 14:18 /proc/28499/ns/net -> net:[4026532281]
    
    $ ls -l  /proc/25686/ns/net
    lrwxrwxrwx 1 root root 0 Aug 13 14:05 /proc/25686/ns/net -> net:[4026532281]
    

    12.Docker registry

    存放镜像的系统,docker registry。可以自己本地搭建

    13.Volume 数据卷

    容器里的文件,怎么才能宿主机获取到?
    宿主机上的文件和目录,怎么才能让柔情器里的进程访问到?

    Volumn机制,允许将宿主机上指定目录或文件,挂载到容器里进行读取和修改操作。

    $ docker run -v /home:/test
    

    把宿主机的目录/home挂载进容器中的/test目录

    在rootfs准备好之后,chroot执行之前,把指定宿主机目录挂载到指定的容器目录/var/lib/docker/aufs/mnt/可读写层/test上
    也是在容器进程创建好后,namespace已经开启了

    注意事项

    注意:这里提到的"容器进程",是 Docker 创建的一个容器初始化进程 (dockerinit),而不是应用进程 (ENTRYPOINT + CMD)。dockerinit 会负责完成根目录的准备、挂载设备和目录、配置 hostname 等一系列需要在容器内进行的初始化操作。最后,它通过 execv() 系统调用,让应用进程取代自己,成为容器里的 PID=1 的进程。
    

    用到了linux系统的绑定挂载技术bind mount机制。将目录挂载到另一个目录,但最后其实是inode

    对/test目录的操作操作,都实际发生在宿主机的对应目录,而不会影响容器镜像的内容

    commit会把/test给提交上去么?
    不会,docker commit是发生在宿主机空间的,mount namespace隔离作用,宿主机不知道这个绑定挂载的存在。
    宿主机来看容器中的可读写层/test目录始终是空的。

    14.查看数据卷volume 的id

    
    $ docker run -d -v /test helloworld
    cf53b766fa6f
    
    
    $ docker run -d -v /test helloworld
    cf53b766fa6f
    
    
    $ ls /var/lib/docker/volumes/cb1c2f7221fa/_data/
    
    
    $ docker exec -it cf53b766fa6f /bin/sh
    cd test/
    touch text.txt
    
    回到宿主机
    
    $ ls /var/lib/docker/volumes/cb1c2f7221fa/_data/
    text.txt
    Volume里的信息,不会被docker commit提交掉,但这个挂载点目录/test 本身,会出现在镜像中
    

    15.总结

    全景图

    这个容器进程“python app.py”,运行在由 Linux Namespace 和 Cgroups 构成的隔离环境里;而它运行所需要的各种文件,比如 python,app.py,以及整个操作系统文件,则由多个联合挂载在一起的 rootfs 层提供。

    这些 rootfs 层的最下层,是来自 Docker 镜像的只读层。在只读层之上,是 Docker 自己添加的 Init 层,用来存放被临时修改过的 /etc/hosts 等文件。而 rootfs 的最上层是一个可读写层,它以 Copy-on-Write 的方式存放任何对只读层的修改,容器声明的 Volume 的挂载点,也出现在这一层。

    原创:做时间的朋友
  • 相关阅读:
    课后作业
    动手动脑
    原码,补码,反码
    CodingSouls团队项目冲刺-个人概况(7)
    《人月神话》阅读笔记03
    数据库学习
    家庭小账本——数据库的编写与测试
    家庭小账本——适配器的编写与测试
    UI高级组件
    UI高级组件
  • 原文地址:https://www.cnblogs.com/PythonOrg/p/14913957.html
Copyright © 2011-2022 走看看