Cgroups

Linux的命名空间 (namespaces) 可以为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法。但是并不能够为我们提供物理资源上的隔离。

在同一台机器上运行的多个容器会共同占用宿主机器的物理资源,其中某个容器正在执行 CPU 密集型的任务,就会影响其他容器中任务的性能与执行效率,导致多个容器相互影响并且抢占资源。

Control Groups(简称 CGroups)就是能够隔离宿主机器上的物理资源,例如 CPU、内存、磁盘 I/O 和网络带宽。

对资源的配额和度量

可配额/可度量 - Control Groups (cgroups)
可配额/可度量 - Control Groups (cgroups)
Cgroups 实现了对资源的配额和度量
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
blkio:这个子系统设置限制每个块设备的输入输出控制。例如:磁盘,光盘以及 USB 等等;
cpu:这个子系统使用调度程序为 cgroup 任务提供 CPU 的访问;
cpuacct:产生 cgroup 任务的 CPU 资源报告;
cpuset:如果是多核心的CPU,这个子系统会为 cgroup 任务分配单独的 CPU 和内存;
devices:允许或拒绝 cgroup 任务对设备的访问;
freezer:暂停和恢复 cgroup 任务;
memory:设置每个 cgroup 的内存限制以及产生内存资源报告;
net_cls:标记每个网络包以供 cgroup 方便使用;
ns:名称空间子系统;
pid: 进程标识子系统。

[root@localhost ~]# cd /sys/fs/cgroup/
[root@localhost cgroup]# ll
总用量 0
dr-xr-xr-x 5 root root 0 1月 26 18:57 blkio
lrwxrwxrwx 1 root root 11 1月 26 18:57 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 1月 26 18:57 cpuacct -> cpu,cpuacct
dr-xr-xr-x 5 root root 0 1月 26 18:57 cpu,cpuacct
dr-xr-xr-x 2 root root 0 1月 26 18:57 cpuset
dr-xr-xr-x 5 root root 0 1月 26 18:57 devices
dr-xr-xr-x 2 root root 0 1月 26 18:57 freezer
dr-xr-xr-x 2 root root 0 1月 26 18:57 hugetlb
dr-xr-xr-x 5 root root 0 1月 26 18:57 memory
lrwxrwxrwx 1 root root 16 1月 26 18:57 net_cls -> net_cls,net_prio
dr-xr-xr-x 2 root root 0 1月 26 18:57 net_cls,net_prio
lrwxrwxrwx 1 root root 16 1月 26 18:57 net_prio -> net_cls,net_prio
dr-xr-xr-x 2 root root 0 1月 26 18:57 perf_event
dr-xr-xr-x 5 root root 0 1月 26 18:57 pids
dr-xr-xr-x 2 root root 0 1月 26 18:57 rdma
dr-xr-xr-x 5 root root 0 1月 26 18:57 systemd

CPU子系统

Cgroups-CPU子系统
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
cpu.shares:可出让的能获得 CPU 使用时间的相对值。
cpu.cfs_period_us:cfs_period_us 用来配置时间周期长度,单位为 us(微秒)。
cpu.cfs_quota_us:cfs_quota_us 用来配置当前 Cgroup 在 cfs_period_us 时间内最多能使用的 CPU
时间数,单位为 us(微秒)。
cpu.stat:Cgroup 内的进程使用的 CPU 时间统计。
nr_periods:经过 cpu.cfs_period_us 的时间周期数量。
nr_throttled:在经过的周期内,有多少次因为进程在指定的时间周期内用光了配额时间而受到限制。
throttled_time:Cgroup 中的进程被限制使用 CPU 的总用时,单位是 ns(纳秒)。

