容器的本质

2023-03-06 18:56:26 浏览数 (1)

# 前言

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

# NameSpace

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

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

举个例子

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

代码语言:javascript复制
# docker run -it busybox /bin/sh

/ # ps aux
PID   USER     COMMAND
    1 root     /bin/sh
    8 root     ps aux

/ # echo $$
1

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

代码语言:javascript复制
[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

# chroot

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

# 手动构造一个容器

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

代码语言:javascript复制
#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");
  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;
}
代码语言:javascript复制
[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!

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

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

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

代码语言:javascript复制
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;
}

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

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

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

代码语言:javascript复制
docker export 48ab2ddd04dc | tar -C ./testfs -xvf -

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

代码语言:javascript复制
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;
}

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

代码语言:javascript复制
[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

# cgroup

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

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

代码语言:javascript复制
[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

如何限制进程CPU

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

代码语言:javascript复制
$ while : ; do : ; done &

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

代码语言:javascript复制
# 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%

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

代码语言:javascript复制
[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

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

代码语言:javascript复制
# cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us
100000
# cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
-1

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

代码语言:javascript复制
echo 20000 > /sys/fs/cgroup/cpu//cpu.cfs_quota_us

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

代码语言:javascript复制
# echo 2503037 > tasks
# cat tasks
2503037

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

代码语言:javascript复制
# 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

容器被cgroup的情况

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

代码语言:javascript复制
docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash

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

代码语言:javascript复制
[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

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

代码语言:javascript复制
[root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# cat cpu.cfs_quota_us 
20000
[root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# cat tasks
2560954

0 人点赞