• 运维开发实践 - Docker - 容器实现原理


    1.Docker容器是什么

    按照Docker官网,容器是运行在宿主机上的一个进程,但与宿主机上的其他进程相隔离;

    2.容器实现原理

    这种隔离机制使用了内核中的namespace和cgroups功能;

    2.1.Linux namespace

    Linux通过将系统的资源放置在不同的namespace下,实现资源的隔离;

    类型解释
    Network隔离网络资源
    Mount隔离文件系统的挂载点
    UTS隔离主机名和域名信息
    IPC隔离进程间通信
    PID隔离进程ID
    User隔离用户和用户组ID

    clone系统调用:创建子进程

    # flags: 控制新创建的进程隔离的资源
    int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
    
    • 1
    • 2
    flag隔离资源描述
    CLONE_NEWCGROUP子进程的cgroup资源和当前进程隔离隔离Cgroup根目录下不同层级目录的权限
    CLONE_NEWIPC子进程的ipc资源和当前进程隔离隔离当前在不同进程间传递和交换信息的范围
    CLONE_NEWNET…network…隔离子进程的网络栈,路由表,防火墙规则等
    CLONE_NEWNS…mount…隔离文件系统的挂载点
    CLONE_NEWPID…pid…隔离进程的ID空间
    CLONE_NEWUSER…user…隔离用户uid,gid在宿主机中的权限
    CLONE_NEWUTS…UTS…隔离子进程的主机名,hostname和NIS域名

    (NIS域名:Network information service,用共享网络信息的集中存储)

    # 查看当前进程树
    pstree -p
    
    • 1
    • 2

    在这里插入图片描述

    # 查看当前所有进程
    # 在linux中一切皆文件,如下图我们可以看出进程本身其实也只是一个文件
    ls /proc
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    # 查看pid=1的进程的namespace
    ls -al /proc/1/ns
    
    • 1
    • 2

    在这里插入图片描述

    mount: 文件挂载

    // source: 挂载源地址
    // target: 挂载目标地址
    // filesystemtype: 系统类型
    // mountflags: 挂载源文件访问标志
    // data: 文件系统特有的参数
    int mount( const char* source, const char* target, const char* filesystemtype, unsigned long mountflags, const void * data);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    // container.c

    #define _GNU_SOURCE
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define STACK_SIZE (1024 * 1024)
    static char container_stack[STACK_SIZE];
    char* const container_args[] = {
            "/bin/bash",
            NULL
    };
    
    int container_main(void* arg){
            printf("container_main\n");
            sethostname("container0",10);
            mount("proc", "/proc", "proc", 0, NULL);
            mount("none", "/tmp", "tmpfs", 0, "");
            printf("Info: %s\n",strerror(errno));
            chroot("./");
            perror("chroot");
            chdir("/");
            perror("chdir");
            execv(container_args[0], container_args);
            return 1;
    }
    
    int main(){
        int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS|CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWUSER|CLONE_NEWNET| SIGCHLD , NULL);
        waitpid(container_pid, NULL, 0);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    # 进入自定义的容器中
    gcc container.c -o container && ./conatiner
    
    # 检查不同命名空间下资源的隔离
    
    • 1
    • 2
    • 3
    • 4

    宿主机上
    在这里插入图片描述
    容器中
    在这里插入图片描述

    通过比较上述宿主机以及容器间的这些资源,我们可以发现容器间的这些资源相互隔离了;

    CLONE_NEWCGROUP
    在这里插入图片描述

    # CLONE_NEWNS
    # 默认情况下clone方法会拷贝宿主机的所有挂载点
    # 因此我们可以看到容器比宿主机多出一个新增的挂载点
    mount | grep proc
    # CLONE_NEWCGROUP
    ls /sys/fs/cgroup
    cat /sys/fs/cgroup/cpu/tasks
    # CLONE_NEWIPC
    # 宿主机和容器的ipcs队列均为空因此无法比较
    # ipcs用于查看当前系统进程间通信的状况
    ipcs
    # CLONE_NEWUTS
    # 主机名称
    hostname
    # CLONE_NEWPID
    # 查看当前进程pid
    ps
    # CLONE_NEWUSER
    # 查看当前用户
    whoami
    # CLONE_NEWNET
    # 查看当前网络
    ip a
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    2.2.Linux cgroups

    对进程设置资源(cpu, memory, 磁盘,带宽)限制
    在这里插入图片描述
    cgroup可对进程使用的以下资源进行限制

    资源解释
    blkio磁盘吞吐量
    cpucpu使用量
    cpuacctcpu使用率的统计报告
    cpuset分配独立CPU和内存节点
    devices控制设备访问
    freezer挂起或恢复进程
    memory内存使用量
    net_cls标记网络数据包从而限制带宽
    net_prio网络数据包优先级
    perf_event使用perf工具监控进程
    pids任务数量
    systemd控制管理系统资源

    Cgroup net_cls&net_prio

    使用cgroup给某个进程可以生成的任务数量进行限制(pids)

    mkdir /sys/fs/cgroup/pids/container
    cd /sys/fs/cgroup/pids/container
    
    # 最多只能运行一个进程
    echo 1 > pids.max
    
    # 对当前进程进行限制
    echo $$ > cgroup.procs
    # 再次运行ls发现无法执行子进程
    ls
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    Cgroup Usage
    Cgroup Memory & Pids Usage

    2.3.文件挂载隔离实践

    当我们进入docker容器时,发现其文件与宿主机中的相互独立…
    这使用的是mount namespace,使得容器文件和宿主机文件相隔离

    chroot: 将指定的目录设为新进程的根目录;

    • 限制用户权限
    • 用户环境与宿主机环境隔离

    (1) 执行以下脚本在 ~/test 目录下拷入基本命令

    # test.sh
    basedir=~/test
    commands=(/bin/ls /bin/bash /bin/cat /bin/chmod)
    mkdir -p $basedir
    cd $basedir
    for(( i=0;i<${#commands[@]};i++ ));do
            list=$(ldd ${commands[i]} | egrep -o '/(.*?) ')
            mkdir -p $basedir`dirname ${commands[i]}`;
            cp ${commands[i]} $basedir${commands[i]};
            for dependency in $list;do
                   # echo ${commands[i]} - $dependency;
                    mkdir -p $basedir`dirname $dependency`;
                    cp -v $dependency $basedir$dependency
            done;
    done;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    (2)

    
    mkdir -p ~/test && cd ~/test
    # 下载hello world项目,里面已经打好了一个helloworld程序
    # 目前支持 linux-arm, linux-arm64, linux-arm64
    git clone https://gitee.com/Liyuan-1/helloworld.git
    
    # 运行上述test.sh脚本
    chmod +x test.sh && ./test.sh
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    到这一步,我们就将应用在容器中运行所需的文件准备好了;
    在这里插入图片描述
    如下图我们可以看到新创建的进程将~/test作为了根目录,且与宿主机文件隔离;

    # 以 ~/test 作为新进程(容器)的根目录
    chroot ~/test
    /bin/ls -al
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    2.4.容器运行实践

    容器只是一个进程,不过容器中文件与宿主机文件相隔离

    # 将上述准备好的~/test作为新进程的根目录
    chroot ~/test
    /bin/ls -al /helloworld/HelloWorld-Golang/
    
    # 为你的宿主机所能执行的文件添加可执行权限
    /bin/chmod +x /helloworld/HelloWorld-Golang/main-linux-arm
    
    # 运行
    /helloworld/HelloWorld-Golang/main-linux-arm
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    由于此处我们并未做网络相关的隔离,因此该进程(容器)共享宿主机网络资源;
    在这里插入图片描述

    3.容器资源隔离运行实践

    接下来我们使用clone函数创建一个新的进程(容器),对其进行资源隔离,并运行上述HelloWorld程序;

    3.1. 准备可执行文件

    (1) 首先我们拷贝一些基础命令以及启动HelloWorld应用所需要的文件到~/test目录下

    # init_commands.sh
    basedir=~/test
    commands=(/usr/sbin/ip /bin/ps /bin/hostname /bin/whoami /bin/ls /bin/bash /bin/cat /bin/chmod)
    mkdir -p $basedir
    cd $basedir
    for(( i=0;i<${#commands[@]};i++ ));do
            list=$(ldd ${commands[i]} | egrep -o '/(.*?) ')
            mkdir -p $basedir`dirname ${commands[i]}`;
            cp ${commands[i]} $basedir${commands[i]};
            for dependency in $list;do
                   # echo ${commands[i]} - $dependency;
                    mkdir -p $basedir`dirname $dependency`;
                    cp -v $dependency $basedir$dependency
            done;
    done;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    mkdir -p ~/test && cd ~/test
    
    # 执行上述shell脚本
    # 拷贝基础命令到~/test目录下
    chmod +x ./init_commands.sh && ./init_commands.sh
    
    # 拷贝HelloWorld程序
    git clone https://gitee.com/Liyuan-1/helloworld.git
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    3.2. 运行容器

    (2) 使用clone创建一个和宿主机资源相隔离的进程,并将上述生成的dir作为容器运行的根目录
    // container.c

    #define _GNU_SOURCE
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define STACK_SIZE (1024 * 1024)
    static char container_stack[STACK_SIZE];
    char* const container_args[] = {
            "/bin/bash",
            NULL
    };
    
    int container_main(void* arg){
            printf("container_main\n");
            sethostname("container0",10);
            mount("proc", "/proc", "proc", 0, NULL);
            mount("none", "/tmp", "tmpfs", 0, "");
            chroot("./");
            perror("chroot");
            chdir("/");
            perror("chdir");
            printf("mount:  %s\n",strerror(errno));
            execv(container_args[0], container_args);
            return 1;
    }
    
    int main(){
        int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS|CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWUSER|CLONE_NEWNET| SIGCHLD , NULL);
        waitpid(container_pid, NULL, 0);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    gcc container.c && ./a.out
    
    /bin/chmod +x main-linux-arm && ./main-linux-arm
    
    # 检查该进程(容器)资源隔离是否生效
    /bin/hostname
    /usr/sbin/ip a
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    在这里插入图片描述

    4.docker容器运行分析

    当我们运行一个nginx容器时,总共分为以下几个步骤;

    4.1. docker pull nginx

    在该步骤,我们会将nginx镜像拉取下来,其实这个镜像就是一个文件夹,里面包含运行nginx所需要的文件,然后使用chroot将该文件设置为新进程的根目录,通过docker inspect 我们也可以看出;

    docker pull nginx
    docker run -itd nginx
    # 查看该nginx容器详细信息,如下图所示
    docker inspect ${containerID} | grep MergedDir
    # 该目录即为当前运行的nginx容器的根目录
    ls -al ${MergedDir}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    (1) UnionFS (Union File System)

    UnionFS: 将多个文件夹挂载到同一个目录上

    假设我有2个目录,下面分别有一些文件;
    在这里插入图片描述

    # 将 ./test00  ./test01 ./test02 挂载至 ./liyuan 目录下
    # 上层的目录的文件会覆盖下层的目录
    # lowerdir: 指定用户需要挂载的lower层目录,可使用`:`分隔
    # upperdir: 指定用户需要挂载的upper目录
    # workdir: 指定文件系统的工作基础目录,挂载后内容会被清空,且在使用过程中其内容用户不可见
    mkdir -p ./liyuan &&  mount -t overlay overlay -o lowerdir=./test00,upperdir=./test01,workdir=./test02  ./liyuan
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    docker使用了UnionFS,可以将多个不同位置的目录联合挂载到同一个目录下,如下图,nginx运行的容器由多个lowerDir,UpperDir,WorkDir一起被合并挂载到了 MergeDir上;
    在这里插入图片描述
    在这里插入图片描述

    4.2. docker run -it nginx

    在该步骤,我们相当于首先将lowerDir, upperDir, workDir通过UnionFS方式挂载至MergedDir中,然后运行 chroot 将该MergedDir作为新进程根目录, 然后主动运行应用;

    3. 总结

    容器只是运行在宿主机上的进程,其本质只是将文件等资源与宿主机相隔离,但共享整个操作系统内核,因此你对操作系统内核配置的修改会影响到运行在该宿主机上的所有容器;

    tip: 如果你有任何疑问,欢迎留言,也欢迎关注我的公众号 “从零开始的Go学习”

  • 相关阅读:
    Docker快速部署Mysql
    「Python」身份运算符 —— is 与 is not
    多位大佬合力讲解23种设计模式,这不是轻松拿下
    倒置字符串
    网络安全(黑客)自学
    SpringCloud 学习笔记总结 (七)
    windows服务器iis系统部署https
    数字化时代的革新,浅谈数字化供应链究竟有何意义
    [Azure - VM] 重新部署VM
    【Linux】Ubuntu设置自动登录图形界面和远程shell
  • 原文地址:https://blog.csdn.net/Yuan_xii/article/details/127911713