[root@localhost cgroup]# cd cpu
[root@localhost cpu]# ll
总用量 0
-rw-r--r-- 1 root root 0 1月 26 19:04 cgroup.clone_children
-rw-r--r-- 1 root root 0 1月 26 18:57 cgroup.procs
-r--r--r-- 1 root root 0 1月 26 19:04 cgroup.sane_behavior
-rw-r--r-- 1 root root 0 1月 26 19:04 cpuacct.block_latency
-rw-r--r-- 1 root root 0 1月 26 19:04 cpuacct.cgroup_wait_latency
-rw-r--r-- 1 root root 0 1月 26 19:04 cpuacct.enable_sli
-rw-r--r-- 1 root root 0 1月 26 19:04 cpuacct.ioblock_latency
-r--r--r-- 1 root root 0 1月 26 19:04 cpuacct.proc_stat
-r--r--r-- 1 root root 0 1月 26 19:04 cpuacct.sched_cfs_statistics
-r--r--r-- 1 root root 0 1月 26 19:04 cpuacct.stat
-rw-r--r-- 1 root root 0 1月 26 19:04 cpuacct.usage
-r--r--r-- 1 root root 0 1月 26 19:04 cpuacct.usage_all
-r--r--r-- 1 root root 0 1月 26 19:04 cpuacct.usage_percpu
-r--r--r-- 1 root root 0 1月 26 19:04 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0 1月 26 19:04 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0 1月 26 19:04 cpuacct.usage_sys
-r--r--r-- 1 root root 0 1月 26 19:04 cpuacct.usage_user
-rw-r--r-- 1 root root 0 1月 26 19:04 cpuacct.wait_latency
-rw-r--r-- 1 root root 0 1月 26 19:04 cpu.bvt_warp_ns
-rw-r--r-- 1 root root 0 1月 26 19:04 cpu.cfs_burst_us
-rw-r--r-- 1 root root 0 1月 26 19:04 cpu.cfs_init_buffer_us
-rw-r--r-- 1 root root 0 1月 26 19:04 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 1月 26 18:57 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 1月 26 19:04 cpu.ht_stable
-rw-r--r-- 1 root root 0 1月 26 19:04 cpu.identity
-rw-r--r-- 1 root root 0 1月 26 18:57 cpu.rt_period_us
-rw-r--r-- 1 root root 0 1月 26 19:04 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 1月 26 18:57 cpu.shares
-r--r--r-- 1 root root 0 1月 26 19:04 cpu.stat
drwxr-xr-x 2 root root 0 1月 26 18:57 machine.slice
-rw-r--r-- 1 root root 0 1月 26 19:04 notify_on_release
-rw-r--r-- 1 root root 0 1月 26 19:04 pool_size
-rw-r--r-- 1 root root 0 1月 26 19:04 release_agent
drwxr-xr-x 42 root root 0 1月 26 18:57 system.slice
-rw-r--r-- 1 root root 0 1月 26 19:04 tasks
drwxr-xr-x 2 root root 0 1月 26 18:57 user.slice
创建一个cpu子系统cpudemo,所有的文件都会自动创建完成
创建一个cpu的子系统
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
32
33
34
35
[root@localhost cpu]# mkdir cpu_demo
[root@localhost cpu]# cd cpu_demo/
[root@localhost cpu_demo]# ll
总用量 0
-rw-r--r-- 1 root root 0 1月 26 19:05 cgroup.clone_children
-rw-r--r-- 1 root root 0 1月 26 19:05 cgroup.procs
-rw-r--r-- 1 root root 0 1月 26 19:05 cpuacct.block_latency
-rw-r--r-- 1 root root 0 1月 26 19:05 cpuacct.cgroup_wait_latency
-rw-r--r-- 1 root root 0 1月 26 19:05 cpuacct.enable_sli
-rw-r--r-- 1 root root 0 1月 26 19:05 cpuacct.ioblock_latency
-r--r--r-- 1 root root 0 1月 26 19:05 cpuacct.proc_stat
-r--r--r-- 1 root root 0 1月 26 19:05 cpuacct.sched_cfs_statistics
-r--r--r-- 1 root root 0 1月 26 19:05 cpuacct.stat
-rw-r--r-- 1 root root 0 1月 26 19:05 cpuacct.usage
-r--r--r-- 1 root root 0 1月 26 19:05 cpuacct.usage_all
-r--r--r-- 1 root root 0 1月 26 19:05 cpuacct.usage_percpu
-r--r--r-- 1 root root 0 1月 26 19:05 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0 1月 26 19:05 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0 1月 26 19:05 cpuacct.usage_sys
-r--r--r-- 1 root root 0 1月 26 19:05 cpuacct.usage_user
-rw-r--r-- 1 root root 0 1月 26 19:05 cpuacct.wait_latency
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.bvt_warp_ns
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.cfs_burst_us
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.cfs_init_buffer_us
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.ht_stable
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.identity
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.rt_period_us
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 1月 26 19:05 cpu.shares
-r--r--r-- 1 root root 0 1月 26 19:05 cpu.stat
-rw-r--r-- 1 root root 0 1月 26 19:05 notify_on_release
-rw-r--r-- 1 root root 0 1月 26 19:05 pool_size
-rw-r--r-- 1 root root 0 1月 26 19:05 tasks
查看系统进程,随便找一个CPU负载较高的进程
1
2
3
4
5
6
7
8
9
[root@localhost cpu_demo]# top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
13131 root 20 0 113292 3088 2900 S 23.9 0.1 0:06.11 sh
10 root 20 0 0 0 0 I 0.7 0.0 0:00.48 rcu_sched
16 root 20 0 0 0 0 S 0.3 0.0 0:00.04 ksoftirqd/1
541 root 20 0 0 0 0 I 0.3 0.0 0:00.13 kworker/1:3-eve
1196 root 20 0 1036736 51616 30480 S 0.3 1.4 0:07.27 containerd
1 root 20 0 125616 5492 4000 S 0.0 0.1 0:01.19 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.01 kthreadd
告诉cgroup要控制的是哪个进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@bluecusliyou cpudemo]# echo 13131 > cgroup.procs

