docker 容器

docker 来源: 我终于实现了前辈们的梦想

docker实际上,就是一个系统联合几个组件一直在欺骗一个进程,主要依靠了三个帮凶 namespace,chroot,cgroup

namespace进程的隔离

Linux提供了namespace实现的进程隔离,如果想给一个进程"指定"一个PID只需要调用一个clone()函数即可,这样创建出来的进程在宿主机进程号还是随机的,但是在这个namespace中他的进程号就是1.

namespace只是在进程层面将她隔离开了,运行在namespace中的进程看不到其他的进程,但是宿主机的其他资源(CPU,内存,文件系统)还都是系统的,也就是这些资源依然是共享的.

简单举个例子,现在有一个公司叫做Linux.公司初创的时候就会自己有一些"元老",也就是系统进程.公司运转起来了就会不断地招人进来干活,也就是创建/删除 进程.

每个员工(进程)进入公司的时候都会给发一个工牌,工牌上面有一个他自己在这个公司里唯一的工号(PID),如100,公司里所有人都有这么一个,而1号工号,就是属于老板的.往下的2,3,4,5…也就是属于公司的"元老"们的.

容器就是在这个人进入公司后将它带到一个格子间,告诉他你的工号是"1",这个格子间就是你的办公室,你这个格子间怎么规划不管,只需要开始你的工作就行了.

但实际上,这个员工还拥有他在这个公司的"工号",如:100,只不过他不知道,认为自己就是老板罢了.

使用cgroup限制进程所能使用的资源

cgroup就是用来限制进程的CPU和内存等资源的.

cgroup被应用了Linux的"一切皆文件",他的目录在/sys/fs/cgroup下面,

首先在cgroup目录下创建一个组,也就是一个目录

mkdir /sys/fs/cgroup/cpu/container

接下来查看下这个组,cgroup已经创建好了所需的文件

[root@es1 ~]# ls /sys/fs/cgroup/cpu/container
cgroup.clone_children  cgroup.procs  cpuacct.usage         cpu.cfs_period_us  cpu.rt_period_us   cpu.shares  notify_on_release
cgroup.event_control   cpuacct.stat  cpuacct.usage_percpu  cpu.cfs_quota_us   cpu.rt_runtime_us  cpu.stat    tasks

接下来创建一个进程

while : ; do : ; done &

他会什么都不做一直循环,但是会消耗大量的CPU资源.

最好在单核主机上尝试,如果主机资源过大效果不会很明显.但是能看到一颗核使用率百分百.

[root@es1 ~]# top
top - 20:44:54 up 13 days,  6:39,  1 user,  load average: 1.25, 0.80, 0.66
Tasks: 204 total,   2 running, 202 sleeping,   0 stopped,   0 zombie
%Cpu0  :  1.3 us,  0.0 sy,  0.0 ni, 98.3 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu1  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  4.3 us,  0.3 sy,  0.0 ni, 95.0 id,  0.3 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  : 17.3 us,  0.3 sy,  0.0 ni, 82.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu4  : 89.7 us,  9.6 sy,  0.0 ni,  0.3 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu5  : 30.3 us,  0.3 sy,  0.0 ni, 68.7 id,  0.7 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu6  :  2.7 us,  0.0 sy,  0.0 ni, 97.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu7  : 28.7 us,  0.0 sy,  0.0 ni, 71.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 32780784 total,  4227068 free,  9827028 used, 18726688 buff/cache
KiB Swap:        0 total,        0 free,        0 used. 22371664 avail Mem 

PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
2609 elastic+  20   0   64.6g  13.5g   4.4g S 184.7 43.1  18427:33 java
12890 root      20   0  115436    584    176 R 100.0  0.0   1:22.94 bash   

截取部分内容,CPU1 用户态使用率百分百.同时有一个PID12890的bash进程使用了百分一百的CPU(就是刚才创建的循环)

接下来使用cgroup限制进程使用资源,默认情况下刚才创建的那个"组"不会被关联到任何进程上,所以想让他对刚才的进程生效,则需要将进程的PID写入到指定文件.

echo '12890' >/sys/fs/cgroup/cpu/container/tasks

接下来限制CPU

[root@es1 ~]# cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us 
-1

对应目录下的cpu.cfs_quota_us 文件内容为"-1",也就是不作任何限制.

echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us

20000意味着在CPU100ms的时间里,可以使用20ms的时间,也就是限制为使用率20%.

接下来重新查看使用率

[root@es1 ~]# top
top - 20:52:11 up 13 days,  6:47,  1 user,  load average: 1.47, 1.44, 1.04
Tasks: 204 total,   4 running, 200 sleeping,   0 stopped,   0 zombie
%Cpu(s):  9.5 us,  0.0 sy,  0.0 ni, 90.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 32780784 total,  5483900 free,  9826072 used, 17470812 buff/cache
KiB Swap:        0 total,        0 free,        0 used. 22372168 avail Mem 

PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
2609 elastic+  20   0   64.0g  12.7g   3.6g S  55.1 40.5  18433:55 java
12890 root      20   0  115436    584    176 R  20.3  0.0   8:31.47 bash

进程12890使用率只有20%左右了.

cgroup没办法对进程精准限制CPU的使用率,所以会在20%这个值上下产生浮动.docker无非也就是在创建容器的时候同时创建一个cgroup组,将对应的PID写入进去,所以docker只能是个"单进程"模式,如果同时包含两个进程,那么只有主进程的资源限制可以生效了.

