做为一种容器虚拟化技术,Docker深度应用了操做系统的多项底层支持技术。linux
早期版本的Docker是基于已经成熟的Linux Container(LXC)技术实现的。自Docker 0.9版本起,Docker逐渐从LXC转移到新的libcontainer(https://github.com/docker/libcontainer)上,而且积极推进开放容器规范runc,试图打造更通用的底层容器虚拟化库。git
从操做系统功能上看,目前Docker底层依赖的核心技术主要包括Linux操做系统的命名空间(Namespace)、控制组(Control Group)、联合文件系统(Union File System)和Linux网络虚拟化支持。github
Docker目前采用了标准的C/S架构。客户端和服务端既能够运行在一个机器上,也可运行在不一样机器上经过socket或者RESTful API来进行通讯。docker
1.服务端ubuntu
Docker Daemon通常在宿主主机后台运行,做为服务端接受来自客户的请求,并处理这些请求(建立、运行、分发容器)。后端
在设计上,Docker Daemon是一个模块化的架构,经过专门的Engine模块来分发管理各个来自客户端的任务。缓存
Docker服务端默认监听本地的unix:///var/run/docker.sock套接字,只容许本地的root用户或docker用户组成员访问。能够经过-H选项来修改监听的方式。安全
例如,让服务端监听本地的TCP链接1234端口,以下所示:bash
$ docker daemon -H 0.0.0.0:1234网络
此外,Docker还支持经过HTTPS认证方式来验证访问。
Debian/Ubuntu 14.04等使用upstart管理启动服务的系统中,Docker服务端的默认启动配置文件在/etc/default/docker。对于使用systemd管理启动服务的系统,配置文件在/etc/systemd/system/docker.service.d/docker.conf。
2.客户端
Docker客户端为用户提供一系列可执行命令,用户用这些命令与Docker Daemon交互。
用户使用的Docker可执行命令即为客户端程序。与Docker Daemon不一样的是,客户端发送命令后,等待服务端返回,一旦收到返回后,客户端马上执行结束并退出。用户执行新的命令,须要再次调用客户端命令。一样,客户端默认经过本地的unix:///var/run/docker.sock套接字向服务端发送命令。若是服务端没有监听在默认的地址,则须要客户端在执行命令的时候显式指定服务端地址。
例如,假定服务端监听在本地的TCP链接1234端口tcp://127.0.0.1:1234,只有经过-H参数指定了正确的地址信息才能链接到服务端,以下所示:
$ docker version
Client: Version: 1.12.0 API version: 1.24 Go version: go1.6.3 Git commit: 8eab29e Built: Thu Sep 28 22:00:36 2016 OS/Arch: linux/amd64 Cannot connect to the Docker daemon. Is the docker daemon running on this host?
$ docker -H tcp://127.0.0.1:1234 version
Client: Version: 1.12.0 API version: 1.24 Go version: go1.6.3 Git commit: 8eab29e Built: Thu Sep 28 22:00:36 2016 OS/Arch: linux/amd64 Server: Version: 1.12.0 API version: 1.24 Go version: go1.6.3 Git commit: 8eab29e Built: Thu Sep 28 22:00:36 2016 OS/Arch: linux/amd64
3.新的架构设计
C/S架构给Docker基本功能的实现带来了许多便利,但同时也引入了一些限制。
使用Docker时,必需要启动并保持Docker Daemon的正常运行,它既要管理容器的运行时,又要负责提供对外部API的响应。而一旦Docker Daemon服务不正常,则已经运行在Docker主机上的容器也每每没法继续使用。
Docker团队已经意识到了这个问题,在较新的版本(1.11.0+)中,开始将维护容器运行的任务放到一个单独的组件containerd中来管理,而且支持OCI的runc规范。原先的对客户端API的支持则仍然放在Docker Daemon,经过解耦,大大减小了对Docker Daemon的依赖。同时,新的架构提升了启动容器的速度,一项测试代表,能够达到每秒启动超过100个容器。
命名空间(namespace)是Linux内核的一个强大特性,为容器虚拟化的实现带来极大便利。
利用这一特性,每一个容器均可以拥有本身单独的命名空间,运行在其中的应用都像是在独立的操做系统环境中同样。命名空间机制保证了容器之间彼此互不影响。
在操做系统中,包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU等资源,全部的资源都是应用进程直接共享的。要想实现虚拟化,除了要实现对内存、CPU、网络IO、硬盘IO、存储空间等的限制外,还要实现文件系统、网络、PID、UID、IPC等的相互隔离。前者相对容易实现一些,后者则须要宿主主机系统的深刻支持。
随着Linux系统对于命名空间功能的逐步完善,如今已经能够实现这些需求,让进程在彼此隔离的命名空间中运行。虽然这些进程仍在共用同一个内核和某些运行时环境(runtime,例如一些系统命令和系统库),可是彼此是不可见的,而且认为本身是独占系统的。
1.进程命名空间
Linux经过命名空间管理进程号,对于同一进程(即同一个task_struct),在不一样的命名空间中,看到的进程号不相同,每一个进程命名空间有一套本身的进程号管理方法。进程命名空间是一个父子关系的结构,子空间中的进程对于父空间是可见的。新fork出的进程在父命名空间和子命名空间将分别有一个进程号来对应。
例如,查看Docker主进程的pid进程号是5989,以下所示:
$ ps -ef |grep docker
root 5989 5988 0 14:38 pts/6 00:00:00 docker -d
新建一个Ubuntu的“hello world”容器:
$ docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world;sleep 1; done"
ec559327572b5bf99d0f80b98ed3a3b62023844c7fdbea3f8caed4ffa5c62e86
查看新建容器进程的父进程,正是Docker主进程5989:
$ ps -ef |grep while
root 6126 5989 0 14:41 ? 00:00:00 /bin/sh -c while true; do echo hello world; sleep 1; done
2.网络命名空间
若是有了pid命名空间,那么每一个命名空间中的进程就能够相互隔离,可是网络端口仍是共享本地系统的端口。
经过网络命名空间,能够实现网络隔离。网络命名空间为进程提供了一个彻底独立的网络协议栈的视图,包括网络设备接口、IPv4和IPv6协议栈、IP路由表、防火墙规则、sockets等,这样每一个容器的网络就能隔离开来。Docker采用虚拟网络设备(Virtual Network Device)的方式,将不一样命名空间的网络设备链接到一块儿。默认状况下,容器中的虚拟网卡将同本地主机上的docker0网桥链接在一块儿。
使用brctl工具能够看到桥接到宿主主机docker0网桥上的虚拟网口:
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.56847afe9799 no veth4148 vethd166 vethd533
3.IPC命名空间
容器中进程交互仍是采用了Linux常见的进程间交互方法(Interprocess Communication,IPC),包括信号量、消息队列和共享内存等。PID Namespace和IPC Namespace能够组合起来一块儿使用,同一个IPC命名空间内的进程能够彼此可见,容许进行交互;不一样空间的进程则没法交互。
4.挂载命名空间
相似于chroot,将一个进程放到一个特定的目录执行。挂载命名空间容许不一样命名空间的进程看到的文件结构不一样,这样每一个命名空间中的进程所看到的文件目录彼此被隔离。
5.UTS命名空间
UTS(UNIX Time-sharing System)命名空间容许每一个容器拥有独立的主机名和域名,从而能够虚拟出一个有独立主机名和网络空间的环境,就跟网络上一台独立的主机同样。默认状况下,Docker容器的主机名就是返回的容器ID:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ec559327572b ubuntu:14.04 /bin/sh -c 'while tr 18 minutes ago Up 18 minutes furious_goodall
$ docker inspect -f {{".Config.Hostname"}} ec5
ec559327572b
6.用户命名空间
每一个容器能够有不一样的用户和组id,也就是说能够在容器内使用特定的内部用户执行程序,而非本地系统上存在的用户。
每一个容器内部均可以有root账号,但跟宿主主机不在一个命名空间。
经过使用隔离的用户命名空间能够提升安全性,避免容器内进程获取到额外的权限。
控制组(CGroups)是Linux内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。只有能控制分配到容器的资源,才能避免多个容器同时运行时对宿主机系统的资源竞争。控制组能够提供对容器的内存、CPU、磁盘IO等资源进行限制和计费管理。控制组的设计目标是为不一样的应用状况提供统一的接口,从控制单一进程(好比nice工具)到系统级虚拟化(包括OpenVZ、Linux-VServer、LXC等)。
具体来看,控制组提供:
安装Docker后,用户能够在/sys/fs/cgroup/memory/docker/目录下看到对Docker组应用的各类限制项,包括:
$ cd /sys/fs/cgroup/memory/docker
$ ls
用户能够经过修改这些文件值来控制组限制Docker应用资源。
例如,经过下面的命令可限制Docker组中全部进程使用的物理内存总量不超过100MB:
$ sudo echo 104857600 >/sys/fs/cgroup/memory/docker/memory.limit_in_bytes
进入对应的容器文件夹,能够看到对应容器的一些状态:
$ cd 42352bb6c1d1c5c411be8fa04e97842da87d14623495189c4d865dfc444d12ae/
$ ls
$ cat memory.stat
在开发容器工具时,每每须要一些容器运行状态数据,这时就能够从这里获得更多的信息。
能够在建立或启动容器时为每一个容器指定资源的限制,例如使用-c|--cpu-shares[=0]参数来调整容器使用CPU的权重;使用-m|--memory[=MEMORY]参数来调整容器使用内存的大小。
联合文件系统(UnionFS)是一种轻量级的高性能分层文件系统,它支持将文件系统中的修改信息做为一次提交,并层层叠加,同时能够将不一样目录挂载到同一个虚拟文件系统下,应用看到的是挂载的最终结果。
联合文件系统是实现Docker镜像的技术基础。Docker镜像能够经过分层来进行继承。例如,用户基于基础镜像(用来生成其余镜像的基础,每每没有父镜像)来制做各类不一样的应用镜像。这些镜像共享同一个基础镜像层,提升了存储效率。此外,当用户改变了一个Docker镜像(好比升级程序到新的版本),则会建立一个新的层(layer)。所以,用户不用替换整个原镜像或者从新创建,只须要添加新层便可。用户分发镜像的时候,也只须要分发被改动的新层内容(增量部分)。这让Docker的镜像管理变得十分轻量级和快速。
1.Docker存储
Docker目前经过插件化方式支持多种文件系统后端。Debian/Ubuntu上成熟的AUFS(Another Union File System,或v2版本日后的Advanced Multilayered Unification File System),就是一种联合文件系统实现。AUFS支持为每个成员目录(相似Git的分支)设定只读(readonly)、读写(readwrite)或写出(whiteout-able)权限,同时AUFS里有一个相似分层的概念,对只读权限的分支能够在逻辑上进行增量地修改(不影响只读部分的)。
Docker镜像自身就是由多个文件层组成,每一层有惟一的编号(层ID)。
能够经过docker history查看一个镜像由哪些层组成。例如查看ubuntu:14.04镜像由4层组成,每层执行了不一样的命令:
$ docker history ubuntu:14.04
IMAGE CREATED CREATED BY SIZE COMMENT
2a274e3405ec 13 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
df697c8b1bf4 13 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.895 kB
371166fb96e0 13 months ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 194.5 kB
69191ca023af 13 months ago /bin/sh -c #(nop) ADD file:c8f078961a543cdefa 188.1 MB
对于Docker镜像来讲,这些层的内容都是不可修改的、只读的。而当Docker利用镜像启动一个容器时,将在镜像文件系统的最顶端再挂载一个新的可读写的层给容器。容器中的内容更新将会发生在可读写层。当所操做对象位于较深的某层时,须要先复制到最上层的可读写层。当数据对象较大时,每每意味着IO性能较差。所以,通常推荐将容器修改的数据经过volume方式挂载,而不是直接修改镜像内数据。此外,对于频繁启停Docker容器的场景下,文件系统的IO性能也将十分关键。具体看,Docker全部的存储都在Docker目录下,以Ubuntu系统为例,默认路径是/var/lib/docker。
在这个目录下面,存储由Docker镜像和容器运行相关的文件和目录,可能包括aufs、containers、graph、image、init、linkgraph.db、network、repositories-aufs、swarm、tmp、trust、volumes等。
最关键的就是aufs目录,这是aufs文件系统所在,保存Docker镜像相关数据和信息。该目录包括layers、diff和mnt三个子目录。1.9版本和以前的版本中,命名跟镜像层的ID是匹配的,而自1.10开始,层数据相关的文件和目录名与层ID再也不匹配。
layers子目录包含层属性文件,用来保存各个镜像层的元数据:某镜像的某层下面包括哪些层。
例如:某镜像由5层组成,则文件内容应该以下:
# cat aufs/layers/78f4601eee00b1f770b1aecf5b6433635b99caa5c11b8858dd6c8cec03b4584f-init
d2a0ecffe6fa4ef3de9646a75cc629bbd9da7eead7f767cb810f9808d6b3ecb6
29460ac934423a55802fcad24856827050697b4a9f33550bd93c82762fb6db8f
b670fb0c7ecd3d2c401fbfd1fa4d7a872fbada0a4b8c2516d0be18911c6b25d6
83e4dde6b9cfddf46b75a07ec8d65ad87a748b98cf27de7d5b3298c1f3455ae4
diff子目录包含层内容子目录,用来保存全部镜像层的内容数据。
例如:# ls aufs/diff/78f4601eee00b1f770b1aecf5b6433635b99caa5c11b8858dd6c8cec03b4584f-init/
dev etc
mnt子目录下面的子目录是各个容器最终的挂载点,全部相关的AUFS层在这里挂载到一块儿,造成最终效果。一个运行中容器的根文件系统就挂载在这下面的子目录上。一样,1.10版本以前的Docker中,子目录名和容器ID是一致的。其中,还包括容器的元数据、配置文件和运行日志等。
2.多种文件系统比较
Docker目前支持的联合文件系统种类包括AUFS、OverlayFS、btrfs、vfs、zfs和Device Mapper等。
各类文件系统目前的支持状况以下:
总结一下,AUFS和Device Mapper的应用最为普遍,支持也相对成熟,推荐生产环境考虑。长期来看,OverlayFS将可能具备更好的特性。
Docker的本地网络实现其实就是利用了Linux上的网络命名空间和虚拟网络设备(特别是veth pair)。
1.基本原理
直观上看,要实现网络通讯,机器须要至少一个网络接口(物理接口或虚拟接口)与外界相通,并能够收发数据包;此外,若是不一样子网之间要进行通讯,须要额外的路由机制。
Docker中的网络接口默认都是虚拟的接口。虚拟接口的最大优点就是转发效率极高。这是由于Linux经过在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将被直接复制到接收接口的接收缓存中,而无需经过外部物理网络设备进行交换。对于本地系统和容器内系统来看,虚拟接口跟一个正常的以太网卡相比并没有区别,只是它速度要快得多。
Docker容器网络就很好地利用了Linux虚拟网络技术,在本地主机和容器内分别建立一个虚拟接口,并让它们彼此连通(这样的一对接口叫作veth pair)。
通常状况下,Docker建立一个容器的时候,会具体执行以下操做:
完成这些以后,容器就可使用它所能看到的eth0虚拟网卡来链接其余容器和访问外部网络。用户也能够经过docker network命令来手动管理网络。
在使用docker run命令启动容器的时候,能够经过--net参数来指定容器的网络配置。
有5个可选值bridge、none、container、host和用户定义的网络:
3.手动配置网络
用户使用--net=none后,Docker将不对容器网络进行配置。
下面,将手动完成配置网络的整个过程。
首先,启动一个/bin/bash容器,指定--net=none参数:
$ docker run -i -t --rm --net=none base /bin/bash
root@63f36fc01b5f:/#
在本地主机查找容器的进程id,并为它建立网络命名空间:
$ docker inspect -f '{{.State.Pid}}' 63f36fc01b5f
2778
$ pid=2778
$ sudo mkdir -p /var/run/netns
$ sudo ln -s /proc/$pid/ns/net /var/run/netns/$pid
检查桥接网卡的IP和子网掩码信息:
$ ip addr show docker0
21: docker0: ...
inet 172.17.42.1/16 scope global docker0
...
建立一对“veth pair”接口A和B,绑定A接口到网桥docker0,并启用它:
$ sudo ip link add A type veth peer name B
$ sudo brctl addif docker0 A
$ sudo ip link set A up
将B接口放到容器的网络命名空间,命名为eth0,启动它并配置一个可用IP(桥接网段)和默认网关:
$ sudo ip link set B netns $pid
$ sudo ip netns exec $pid ip link set dev B name eth0
$ sudo ip netns exec $pid ip link set eth0 up
$ sudo ip netns exec $pid ip addr add 172.17.42.99/16 dev eth0
$ sudo ip netns exec $pid ip route add default via 172.17.42.1
以上,就是Docker配置网络的具体过程。
当容器终止后,Docker会清空容器,容器内的网络接口会随网络命名空间一块儿被清除,A接口也会自动从docker0卸载并清除。
此外,在删除/var/run/netns/下的内容以前,用户可使用ip netns exec命令在指定网络命名空间中进行配置,从而更新容器内的网络配置。