# 控制CPU的占比就是控制 cpu.cfs_quota_us占cpu.cfs_period_us的比例
[root@localhost cpu_demo]# cat cpu.cfs_period_us
100000

# -1 就是不做控制
[root@localhost cpu_demo]# cat cpu.cfs_quota_us
-1

# 1000/100000就是最高控制在1%
[root@bluecusliyou cpudemo]# echo 1000 > cpu.cfs_quota_us

#查看系统进程,CPU占用降低了
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1196 root 20 0 1036736 51616 30480 S 1.0 1.4 0:22.68 containerd
10 root 20 0 0 0 0 I 0.3 0.0 0:04.68 rcu_sched
1187 root 20 0 574296 24300 10980 S 0.3 0.6 0:00.47 tuned
13131 root 20 0 113292 3088 2900 S 0.3 0.1 1:50.18 sh
46353 root 20 0 113560 3480 2968 S 0.3 0.1 0:01.37 bash

Union FS

Linux 的命名空间和控制组分别解决了不同资源隔离的问题,前者解决了进程、网络以及文件系统的隔离,后者实现了 CPU、内存等资源的隔离,但是在 Docker 中还有另一个非常重要的问题需要解决,也就是镜像的存储和分发问题。

联合文件系统

Docker 支持了不同的存储驱动,包括 aufs、devicemapper、overlay2、zfs 和 vfs 等等,在最新的 Docker 中,overlay2 取代了 aufs 成为了推荐的存储驱动,但是在没有 overlay2 驱动的机器上仍然会使用 aufs 作为 Docker 的默认驱动。

存储驱动 Docker Containerd
AUFS 在Ubuntu或者Debian上支持 不支持
OverlayFS 支持 支持
Device Mapper 支持 支持
Btrfs 社区版本在Ubuntu或者Debian上支持,企业版本在SLES上支持 支持
ZFS 支持 不支持
通过命令可以查看当前机器的文件系统
1
2
[root@localhost ~]# docker info | grep Storage
Storage Driver: overlay2

UnionFS(联合文件系统)其实是一种为 Linux 操作系统设计的用于把多个文件系统联合到同一个挂载点的文件系统服务。而 AUFS 即 Advanced UnionFS 其实就是 UnionFS 的升级版,它能够提供更优秀的性能和效率。

OverlayFS 也是一种与 AUFS 类似的联合文件系统,同样属于文件级的存储驱动,包含了最初的Overlay 和更新更稳定的 overlay2。

Overlay只有两层:upper层和Lower层

Lower 层代表镜像层,upper 层代表容器可写层。

