Docker容器本质上是宿主机上的进程。Docker通过nampespace实现了资源隔离,通过cgroups实现了资源限制,通过写时复制机制(copy-on-write)实现了高效的文件操作。
namespace6项隔离表
namespace | 系统调用参数 | 隔离内容 |
---|---|---|
UTS | CLONE_NEWUTS | 主机名与域名 |
IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 |
PID | CLONE_NEWPID | 进程编号 |
Network | CLONE_NEWNET | 网络设备、网络栈、端口等 |
Mount | CLONE_NEWNS | 挂载点(文件系统) |
User | CLONE_NEWUSER | 用户和用户组 |
cgroups是Linux内核提供的一种机制,这种机制可以根据需求把一系列系统任务及其子任务整合(或分割)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架;
通俗地来讲,cgroups可以限制、记录任务组所使用的物理资源(包括CPU、memory、IO等),为容器实现虚拟化提供了基本保证,是构建docker等一系列虚拟化管理工具的基石;
实现cgroups的主要目的是为不同用户层面的资源管理,提供一个统一化的接口,从单个任务的资源控制到操作系统层面的虚拟化,cgroups提供了一下四大功能。
cgroups的作用
在2013年Docker刚发布的时候,它是一款基于LXC的开源容器管理引擎。把LXC复杂的容器创建与使用方式简化为Docker自己的一套命令体系。随着Docker的不断发展,它开始有了更为远大的目标,那就是反向定义容器的实现标准,将底层实现都抽象化到Libcontainer的接口。这就意味着,底层容器的实现方式变成了一种可变的方案,无论是使用namespace、cgroups技术抑或是使用systemd等其他方案,只要实现了Libcontainer定义的一组接口,Docker都可以运行。这也为Docker实现全面的跨平台带来了可能。
licontainer特性
目前版本的Libcontainer,功能实现上涵盖了包括namespaces使用、cgroups管理、Rootfs的配置启动、默认的Linux capability权限集、以及进程运行的环境变量配置。内核版本最低要求为2.6,最好是3.8,这与内核对namespace的支持有关。
Docker通过driver模块来实现对Docker容器执行环境的定制。当需要创建Docker容器时,可以从Docker registry中下载镜像,并通过镜像管理驱动graphdriver将下载的镜像以graph的形式存储在本地;当需要为Docker容器创建网络环境时,则通过网络管理驱动networkdriver创建并配置Docker容器的网络环境;当需要限制Docker容器运行资源或执行用户指令等操作时,则通过execdriver来完成。libcontainer是一个独立的容器管理包,networkdriver和execdriver都通过libcontainer来实现对容器的具体操作,包括利用UTS、IPC、PID、Network、Mount、User等namespace实现容器之间的资源隔离和利用cgroup实现对容器的资源限制。当运行容器的命令执行完毕后,一个实际的容器就处于运行状态,该容器拥有独立的文件系统、安全且相互隔离的运行环境。
Docker架构总览
Docker doemon是Docker最核心的后台进程,它负责响应来自Docker Client的请求,然后将这些请求翻译成系统调用完成容器管理操作。该进程会在后台启动一个API Server,负责接收有Docker Client发送的请求;接收到的请求将通过Docker daemon内部的一个路由分发调度,再由具体的函数来执行请求;
Docker Client是一个泛称,用来向指定的Docker daemon发起请求,执行相应的容器管理操作。它既可以是docker命令行工具,也可以是任何遵循了Docker API的客户端,包括C#、Java、Go、Ruby、Javascript等;
graph组件负责维护已下载的镜像信息及它们之间的关系,所以大部分Docker镜像相关的操作都会由graph组件来完成。graph通过镜像“层”和每层的元数据来记录这些镜像的信息,用户发起的镜像管理操作最终都转换成了graph对这些层和元数据的操作。但正是由于这个原因,而且很多时候Docker操作都需要加载当前Docker daemon维护着的所有镜像信息,graph组件常常会成为性能瓶颈。
Docker daemon通过GraphDB记录它所维护的所有容器(节点)以及它们之间的link关系(边),这也就是为什么这里采用了一个图结构来保存这些数据。具体来说,GraphDB就是一个基于SQLite的最简单版本的图形数据库,能够为调用者提供节点增、删、遍历、连接、所有的父子节点的查询等操作。这些节点对应的就是一个容器,而节点间的边就是一个Docker link关系。每创建一个容器,Docker daemon都会在GraphDB里添加一个节点,而当我们为某个容器设置了link操作后,在GraphDB中就会为它创建一个父子关系,即一条边。显然,虽然名字容易混淆,但是GraphDB与前面提到的负责镜像操作的graph组件没有多大关系。
前面提到,Docker daemon负责将用户请求转译成系统调用,进而创建和管理容器的核心进程。而在具体实现过程中,为了将这些系统调用抽象成为统一的操作接口,方便调用者使用,Docker把这些操作分类成容器管理驱动、网络管理驱动、文件存储驱动3种,分别对应execdriver、networkdriver和graphdriver。
execdriver是对Linux操作系统的namespace、cgroups、apparmor、SELinux等容器运行所需的系统操作进行的一层二次封装,其本质作用类似LXC,但是功能更全面。这也是为什么LXC会作为execdriver的一种实现而存在。当然,execdriver最主要的实现也是现在的默认实现是Docker官方编写的libcontainer库。
networkdriver是对容器网络环境操作所进行的封装。对于容器来说,网络设备的配置相对比较独立,并且应该允许用户进行更多的配置,所以在Docker中,这一部分是单独作为一个driver来设计和实现的。这些操作具体包括创建容器通信所需的网络,容器的network namespace,这个网络所需的虚拟网卡,分配通信所需的IP,服务访问的端口和容器与宿主机之间的端口映射,设置hosts、resolv.conf、iptables等。
graphdriver是所有与容器镜像相关操作的最终执行者。graphdriver会在Docker工作目录下维护一组与镜像层对应的目录,并记下容器和镜像之间关系等元数据。这样,用户对镜像的操作最终会被映射成对这些目录文件以及元数据的增删改查,从而屏蔽掉不同文件存储实现对于上层调用者的影响。目前Docker已经支持的文件存储实现包括aufs、btrfs、devicemapper、overlay和vfs。
yum -y remove docker
yum -y install docker-io
service docker start //会启动docker守护进程,或者docker daemon &
docker [OPTIONS] COMMAND [arg…]
OPTIONS为-d时,Docker就会创建一个运行在宿主机的daemon进程,否则,按照用户声明的COMMAND向指定的Docker daemon发送对应的请求。
使用libcontainer可以快速构建起应用的运行环境,但是当需要进行容器迁移、对容器的运行环境进行全盘打包时,libcontainer就束手无策了。Docker镜像技术用来解决此问题,作为Docker管理文件系统以及运行环境的强有力补充。
Docker的镜像是由一系列的只读层组合而来的,当启动一个容器时,Docker加载镜像的所有只读层,并在最上层加入一个读写层。这个设计使得Docker可以提高镜像构建、存储和分发的效率,节省了时间和存储空间。
Docker镜像含有启动Docker容器所需的文件系统结构及其内容,因此是启动一个Docker容器的基础。Docker镜像采用分层的结构构建,最底层是bootfs,之上的部分为rootfs。
bootfs是Docker镜像最底层的引导文件系统,包括bootloader和操作系统内核,类似于传统的Linux/Unix引导文件系统。然而,Docker用户很少有机会直接与bootfs打交道,并且,在容器启动完毕之后,为了节省内存空间,bootfs将会被卸载。
rootfs位于bootfs之上,是Docker容器在启动时内部进程可见的文件系统,即Docker容器的根目录。rootfs通常包含一个操作系统运行所需的文件系统,例如可能包含典型的类Unix操作系统中的目录系统,如/dev、/proc、/bin、/etc、/lib、/usr、/tmp及运行Docker容器所需的配置文件、工具等。
存储驱动根据操作系统底层的支持提供了针对某种文件系统的初始化操作以及对镜像层的增、删、改、查和差异比较等操作。目前存储系统的接口已经有aufs、btrfs、device mapper、vfs、overlay这5种具体实现,其中vfs不支持写时复制,是为使用volume提供的存储驱动,仅仅做了简单的文件挂载操作;
aufs是一种支持联合挂载的文件系统,简单来说就是支持将不同目录挂载到同一个目录下,这些挂载操作对用户来说是透明的,用户在操作该目录时不会觉得与其他目录有什么不同。这些目录的挂载是分层次的,通常来说最上层是可读写层,下层是只读层。所以,aufs的每一层都是一个普通文件系统。
Linux2.6内核中提供的一种从逻辑设备到物理设备的映射框架机制,在该机制下,用户可以很方便地根据自己的需要制定实现存储资源的管理策略。
Device Mapper包含3个概念:映射设备、映射表、目标设备;映射设备是内核向外提供的逻辑设备。一个映射设备通过一个映射表与多个目标设备映射起来,映射表包含了多个多元组,每个多元组记录了这个映射设备的起始地址、范围与一个目标设备的地址偏移量的映射关系。目标设备可以是一个屋里设备,也可以是一个映射设备,这个映射设备可以继续向下迭代。一个映射设备最终通过一棵映射树映射到屋里设备上。Device Mapper的本质功能是根据映射关系描述IO处理规则,当映射设备接收到IO请求时,这个IO请求会根据映射表逐级转发,直到这个请求最终传到最底层的物理设备上。
写入时复制(Copy-on-write)
是一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。
第一代Unix系统实现了一种傻瓜式的进程创建:当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种行为是非常耗时的,因为它需要:
- 为子进程的页表分配页帧
- 为子进程的页分配页帧
- 初始化子进程的页表
- 把父进程的页复制到子进程相应的页中这种创建地址空间的方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容。在大多数情况下,这样做常常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。
现在的Linux内核采用一种更为有效的方法,称之为写时复制(Copy On Write,COW)。这种思想相当简单:父进程和子进程共享页帧而不是复制页帧。然而,只要页帧被共享,它们就不能被修改,即页帧被保护。无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这时内核就把这个页复制到一个新的页帧中并标记为可写。原来的页帧仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。
相对数据存储而言,Docker镜像存在的问题:
为了解决上述问题,Docker引入了数据卷机制;volume的生存周期独立于容器的生存周期,即使删除容器,volume仍然会存在,没有任何使用的volume也不会被删除;本质上市容器中一个特殊的目录(挂载点);
veth类似于交换机上的端口,可以将多个容器或虚拟机链接在其上,这些veth位于第二层,所以不需要配置IP信息;(网桥属于二层设备,不应该有IP的)但docker0是普通的linux网桥,它是可以在上面配置IP的,可以认为其内部有一个可以用于配置IP信息的网卡接口。在Docker的桥接网络模式中,docker0的IP地址作为连于之上的容器的默认网关地址存在;
bridge模式
Docker默认的网络模式,将创建出来的docker容器链接到Docker网桥上(docker0或其他自定义网桥):
host模式
这种模式Docker Server将不为Docker容器创建网络协议栈,即不会创建独立的network namespace,Docker容器中的进程处于宿主机的网络环境中,相当于Docker容器和宿主机共用同一个network namespace,使用宿主机的网卡、IP和端口等信息;
container模式
新创建的容器和已经存在的某个容器共享同一个network namespace;
none模式
Docker容器拥有自己的network namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器除了network namespace自带的loopback网卡外没有其他任何网卡、IP、路由等信息,需要用户为Docker容器添加网卡、配置IP等;
Dockerfile十一个文本格式的配置文件,用户可以使用Dockerfile快速创建自定义的镜像。
一般而言Dockerfile分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令和容器启动时执行命令。
# 注释
# this dockerfile uses the ubuntu image
# VERSION 2 - EDITION 1
# Author: docker_user
# Command format: Instruction [arguments / command] ...
# 第一行必须制定基于的基础镜像
FROM ubuntu
# 维护者信息
MAINTAINER userName [email protected]
# 镜像的操作命令
RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf
# 容器启动时执行的命令
CMD /usr/sbin/nginx
RUN指令是将对镜像执行跟随的命令,每运行一条RUN指令,镜像添加新的一层,并提交;最后CMD指令,用来制定运行容器时的操作命令。
编写完成Dockerfile之后,可以通过docker build命令来创建镜像。
格式:docker build [选项] 路径
该命令将读取指定路径下(包括子目录)的Dockerfile,并将该路径下所有内容发送给Docker服务端,由服务端来创建镜像。因此一般建议放置Dockerfile的目录为空目录。
要指定镜像的标签信息,可以通过-t选项。
docker build -t build_repo/first_image /tmp/docker_builder/
指定Dockerfile所在路径为/tmp/docker_builder/,并且希望生成镜像标签为build_repo/first_image
官方文档
子命令分类 | 子命令 |
---|---|
Docker环境信息 | info、version |
容器生命周期管理 | create、exec、kill、pause、restart、rm、run、start、stop、unpause |
镜像仓库命令 | login、logout、pull、push、search |
镜像管理 | build、images、import、load、rmi、save、tag、commit |
容器运维操作 | attach、export、inspect、port、ps、rename、stats、top、wait、cp、diff |
系统日志信息 | events、history、logs |
获取镜像
docker pull name:tag
docker pull centos:7.2
docker pull centos //默认选择latest版本
查看镜像信息
docker iamges //列出本地镜像
docker inspect imageId //查看镜像详细信息
docker tag centos:latest centos:7.2 //给镜像打新的标签centos:latest
搜索镜像
docker search mysql
删除镜像
docker rmi imageName/imageId
当同一个镜像拥有多个标签时,只删除了指定的标签,不影响镜像文件;
本地有容器关联时,不能删除镜像;
创建镜像
基于已有镜像的容器创建
docker commit -a 作者信息 -m 提交信息 -p 提交时暂停容器运行 containerId repositoryName //返回新的镜像ID
基于本地模板导入
…
存出镜像
docker save -o centos7.tar centos:latest
载入镜像
docker load -i centos7.tar
上传镜像
docker push NAME[:TAG] //默认上传到DockerHub
创建容器
docker create -it centos:latest
docker start containerId //启动容器
创建并启动
docker run -it centos:latest /bin/bash
守护状态运行
docker run -d centos:latest /bin/bash
查看本机的所有容器
docker ps -a/-l
获取容器的输出
docker logs containerId
终止/启动/重启容器
docker stop/start/restart containerId
进入容器
docker exec -it containerId /bin/bash
删除容器
docker rm containerId
导出容器
docker export -o centos.tar containerId
导入容器,生成镜像
docker import centos.tar centos:latest
导入容器快照将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态);而镜像存储文件将保存完整记录,体积也要大,此外,从容器快照文件导入时可以重新制定标签等元数据信息;
DockerHub
挂载一个主机目录作为数据卷
docker run -it -d –name web -v /src/webapp:/opt/webapp volumeName centos:latest /bin/bash
读写权限:rw/ro
docker run -it -d –name web -v /src/webapp:/opt/webapp:rw volumeName centos:latest /bin/bash
数据卷容器
创建数据卷容器
docker run -it -v /dbdata –name dbdata centos:latest
–volumes-from挂载dbdata容器
docker run -it –volumes-from dbdata –name db1 centos:latest
docker run -it –volumes-from dbdata –name db2 centos:latest
如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时显式使用docker rm -v命令来指定同时删除关联的容器;
备份
docker run –volumes-from dbdata -v $(pwd):/backup –name worker centos
tar cvf /backup/backup.tar /dbdata
首先用centos镜像创建了一个容器worker,使用–volumes-from dbdata参数来让worker容器挂载dbdata容器的数据卷(即dbdata数据卷);
使用-v $(pwd):/backup参数来挂载本地的当前目录到worker容器的/backup目录;
worker容器启动后,使用了tar cvf /backup/backup.tar /dbdata命令来将/dbdata下内容备份为容器内的/backup/backup.tar,即宿主主机当前目录下的backup.tar;
恢复
docker run -v /dbdata –name dbdata2 centos /bin/bash
首先创建一个带有数据卷的容器dbdata2,然后创建另一个新的容器,挂载dbdata2的容器,并使用untar解压备份文件到所挂载的容器卷中即可;
docker run –volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf /backup/backup.tar
端口映射
docker run -d -p 5000:5000 -p 3000:80 training/webapp /bin/bash
查看端口映射配置
docker port containerName 5000
容器互联
docker run -d –name db centos:latest
docker run -d -P –name web –link db:linkName centos:latest /bin/bash
Ambassador容器
…