文章目录
  1. 1. Linux CGroup
  2. 2. Linux Namespace
  3. 3. AUFS
  4. 4. 基于Docker的开发流程
  5. 5. Docker镜像文件浅析

Docker应该是最近几年最火的云计算技术了,为应用的构建方式带来了一系列的变革。本文试图介绍一些关于Docker的原理和基本操作。

Docker以容器(Container)的方式,对计算机的各种资源作出隔离,以达到一个类似虚拟机(Virtual Machine)的效果。与虚拟机主要的不同在于,虚拟机是对硬件的抽象,也就是说,需要在虚拟机上先运行操作系统,然后在操作系统中运行应用程序;而容器是对资源的抽象,也就是说,容器是建立在操作系统之上,将操作系统提供的资源予以隔离并分别提供给不同的应用。两种虚拟化的方式各有千秋,从隔离度上来说,虚拟机要比容器强,但在性能上,容器的方式更优,因为容器少运行了一层操作系统。

本文使用Ubuntu 14.04 LTS完成实验。

Linux CGroup

Linux的CGroup技术为Docker提供了系统资源上的隔离,常用的被隔离资源有cpu、内存、网络流量等等1。提供这些资源隔离的主要目的是,让单个容器无法占用全部资源,这样会影响系统中其他容器或进程。

CGroup支持隔离的资源有以下一些。

1
2
3
4
5
6
7
8
9
10
blkio — 这​​​个​​​子​​​系​​​统​​​为​​​块​​​设​​​备​​​设​​​定​​​输​​​入​​​/输​​​出​​​限​​​制​​​,比​​​如​​​物​​​理​​​设​​​备​​​(磁​​​盘​​​,固​​​态​​​硬​​​盘​​​,USB 等​​​等​​​)。
cpu — 这​​​个​​​子​​​系​​​统​​​使​​​用​​​调​​​度​​​程​​​序​​​提​​​供​​​对​​​ CPU 的​​​ cgroup 任​​​务​​​访​​​问​​​。​​​
cpuacct — 这​​​个​​​子​​​系​​​统​​​自​​​动​​​生​​​成​​​ cgroup 中​​​任​​​务​​​所​​​使​​​用​​​的​​​ CPU 报​​​告​​​。​​​
cpuset — 这​​​个​​​子​​​系​​​统​​​为​​​ cgroup 中​​​的​​​任​​​务​​​分​​​配​​​独​​​立​​​ CPU(在​​​多​​​核​​​系​​​统​​​)和​​​内​​​存​​​节​​​点​​​。​​​
devices — 这​​​个​​​子​​​系​​​统​​​可​​​允​​​许​​​或​​​者​​​拒​​​绝​​​ cgroup 中​​​的​​​任​​​务​​​访​​​问​​​设​​​备​​​。​​​
freezer — 这​​​个​​​子​​​系​​​统​​​挂​​​起​​​或​​​者​​​恢​​​复​​​ cgroup 中​​​的​​​任​​​务​​​。​​​
memory — 这​​​个​​​子​​​系​​​统​​​设​​​定​​​ cgroup 中​​​任​​​务​​​使​​​用​​​的​​​内​​​存​​​限​​​制​​​,并​​​自​​​动​​​生​​​成​​​​​内​​​存​​​资​​​源使用​​​报​​​告​​​。​​​
net_cls — 这​​​个​​​子​​​系​​​统​​​使​​​用​​​等​​​级​​​识​​​别​​​符​​​(classid)标​​​记​​​网​​​络​​​数​​​据​​​包​​​,可​​​允​​​许​​​ Linux 流​​​量​​​控​​​制​​​程​​​序​​​(tc)识​​​别​​​从​​​具​​​体​​​ cgroup 中​​​生​​​成​​​的​​​数​​​据​​​包​​​。​​​
net_prio — 这个子系统用来设计网络流量的优先级
hugetlb — 这个子系统主要针对于HugeTLB系统进行限制,这是一个大页文件系统。

下面举个简单的例子说明一下,假设有一个死循环的程序,

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("PID [%5d]\n", getpid());
int i = 0;
for(;;) i++;
return 0;
}

该程序先打印自己的pid,然后进入死循环。编译后运行,然后运行top查看cpu占用率,

1
2
$ ./test
PID [ 9271]

1
2
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
9271 arnes 20 0 4204 632 544 R 100.0 0.0 0:27.75 test

不要退出./test,然后在/sys/fs/cgroup/cpu/下建立一个目录test(需要root权限),然后在cpu.cfs_quota_us中写入30000。