容器视图
容器视图
UnionFS演示
创建4个文件夹
1
2
# 创建4个文件夹
[root@localhost overlay_FS]# mkdir upper lower merged work
向文件夹中写入文件
1
2
3
4
5
6
7
8
# lower文件夹写入两个文件in_lower.txt  in_both.txt
# upper文件夹写入两个文件in_upper.txt in_both.txt
# 两个文件夹有同名的文件in_both.txt

[root@localhost overlay_FS]# echo "from lower" > lower/in_lower.txt
[root@localhost overlay_FS]# echo "from lower" > lower/in_both.txt
[root@localhost overlay_FS]# echo "from upper" > upper/in_upper.txt
[root@localhost overlay_FS]# echo "from upper" > upper/in_both.txt
挂载文件夹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 挂载两个文件夹文件到合并文件夹
[root@localhost overlay_FS]# sudo mount -t overlay overlay -o lowerdir=`pwd`/lower,upperdir=`pwd`/upper,workdir=`pwd`/work `pwd`/merged

# 上面的命令可以将"lowerdir"和"upperdir"堆叠到/merged目录
"workdir"工作目录要求是和"upperdir"目录同一类型文件系统的空目录(也可以省略也可以省略upperdir和workdir参数)
但/merged为只读属性了。也可支持多lowerdir目录堆叠:例如
mount-t overlay -o lowerdir=/lower1:/lower2:/lower3,upperdir=/upper,workdir=/workoverlay /merged

# 查看挂载状态
[root@localhost merged]# df -h
文件系统 容量 已用 可用 已用% 挂载点
devtmpfs 1.8G 0 1.8G 0% /dev
tmpfs 1.8G 0 1.8G 0% /dev/shm
tmpfs 1.8G 9.1M 1.8G 1% /run
tmpfs 1.8G 0 1.8G 0% /sys/fs/cgroup
/dev/mapper/ao-root 59G 5.2G 54G 9% /
/dev/sda1 1014M 207M 808M 21% /boot
tmpfs 369M 0 369M 0% /run/user/0
overlay 59G 5.2G 54G 9% /root/overlay_FS/merged
查看结果
1
2
3
4
5
# 最终的文件是三个,同名的文件是下层的被上层的覆盖了
[root@localhost merged]# ls
in_both.txt in_lower.txt in_upper.txt
[root@localhost merged]# cat in_both.txt
from upper

容器和镜像是什么

我们首先需要理解

Docker 是如何构建并且存储镜像的,也需要明白 Docker 的镜像是如何被每一个容器所使用的

镜像其实本质就是一个文件,用来打包运行环境和软件,他包含运行某个软件所需的所有内容,包括代码、运行时库、环境变量和配置文件。

Docker 中的每一个镜像都是由一系列只读的层组成的,Dockerfile 中的每一个命令都会在已有的只读层上创建一个新的层。

如下有一个DockerFile文件
1
2
3
4
FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py

上述的 Dockerfile 文件会构建一个拥有四层 layer 的镜像。每一个镜像层都是建立在另一个镜像层之上的,同时所有的镜像层都是只读的,当镜像被 docker run 命令创建时就会在镜像的最上层添加一个可写的层,也就是容器层,所有对于运行时容器的修改其实都是对这个容器读写层的修改。

image layer
image layer

容器的本质也是一个文件,容器和镜像的区别就在于,所有的镜像都是只读的,而每一个容器其实等于镜像加上一个可读写的层,也就是同一个镜像可以对应多个容器。

images and container
images and container
可以查看镜像详情,看到文件存储的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@localhost ~]# docker image inspect nginx
[
{
......
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/3d8f56cf2379126fcbaa88f940c03f66ea6aa079802f377c40f50c6f567fc174/diff:/var/lib/docke17a269c6a427f27468f94d/diff:/var/lib/docker/overlay2/35eebd15b66513d9a5c3c5b85e3f2d3e0489230de95a563d016792f4e66299bf/diff:/var/lib/dockerd8c761ecc24c689565953/diff:/var/lib/docker/overlay2/e1987ca3291ce593d81807a8dccc1028e24981f45c7574d282d670b58d5b551c/diff:/var/lib/docker/93dda3ee4591203d1478/diff",
"MergedDir": "/var/lib/docker/overlay2/ffd818510e46c535d7149e21310e2b2a82546bdccb923d3e528c606c6cff2d8e/merged",
"UpperDir": "/var/lib/docker/overlay2/ffd818510e46c535d7149e21310e2b2a82546bdccb923d3e528c606c6cff2d8e/diff",
"WorkDir": "/var/lib/docker/overlay2/ffd818510e46c535d7149e21310e2b2a82546bdccb923d3e528c606c6cff2d8e/work"
},
"Name": "overlay2"
},
......
}
}
]

