怎样做网站优化 知乎五合一自助建站网站
Docker:namespace环境隔离 & CGroup资源控制
- Docker
- 虚拟机
- 容器
- namespace
- 相关命令
- dd
- mkfs
- df
- mount
- unshare
- 进程隔离
- 文件隔离
- CGroup
- 相关命令
- pidstat
- stress
- cgroup控制
- 内存控制
- CPU控制
Docker
在开发中,经常会遇到环境问题,比如程序依赖某个库,库又要具体的版本,以及某些函数必须在指定平台使用,这就会为开发带来很大的麻烦。
为此,有人提出采用虚拟化技术,为软件虚拟出一个环境。就像是在一个冰天雪地的地方建了一个花房养花,花房内温度湿度都刚刚好,将花房与外部的冰雪隔离开。
这种实现环境隔离的技术,主要有虚拟机和容器两种,Docker属于容器化的隔离技术。
虚拟机
所谓虚拟机,其实就是把一台物理主机虚拟为多台逻辑上的计算机。多台虚拟机共用物理上的同一台计算机,而每个逻辑上的计算机可以运行不同的操作系统,安装不同的库,从而提供不同的环境。

如图,上图红色部分与蓝色部分是两个不同的虚拟机,虚拟机技术在硬件层之上,在操作系统层就开始进行隔离。虚拟机通过伪造一个硬件的抽象接口,把操作系统嫁接到硬件上。

在硬件层与操作系统层之间,会存在一个虚拟化层,这其实就是一个软件,该层会负责分配硬件资源。
可以看出,如果想要创建多个虚拟机,就要在一台物理主机上跑多个操作系统,这其实要不小的开销,而容器是一种更加轻量的隔离技术。
容器
容器也是一种虚拟化的实现技术,它在操作系统之上进行环境隔离,每个容器可以有自己的一套工具和库,但是它们共享操作系统的内核!

如图,红色和蓝色区域,是两个不同的容器,它们的网络,文件系统等等都是隔离的,互不影响。
因为使用同样的操作系统内核,所以它们的系统调用接口自然就相同,但是基于相同的系统调用接口,配置了不同的库,不同的文件系统,那么最后两个容器就不同。

比如说上图,可以通过容积隔离技术在一个centOS操作系统上,运行不同版本的Ubuntu容器。这听起来很异想天开,但其实不然。
每个容器有自己独立的用户空间,这包括文件系统、库和用户级工具。用户操作接口是用户在容器内操作系统时接触到的命令行工具、库和应用。因此可以在上层的容器中,执行Ubuntu的命令,虽然内核是CentOS的。
相比于虚拟机技术,容器化技术非常轻量,容器相当于一个跑在操作系统上的进程,启动一个进程的速度是非常快的,通过容器技术,只需几秒钟就在主机上打开一个新的操作系统。
而容器化技术,目前最流行的实现方案就是Docker。
那么容器化技术是如何实现各种资源的隔离的?对Linux来说,它依赖于namespace和CGroup技术,这两个技术是由Linux内核提供的。
namespace:实现进程,文件系统,用户等资源隔离CGroup:实现CPU,内存,网络等资源隔离
namespace
namespace 是 Linux 内核用来隔离内核资源的方式。通过 namespace 可以让进程只能看到与自己相关的一部分资源,不同namespace的进程感觉不到对方的存在。
具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中。namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个namespace 中的系统资源只会影响当前namespace 里的进程,对其他namespace 中的进程没有影响。
常见namespace:
namespace | 隔离资源 |
|---|---|
| UTS | 主机名和域名 |
| IPC | 进程间通信:信号量、消息队列、共享内存 |
| PID | 进程 |
| NetWork | 网络设备,端口等 |
| Mount | 文件系统 |
| User | 用户 |
解释:
UTS:每个UTS namespace都可以有自己独立的主机和域名IPC:每个IPC namespace内部的进程可以进行进程间通信,但是不能跨越IPC namespace进行通信,在逻辑上这算跨主机通信PID:每个PID namespace都有自己独立的进程pid系统,不同的PID namespace可以出现相同的pidNetWork:每个NetWork namespace都有自己独立的网络设备,IP地址,路由表,端口号等Mount:每个Mount namespace有自己的文件系统,互相不能看到对方的文件User:每个User namespace都有自己独立的用户和用户组
相关命令
dd
dd可以从指定输入流读取数据,并输出到指定输出流。
参数:
if=文件名:从指定文件读取数据,如果不指定,则默认从标准输入读取of=文件名:输出数据到指定文件,如果不指定,则默认输出到标准输出bs=xxx:设定一个块block的大小coun=blocks:仅仅从输入流拷贝blocks个块,结合上一个参数,可以指定要读取的数据个数
创建一个指定大小的空文件:
dd if=/dev/zero of=test.txt bs=1M count=80
以上指令,就是创建了一个80M的空文件,输入流是/dev/zero,这是一个会不断产生空白字符的文件,也就是ASCII中字符编码为0的字符。使用这个文件作为输入流,可以快速初始化一个空文件。