1
2
3
4
5
6
7
# mkdir test
# ls test
cgroup.clone_children cpu.cfs_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.stat tasks
# echo 30000 > /sys/fs/cgroup/cpu/test/cpu.cfs_quota_us

但是运行下面命令之后,cpu立刻降低到30%(与之前的30000对应),

1
# echo 9271 >> /sys/fs/cgroup/cpu/test/tasks

1
2
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
9271 arnes 20 0 4204 796 716 R 29.9 0.0 1:55.29 test

可以看到,使用CGroup限制cpu占用率已经生效。

Linux Namespace

只有资源的隔离显然不足以隔离不同的应用,还需要有对访问权限的隔离,这就是Linux Namespace的主要作用。Linux Namespace是Linux提供的一种内核级别环境隔离的方法2,主要是在使用clone系统调用的时候,添加不同的隔离效果参数来达到。注意,此处的隔离仅仅是应用层面的隔离,这些clone出来的进程仍然使用相同的Linux内核。

可以达到的不同的隔离效果有:

分类 系统调用参数 相关内核版本
Mount namespaces CLONE_NEWNS Linux 2.4.19
UTS namespaces CLONE_NEWUTS Linux 2.6.19
IPC namespaces CLONE_NEWIPC Linux 2.6.19
PID namespaces CLONE_NEWPID Linux 2.6.24
Network namespaces CLONE_NEWNET 始于Linux 2.6.24 完成于 Linux 2.6.29
User namespaces CLONE_NEWUSER 始于 Linux 2.6.23 完成于 Linux 3.8)

下面举例看一下Namespace的隔离效果,假设有如下c代码,

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
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int container_main(void* arg)
{
/* 查看子进程的PID,我们可以看到其输出子进程的 pid 为 1 */
printf("Container [%5d] - inside the container!\n", getpid());
sethostname("container",10);
system("mount -t proc proc /proc");
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent [%5d] - start a container!\n", getpid());
/*启用PID namespace - CLONE_NEWPID*/
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}

以上代码主要使用了CLONE_NEWPID | CLONE_NEWNS两个参数,在Namespace中隔离出pid=1的父进程,同时使用文件系统隔离,将/proc与外界隔离开来,这样也就看不到容器外的进程。在这样的环境下运行bash,可以看到如下效果(需要root运行),

1
2
3
4
5
6
7
8
9
10
11
12
$ sudo ./test
Parent [10328] - start a container!
Container [ 1] - inside the container!
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 18:12 pts/7 00:00:00 /bin/bash
root 107 1 0 18:20 pts/7 00:00:00 ps -ef
# exit
exit
Parent - container stopped!

可见,在Namespace的隔离下,bash已经获得pid=1的特权,并且,运行ps也看不到外界的进程。

AUFS

Docker三大基础技术中,唯一还没有进入Linux内核的就是AUFS。AUFS的作用,是把不同物理位置的目录合并mount到同一个目录中3。具体实现上,AUFS采用的是层状结构,把文件的改动通过不同的层,一层层叠加上去,每一层的改动都依赖于上一层,最终看到的结果实际上是文件叠加后的结果。实现的原理上有些类似于git。

基于Docker的开发流程

传统软件开发中,经常会出现的问题就是环境的不一致、部署困难、权限管理不便等等问题。例如,开发提交代码之后,运维人员需要根据开发的架构编写部署脚本;在部署环境确定之后,开发人员需要一套一致的环境来开发应用,但是这样的环境需要通过虚拟机来搭建,比较费时;如果共用开发环境的话,开发人员需要修改某个系统文件,需要申请系统权限,而他要改的很可能仅仅是一个host文件。

种种的不便利,都是因为“环境”的不统一,而统一环境,正是Docker容器所解决的问题。如果在开发流程中使用docker,那么运维的部分工作将转为开发工作,比如部分环境变量中的信息,端口信息,数据库连接信息等等,都可以包含在容器中,而运维人员需要负责的仅仅是容器与容器之间的连接。一个系统中的多个容器可以部署在不同的机器上(比如测试环境、生产环境),也可以 部署在一台机器上(比如开发环境),在引入容器这一层抽象的同时,并没有像虚拟机带来的运行效率降低等问题,这也是Docker能够被广泛接受的原因。

由于环境统一,对环境的依赖降低,持续集成变得更加容易完成;由于环境统一,应用的模块化变得更加容易,SOA、微服务等架构,会更加容易被部署;同样由于环境统一,单个模块的更新、优化、版本控制也会变得更加容易。

