docker基础学习之镜像篇
docker初探
1. 容器技术原理
2. 为什么使⽤容器
与传统软件⾏业的开发、 运维相⽐,容器虚拟化可以更⾼效地构建应⽤,也更容易管理维护。举个简单的例⼦, 常⻅的LAMP组合开发⺴站,按照传统的做法⾃然是各种安装,然后配置,再然后测试,发布, 中间⿇烦事⼀⼤堆,相信不少同⾏都深有体会。
过了⼀段时间,⽤户体增加,服务器需要搬迁到更合适的机房,往往需要再执⾏⼀次以前的部署步骤, 还包括数据的导出导⼈, 极⼤地花费了运维⼈员的时间。最可怕的是搬迁后因为⼀些不可预知的原因导致软件⽆法正常运⾏,只能⼀头扎进代码中Bug 如果使⽤容器技术,运维只需要⼀个简单的命令即可部署 整套LAMP环境,并且⽆需复杂的配置与测试,即便搬迁也只是打包传输即可,即使在另⼀台机器上,软件也不会出现 "⽔⼟不服" 的情况。这⽆疑节省了运维⼈员的⼤量时间。
⽽对于开发来说,⼀处构建,到处运⾏⼤概是梦寐以求的事情,这也是很多跨平台语⾔的宣传标语之⼀,但是不管是怎样的跨平台语⾔在很多细节上都需要不少调整才能运⾏在另⼀个平台上。但容器技术
则不 样, 开发者可以使⽤熟悉的编程语⾔ 开发软件 ,之后⽤容器技术 打包构建,便可以⼀键运⾏在所有⽀持该容器技术的平台上。
3. 容器原理
容器的核⼼技术是Cgroup与Namespace,在此基础上还有⼀些其他⼯具共同构成容器技术。从本质上来说容器是宿主机上的进程,容器技术通过Namespace实现资源隔离,通过Cgroup 实现资源控制,通过rootfs实现⽂件系统隔离,再加上容器引擎⾃⾝的特性来管理容器的⽣命周期。
简单地说,这⾥所说的Docker的早期其实就相当于LXC的管理引擎,LXC是Cgroup的管理⼯具, Cgroup是Namespace的⽤户空间管理接⼝。Namespace是Linux内核在task_struct中对进程组管理的基础机制。
1. Namespace
想要实现资源隔离,第⼀个想到的就是chroot命令,通过它可以实现⽂件系统隔离,这也是最早的容器技术。但是在分布式的环境下,容器必须要有独⽴的IP、端⼝、路由等,⾃然就有了⽹络隔离。同时,也需要考虑进程通信隔离权限隔离等,因此⼀个容器基本上需要做到6项基本隔离,也就是Linux内核中提供的6种Namespace隔离.
当然,完善的容器技术还需要处理很多⼯作。
对Namespace的操作,主要是通过clone、setns、unshare这三个系统调⽤来完成的。
clone可以⽤来创建新的Namespace。clone有⼀个flags参数,这些flags参数以CLONE NEW*为格式,包括CLONENEWNS、CLONE NEWIPC、CLONE NEWUTS、 CLONE NEWNET, CLONE NEWPID和CLONE NEWUSER,传⼊这些参数后,由clone创建出来的新进程就位于新的Namesapce之中。
因为Mount Namespace是第⼀个实现的,Namesapce当初实现没有考虑到还有其他Namespace出现,因此⽤了CLONE_NEWNS的名字,⽽不是CLONE_NEWMNT之类的名字,其它CLONE_NEW*都可以看名字知道⽤途。
那么,如何为已有的进程创建新的Namesapce呢,这就需要unshare。使⽤unshare调⽤的进程会被放到新的Namespace⾥。
⽽set ns则是将进程放到已有的Namespace中,docker exe命令的实现就是set ns。
事实上,开发Namespace的主要⽬的之⼀是实现轻量级的虚拟化服务,在同⼀个Namespace下的进程可以彼此响应,⽽对外进程隔离,这样在⼀个Namespace下进程仿佛处于⼀个独⽴的系统环境中,以
达到容器的⽬的。
先了解⼀下进程的Namespace
这⾥的$$是指当前的进程的ID号,可以看到后⾯的ID,这表⽰当前进程指向的Namespace,当两个进程指向同⼀串数字时,表⽰它们处于同⼀个Namespace下。
1. 认识Cgroup
Cgroup是 controlgroups的缩写,是Linux内核提供的⼀种可以限制、记录、隔离进程组(process groups)所使⽤的物理资源(如:CPU,内存,IO等等)的机制。它最初由Google的⼯程师提出,后来被整合进Linux内核。 Cgroup也是LXC为实现虚拟化所使⽤的资源管理⼿段,因此可以说没有Cgroup就没有LXC 。
⽬前,Cgroup有⼀套进程分组框架,不同资源由不同的⼦系统控制。⼀个⼦系统就是⼀个资源控制器,⽐如CPU⼦系统就是控制CPU时间分配的⼀个控制器。⼦系统必须附加(attach)到⼀个层级上才能起作⽤,⼀个⼦系统附加到某个层级以后,这个层级上的所有控制族(control group ) 都受这个⼦系统的控制。
Cgroup各个⼦系统的作⽤如下。
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:设置进程的⽹络流量优先级。
Huge_tlb:限制HugeTLB的使⽤。
Perf_event:允许 Perf⼯具基于Cgroup分组做性能监测。
1. 容器的创建
系统调⽤clone创建新进程,拥有⾃⼰的Namespace。
该进程拥有⾃⼰的PID,mount,user,net,ipc,uts namespace
4. docker基础
Docker是⼀个重新定义了程序开发测试、交付和部署过程的开放平台。在Docker的世界⾥,容器就是集装箱,我们的代码都被打包到集装箱⾥;Docker就是集船坞、货轮、装卸、搬运于⼀体的平台,帮你把应⽤软件运输到世界各地,并迅速部署。
5. docker架构
我们知道Docker是⼀个构建、发布、运⾏分布式应⽤的平台,Docker平台整体可以看成由Docker 引擎(运⾏环境+打包⼯具)、DockerRegistry ( API +⽣态系统)两部分组成。其中Docker引擎可以分为守护进程和客户端两⼤部分。Docker引擎的底层是各种操作系统以及云计算基础设施,⽽上层则是各种应⽤程序和管理⼯具,每层之间都是通过API来通信的。
1. docker client
Docker引擎可以直观地理解为在某⼀台机器上运⾏的Docker程序,实际上它是⼀个C/S结构的软件,有⼀个后台守护进程在运⾏,每次我们运⾏Docker 命令的时候实际上都是通过RESTful Remote API来和守护进程进⾏交互的,即使是在同⼀台机器上也是如此。
可以使⽤docker version查看版本,可以看到Client和server就是上图的Docker CLI和docker daemon。
1. Docker Daemon
daemon就是⼀个守护进程,实际上它就是驱动整个docker的核⼼引擎,在0.9版本前docker客户端和服务端是统⼀在⼀个⼆进制⽂件中,后来为了更好的管理,划分为四个⼆进制⽂件:docker,container,docker-container-shim和docker-runc.
1. docker镜像
Docker镜像是Docker系统中的构建模块(Build Component ),是启动⼀个Docker容器的基础。
Docker镜像采⽤分层的结构构建,最底层是bootfs,这是⼀个引导⽂件系统,⼀般⽤户很少会直接与其交互,在容器启动之后会⾃动卸载bootfs,bootfs之上是rootfs,rootfs是docker容器在启动时内部可见的⽂件系统,就是我们⽇常所见的"/"⽬录。
Docker镜像使⽤了联合挂载技术和写时复制技术,关于这些内容会在下⾯章节详细介绍。利⽤这两项技术,Docker可以只在⽂件系统发⽣变化时才会把⽂件写到可读/写层,⼀层层叠加,不仅有利于版本管理,还有利于存储管理。
1. docker容器
在Docker 的世界中,容器是核⼼,是⼀个基于 Docker 镜像创建、包含为运⾏某⼀特定程序所有需要的OS,软件、配置⽂件和数据,是⼀个可移植的运⾏单元。不过从宿主机来看,它只是⼀个简单的⽤户进程⽽已。关于容器的知识,在稍后会有详细介绍,在这⾥读者只需要知道容器是从镜像创建的运⾏实例,它是⼀个独⽴的沙盒。
容器很好地诠释了集装箱的理念,开发⼈员不⽤关⼼容器内部是什么应⽤,只管传输、运⾏即可,这是⼀种标准化的集装和运输⽅式,正因为 Docker 把容器技术进⾏了体验友好的封装,才使得容器技术迅速推⼴普及。
1. docker仓库
Github上有着海量的代码仓库,类似的,在Docker中,当开发者想要构建⼀个镜像或运⾏⼀个容器时,⼀般要先有个现成的镜像才可以执⾏构建或者运⾏,⽽本地⼜没有该特定镜像时怎么办呢?Dock
er提出了Registry的概念,⽤户可以将⾃⼰的镜像上传到Registry 上,如果是公开的,那么全世界的⽤户都可以拉取这个镜像来操作,可以说Registry就是⼀个"软件商店"。Registry类似传统运输业中的船坞、中转站⼀样,是⼀个集中存放 "集装箱"(镜像)的地⽅。
除了这两个官⽅的地址,⽤户还可以搭建⾃⼰私有的Registry,⽤来存储⾮公开的镜像。
6. Docker镜像
Docker最核⼼也是最基础的部分—镜像,docker镜像是docker整个体系中最基础的部分,docker镜像是容器的初始状态,docker 镜像的构建,维护都对容器的运⾏有着极⼤的影。
7. 认识镜像
1.镜像结构
上⾯命令中先是把镜像导出为tar归档⽂件,然后解压,最后可以看到hello-world这个镜像中⼀共有四个⽂件(夹),这些像乱码⼀样的⽂件夹其实是镜像的⼀个层(layer)。镜像包含着数据以及必要的元数据,这些数据就是层(layer),⽽元数据则是⼀些JSON⽂件,元数据是⽤来描述镜像的信息,包括数据之间的关系、容器配置信息等。
上⾯解压的镜像所显⽰的每⼀个层(layer)⽂件夹意味着它是由⼀句Dockerfile命令⽣成的。在构建镜像的过程中,像Run、COPY、ADD、CMD等命令都会⽣成⼀个新的镜像层,⼀个镜像就是不断在上⼀个镜像层的基础上叠加上去的。为了更直观地了解⼀个镜像的历史,可以使⽤docker history看镜像的历史。
可以看到镜像只有两句构建命令,第⼀句是copy可执⾏⽂件,第⼆句是cmd命令,负责在启动时执⾏的命令。构建过程由下到上,⼀层⼀层叠加,每⼀层的内容独⽴存储在镜像层中。
镜像在本地存储⺫录为/var/lib/docker/,containers是存放容器的信息,image是镜像的信息,network是容器⺴络信息,plugins 是插件信息,swarm是集信息。volumes是数据卷信息。
本地存储的镜像数据与层数据在image⽂件夹中是分开存储的,imagedb保存了本地全部镜像的元数据,⽽layerdb保存了本地镜像的全部镜像层。
2.存储原理
上⾯说到镜像内容与元数据是分开的,那么docker是如何把这些内容整合然后把⼀个完整镜像显⽰在⽤户眼前,以hello-world为例,通过docker inspect命令查看镜像的详细信息。
注意rootfs中的信息,docker daemon⾸先通过image的元数据得知全部layer的ID,再根据layer的元数
据梳理出顺序,最后使⽤联合挂载技术还原容器启动所需要的rootfs和基本配置信息。运⾏的容器实际上就像是在这些镜像层上新建⼀个动态的层。
现在我们已经知道镜像是⼀种像"千层饼"⼀样的结构, 但问题是Docker是如何把这么多的镜像层统筹起来变为⼀个可运⾏的容器呢?
这⾥就需要引⼊⼀项技术联合挂载。
联合挂载会把多个⺫录挂载到同⼀个⺫录(甚⾄可能对应不同的⽂件系统)下, 并对外显⽰这些⺫录的整合形态。Docker中使⽤的AUFS ( AnotherUnionFS)就是⼀种联合⽂件系统。
联合⽂件系统在⽇常使⽤的电脑中有⼀个地⽅经常会⽤到,那就是Linux系统的LiveCD,我们 使⽤发⾏版时⼀般都有⼀个LiveCD供⽤户体验。它的原理就是在原有的系统⺫录之上附加⼀层可读可写的⽂件层, 任何⽂件改动都会被写到这个⽂件层中, 这种技术就是写时复制。 关于写时复制的信息可以查看Overlay⽂件系统的资料。
这⾥需要特别注意⼀点,因为不理解写时复制的特性,在以后构建镜像过程中,⼤部分新⼦都会有⼀个误区就是"删除⼀个⽂件必定会导致镜像体积变⼩" 。
实际上并⾮如此, 举个简单的例⼦, 有⼀个镜像, 内部有⼀个 lOOMB 的⽂件,现在基于该镜像(FR
OM命令)构建⼀个新的镜像,在构建过程中执⾏了删除那个 lOOMB 的⽂件的命令,那么现在镜像体积变⼩了吗?
当然没有,因为根据联合挂载与写时复制的特点, 删除底层⽂件系统的⽂件或者⺫录时,会在上层建⽴⼀个同名的主次设备号都为0的字符设备,并不会删除底层⽂件系统的⽂件或者⺫录, 只是整合后的rootfs让⽤户看不到那些⽂件⽽已。
所以正确的解决办法是从底层的⽂件系统着⼿,在最初的镜像层删掉 lOOMB ⽂件才是减少镜像体积的办法。
8. 镜像操作
1. 拉取镜像。
拉取镜像的命令,通过docker pull不仅可以拉取docker hub的镜像,还可以通过指定仓库地址拉取私有仓库镜像。
使⽤docker pull -a会把所有的标签都拉取到本地,使⽤—disable-content-trust=false会在拉取时校验镜像保证传输安全,默认是关闭。
镜像拉取很简单,如果想获取其它⽤户的镜像,可以在docker hub上搜索。
1. 镜像创建
构建镜像
docker bulid是构建镜像⽤到的重要命令。
详情可以使⽤docker build –help查看。
例⼦:构建⼀个镜像并打上标签,后⾯的"."是表⽰当前⽬录,docker构建镜像是要注意上下⽂的,dockerfile⽂件要放在知道的地⽅。
镜像提交
除了使⽤docker build构建镜像,还可以使⽤commit提交镜像。docker commit会把容器提交打包为镜像,这样提交的镜像会保存容器内的数据,⽽且第三⽅⽆法获取镜像的dockerfile,也就⽆法再构建⼀个⼀模⼀样的镜像出来,不推荐使⽤commit。
但是在某些时候,我们需要使⽤docker commit来保存容器的状态,这个时候需要使⽤这个⽅法保存容器。下⾯举个例⼦。
⾸先拉取⼀个镜像下来
查看镜像列表
使⽤这个镜像运⾏容器,并在容器⾥添加⼀个⽂本,注意使⽤docker run it进⼊容器退出后,容器也会停⽌。后⾯会说明不让其停⽌的办法。
把上⾯创建的容器作为镜像提交并打上新的标签
查看镜像,已有新的名字
运⾏提交的镜像,并查看之前创建的⽂本,可以看到可以保存当时容器的状态。
下图中可以看到随着退出容器,容器也停⽌了,执⾏start让它运⾏,可以看到已经是up状态,并可以进⼊。
可以看到:提交的test容器保存了之前⽂件,docker commit参数如下:
-a 添加作者信息
-c 修改dockerfile指令
-m 类似git commit -m提交修改信息
-p 暂停正在commit操作。
3.导⼊导出镜像。
前⾯的使⽤过镜像导出的功能,现在来看具体的镜像导⼊导出⽤法,如果在两台主机之间需要传输镜像,⼀个办法就是把镜像推送到仓库,然后让另⼀台主机拉回来,但是这样有个中转,很⿇烦,有时候我们不想把镜像发布到互联⽹中,⽽⾃⼰搭建私有镜像仓库需要诸多步骤,于是需要⼀组可以导⼊导出镜像的命令。
导出镜像
使⽤docker save可以导出镜像到本地系统;
如下图所⽰:
我们可以把这个⽂件解压,⾥⾯是⼀个基于Libcontainer标准的rootfs,使⽤runc也可以运⾏起来。如忘记了参数,还可以使⽤">"符号导出镜像。
导⼊镜像
使⽤docker load可以加载⼀个导出的镜像包到本地仓库。
或者使⽤"<"符号
4.删除镜像
本地镜像多了,有些不要的,需要删掉,删除镜像的命令为docker rmi,删除镜像时不指定镜像的tag会删除镜像的latest标签。可以在命令后⾯接上多个镜像名称,删除多个镜像。
使⽤docker rmi命令删除镜像时,要确保没有容器使⽤该镜像,也就是说,没有容器是使⽤该镜像启动的,才可以删除,否则会报错。
删除镜像时可以使⽤镜像的ID也可以使⽤镜像名称,docker rmi有⼀个参数-f,该参数可以强制删除镜像,即便有容器正在使⽤该镜像。但是这样只会删除镜像标签,不影响正在运⾏的容器,实际上只要容器还在运⾏,镜像就不会被真正删除,⽤户可以使⽤docker commit操作提交容器来恢复镜像。
#删除⼀个镜像,默认会删除latest标签
#删除⼀个标签
镜像实际上是以ID为标准保存在Docker,中的,即使镜像没有使⽤标签,镜像也是可以存在的,出现这种情况的原因有很多,例如强制删除了⼀个正在运⾏着容器的镜像,⼜或者构建的新镜像的tag覆盖了原来旧镜像的tag等。
时间长了,我们没有tag说明这些镜像是什么作⽤就会很难管理,所以我们需要删除这些镜像,数量少时我们可以⼿动⼀条⼀条地删除,数量多时我们可以配合Docker其他命令,删除所有未打dangling标签的镜像:
docker rmi $(docker image -q -f dangling=true)
#删除所有镜像
docker rmi $(docker image -q)
5.发布镜像。
docker打包镜像因为docker hub是官⽅默认的仓库,镜像最多,⼀般会选择发布到这⾥。在推送之前需要登录docker hub。没有账号⾃⼰去申请⼀个。
#登录docker hub
#docker login
查镜像
#docker search ubuntu
#下拉镜像
#docker pull ubuntu
推送镜像
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论