整理自《Docker技术入门与实践》(杨保华 戴王剑 曹亚仑) - Docker核心技术一文。
Docker是一种基于Linux Container(LXC)技术实现的容器虚拟化技术,现又引入libcontainer。从操作系统功能上的角度出发,Docker的核心可分为:Linux操作系统的命名空间(Namespaces)、控制组(Control Groups)、联合文件系统(Union File System)、Linux虚拟网络。
基础架构
Docker采用的是标准的C/S架构,客户端和服务端可以运行在同一机器上,也可以通过socket或RESTful API进行通信。
-
服务端
Docker daemon一般作为服务端运行在宿主机的后台,它是一个非常松耦合的架构,会通过专门的Engine模块来分发管理来自各个客户端的任务,并根据请求来创建、运行、分发容器。Docker支持通过HTTPS认证方式来验证访问。Docker daemon默认监听本地的unix:///var/run/docker.sock
套接字,只允许本地的root访问,但是可以通过-H选项来修改监听的方式。例如Ubuntu系统中,Docker daemon的默认启动配置就在/etc/default/docker
中。$ vim /etc/default/docker # Docker Upstart and SysVinit configuration file # # THIS FILE DOES NOT APPLY TO SYSTEMD # # Please see the documentation for "systemd drop-ins": # https://docs.docker.com/engine/articles/systemd/ # # Customize location of Docker binary (especially for development testing). #DOCKER="/usr/local/bin/docker" # Use DOCKER_OPTS to modify the daemon startup options. #DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4" # If you need Docker to use an HTTP proxy, it can also be specified here. #export http_proxy="http://127.0.0.1:3128/" # This is also a handy place to tweak where Docker's temporary files go. #export TMPDIR="/mnt/bigdrive/docker-tmp"
客户端
客户端为用户提供了一系列可执行的命令,从而达到与Docker daemon交互的目的。同样的,客户端则默认通过本地的unix:///var/run/docker.sock
套接字向服务端发送指令。不同的是服务端会一直处于监听状态,而客户端在发送指令后等待服务端返回,一旦收到返回就会立即执行结束并退出。在交互过程中,如果服务端未监听到默认套接字,则需要客户端在执行命令的时候显示指定套接字。如服务端在监听本地的9527端口,那么如果要查询Docker的版本信息:sudo docker -H tcp://127.0.0.1:9527 version
命名空间
命名空间是Linux内核为实现容器虚拟化而引入的特性。每个容器都有自己的命名空间,这保证了容器之间的互不影响。利用该特性,容器实现了在内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU等资源的隔离,而不再是应用进程直接共享的状态。
进程命名空间
Linux通过命名空间管理进程号,同一进程在不同的命名空间中的进程号是不同的。进程命名空间是一个父子关系的结构,子空间的进程可看到父进程的ID。网络命名空间
通过网络命名空间可以实现网络的完全隔离。一个网络命名空间为进程提供了一个完全独立的网络协议栈的视图。包括网络设备接口、IPv4和IPv6协议栈、IP路由表、防火墙规则、sockets等。Docker可采用虚拟网络设备(Virtual Network Device)的方式将不同命名空间的网络设备连接在一起。默认情况下,容器的虚拟网卡将与宿主机的docker0网桥连接在一起。IPC命名空间
进程间交互(Interprocess Communication - IPC)的信息包括信号量、消息队列、共享内存等。同一IPC命名空间的进程可以交互;否则不行。PID命名空间和IPC命名空间可以组合使用。挂载命名空间
挂载命名空间可以将一个进程放到一个特定的目录执行,且允许不同命名空间的进程看到的文件结构不同,将各个命名空间中的进程看到的文件目录隔离。UTS命名空间
UTS(UNIX Time-sharing System)命名空间可以另每个容器拥有独立的主机名和域名,从而虚拟出一个拥有独立主机名和独立网络空间的环境。默认情况下,Docker容器的主机名就是容器的ID。用户命名空间
每个容器拥有不同的用户和组ID,容器可以使用自身内部的特定用户执行程序,而非宿主机系统上存在的用户。每个容器内部都可以有root账号,且跟宿主机不在同一命名空间。
控制组
控制组(CGroup)用于对共享资源进行隔离、限制、审计等。控制分配到容器的资源可避免多个容器同时运行时造成的系统资源竞争。控制组的目标:为不同应用情况提供统一的接口,从控制单一进程到系统级虚拟化。控制组具备以下功能:
资源限制(Resource Limiting)
组可以设置为不超过设定的内存限制;优先级(Priority)
可以让一些组优先得到更多的CPU等资源;资源审计(Accounting)
可以使用cpuacct子系统统计某个进程使用的CPU时间;隔离(Isolation)
为组隔离命名空间(进程、网络、文件系统等的隔离);控制(Control)
挂起、恢复、重启等操作;
/sys/fs/cgroup/memory/docker
目录下可以看到对Docker应用的各种限制项:
$ ls /sys/fs/cgroup/memory/docker
#返回结果如下:
0ad2418d6f1bcf17fe5e11071f5b7d538beb9be09847df3f18c596b72258a238 dfbfc284e5a70d79262fede4137b596d0016d9ea2f4f3d28c45cfb284bfd6a54 memory.max_usage_in_bytes
138563cf074e3652c29d3785b618966c3da2db32522f4010bd1148128bbbd10e ff9af226242bd90e6b05e92e5453ce7fc879f4a425276534701c1b091b027444 memory.move_charge_at_immigrate
36cc81e8c6fee35e553eeecdc179a06e38bac49a3a82fd2147d6390309c5833a memory.failcnt memory.numa_stat
3740d5bede34056a53bdce5ba7f18756457262397eaea079b571423a98d61553 memory.force_empty memory.oom_control
3e25c30117030f7f8cbedd08bb9311457fb4cdb64300d76aa0ba7a0cda4f3e21 memory.kmem.failcnt memory.pressure_level
40af5268aeeeb4637283f39e2d22dd34ec7ea73704f2e05a4705890148c72158 memory.kmem.limit_in_bytes memory.soft_limit_in_bytes
59a96d5e5bb0dccf983f2e0a6d05c119af4d053abef11ff336c8d978064061eb memory.kmem.max_usage_in_bytes memory.stat
8129411947b0713d47a2e05304e6ec3a5ec6eacf5c4ce142e3d867f387fde531 memory.kmem.slabinfo memory.swappiness
be000c273f97427f7205cdaf6735f145486b584366b8460a83b6b6cba44af248 memory.kmem.tcp.failcnt memory.usage_in_bytes
cgroup.clone_children memory.kmem.tcp.limit_in_bytes memory.use_hierarchy
cgroup.event_control memory.kmem.tcp.max_usage_in_bytes notify_on_release
cgroup.procs memory.kmem.tcp.usage_in_bytes tasks
d0767e3dc5ca913816ed1f9b6c60fb0da8c27cfae0c5f168e0ee40b185b9c831 memory.kmem.usage_in_bytes
d54ea29dd70b792ba6aa58df056eb156408f3cc96b284d33c04c32cecc83d341 memory.limit_in_bytes
通过修改这些文件值来限制Docker占用的资源。如限制Docker组中的所有进程使用的物理内存总量不超过100MB:
$ sudo echo 104857600 >/sys/fs/cgroup/memory/docker/memory.limit_in_bytes
查看对应的容器文件夹的内容,可以看到对应容器的一些状态:
$ cd /sys/fs/cgroup/memory/docker/d54ea29dd70b792ba6aa58df056eb156408f3cc96b284d33c04c32cecc83d341
$ cat memory.stat
#返回结果如下:
cache 31596544
rss 103755776
rss_huge 69206016
mapped_file 19787776
writeback 0
pgpgin 34059
pgpgout 19921
pgfault 40135
pgmajfault 346
inactive_anon 45056
active_anon 103804928
inactive_file 22183936
active_file 9318400
unevictable 0
hierarchical_memory_limit 18446744073709551615
total_cache 31596544
total_rss 103755776
total_rss_huge 69206016
total_mapped_file 19787776
total_writeback 0
total_pgpgin 34059
total_pgpgout 19921
total_pgfault 40135
total_pgmajfault 346
total_inactive_anon 45056
total_active_anon 103804928
total_inactive_file 22183936
total_active_file 9318400
total_unevictable 0
在容器工具的开发过程中,往往需要查看一些容器运行的状态数据,此时就可以从这里获取更多的信息。另外,也可以在创建或启动容器的时候为每个容器指定资源的限制。可以通过docker run --help
来查看帮助信息。也可以参考“Docker(1.11.1)命令”。
联合文件系统
联合文件系统(UFS)是一种轻量级的高性能分层文件系统,支持将文件系统中的修改信息作为一次提交,并层层叠加,同时可以将不同目录挂载到同一虚拟文件系统下。UFS是实现Docker镜像的技术基础,镜像可以通过分层来进行继承。例如,用户基于基础镜像(没有父镜像的镜像被称为基础镜像)来构建各种不同用途的镜像,而这些镜像共享同一个基础镜像,提高了存储效率。而当用户改变了一个Docker镜像(如升级程序,添加/修改文件等),则一个新的的镜像层(layer)会被创建。因此,新镜像只是在原有的镜像层上添加新的镜像层即可,而不需删除或替换。在分发镜像的时候,也只需分发新增的镜像层。这让Docker的镜像管理变得十分轻量与迅速。
Docker中使用的AUFS(Another Union File System 或 v2版本以后的 Advanced Multi-layered Unification File System)就是一种UFS。AUFS支持位每一个成员目录设定只读、读写、写出权限。同时,AUFS有一个类似分层的概念,对只读权限的分支可以逻辑上进行增量的修改而不影响只读部分。
Docker利用镜像启动容器时,将利用镜像分配文件系统并且挂载一个新的可读写的层给容器,容器会在该文件系统中创建,并且这个可读写的层会被添加到镜像中。
Docker目前支持的联合文件系统类型包括:AUFS、btrfs、vfs和DeviceMapper等。
Docker网络实现
Docker网络的实现其实是利用Linux上的网络命名空间和虚拟网络设备(特别是veth pair)。
基本原理
要想实现网络通信,机器至少需要一个网络接口(物理接口或虚拟接口)与外界相通,并可以收发数据包;另外,如果不同子网之间要进行通信,则需要额外的路由机制。Docker的网络接口默认都是虚拟接口。虚拟接口的最大优势就是转发效率极高!之所以会这样,那是因为Linux通过在内核中进行数据复制来实现虚拟接口间的数据转发,即直接复制发送接口的发送缓存中的数据包到接收接口的接收缓存中,而无需通过外部物理网络设备进行交换。对于本地系统和容器内系统来看,虚拟接口和一个正常的以太网卡相比并无区别,只是虚拟接口的速度要快得多。-
网络创建过程
创建一对虚拟接口,分别放到宿主机和容器的命名空间中;
宿主机一端的虚拟接口连接到默认的docker0网桥或指定网桥上,并具有一个以veth开头的唯一的名字;
容器一端的虚拟接口将被放到容器中,并修改名称为eth0,且这个接口只对该容器的命名空间可见;
从网桥可用地址段中获取一个空闲的地址分配给容器的eth0(如
172.17.0.2/16),并配置默认路由网关为docker0网卡的内部接口docker0的IP地址(如 172.17.42.1/16);完成以上这些,容器就可以使用自身可见的eth0虚拟网卡来连接其他容器和访问外部网络。另外,可以在容器创建启动时通过
--net
参数来指定容器的网络配置,请参考“Docker网络”。
网络配置细节
使用--net=none
运行容器后,Docker将不对容器网络进行配置。接下来将演示手动配置网络。
启动一个/bin/bash
容器,指定--net=none
参数:
$ sudo docker run -i -t --rm --net=none ubuntu /bin/bash
在宿主机中查找容器的进程ID,并为容器创建网络命名空间:
$ sudo docker inspect -f '{{.State.Pid}}' ContainerID
#返回结果(一串数字):pidnum
$ pid=pidnum
$ sudo mkdir -p /var/run/netns
$ sudo ln -s /proc/$pid/ns/net /var/run/netns/$pid
检查桥接网卡的IP和子网掩码信息:
$ ip addr show 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
命令在指定网络命名空间中进行配置,从而影响容器内的网络。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。