美文网首页
Docker 菜鸟篇

Docker 菜鸟篇

作者: 33d31a1032df | 来源:发表于2019-02-12 14:11 被阅读3次

    容器是特殊的进程

    容器只是运行在宿主机上的一种特殊的进程,使用的还是同一个宿主机的操作系统内核。

    注意:

    • 不能在 Windows 宿主机上运行 Linux 容器
    • 不能在低版本的 Linux 宿主机上运行高版本的 Linux 容器

    用 docker 演示启动一个 nginx 服务,操作如下:

    1. 准备一台机器,配置如下:
    机器名 IP 系统 内核 配置
    centos 192.168.56.103 centos 7.5.1804 linux 3.10.0 2 核 / 4G 内存

    安装如下常用工具:

    [root@centos ~]# yum install -y vim wget tree
    
    1. 安装 docker-1.13.1
    [root@centos ~]# yum install -y docker-1.13.1-75.git8633870.el7.centos.x86_64
    [root@centos ~]# systemctl start docker
    [root@centos ~]# systemctl enable docker
    
    1. 运行一个 nginx 容器
    [root@centos ~]# docker run -d --name nginx nginx:1.12.2
    
    1. 查看宿主机进程
    [root@centos ~]# ps -ef | grep nginx
    root      2756  2741  0 17:44 ?        00:00:00 nginx: master process nginx -g daemon off;
    101       2778  2756  0 17:44 ?        00:00:00 nginx: worker process
    
    1. 最后记得关闭 nginx 容器
    [root@centos ~]# docker container rm -f nginx
    

    容器是一种沙盒技术

    容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。

    对于 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

    1. 启用 Linux Namespace 配置
    2. 设置 Linux Control Group 参数
    3. 切换进程的根目录(change root file system)

    Linux Namesapce

    Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。

    用 docker 执行 ping 命令,演示 PID Namespace,操作如下:

    1. 运行一个 busybox 容器(busybox 是一个软件工具箱,里面集成了些常用的命令以及工具),并执行 ping 命令
    [root@centos ~]# docker run -d --name busybox busybox:1.29.3 ping baidu.com
    
    1. 查看 busybox 容器内的进程,ping 进程的 PID 为 1
    [root@centos ~]# docker exec busybox ps
    PID   USER     TIME  COMMAND
        1 root      0:00 ping baidu.com
    
    1. 查看宿主机进程,同一个 ping 进程的 PID 却为 1744
    [root@centos ~]# ps -ef | grep ping
    root      1744  1728  0 15:53 ?        00:00:00 ping baidu.com
    
    1. 最后记得关闭 busybox 容器
    [root@centos ~]# docker container rm -f busybox
    

    这种技术,就是 Linux 里面的 Namespace 机制。而 Namespace 的使用方式也非常有意思:它其实只是 Linux 创建新进程的一个可选参数。

    我们知道,在 Linux 系统中创建线程的系统调用是 clone(),比如:

    int pid = clone(main_function, stack_size, SIGCHLD, NULL); 
    

    这个系统调用就会为我们创建一个新的进程,并且返回它的进程号 PID。

    而当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:

    int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 
    

    这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1,之所有说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的 PID 还是真实的数值,比如:1744。

    而除了刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace。用来对各种不同的进程上下文进程“障眼法”操作,比如:

    • Mount Namespace 用于让被隔离进程只看到当前 Namespace 里的挂载点信息
    • Network Namespace 用于让被隔离进程只看到当前 Namespace 里的网络设备和配置

    Linux Control Group

    Linux CGroup 的主要作用就是限制进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。

    在 Linux 中,CGroup 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。可以通过 mount 指令把它们展示出来,比如:

    [root@centos ~]# mount -t cgroup
    cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
    cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
    cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)
    cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls)
    cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
    cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
    cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
    cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
    cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
    cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)
    cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
    

    用 docker 执行 while 循环,演示 CPU CGroup,操作如下:

    1. 运行一个 busybox 容器,并限制只允许使用 20% 的 CPU,while 循环可以模拟跑满 CPU,操作如下:
    [root@centos ~]# docker run -d --cpu-period=100000 --cpu-quota=20000 --name busybox busybox:1.29.3 /bin/sh -c "while : ; do : ; done"
    52f0ea4715b26f56bb27b46aedaaa326c24040afe520f840e18ace3f7bf99e19
    
    1. 查看宿主机 top
    [root@centos ~]# top
    top - 17:36:55 up  1:53,  1 user,  load average: 0.01, 0.06, 0.16
    Tasks:  99 total,   2 running,  97 sleeping,   0 stopped,   0 zombie
    %Cpu0  :  0.3 us,  0.3 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    %Cpu1  :  7.3 us, 13.0 sy,  0.0 ni, 79.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    KiB Mem :  3881016 total,  3492996 free,   146688 used,   241332 buff/cache
    KiB Swap:  4063228 total,  4063228 free,        0 used.  3475308 avail Mem 
    
      PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                            
     4517 root      20   0    1236      4      0 R  20.3  0.0   0:06.48 sh
    
    1. 查看 /sys/fs/cgroup
    [root@centos ~]# cat /sys/fs/cgroup/cpu/system.slice/docker-52f0ea4715b26f56bb27b46aedaaa326c24040afe520f840e18ace3f7bf99e19.scope/cpu.cfs_period_us 
    100000
    [root@centos ~]# cat /sys/fs/cgroup/cpu/system.slice/docker-52f0ea4715b26f56bb27b46aedaaa326c24040afe520f840e18ace3f7bf99e19.scope/cpu.cfs_quota_us
    20000
    
    1. 最后记得关闭 busybox 容器
    [root@centos ~]# docker container rm -f busybox
    

    一段小程序

    用 C 演示 Mount Namespace 的使用,让被隔离进程只看到当前 Namespace 里的 /tmp 目录。

    1. C 代码
    [root@centos ~]# cat > ns.c <<EOF
    #define _GNU_SOURCE
    #include <sys/mount.h> 
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <stdio.h>
    #include <sched.h>
    #include <signal.h>
    #include <unistd.h>
    #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 - inside the container!\n");
      // 如果你的机器的根目录的挂载类型是 shared,那必须先重新挂载根目录
      mount("", "/", NULL, MS_PRIVATE, "");
      mount("none", "/tmp", "tmpfs", 0, "");
      execv(container_args[0], container_args);
      printf("Something's wrong!\n");
      return 1;
    }
    
    int main()
    {
      printf("Parent - start a container!\n");
      int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);
      waitpid(container_pid, NULL, 0);
      printf("Parent - container stopped!\n");
      return 0;
    }
    EOF
    
    1. 安装编译环境
    [root@centos ~]# yum install -y gcc gcc-c++ cmake
    
    1. 编译 C 代码
    [root@centos ~]# gcc -o ns ns.c
    
    1. 观察宿主机的挂载信息,并查看 /tmp 目录,你会看到好多文件
    [root@centos ~]# mount -l | grep /tmp
    [root@centos ~]# ls /tmp
    
    1. 执行程序,进入隔离进程,观察挂载信息,并查看 /tmp 目录,你不会看到任何宿主机的文件
    [root@centos ~]# ./ns
    Parent - start a container!
    Container - inside the container!
    [root@centos ~]# mount -l | grep /tmp
    none on /tmp type tmpfs (rw,relatime,seclabel)
    [root@centos ~]# ls /tmp
    
    1. 退出隔离进程,再次观察宿主机的挂载信息,和查看 /tmp 目录,你又能看到好多文件
    [root@centos ~]# exit
    exit
    Parent - container stopped!
    [root@centos ~]# mount -l | grep /tmp
    [root@centos ~]# ls /tmp
    

    不难想到,我们可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以容器就可以在里面随便折腾了。

    而这个挂载在容器根目录上,用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫做:rootfs(根文件系统)。

    rootfs

    假设,我们现在有一个 fs 目录,想要把它作为一个 /bin/bash 进程的根目录。

    1. 创建 fs 目录
    [root@centos ~]# mkdir -p fs/{bin,lib64}
    
    1. 拷贝 bash 和 ls 命令到 fs 目录对应的 bin 路径下
    [root@centos ~]# cp /bin/{bash,ls} fs/bin
    
    1. 把 bash 和 ls 命令所需要的 so 文件也拷贝到 fs 目录对应的 lib 路径下
    [root@centos ~]# files=$(ldd /bin/{bash,ls} | egrep -o '/lib.*\.[0-9]' | sort | uniq)
    [root@centos ~]# for file in $files; do cp $file fs$file; done
    [root@centos ~]# tree fs
    fs
    ├── bin
    │   ├── bash
    │   └── ls
    └── lib64
        ├── ld-linux-x86-64.so.2
        ├── libacl.so.1
        ├── libattr.so.1
        ├── libcap.so.2
        ├── libc.so.6
        ├── libdl.so.2
        ├── libpcre.so.1
        ├── libpthread.so.0
        ├── libselinux.so.1
        └── libtinfo.so.5
    
    2 directories, 12 files
    
    1. 执行 chroot 命令,告诉操作系统,我们将使用 fs 目录作为 /bin/bash 进程的根目录,并查看“/”目录下的文件
    [root@centos ~]# chroot fs /bin/bash
    bash-4.2# /bin/ls /
    bin  lib64
    

    我们发现,它返回的都是 fs 目录下面的内容,而不是宿主机的内容。

    更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”成 fs 目录了。

    需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包含操作系统内核。实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。

    这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

    本文内容摘自极客时间的《深入剖析Kubernetes》

    深入剖析Kubernetes

    相关文章

      网友评论

          本文标题:Docker 菜鸟篇

          本文链接:https://www.haomeiwen.com/subject/tvfkeqtx.html