Docker镜像文件浅析

由于Docker Hub比较慢,本文从国内的DockerPool上来获取镜像,用来展示AUFS的层状结构,以java 8的jdk为例4。先获取这个镜像,

1
2
3
4
5
6
7
8
9
$ sudo docker pull dl.dockerpool.com:5000/java:8-jdk
Pulling repository dl.dockerpool.com:5000/java
816120cec693: Download complete
511136ea3c5a: Download complete
bb250545c9c9: Download complete
f872462c7730: Download complete
0b98a314e4e3: Download complete
Status: Downloaded newer image for dl.dockerpool.com:5000/java:8-jdk
dl.dockerpool.com:5000/java: this image was pulled from a legacy registry. Important: This registry version will not be supported in future versions of docker.

注意,为了获取该镜像,需要在docker的配置文件/etc/default/docker中添加参数DOCKER_OPTS="--insecure-registry dl.dockerpool.com:5000",本文使用的是Ubuntu,如果是Centos的话,需要修改/etc/sysconfig/docker。从获取的log中发现,总共获取了5个镜像,分别是

1
2
3
4
5
816120cec693
511136ea3c5a
bb250545c9c9
f872462c7730
0b98a314e4e3

这几个镜像最上面的816120cec693是最上层的镜像文件。运行docker images可以证实这一点。

1
2
3
4
5
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
hello-world latest 975b84d108f1 8 weeks ago 960 B
dl.dockerpool.com:5000/java 8-jre 5ceb47bbfcb2 13 months ago 284.2 MB
dl.dockerpool.com:5000/java 8-jdk 816120cec693 13 months ago 634.9 MB

docker的镜像存储在/var/lib/docker下,需要用root账户来查看。在该目录下查找镜像文件816120cec693,如下

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
# find . -name "816120cec693*" | xargs ls -al
-rw-r--r-- 1 root root 260 12月 13 16:08 ./aufs/layers/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5
./aufs/diff/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5:
总用量 40
drwxr-xr-x 10 root root 4096 12月 13 16:18 .
drwxr-xr-x 14 root root 4096 12月 13 16:08 ..
drwxr-xr-x 2 root root 4096 10月 24 2014 bin
drwxr-xr-x 33 root root 4096 10月 24 2014 etc
drwxr-xr-x 4 root root 4096 8月 17 2014 lib
drwxrwxrwt 3 root root 4096 10月 24 2014 tmp
drwxr-xr-x 8 root root 4096 10月 21 2014 usr
drwxr-xr-x 5 root root 4096 10月 21 2014 var
-r--r--r-- 1 root root 0 10月 24 2014 .wh..wh.aufs
drwx------ 2 root root 4096 10月 24 2014 .wh..wh.orph
drwx------ 2 root root 4096 10月 24 2014 .wh..wh.plnk
./aufs/mnt/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5:
总用量 8
drwxr-xr-x 2 root root 4096 12月 13 16:08 .
drwxr-xr-x 14 root root 4096 12月 13 16:08 ..
./graph/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5:
总用量 716
drwx------ 2 root root 4096 12月 13 16:18 .
drwx------ 11 root root 4096 12月 13 16:18 ..
-rw------- 1 root root 1330 12月 13 16:18 json
-rw------- 1 root root 9 12月 13 16:18 layersize
-rw------- 1 root root 710180 12月 13 16:18 tar-data.json.gz

可见,docker镜像的id实际上是一个更长的id,816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5,通常情况下,docker只显示前12位当做id。docker的本地文件中,./aufs/layers/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5是一个文本文件,里面记录了本镜像的所有父镜像id。

1
2
3
4
5
# more ./aufs/layers/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5
0b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1
f872462c773080ba038ec82f9c51c026dc6829b623994512b861b9e2109b4e2f
bb250545c9c9c8e0e24a56071683ab3556bf8049fd9e87d7cff90064beb901e6
511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158