可以看到,最后创建了一个大小为83886080 byte的文件,其实就是80 M。
mkfs
mkfs用于在设备上创建一个文件系统,俗称格式化。
mkfs [option] filesys [blocks]
选项:
-t:要创建的文件系统类型,比如ext3、ext4
其中filesys是被格式化的文件,而block是文件系统的磁盘块数,可以省略。
把刚才创建的文件进行格式化:
mkfs -t ext4 test.txt

这样就把刚才的空文件进行了格式化,变成了一个文件系统。
df
df用于显示Linux中的文件系统磁盘使用情况。
选项:
-h:以更加可视化的形式输出,默认情况下数据以字节为单位,加上该选项后,会自动转化为GB,MB等单位-T:显示文件系统的类型

查看当前的文件系统,可以看到,其不包含刚才格式化的test.txt,因为他只是一个被格式化的文件,还没有被挂载。
mount
mount用于挂载文件系统,相当于给文件系统一个访问入口。
比如说你在电脑上插入一个U盘,它往往会显示为E盘或者其它盘符。这个盘符就是一个访问入口,因为U盘本身就是一个文件系统,如果想要访问这个文件系统的内容,Windows自然要提供一个入口,因此它自动分配一个E盘,让用户可以通过E盘访问U盘。
同样的,刚才格式化test.txt为一个文件系统,现在要将其挂载起来,才能访问这个文件系统。
mount [option] device dir
device:被挂载的文件dir:挂载到的位置
选项:
-t:挂载文件的类型,比如ext3、ext4,但是可以不填,mount会自动识别文件系统的类型
把刚才的test.txt挂载到当前/mymount目录下:

首先创建一个空目录/mymount,随后把test.txt挂载到这个目录下,随后可以看到,/mymount出现了新的内容。
随后通过df -t ext4查看系统的文件系统,可以看到/dev/loop0文件系统,被挂载到了/mymount下,说明成功挂载了一个文件系统。
如果想要删除这个文件系统,可以执行:
umount /mymount
以上所有命令,是在完成一个文件系统的创建,便于后续测试文件系统的隔离性。
接下来看看Linux提供的创建namespace的命令:
unshare
unshare用于执行一个进程,并且为这个进程提供一个独立的namespace。
unshare [option] program
program:要执行的程序
选项:
-i --ipc:不共享IPC空间-m --mount:不共享Mount空间-n --net:不共享Net空间-p --pid:不共享PID空间-u --uts:不共享UTS空间-U --user:不共享用户空间--fork:创建一个子进程执行program--mount-proc:挂载一个新的/proc到命名空间内
此处这个--fork有一点点绕,因为ushare这个命令,本身也是一个进程,是在宿主机环境运行的。
如果直接执行unshare,流程如下:
unshare创建一个新的命名空间unshare执行program,但是没有创建新的进程,而是直接用program替换了unshare本身