chroot

在将进程和宿主机资源都隔离开以后,只剩下一个 文件系统 了.

docker中每一个容器都有一个"自己"的根目录,但是如果在宿主机执行df -h这样的命令,还是可以看到所有的容器的,所以容器内的"根目录"实际上是依赖于宿主机的文件系统而存在的.无非就是把宿主机的某个目录 "挂载"到了容器中,同时这个只对容器中的进程可见.

首先创建了一个容器,然后查看下主机的目录挂载

[root@worker3 ~]# docker ps -a 
CONTAINER ID   IMAGE                       COMMAND                CREATED       STATUS       PORTS     NAMES
177db9617483   528909316/check:debian_11   "tail -f /etc/hosts"   2 weeks ago   Up 2 weeks             net2
[root@worker3 ~]# df -h
Filesystem               Size  Used Avail Use% Mounted on
devtmpfs                 3.9G     0  3.9G   0% /dev
tmpfs                    3.9G     0  3.9G   0% /dev/shm
tmpfs                    3.9G  401M  3.5G  11% /run
tmpfs                    3.9G     0  3.9G   0% /sys/fs/cgroup
/dev/mapper/centos-root   48G   20G   28G  42% /
/dev/sda1               1014M  178M  837M  18% /boot
overlay                   48G   20G   28G  42% /data/docker/overlay2/072a996f43e54fc20475a5c2df7c61856bfd30525c7ec955c157390b6ad78144/merged
tmpfs                    796M     0  796M   0% /run/user/0

只有一个容器所以只产生一个挂载,接下来看下类型为overlay的这条挂载目录下的内容:

[root@worker3 ~]# ls /data/docker/overlay2/072a996f43e54fc20475a5c2df7c61856bfd30525c7ec955c157390b6ad78144/merged
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

接下来进入容器查看下根目录

[root@worker3 ~]# docker exec -it 177db9617483 /bin/bash
root@net2:/# ls
bin  boot  dev	etc  home  lib	lib64  media  mnt  opt	proc  root  run  sbin  srv  sys  tmp  usr  var

一模一样,为了证实两个是一个目录,在容器中写入文件一些东西,然后退出容器查看主机的对应目录下,是否出现了相同的文件切内容相同

echo "hello world!" >mount_test.txt
root@net2:/# echo "hello world!" >mount_test.txt
root@net2:/# cat mount_test.txt 
hello world!
root@net2:/# 
exit
[root@worker3 ~]# cat /data/docker/overlay2/072a996f43e54fc20475a5c2df7c61856bfd30525c7ec955c157390b6ad78144/merged/mount_test.txt 
hello world!

所以容器就是把主机上的一个目录"挂载"进去,用这个目录作为容器的"根目录".

实现这个过程,就是chroot完成的.

创建一个"根目录"

首先创建一个目录,选在了/test下面.创建了一个container1的目录作为假设的容器的根目录.

mkdir -p /test/container1

然后给这个"容器"创建一些基础目录存放一些基础命令

mkdir -p /test/container1/{bin,lib64,lib}

接下来复制个命令给他,同时把所需的库文件一并复制进去

cp /bin/{bash,ls} /test/container1/bin
list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
for i in $list; do cp "$i" "/test/container1${i}"; done
list="$(ldd /bin/bash | egrep -o '/lib.*\.[0-9]')"
# \cp :如果存在相同文件覆盖不提示
for i in $list; do \cp "$i" "/test/container1${i}"; done

ldd命令可以查看一个二进制可执行文件(比如命令)所依赖的库文件,然后全部复制到"容器"的对应目录

接下来,就是用chroot进入这个"容器"的"根目录"

chroot /test/container1/ /bin/bash

进入以后,直接敲任何命令都是找不到的,因为默认的环境变量都是空的.

[root@worker3 ~]# chroot /test/container1/ /bin/bash
bash-4.2# ls
bash: ls: command not found

声明一下变量,当执行命令时去/bin下面找.

PATH=/bin
bash-4.2# PATH=/bin
bash-4.2# ls
bin  lib  lib64
bash-4.2# 

重新执行ls,可以查看到根目录的内容了.不过只有三个目录都是刚才创建的,而命令也只有bash和ls.因为别的都没有复制到容器里.

接下来ctrl +D就能退出这个"容器"了.

所以docker的"镜像"所包含的内容,实际上就是一个个的目录,下面存放了各种库文件.当部署一个容器的时候将这个压缩包"解压"到一个指定的位置,先用namespace将进程隔离开,然后cgroup给他施加一定的限制,随后给他挂载一些目录放些基础命令,容器就运转起来了.但是这个容器本身并不包含系统内核文件,所以所有的容器都是依托于Linux内核而存在的一个特殊进程.

把整个系统看做一个房子,一个容器就相当于房子里的一个箱子.

当创建一个进程的时候,namespace把他放进一个箱子里,让他看不见箱子外面的世界,cgroup来给他固定一个大小:箱子最大可以占用整个房间的多大空间.最后,chroot来在箱子里"装修"一下,比如装个电灯泡刷个大白,让他从内部看起来和外面的房间几乎没什么区别.

最后进程在这个箱子里开始工作,他可以随意改变箱子里面的一切这都不会影响到外面的房间,但是他这个箱子的空间是共享的外面房子的空间.箱子也是依托于这个房间而存在的.

参考文献

极客时间 张磊 深入剖析Kubernetes
码农翻身 我终于实现了前辈们的梦想
Docker overview

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