Docker提供的命令也非常多,笔者在此以镜像的完整生命周期为顺序来介绍其基本使用。
1.安装与运行
□安装
Docker的主程序叫做Docker Engine,由于其实现是基于Linux系统,所以它在不同的操作系统中也对应有不同的安装方法。
首先Docker可以运行在绝大多数平台的Linux发行版中,可以通过Redhat系列(RHEL、CentOS、Fedora等)、Ubuntu、Gentoo、Arch等系统的包管理器直接安装。比如在Ubuntu 14.04中直接运行“apt-get install docker.io”即可。通过包管理器安装的docker版本并不保证是最新的,我们也可以通过以下命令安装最新版本。
Docker对系统并没有太多要求,但比较重要的是我们需要确定Linux内核版本在3.10以上。另外,Redhat系列的系统一定也开启了SELinux,对于普通用户而言我们需要将SELinux设置为“permissive”,否则会遇到很多Permission Denied的错误;对于要启用SELinux的“enforcing”的用户,可以参考https://www.mankier.com/8/docker_selinux进行相关设置。
由于docker是一个会请求系统特权的程序,默认情况下普通用户需要使用sudo或者以root用户运行它,还有一种做法是将当前普通用户添加至“docker”用户组中,命令形似“usermod-aG docker”,操作后可能需要再次登录当前用户。
若在Windows或OSX系统中使用docker,我们仍然需要借助于虚拟化(本书成文时微软已经在Windows Server 2016以及Win 10中支持了native docker daemon),即在系统中启动虚拟机,通过本地docker客户端连接至虚拟机中的daemon。
我们可以手动搭建运行docker daemon的虚拟机,然后在Windows、OS X或者虚拟机中运行docker client。在此笔者推荐使用Docker Toolbox(https://www.docker.com/toolbox),它内置了docker daemon需要的最小运行环境虚拟机(boot2docker)以及各种官方工具(Compose、Engine、Machine、Kitematic等)。
使用Docker Toolbox时,我们需要注意虚拟机的IP设置,因为容器实例是运行于虚拟机boot2docker中,这点对后面要讲的端口映射很重要。
□基本使用
使用Docker前我们首先要确定docker daemon已经运行,可在主机中使用命令“service docker status”查看(启动之前可在主机目录中修改daemon配置,Debian/Ubuntu一般为/etc/default/docker,使用systemd的系统为/etc/sysconfig/docker)。确定以后,在控制台中输入“docker run Ubuntu echo hello world”(本书使用的系统为Ubuntu 14.04 Desktop,其他系统中Docker行为根据版本可能有些许差异)。
这个过程中,Docker会首先尝试从本地寻找镜像,未果即从默认的Docker Hub里下载到本地(由于未指定版本,默认下载Ubuntu的latest),然后执行命令“echo hello world”,运行结果输出到我们的标准输出终端。由于我们指定了“--rm”,容器在退出后会被自动删除。需要读者注意的是,Docker提供的命令行操作(API中也是如此)同git一样,我们不需要输入完整的镜像或者容器id即可对其进行相应操作。
我们也可以使用交互式选项,“-i”和“-t”会指定使用交互式的会话输入并且将输出其绑定至tty,如下所示。
Docker镜像的属性有repository、tag、image id等,repository可理解为此镜像的主分支,tag可理解为此镜像的分支版本,如下所示。
Docker容器的属性有container id、image、command、ports、names等,其中image即表示此容器的源镜像,commands是容器中此时运行的命令,ports是命令的于主机上的监听端口,names表示此容器的随机别名,如下所示。
□常用命令
Docker的命令格式形如“docker command[options...]object[args...]”,查询具体的命令用法只要在命令后添加“--help”选项即可,具体的命令用法参考https://docs.docker.com/engine/reference/commandline/。针对不同的操作对象,docker的命令可以分为以下几类:
●镜像
对镜像的操作有:构建(build)、列举(images)、从容器存档导入(import)、历史变化(history)、查看详细信息(inspect)、保存至文件(save)、从文件导入(load)、删除(rmi)、(运行run)、打标签(tag)。
其中入门读者容易混淆的可能是import与load命令的差别,前者的导入对象是容器存档命令(export)产生的存档文件,其操作对象为容器实例,而后者的操作对象仅限于镜像,即对此镜像进行打包与导入;build命令将根据Dockerfile创建新的镜像,我们将在接下来的小节中进行详细说明;history则用于查看镜像历史变化;tag标签对于自建registry非常有用。
●容器
对容器操作有:连接终端(attach)、创建(create)、拷贝文件(cp)、查看变化(diff)、提交(commit)、执行新命令(exec)、容器存档(export)、查看详细信息(inspect)、终止(kill)、查看输出日志(logs)、查看端口映射(port)、查看运行容器(ps)、重命名(rename)、重启(restart)、删除(rm)、启动(start)、停止(stop)、实时资源用度(stats)、查看运行进程(top)、暂停(pause)、继续(unpause)、更新资源限制(update)、等待退出(wait)。
create命令将根据参数生产一个容器实例,然后通过start启动;exec与run的差别在于前者将在已经运行的实例中运行命令,后者将会创建一个新的实例去运行命令;如果提交时指定的repository和tag已经存在,Docker会自动将之镜像的repository和tag设置为none;stop与pause的区别在于前者会终止实例,服务状态同时终结,而后者会将这个实例进入挂起状态,服务状态被保存,可通过unpause命令继续运行。
●Registry
关于registry的操作有:登录(login)、注销(logout)、搜索(search)、获取(pull)、上传(push)。
其中login与logout的账户默认是Docker Hub的账户,pull与search将会搜索Docker Hub中的全部开放镜像,push默认向用户的Docker Hub专属空间中上传镜像。
●网络
网络命令(network)的子命令有连接(connect)、断开(disconnect)、创建(create)、删除(rm)、列举(ls)、查看相信信息(inspect),它们主要是对网络服务和具体接口进行操作。
●存储
存储卷命令(volume)的子命令有创建(create)、删除(rm)、列举(ls)、查看详细信息(inspect),我们同样将在下面章节中结合各种存储后端进行介绍。
●其他
Daemon服务事件(event)、查看版本(version)。event命令可查看的事件种类有镜像、容器、存储、网络的状态变化,并且可指定时间段与输出内容;版本则会显示Docker客户端与服务端的版本号以及构建信息等。
2.存储模型
Docker的存储主要包括两部分内容,一是数据卷,用于容器实例与主机交互数据,二是存储卷,用于保存容器实例的镜像文件。我们首先看一下存储卷、数据卷、镜像、容器的存储表示与其之间的关系,然后分别对数据卷(data volume)的操作和存储驱动(storage driver)的选择进行说明。
□存储基础
●存储分层
Docker中每一个镜像都是由一系列只读的不同文件系统层组成,它们叠加在一起表示一个完整的根文件系统镜像,图5-2是Ubuntu 15.04镜像的不同文件系统层。这里需要注意的是,我们从Docker Hub获取的镜像,其层数并不是固定的,而是随着镜像系统更新(提交)次数而递增。
图5-2 Ubuntu 15.04的Docker镜像文件系统层
这些文件系统层由存储卷封装以提供统一的文件视图。用户创建新的容器实例时,会在这些只读镜像层(image layers)上添加一层Thin Provision的容器层(container layer),如图5-3所示。所有对容器的读写操作都会体现在容器层,比如新建、修改、删除文件。
图5-3 Ubuntu 15.04的Docker镜像层与容器层
可以看到,镜像的每一层都有一段标识,在较旧的版本中它们是随机ID,而在1.10版本以后它们则是每一层的内容哈希,这点变化是出于registry合法性校验的安全考虑。至于容器层仍然是随机ID。
相关链接
关于docker升级
Docker 1.10版本于2016年初刚刚发布并且改变了镜像的标识算法,而很多企业在这之前就已经部署了大量docker实例并且已经放入生产环境。虽然更新Docker版本以后镜像的升级是静默自动完成,但期间daemon会消耗大量CPU资源进行校验而影响容器性能。为此,docker提供了工具以方便用户在空闲时升级镜像,参考https://github.com/docker/v1.10-migrator/releases。
如果以“docker upgrade”为关键字进行搜索的话,会发现类似的问题普遍存在。我们很难保证版本的平滑升级,所以建议读者根据业务需求选择是否升级。
容器与镜像两种存储模型的最大区别是前者是在镜像基础上的可写层,如果容器被删除,即表示镜像之上与这个容器相关联的可写层被删除,但是镜像不会发生变化。正因为这点特性,镜像才可以被多个容器共用,容器的可写层之间相互隔离,如图5-4所示。
图5-4 多个容器共用镜像
●写时复制(Copy-On-Write)
存储驱动负责管理容器的可写层与镜像只读层,具体的实现根据驱动的不同而有所差异。Docker的存储模型中除了引入堆栈式的分层存储外,它还使用了另一个常用存储策略——写时复制(Copy-On-Write)。
为了方便读者理解写时复制技术,笔者使用如下过程进行介绍。有小红和小强两位同学,在不同的班级由不同的数学教师教课,但是他们两人只有一本习题册。教师布置了一些课后作业,比如小红的教师布置的是习题册第10、11页的所有题目,小强老师布置的是第8、10页的所有题目。由于习题册只有一本,小红和小强不能将答案写到习题册上,所以小红将习题册的第10、11页抄写了一份再填写答案,小强也从习题册上抄写了第8、10两页填写答案。这样一来,习题册既没有被涂改,两人的作业也被各自完成。我们将小红和小强视为系统进程,不同班级不同教师可视为不同的进程寻址空间,习题册视为两进程之间的共享数据,抄写题目即是“复制”、填写答案即是“写”,这个过程就类似写时复制技术的实现了。
Docker通过写时复制技术可以节约镜像的空间占用,又可以一定程度上提高容器实例性能。在Linux系统中,docker镜像通常被保存在/var/lib/docker/中,具体位置根据使用的存储驱动不同而不同。比如下面以Device Mapper作为存储驱动,获取Ubuntu镜像并查看系统目录内容。
我们再根据这个镜像创建一个新的镜像,查看目录变化。
通过对比我们即可发现,新建的镜像Ubuntu:test仅在imagedb与metadata中多了两条记录,并且新镜像(73e4)的“父”镜像指向之前的Ubuntu:15.04(d1b5),如图5-5所示。
图5-5 镜像文件系统层共享
这种镜像共享技术让docker的镜像存储空间得以节约,接下来我们看一下写时复制策略是如何让容器更加高效的。
从前文我们已经得知所有的容器都被格子保存在一个ThinProvisioning的容器层中,其依赖的镜像层属于不可改变的只读内容,因而多个容器实例可以共享一个镜像。当容器中的文件被改变时,Docker就用利用存储驱动的特性进行写时复制操作。对于AUFS和OverlayFS来说,写时复制的过程与前文的举例相似,而对于Btrfs、ZFS以及其他的存储驱动,写时复制的实现细节则略有不同。
□数据卷与存储驱动
容器实例新建或者修改的文件越多,其可写层占用的空间也就越大,如果要写入大量文件的话最好将其写入至数据卷中。另外,写时复制发生时也会降低主机系统性能。接下来,我们便针对数据卷和存储驱动分别进行介绍。
●数据卷
为了解决容器存储大量内容的问题,Docker引入了数据卷,基本存储架构如图5-6所示。除了使用最基本的数据卷外,我们也可以将数据卷通过特定的容器暴露出来为其他容器所用。
图5-6 基本数据卷挂载示意
默认的数据卷形式是UFS(Union File System)格式的文件,它具有以下特性:容器创建时数据卷则被立即加载,如果容器中含有指定存放至数据卷的文件,这些文件就会在初始化时被docker daemon从原始镜像拷贝至数据卷中;数据卷可以在容器之间共享、复用;数据卷的内容变化是即时生效的,即容器之间可以实时看到文件的变化;数据卷不会反映到容器更新操作中(commit),即容器保存为镜像时不会保留数据卷信息,从保存的镜像启动容器不会自动挂载之前的数据卷;数据卷相对独立,不会随着容器的删除而被删除,它也不会回收容器被删除后留下的“垃圾”文件。
数据卷也可以是其他形式,比如客户端目录、客户端文件。我们先以默认的UFS数据卷为例。
可以看到,容器5b326d232d03挂载了数据卷4afe078e5到内部的/volume。如果此时向主机上的数据卷路径中写入内容,容器中也会立即发现它,如下所示。
如果另一个容器想复用这个容器的卷,只需要添加“--volumes-from容器名”即可,如下所示。
根据数据卷的复用特性,我们可以选择特定的容器作为数据卷容器,以给其他机器统一提供私有或者共享数据卷。(www.xing528.com)
接下来我们使用客户端目录作为数据卷,这里目录可以是Linux、Windows、OSX的路径,比如“/opt/docker-vol”、“C:\Users\demo\docker-vol”、“/Users/demo/docker-vol”。另外,在SELinux标志的帮助下,我们可以在挂载的数据卷后添加“-Z”或者“-z”以表示此卷是容器所独占或是一个共享卷,默认为共享卷。挂载客户端目录的相关操作如下所示。
挂载客户端文件的方法如下所示。
以上就是关于数据卷的大部分操作了,需要读者注意的是,由数据卷可以在多个容器之间共享,那么我们就要小心同时读写文件带来的数据不一致现象,因为docker对容器之间的共享数据卷并不提供锁机制。
●存储驱动
在Docker中,存储驱动用于给容器和镜像文件提供卷管理功能,目前它支持的文件系统包括OverlayFS、AUFS、Btrfs、Device Mapper、VFS(非内核中的VFS)、ZFS。
Docker的存储驱动是属于“插件式”实现,驱动根据各自的文件系统特性管理Docker镜像或者容器文件,以更灵活地适应不同的系统环境。可以通过命令“docker info”查看当前docker daemon的存储驱动。
可以看出,当前的存储驱动为devicemapper,它的底层文件系统是由devicemapper创建的块设备,并且已被格式化为xfs。如果我们关闭docker服务,然后指定不同的存储驱动,就会有如下现象。
存储驱动从Device Mapper更换至OverlayFS以后,之前的容器与镜像不再被识别,并且也暴露出了实际存储文件系统为extfs(笔者实验环境为Ext4)。
如果我们尝试在这个extfs上使用Btrfs作为存储驱动的docker daemon,就会报错,必须在与之对应的Btrfs文件系统上启动才会成功。
通过以上实验我们可以了解到两点,一是某些存储后端必须使用与之一致的文件系统才可启动,比如btrfs和zfs;另一点即是由于更换了存储驱动,之前存储驱动所管理的容器和镜像不一定能继续使用,所以有必要在正式使用Docker之前选择当前环境下性能最优的存储驱动和文件系统组合。
本书以稳定性、经验和文件系统特性的角度对其进行选择:
●稳定性
稳定性是我们首先要考虑的要素。Docker在不同的发行版中都有不同的默认设置,这些设置往往是他们认为稳定性最佳的。在没有长期测试之前,建议使用其推荐配置。另外,Docker公司也推出了一系列的商业服务,其中就包括存储后端与文件系统组合的选择。
●用户经验
如果读者对某种文件系统比较熟悉,可以优先选择与之对应的存储驱动,具体可参考表5-2。
●文件系统特性
表5-2 文件系统特性
3.用户网络
Docker的网络命令虽然简单(create、connect、ls、rm、disconnect、inspect),但是近来它以libnetwork作为主要网络提供商,可选的网络类型更加丰富了,并且本身也支持第三方插件。
Docker的默认用户网络有bridge、OverlayFS两种类型,前者用于单机环境,后者用于多机组网。本节内容将以现有网络为基础,讲解docker网络基本操作,然后针对多机组网方法予以简单介绍。
□基本操作
Bridge是docker中的最基本网络类型,它的参数可分别在docker daemon启动时和创建时设置。接下来笔者以默认参数创建bridge网络为例。
然后在docker客户端使用“--net”选项即可使用此网络。
如果要在网络中添加局域网主机名解析,可以在docker的运行命令添加参数形似“--netalias主机名”以设置此容器的主机名,也可以添加“--link container2:c2”设置映射容器的主机名。
对容器添加网络可以使用“network connect”命令,如下所示。
使用“network disconnect”断开网络。
删除网络时需要所有容器已经与当前网络断开连接。
□多机组网
Docker多机组网是用通道技术实现,比如官方提供的OverlayFS以及第三方的weave、OpenvSwitch(GRE/Geneve/VXLAN/...)、pipeworks、flannel(rudder)等 网 络 插 件(Kubernetes、Mesos的集群管理方案中可使用其单独实现的网络驱动或者是已有的网络插件)。笔者接下来将以第三方插件weave示例如何进行多机组网。
Weave将会在主机中分别创建网桥,每个容器通过“weave run”提供的IP地址和掩码连接到网络,也就是连接到weave容器提供的weave router。Weave router使用pcap工具拦截所有通过它的流量,然后建立自己的端到端路由表进行转发。
在主机192.168.0.30中下载并建立weave router。
启动后,使用如下命令创建IP为10.10.10.1的容器,然后连接到容器终端。
可以看出多了一个ethwe的网络连接。此时在另一个节点192.168.0.31中同样下载weave到可执行目录中,执行命令形似“weave connect 192.168.0.30”连接到weave router,然后启动IP为10.10.10.2的容器。
然后这两个容器便可以互访了。
4.Dockerfile
Dockerfile是包含了一系列自动创建镜像命令的脚本,我们可以使用“docker build”命令创建镜像,它是编排服务Compose的基础之一,某种意义上相当于Makefile。对Linux发行版较为熟悉的读者也可以使用debootstrap或者yum/mkimage命令构建自己的根文件系统,然后将其打包成Docker镜像,具体步骤可参考https://docs.docker.com/engine/userguide/eng-image/baseimages。
Dockerfile主要是由各种命令与环境变量组成,由于篇幅所限笔者只列出各命令的使用注意事项如表5-3,详细格式参考https://docs.docker.com/engine/reference/builder。
表5-3 Dockerfile字段与含义
一般我们在编写Dockerfile时,会尽量减少最终镜像的存储大小。常用的做法有包括添加清理临时文件命令、在主机中编译应用程序再拷贝至镜像、重用镜像,以及更加激进的存储层压缩(docker export container_id|docker import-sample:flat)等。
5.Registry
Registry用于保存和分发镜像,它也是我们使用Docker构建私有云时的一个重要功能特性。Docker Registry有很多的实现方法,比较常用的是使用registry容器,读者也可以使用“pip install docker-registry”安装docker-registry,然后使用gunicorn构建自己的registry服务。接下来笔者以registry容器为例介绍如何组建自己的registry服务。
首先运行registry容器,并将服务端口5000暴露至主机192.168.0.30的5000端口,如下所示。
接下来,将本地镜像打标签后push到registry容器中。
这样便完成了最简单的本地registry操作了。
需要注意的是,上述过程中忽略了docker的证书验证,因为笔者已提前在docker daemon的启动参数中添加了“--insecure-registry 192.168.0.30”,否则会提示拒绝连接的错误。
如果读者需要使用自建证书或者公共可信证书,可以参考如下过程。
读者可能已经察觉到,这样的registry容器被kill掉以后便丢失了所有的镜像,因而我们便需要一个相对长期存储当做registry的镜像目录。
Registry容器的镜像存储路径默认为/var/lib/registry/,我们可以使用数据卷将客户端或者主机目录挂载至其路径下,比如“docker run-d-p-v/docker_registry:/var/lib/registryrestart=always-name registry registry:2”即将本地的/docker_registry目录用作registry镜像存储。
如果我们需要用户登录registry才能使用,可以使用htpasswd作为用户认证。
既然它是一个基于HTTP协议的实现,那么就很容易地进行各种负载均衡的措施,可参考第10章相关内容。
6.Docker API
Docker daemon提供的API是Docker客户端与其进行通信的基础,其整体设计倾向于REST风格,其服务监听形式以TCP端口和UNIX套接字为主。Docker API的访问使用JSON格式,这一点非常有利于它与其他应用的集成,关于JSON的详细参数可参考https://docs.docker.com/engine/reference/api/docker_remote_api/。
Docker API的适用对象不仅在容器与镜像的整个生命周期,还包括数据卷、网络以及docker daemon服务进程本身(比如检查主机、监视事件)。
以运行镜像为Ubuntu:15.04的容器为例,我们需要将其分解为“创建镜像-运行镜像”两个步骤,如下所示。
除了直接使用REST API操作Docker外,我们也可以使用封装了各种API的dockerpy,详细用法请参考http://docker-py.readthedocs.org/,以下操作是删除我们上文创建的容器示例。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。