以上unshare不带有--fork参数,执行了/bin/bash进程。进入namespace后,可以看到,/bin/bash的父进程是-bash。这个-bash就是宿主机的bash进程。因为unshare是在bash中执行的,所以unshare的父进程是-bash。最后将/bin/bash替换unshare,/bin/bash的父进程就是原先的父进程-bash。
这种情况下,看不到unshare进程,因为/bin/bash就是原先的unshare。
如果加上--fork参数,流程如下:
unshare创建一个新的命名空间unshare在新的命名空间中创建一个新的子进程来执行program。unshare本身不退出

加上--fork参数后,/bin/bash的父进程就变成了unshare,unshare的父进程是-bash。也就是说unshare创建了一个子进程来执行program,而不是亲自执行program。
如果你跟着操作了,此时还处于namespace中,要通过exit来退出,不然会影响后续操作。
进程隔离
现在尝试进行进程隔离,也就是隔离,通过--pid选项完成:
unshare --fork --pid --mount-proc /bin/bash
以上命令,用于创建一个新的PID命名空间,并执行bash进程,也就是执行一个新的命令行。
此处要加上--fork选项,因为要进行进程隔离,而unshare本身是宿主机的进程,如果直接让unshare本身去执行program,那么program就不在新的namespace中,导致错误。

以上示例中,因为没有加上--fork,报错了。
如果只加上--fork选项,此时还是不能观察到进程隔离。因为top、ps等进程监控的命令,是依赖于/proc/PID这个目录的。但是命令没有进行文件系统隔离,所以还是会使用宿主机的/proc/PID目录,导致namespace内部可以看到外部的进程。
为此,unshare命令专门提供了一个参数--mount-proc,在新的namespace挂载一个独立的/proc目录,方便进行进程的监控。
进程隔离结果:

可以看到,创建了新的namespace后,ps -aux只能查到两个进程,一个是bash,一个是grep。这就将namespace内部的进程与宿主机的进程隔离开了。
文件隔离
想要进行文件隔离,创建一个新的namespace,然后在里面创建一个文件系统并挂载。再在外部的宿主机查看是否可以看到这个文件系统。
- 创建一个新的文件隔离命名空间
unshare --mount --fork /bin/bash
- 创建一个指定大小的空文件
dd if=/dev/zero of=data.img bs=1M count=80
- 格式化文件为文件系统
mkfs -t ext4 data.img
- 挂载文件系统
mkdir /mymount
mount -t ext4 data.img /mymount

最后通过df -t ext4,可以看到文件系统已经挂载成功了。
打开一个新的终端,执行df -t ext4:

此时左右终端看到的文件系统不同,左侧的namespace内挂载的新文件系统,右侧终端看不到了,这就是文件隔离。
此时在namespace中执行exit,就会退出这个bash,进而退出namespace,在其内部创建的文件,挂载的文件系统都会自动销毁。

当exit退出后,再次查看文件系统,也找不到刚才挂载的文件系统了。这和刚才两个终端的情况不同,之前是不同namespace之间的文件隔离,而此处是退出namespace后,文件系统已经被销毁了。
CGroup
cgroup的可以把一系列任务,也就是进程划分到一个任务组,并且限制一个任务组的资源占用。比如可以限制一系列任务最多占用多少CPU,占用多少内存等等。
也就是说,namespace完成了不同容器之间环境的隔离,而cgroup完成了每个容器资源的访问限制。
相关命令
为了方便测试CPU与内存压力,此处使用两个工具分别完成产生压力以及压力检测。
pidstat
pidstat用于检测一个进程的CPU、内存、IO、线程等等资源的占用情况。
需要下载:
apt install sysstat
语法:
pidstat [option] [时间间隔] [次数]
选项:
-u:检测CPU使用情况,默认就是该选项-r:检测内存使用情况-d:检测IO使用情况-p:指定进程pid,如果指定ALL则监视所有进程-C:检测通过指定命令启动的进程
直接执行pidstat:

此时会输出所有进程,默认输出CPU占用情况,也就是%CPU这一栏。
通过-p指定进程:

通过-C指定进程:

此处指定bash进程。
通过-r检测内存:

此处%MEM栏就是内存占用情况。
指定检测次数与频率:

