• 容器的本质


    k8s之容器的本质

    使用NameSpace技术来修改进程视图,创建出独立的文件系统、主机名、进程号、网络等资源空间,再使用Cgroups来实现对进程的 CPU、内存等资源的优先级和配额限制,最后使用chroot更改进程的根目录,也就是限制访问文件系统

    NameSpace

    可以创建出独立的文件系统、主机名、进程号、网络等资源空间,实现系统全局资源和进程局部资源的隔离。

    NameSpace有多种隔离类型,像常见的有PID NameSpace可以隔离进程ID、NET Namespace隔离网络设备端口号等。

    举个例子

    NameSpace可以让当前进程只能看到当前Namespace里的进程,看不到宿主机创建的进程。并且运行容器的命令为1号进程。

    # docker run -it busybox /bin/sh
    
    / # ps aux
    PID   USER     COMMAND
        1 root     /bin/sh
        8 root     ps aux
    
    / # echo $$
    1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在宿主机中可以看到该容器进程ID并不为1。再次证明容器也只是宿主机中的一个进程而已。

    [root@k8s-master ~]# ps aux | grep /bin/sh | grep -v grep
    root     1398061  0.1  0.3 1368728 54104 pts/1   Sl+  10:36   0:00 docker run -it mirrors.sangfor.com/busybox /bin/sh
    root     1398102  0.8  0.0   3176   188 pts/0    Ss+  10:36   0:00 /bin/sh
    
    • 1
    • 2
    • 3

    chroot

    可以更改进程的根目录,限制访问文件系统。

    手动构造一个容器

    我们使用clone创建一个子进程,传入的参数是CLONE_NEWPID代表着启用了PID NameSpace,当前进程看到的是一个全新的进程空间,在该命名空间中,自己是1号进程。

    #define _GNU_SOURCE
    #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 - inside the container!\n");
      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_NEWPID | SIGCHLD , NULL);
      waitpid(container_pid, NULL, 0);
      printf("Parent - container stopped!\n");
      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
    [root@k8s-master k8s]# gcc a.c -o a
    
    [root@k8s-master k8s]# ./a 
    Parent - start a container!
    Container - inside the container!
    
    [root@k8s-master k8s]# echo $$
    1
    
    [root@k8s-master k8s]# exit
    Parent - container stopped!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是我们在使用ps aux时,还是看到整个宿主机的进程,并且进程ID为1的还是Systemd,为什么呢?

    这是因为ps命令是读/proc文件系统的,所以我们还需要进行文件系统的隔离。

    我们再使用Mount NameSpace进行文件系统的隔离,在clone中使用CLONE_NEWNS参数。

    int main()
    {
      printf("Parent - start a container!\n");
      int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWPID | CLONE_NEWNS  | SIGCHLD , NULL);
      waitpid(container_pid, NULL, 0);
      printf("Parent - container stopped!\n");
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    编译运行后,发现当前文件系统并未发生变化,这个是因为,创建子进程时,会继承父进程的挂载点。

    所以我们需要在子进程中修改当前的挂载点,并且子进程在新的namespace的挂载动作只影响自身的挂载文件系统。

    先拷贝一个文件系统出来作为我们容器的根文件系统

    docker export 48ab2ddd04dc | tar -C ./testfs -xvf -
    
    • 1

    再挂载proc,并且将testfs作为该进程的根目录

    int container_main(void* arg)
    {  
      printf("Container - inside the container!\n");
    
      if (mount("proc", "testfs/proc", "proc", 0, NULL)) {
         perror("proc");
      }
    
      if ( chdir("./testfs")!=0 ||  chroot("./") != 0) {
         perror("chroot");
      }
    
      execv(container_args[0], container_args);
      printf("Something's wrong!\n");
      return 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    再次运行进入容器中,当前的根目录是上面我们构造的testfs,并且ps aux命令只能看到当前namespace的进程,而看不到宿主机namespace的进程了。

    [root@k8s-worker1 k8s]# ./a
    Parent - start a container!
    Container - inside the container!
    root@k8s-worker1:/# ls
    bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
    root@k8s-worker1:/# ps aux
    USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root           1  0.0  0.0  26680  5452 ?        S    07:42   0:00 /bin/bash
    root          94  0.0  0.0   5352   692 ?        S    07:49   0:00 ./a
    root          95  0.0  0.0   4620  3872 ?        S    07:49   0:00 /bin/bash
    root          99  0.0  0.0   7056  1556 ?        R+   07:49   0:00 ps aux
    root@k8s-worker1:/# echo $$
    1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    cgroup

    可以实现对进程的CPU、内存等资源的配额限制

    cgroup在操作系统中暴露出来的接口是文件系统,会以文件和目录在/sys/fs/cgroup/目录中展示,下面的目录都是子系统,可以限制各种资源:

    [root@iZwz93q4afq8ck02cesqh4Z ~]# ls /sys/fs/cgroup/
    blkio  cpuacct      cpuset   freezer  memory   net_cls,net_prio  perf_event  systemd
    cpu    cpu,cpuacct  devices  hugetlb  net_cls  net_prio          pids
    
    • 1
    • 2
    • 3

    如何限制进程CPU

    执行以下命令将CPU吃到100%

    $ while : ; do : ; done &
    
    • 1

    使用top命令查看是否cpu是否满负载

    # top
    
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                              
    2503037 root      20   0   26672   5412   3520 R  99.0   0.0   0:46.09 -bash                                                                                  99.49%
    
    • 1
    • 2
    • 3
    • 4

    在目录/sys/fs/cgroup/cpu,cpuacct/下创建目录container,操作系统会自动创建资源限制文件

    [root@k8s-worker1 container]# mkdir container
    [root@k8s-worker1 container]# ls
    cgroup.clone_children  cpuacct.usage         cpuacct.usage_percpu_sys   cpuacct.usage_user  cpu.rt_period_us   cpu.stat
    cgroup.procs           cpuacct.usage_all     cpuacct.usage_percpu_user  cpu.cfs_period_us   cpu.rt_runtime_us  notify_on_release
    cpuacct.stat           cpuacct.usage_percpu  cpuacct.usage_sys          cpu.cfs_quota_us    cpu.shares         tasks
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在文件夹下面可以查看到cpu.cfs_quota_us的默认值是-1,代表没有限制,cpu.cfs_period_us默认值为100ms(100000us)

    # cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us
    100000
    # cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
    -1
    
    • 1
    • 2
    • 3
    • 4


    向cpu.cfs_quota_us写入20ms(20000us),也就是每100ms时间里,限制的进程只能适用20ms,也就是这个进程只能使用到20%的CPU带宽

    echo 20000 > /sys/fs/cgroup/cpu//cpu.cfs_quota_us
    
    • 1

    再将限制的进程ID写入到tasks文件中,可以看到该文件中已经包含了该容器进程ID

    # echo 2503037 > tasks
    # cat tasks
    2503037
    
    • 1
    • 2
    • 3

    再使用top可以发现该进程的cpu被限制在20%

    # top
    
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                              
    2503037 root      20   0   26672   5412   3520 R  20.4   0.0   6:43.71 -bash
    
    • 1
    • 2
    • 3
    • 4

    容器被cgroup的情况

    当然docker已经封装好了,直接调用以下命令即可实现上面CPU的限制

    docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
    
    • 1

    可以看到在/sys/fs/cgroup/cpu,cpuacct/docker目录下创建了该容器的目录,目录下面包含了资源限制文件

    [root@k8s-worker1 docker]# pwd
    /sys/fs/cgroup/cpu,cpuacct/docker
    [root@k8s-worker1 docker]# ls
    87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c  cpuacct.usage             cpuacct.usage_percpu_user  cpu.cfs_quota_us   cpu.stat
    cgroup.clone_children                                             cpuacct.usage_all         cpuacct.usage_sys          cpu.rt_period_us   notify_on_release
    cgroup.procs                                                      cpuacct.usage_percpu      cpuacct.usage_user         cpu.rt_runtime_us  tasks
    cpuacct.stat
    [root@k8s-worker1 docker]# cd 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c/
    [root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# ls
    cgroup.clone_children  cpuacct.usage         cpuacct.usage_percpu_sys   cpuacct.usage_user  cpu.rt_period_us   cpu.stat
    cgroup.procs           cpuacct.usage_all     cpuacct.usage_percpu_user  cpu.cfs_period_us   cpu.rt_runtime_us  notify_on_release
    cpuacct.stat           cpuacct.usage_percpu  cpuacct.usage_sys          cpu.cfs_quota_us    cpu.shares         tasks
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在该目录下可以看到cpu.cfs_quota_us设置成了20000,并且tasks中包含了该容器进程的ID

    [root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# cat cpu.cfs_quota_us 
    20000
    [root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# cat tasks
    2560954
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    Flutter快学快用23 架构原理:为什么 Flutter 性能更佳
    RK3399平台开发系列讲解(CPU篇)常见CPU性能问题
    数据库安全如何保障?YashanDB有妙招(上篇)
    基于Java汽车租赁系统设计实现(源码+lw+部署文档+讲解等)
    python
    uview 1 uni-app表单 number digit 的输入框有初始化赋值后,但是校验失败
    研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?
    Spring Data MongoTemplate insert 插入数据报duplicate key的问题
    坚信人类记忆是以大分子物质存储的朋友们请看过来
    删除 13k 行暂存代码后,Linux 5.19 轻装上阵
  • 原文地址:https://blog.csdn.net/qq_22918243/article/details/126359150