Docker应该是最近几年最火的云计算技术了,为应用的构建方式带来了一系列的变革。本文试图介绍一些关于Docker的原理和基本操作。
Docker以容器(Container)的方式,对计算机的各种资源作出隔离,以达到一个类似虚拟机(Virtual Machine)的效果。与虚拟机主要的不同在于,虚拟机是对硬件的抽象,也就是说,需要在虚拟机上先运行操作系统,然后在操作系统中运行应用程序;而容器是对资源的抽象,也就是说,容器是建立在操作系统之上,将操作系统提供的资源予以隔离并分别提供给不同的应用。两种虚拟化的方式各有千秋,从隔离度上来说,虚拟机要比容器强,但在性能上,容器的方式更优,因为容器少运行了一层操作系统。
本文使用Ubuntu 14.04 LTS完成实验。
Linux CGroup
Linux的CGroup技术为Docker提供了系统资源上的隔离,常用的被隔离资源有cpu、内存、网络流量等等。提供这些资源隔离的主要目的是,让单个容器无法占用全部资源,这样会影响系统中其他容器或进程。
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
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
cgroup.clone_children cpu.cfs_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.stat tasks
但是运行下面命令之后,cpu立刻降低到30%(与之前的30000对应),
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提供的一种内核级别环境隔离的方法,主要是在使用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>
#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 [%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());
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!
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
Parent - container stopped!
可见,在Namespace的隔离下,bash已经获得pid=1的特权,并且,运行ps也看不到外界的进程。
AUFS
Docker三大基础技术中,唯一还没有进入Linux内核的就是AUFS。AUFS的作用,是把不同物理位置的目录合并mount到同一个目录中。具体实现上,AUFS采用的是层状结构,把文件的改动通过不同的层,一层层叠加上去,每一层的改动都依赖于上一层,最终看到的结果实际上是文件叠加后的结果。实现的原理上有些类似于git。
基于Docker的开发流程
传统软件开发中,经常会出现的问题就是环境的不一致、部署困难、权限管理不便等等问题。例如,开发提交代码之后,运维人员需要根据开发的架构编写部署脚本;在部署环境确定之后,开发人员需要一套一致的环境来开发应用,但是这样的环境需要通过虚拟机来搭建,比较费时;如果共用开发环境的话,开发人员需要修改某个系统文件,需要申请系统权限,而他要改的很可能仅仅是一个host文件。
种种的不便利,都是因为“环境”的不统一,而统一环境,正是Docker容器所解决的问题。如果在开发流程中使用docker,那么运维的部分工作将转为开发工作,比如部分环境变量中的信息,端口信息,数据库连接信息等等,都可以包含在容器中,而运维人员需要负责的仅仅是容器与容器之间的连接。一个系统中的多个容器可以部署在不同的机器上(比如测试环境、生产环境),也可以 部署在一台机器上(比如开发环境),在引入容器这一层抽象的同时,并没有像虚拟机带来的运行效率降低等问题,这也是Docker能够被广泛接受的原因。
由于环境统一,对环境的依赖降低,持续集成变得更加容易完成;由于环境统一,应用的模块化变得更加容易,SOA、微服务等架构,会更加容易被部署;同样由于环境统一,单个模块的更新、优化、版本控制也会变得更加容易。
Docker镜像文件浅析
由于Docker Hub比较慢,本文从国内的DockerPool上来获取镜像,用来展示AUFS的层状结构,以java 8的jdk为例。先获取这个镜像,
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
816120 cec693: Download complete
511136 ea3c5a: Download complete
bb250545c9c9: Download complete
f872462c7730: Download complete
0 b98a314e4e3: 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
816120 cec693
511136 ea3c5a
bb250545c9c9
f872462c7730
0 b98a314e4e3
这几个镜像最上面的816120cec693
是最上层的镜像文件。运行docker images
可以证实这一点。
1
2
3
4
5
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
hello-world latest 975 b84d108f1 8 weeks ago 960 B
dl.dockerpool.com:5000 /java 8 -jre 5 ceb47bbfcb2 13 months ago 284.2 MB
dl.dockerpool.com:5000 /java 8 -jdk 816120 cec693 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
. - -
- - - - - - .
.
- - .
- - . .
- -
- -
- -
- -
- -
- - - - - - - . . . .
- - - - - - . . . .
- - - - - - . . . .
.
- - .
- - . .
.
- - - - - - .
- - - - - - . .
- - - - - - - -
- - - - - - - -
- - - - - - - - - . .
可见,docker镜像的id实际上是一个更长的id,816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5
,通常情况下,docker只显示前12位当做id。docker的本地文件中,./aufs/layers/816120cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5
是一个文本文件,里面记录了本镜像的所有父镜像id。
1
2
3
4
5
0 b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1
f872462c773080ba038ec82f9c51c026dc6829b623994512b861b9e2109b4e2f
bb250545c9c9c8e0e24a56071683ab3556bf8049fd9e87d7cff90064beb901e6
511136 ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
./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
{
"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=\" 8 u40\"* 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
文件中的值是一样的。
可以看到,镜像的大小大概是513MB。可以与实际的文件大小做对比。
1
2
516 M ./aufs/diff/816120 cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5
可见,大小基本一致。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
{
"architecture" : "amd64" ,
...... // 省略部分无关信息
"id" : "0b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1" ,
"os" : "linux" ,
"parent" : "f872462c773080ba038ec82f9c51c026dc6829b623994512b861b9e2109b4e2f"
}
{
"architecture" : "amd64" ,
...... // 省略部分无关信息
"id" : "f872462c773080ba038ec82f9c51c026dc6829b623994512b861b9e2109b4e2f" ,
"os" : "linux" ,
"parent" : "bb250545c9c9c8e0e24a56071683ab3556bf8049fd9e87d7cff90064beb901e6"
}
{
"Size" : 121995138 ,
"architecture" : "amd64" ,
...... // 省略部分无关信息
"id" : "bb250545c9c9c8e0e24a56071683ab3556bf8049fd9e87d7cff90064beb901e6" ,
"os" : "linux" ,
"parent" : "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"
}
{
"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
816120 cec6934a5bc94e3e62ab3b8934457814221e81c4d19736c990f811f3c5
0 b98a314e4e3c1162b7356824cf175fcb1c60ce81d9ef716d9d3f1b9e56249a1
f872462c773080ba038ec82f9c51c026dc6829b623994512b861b9e2109b4e2f
bb250545c9c9c8e0e24a56071683ab3556bf8049fd9e87d7cff90064beb901e6
511136 ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
以上是Docker镜像文件的AUFS简单介绍。