容器和镜像加载原理

每一个镜像层都是建立在另一个镜像层之上的,同时所有的镜像层都是只读的,只有每个容器最顶层的容器层才可以被用户直接读写,所有的容器都建立在一些底层服务(Kernel)上,包括名称空间、控制组、rootfs 等等。

kernel and container
kernel and container
正常安装的CentOS都是好几个G,为什么Docker的Centos镜像才200M?

典型的 Linux 文件系统组成包含Bootfs(boot file system) rootfs (root file system)
boots(boot file system)主要包含 bootloader和 Kernel,bootloader主要是引导加 kernel,Linux刚启动时会加载bootfs文件系统,当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由 bootfs转交给内核,此时系统也会卸载bootfs。
rootfs(root file system),在 bootfs之上。包含的就是典型 Linux系统中的/dev/proc/bin/etc等标准目录和文件。 rootfs就是各种不同的操作系统发行版,比如 Ubuntu,Centos等等。
对于一个精简的OS,rootfs可以很小,只需要包合最基本的命令,工具和程序库就可以了,因为底层直接用宿主机的kernel,自己只需要提供rootfs就可以了。不同的linux发行版本bootfs基本是一致的, rootfs会有差別,因此不同的发行版可以公用bootfs,所以镜像可以很小。

1
2
3
4
# 查看Cenots镜像的大小
[root@localhost ~]# docker images centos
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 5d0da3dc9764 2 years ago 231MB

rootfs and bootfs
rootfs and bootfs
为什么启动一个容器只需要秒级,而启动一个系统是分钟级?当启动一个

容器时,docker可以直接利用宿主机的操作系统kernel,省略了加载完整系统kernel这个复杂的过程,因此启动一个docker容器只需要几秒钟。

镜像为什么要分层?

我们可以去下载一个镜像,注意观察下载的日志输出,可以看到是一层层的在下载 。 采用这种分层的结构,最大的好处,就是可以资源共享,节省磁盘空间,内存空间,加快下载速度,这种文件的组装方式提供了非常大的灵活性。
比如有多个镜像都从相同的Base镜像构建而来,那么宿主机只需在磁盘上保留一份base镜像,下载镜像的时候可以不用重复下载,同时内存中也只需要加载一份base镜像,这样就可以为所有的容器服务了,而且镜像的每一层都可以被共享。

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
32
#拉取镜像是分层下载的
[root@localhost ~]# docker pull redis
Using default tag: latest
latest: Pulling from library/redis
eff15d958d66: Pull complete
1aca8391092b: Pull complete
06e460b3ba1b: Pull complete
def49df025c0: Pull complete
646c72a19e83: Pull complete
db2c789841df: Pull complete
Digest: sha256:619af14d3a95c30759a1978da1b2ce375504f1af70ff9eea2a8e35febc45d747
Status: Downloaded newer image for redis:latest
docker.io/library/redis:latest

#镜像详情可以看到分层的信息
[root@localhost ~]# docker image inspect redis
[
{
...
"Layers": [
"sha256:e1bbcf243d0e7387fbfe5116a485426f90d3ddeb0b1738dca4e3502b6743b325",
"sha256:58e6a16139eebebf7f6f0cb15f9cb3c2a4553a062d2cbfd1a782925452ead433",
"sha256:503a5c57d9786921c992b7b2216ae58f69dcf433eedb28719ddea3606b42ce26",
"sha256:277199a0027e044f64ef3719a6d7c3842e99319d6e0261c3a5190249e55646cf",
"sha256:d0d567a1257963b9655dfceaddc76203c8544fbf6c8672b372561a3c8a3143d4",
"sha256:a7115aa098139866d7073846e4321bafb8d5ca0d0f907a3c9625f877311bee7c"
]
}
...
}
]