使用NameSpace技术来修改进程视图,创建出独立的文件系统、主机名、进程号、网络等资源空间,再使用Cgroups来实现对进程的 CPU、内存等资源的优先级和配额限制,最后使用chroot更改进程的根目录,也就是限制访问文件系统
可以创建出独立的文件系统、主机名、进程号、网络等资源空间,实现系统全局资源和进程局部资源的隔离。
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
在宿主机中可以看到该容器进程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
可以更改进程的根目录,限制访问文件系统。
我们使用clone创建一个子进程,传入的参数是CLONE_NEWPID代表着启用了PID NameSpace,当前进程看到的是一个全新的进程空间,在该命名空间中,自己是1号进程。
#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;
}
[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参数。
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的挂载动作只影响自身的挂载文件系统。
先拷贝一个文件系统出来作为我们容器的根文件系统
docker export 48ab2ddd04dc | tar -C ./testfs -xvf -
再挂载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;
}
再次运行进入容器中,当前的根目录是上面我们构造的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
可以实现对进程的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
如何限制进程CPU
执行以下命令将CPU吃到100%
$ while : ; do : ; done &
使用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%
在目录/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
在文件夹下面可以查看到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
向cpu.cfs_quota_us写入20ms(20000us),也就是每100ms时间里,限制的进程只能适用20ms,也就是这个进程只能使用到20%的CPU带宽
echo 20000 > /sys/fs/cgroup/cpu//cpu.cfs_quota_us
再将限制的进程ID写入到tasks文件中,可以看到该文件中已经包含了该容器进程ID
# echo 2503037 > tasks
# cat tasks
2503037
再使用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
容器被cgroup的情况
当然docker已经封装好了,直接调用以下命令即可实现上面CPU的限制
docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
可以看到在/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
在该目录下可以看到cpu.cfs_quota_us设置成了20000,并且tasks中包含了该容器进程的ID
[root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# cat cpu.cfs_quota_us
20000
[root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# cat tasks
2560954