此处的1 3表示:每隔一秒检测一次,一共检测三次。
stress
stress是一个压力测试工具,可以对CPU、内存IO等进行压力测试。
这个工具也要下载:
apt install stress
语法:
stress [option]
参数:
-c --cpu N:产生N个进程,每个进程都循环调用sqrt函数产生CPU压力-m --vm N:产生N个进程,每个进程都循环调用malloc free函数,产生内存压力
示例:

左侧使用stress创建了一个进程进行CPU压力输出,右侧检测stress产生的压力,结果一个进程占满了100%的CPU资源。
cgroup控制
接下来看看如何操作cgroup,它并没有现成的指令来控制,而是需要操控配置文件来完成。
/proc/cgroups查看cgroup支持的资源控制:

在/proc/cgroups文件中,包含了cgroup所支持的资源控制的类型,比如CPU、内存等。
查看cgroup挂载信息:
mount | grep cgroup

这里就是每一个资源的控制目录,比如在/sys/fs/cgroup/cpu目录下,就是控制CPU资源的配置文件。
内存控制
创建一个内存的控制组很简单,跳转到目录/sys/fs/cgroup/memory,然后创建一个目录:

此处创建了一个test_memory目录,这就算创建了一个test_memory内存控制组。进入目录后,可以看到这个目录被初始化了很多文件,其中memory.limit_in_bytes这个文件,就是这个cgroup可以使用的最大内存数目,以字节为单位。
想要限制这个cgroup的最大内存数量,直接往文件写入数据即可:
echo "20971520" > memory.limit_in_bytes
此处20971520其实就是20 mb,此后这个cgroup的最大内存占用就不会超过20 mb。
那么要如何把一个进程加入控制组?这里有一个task文件,只需要把进程的PID写入到这个文件中,那么一个进程就算加入了这个cgroup。

如图,创建一个进程,占用50m的内存:
stress -m 1 --vm-bytes 50m
随后在另一个端口通过pidstat查看stress的PID,为15070和15069,其中15069是控制进程,15070是真正在产生内存压力的进程。将15070写入tasks文件中,让其加入cgroup。
结果左侧的stress退出了,无法产生50m的压力。这是因为一开始就限制了cgroup只能占用最多20 m的内存。一旦进程加入后,就会受到限制,从而崩溃。
CPU控制
创建一个CPU的控制组也一样,跳转到目录/sys/fs/cgroup/cpu,然后创建一个目录:

此处创建了一个test_cpu目录,也就死和创建了一个test_cpuCPU控制组。这个目录同样被初始化了很多文件。
其中控制CPU占用的是cpu.cfs_period_us和cpu.cfs_quota_us,这两个文件共同完成CPU资源限制。其中cpu.cfs_period_us作为分母,cpu.cfs_quota_us作为分子,以百分比的形式限制CPU。
比如cpu.cfs_period_us内填入5000,cpu.cfs_quota_us内填入2000。那么该cgroup最多可以占用2000/5000也就是40%的CPU资源。要注意的是,这两个文件的最小值都是1000,不允许使用比1000更小的数字进行配置。另外的cpu.cfs_quota_us的默认值为-1,表示可以占用100%的CPU。
另一个就是tasks文件,同样的只要把PID写入这个文件,就算加入了这个CPU控制组。
启动一个stress进程:

由于stress本身就会尽可能占满CPU,右侧输出窗口每隔一秒输出stress的状态,其一直保持100%的CPU资源占用。
左下角窗口先限制了test_cpu这个控制组的CPU最大占用率是2000/10000,也就是20%。
随后把stress的PID20849写入tasks:

写入后,从右边的窗口可以看出,stress的CPU占用率立马下降,100%到37%最后稳定在20%。
可以cgroup成功对进程的CPU进行了限制。
容器化技术在Linux中基于namespace和cgroup实现,namespace完成不同容器之间的环境隔离,而cgroup完成多个容器对资源的占用限制,合理分配资源。这就是容器化技术,以及docker的最基本原理,也是底层依赖。