./aufs/diff/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5是一个目录,里面记录了镜像的实际文件,从目录结构可以大致看出来这实际上是一个linux根目录。./aufs/mnt/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5默认是一个空目录。./graph/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5是一个目录,用于记录镜像相关的信息,该目录下的3个文件非常有助于理解AUFS的结构。json是记录镜像概要信息的json格式文件,如下

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
49
50
51
52
53
54
55
56
57
58
59
60
61
# more json | python -mjson.tool
{
"Size": 512900817,
"architecture": "amd64",
"config": {
"AttachStderr": false,
"AttachStdin": false,
"AttachStdout": false,
"Cmd": [
"/bin/bash"
],
"Domainname": "",
"Entrypoint": null,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"JAVA_VERSION=8u40"
],
"Hostname": "475879de227c",
"Image": "0b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1",
"Labels": null,
"OnBuild": [],
"OpenStdin": false,
"StdinOnce": false,
"Tty": false,
"User": "",
"Volumes": null,
"WorkingDir": ""
},
"container": "029cecc8157b465ce92e5559d6231261452b571e93734e08da5081eef4a8af41",
"container_config": {
"AttachStderr": false,
"AttachStdin": false,
"AttachStdout": false,
"Cmd": [
"/bin/sh",
"-c",
"apt-get update && apt-get install -y curl openjdk-8-jdk=\"8u40\"* unzip wget"
],
"Domainname": "",
"Entrypoint": null,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"JAVA_VERSION=8u40"
],
"Hostname": "475879de227c",
"Image": "0b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1",
"Labels": null,
"OnBuild": [],
"OpenStdin": false,
"StdinOnce": false,
"Tty": false,
"User": "",
"Volumes": null,
"WorkingDir": ""
},
"created": "2014-10-23T23:08:01.12431458Z",
"docker_version": "1.3.0",
"id": "816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5",
"os": "linux",
"parent": "0b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1"
}

该文件中记录了镜像的大小、系统amd64、hostname、系统id、操作系统、环境变量、以及父镜像id等信息。layersize文件记录了文件大小,这个值和json文件中的值是一样的。

1
2
# more layersize
512900817

可以看到,镜像的大小大概是513MB。可以与实际的文件大小做对比。

1
2
# du -sh ./aufs/diff/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5
516M ./aufs/diff/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5

可见,大小基本一致。tar-data.json.gz中记录了该镜像中的每个文件的信息,文件比较大,在此不在赘述。从json文件中,找到了父镜像0b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1,我们可以继续查看该父镜像的信息,然后查看父镜像的父镜像信息,一直查到根镜像。

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
49
50
51
52
53
54
55
# more 0b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1/json |python -mjson.tool
{
"architecture": "amd64",
...... // 省略部分无关信息
"id": "0b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1",
"os": "linux",
"parent": "f872462c773080ba038ec82f9c51c026dc6829b623994512b861b9e2109b4e2f"
}
# more f872462c773080ba038ec82f9c51c026dc6829b623994512b861b9e2109b4e2f/json | python -mjson.tool
{
"architecture": "amd64",
...... // 省略部分无关信息
"id": "f872462c773080ba038ec82f9c51c026dc6829b623994512b861b9e2109b4e2f",
"os": "linux",
"parent": "bb250545c9c9c8e0e24a56071683ab3556bf8049fd9e87d7cff90064beb901e6"
}
# more bb250545c9c9c8e0e24a56071683ab3556bf8049fd9e87d7cff90064beb901e6/json |python -mjson.tool
{
"Size": 121995138,
"architecture": "amd64",
...... // 省略部分无关信息
"id": "bb250545c9c9c8e0e24a56071683ab3556bf8049fd9e87d7cff90064beb901e6",
"os": "linux",
"parent": "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"
}
# more 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/json |python -mjson.tool
{
"architecture": "x86_64",
"comment": "Imported from -",
"container_config": {
"AttachStderr": false,
"AttachStdin": false,
"AttachStdout": false,
"Cmd": null,
"Domainname": "",
"Entrypoint": null,
"Env": null,
"Hostname": "",
"Image": "",
"Labels": null,
"OnBuild": null,
"OpenStdin": false,
"StdinOnce": false,
"Tty": false,
"User": "",
"Volumes": null,
"WorkingDir": ""
},
"created": "2013-06-13T14:03:50.821769-07:00",
"docker_version": "0.4.0",
"id": "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"
}

最后可见,根镜像id是511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158,该镜像总共5层,根镜像在最下层,本镜像在最上层,按顺序分别是,

1
2
3
4
5
816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5
0b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1
f872462c773080ba038ec82f9c51c026dc6829b623994512b861b9e2109b4e2f
bb250545c9c9c8e0e24a56071683ab3556bf8049fd9e87d7cff90064beb901e6
511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158

以上是Docker镜像文件的AUFS简单介绍。

文章目录
  1. 1. Linux CGroup
  2. 2. Linux Namespace
  3. 3. AUFS
  4. 4. 基于Docker的开发流程
  5. 5. Docker镜像文件浅析

欢迎来到Valleylord的博客!

本博的文章尽量原创。