Docker是一个开源的应用容器引擎,基于Go语言 ,诞生于2013年初。 最初发起者是DotCloud公司((Platform-as-a-Service, PaaS)提供商)开源的一个基于 LXC 的高级容器引擎。 功能Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的Linux机器;实现虚拟化。 主要工作如拉取镜像、编译镜像、运行容器、发布容器等。
Docker从1.13.x(2017.3.2)版本开始,版本分为企业版EE和社区版
从 17.03开始基于时间线进行版本发布, 17.03 代表17年3月,说明这个是个稳定版。
docker解决了软件环境部署复杂的问题。对于一个传统的软件工程,开发人员把写好的代码放到服务器上去运行是一件很头疼的事情,因为常常会出现环境不兼容而导致各种各样的bug。
比如说,开发是windows系统下编写的代码,放到linux服务器上可能会出问题,开发在本地依赖了一个系统自带的驱动,服务器上却没有这个驱动,开发在本地设置了很多环境变量,服务器上又得重新设置。
在软件部署的时候,经常会发生在这台机器上可以跑,但是在另一台机器上却运行失败的情况。运维人员需要不断在开发环境和服务器环境之间调试。
有了docker,只需要简单的几行命令,就可以做到所有运行环境都一致。
说到docker,那就必须要和虚拟机做一下比较,其实docker是和虚拟机是类似的东西,我们应该知道虚拟机就是在我们的操作系统上虚拟出来一个电脑,然后里边可以安装、运行各种各样的软件,和我们真的电脑是差不多的,我们可以拿着这个虚拟好的电脑(其实是一个文件)在按了虚拟机的其他电脑上可以直接运行,里边的东西就不用我们来回安装和配置了,也是很方便的。
docker其实提供的也是这么一种的技术,只不过它比虚拟机效率更加的高,启动快,占用资源小等一系列的优点,而且虚拟机比较笨重,这是因为虚拟机和docker来实现思想上有本质的区别,我们可以通过下边的图对比一下:
虚拟机的运作原理:是虚拟电脑的硬件资源,把硬件资源分配出来,然后虚拟出来多个操作系统,虚拟出来的是一个完整的电脑。
特性 | 容器 | 虚拟机 |
启动 | 秒级 | 分钟级 |
硬盘使用 | 一般为MB | 一般为GB |
性能 | 接近原生 | 弱于 |
系统支持量 | 单机支持上千个容器 | 一般几十个 |
相同点:
不同点:
镜像到底是个什么东西呢,很多人在学习docker的时候都是一头雾水的,可是是歪果仁对镜像情有独钟吧,好多东西都有镜像的概念。比如我们安装系统的.iso文件,其实就是镜像,这里你就可以把镜像认为是一种模板。我们可以使用docker根据这个模板创建容器来运行,其实更可以理解为镜像是好比github上的仓库一样,我们可以克隆下来源代码然后运行,运行起来的代码可以是一个网站、一个应用程序啥的,这就可以叫做容器。说白了,镜像就是一堆静态的模板,运行起来的镜像就是容器。镜像一般需要我们拉取下来,是只读的,这个我们克隆github上的仓库是一样一样的。
docker镜像中有分层的概念,就是一个镜像可能基于好几个镜像,比如一个web运行环境可能需要操作系统ubuntu、数据库mysql、.net core runtime运行时,那我们拉取的这个镜像就会包好这好几个镜像,这就好像我们前边说的打包好的运行环境一样,直接就拉下来一个小电脑一样。
总结起来就是docker镜像就是我们事先做好的一套版本,里面装载着我们所需要的东西和流程,模板之间还可以嵌套。
当我们拉取了一个镜像,然后run一下,就会根据这个镜像运行出来一个容器,运行的容器就好像我们的应用程序一样,可以访问可以停止,我们运用多次run命令,就运行了很多很多容器,也可以说是镜像的实例。从这个角度来看,我们可以把镜像看作是类,容器看作new出来的实例,也是很合适的。
存放镜像的地方就是仓库,就好比存放代码的地方是github一样,我们就把github称为代码的仓库,github算是最大的仓库。那么存放docker镜像的地方我们叫做dockerhub,是docker的中央仓库。其实已经有dockerhub这个网站了(https://hub.docker.com/),这就是 存放docker镜像的官方仓库,好多官方的也保存在这里,保证了镜像的安全性和可靠性,我们可以从上边拉取一下镜像来运行我们的软件。当然我们也可以制作好我们自己镜像推送上去,不过这些肯定是要官方审核的,防止有些人写入一些恶意代码。不过我们可以推到我们自己的dockerhub上去,供我们自己使用,这个就好我们的github账号一样了,属于私有镜像了。
实际上我们的容器就好像是一个简易版的操作系统,只不过系统中只安装了我们的程序运行所需要的环境,前边说到我们的容器是new出来的实例,既然是new出来的实例那就会销毁,那如果销毁了我们的程序产生出的需要持久化的数据怎么办呢,容器运行的时候我们可以进容器去查看,容器一旦销毁就什么都没有了。所以数据卷就是来解决这个问题的,是用来做数据持久化到我们的宿主机上容器间的数据共享,简单的说就是将宿主机的目录映射到容器中的目录,应用程序在容器中的目录读写数据会同步到宿主机上,这样容器产生的数据就可以持久化了,比如我们的数据库容器,就可以把数据存到我们宿主机上的真实磁盘上了。
Docker使用C/S架构,Docker Client通过command(or api)与Docker Daemon(Sever进程)通信实现容器的构建。
用户访问docker的流程,用户并不是直接和docker的守护进程交互,而是通过Docker client和docker的守护进程交互。
tips:
Docker Client和Docker Daemon之间通过在Socket上使用RESTful API进行交互,所以Docker Client和Docker Daemon可以运行在同一个系统上也可以通过远程的方式访问总体
docker总体架构示意图:
1、用户是使用 Docker Client 与 Docker Daemon 建立通信,并发送请求给后者。
2、Docker Daemon 作为 Docker 架构中的主体部分,首先提供 Docker Server 的功能使其可以接受 Docker Client 的请求。
3、Docker Engine 执行 Docker 内部的一系列工作,每一项工作都是以一个 Job 的形式的存在。
4、Job 的运行过程中,当需要容器镜像时,则从 Docker Registry 中下载镜像,并通过镜像管理驱动 Graphdriver 将下载镜像以 Graph 的形式存储。
5、当需要为 Docker 创建网络环境时,通过网络管理驱动 Networkdriver 创建并配置 Docker容器网络环境。
6、当需要限制 Docker 容器运行资源或执行用户指令等操作时,则通过 Execdriver 来完成。
7、Libcontainer 是一项独立的容器管理包,Networkdriver 以及 Execdriver 都是通过 Libcontainer 来实现具体对容器进行的操作。
1、Docker Client是和Docker Daemon建立通信的客户端。用户使用的可执行文件为docker(一个命令行可执行文件),docker命令使用后接参数的形式来实现一个完整的请求命令(例如:docker images,docker为命令不可变,images为参数可变)。
2、Docker Client可以通过以下三种方式和Docker Daemon建立通信:tcp://host:port、unix://pathtosocket和fd://socketfd。
3、Docker Client发送容器管理请求后,由Docker Daemon接受并处理请求,当Docker Client接收到返回的请求相应并简单处理后,Docker Client一次完整的生命周期就结束了。(一次完整的请求:发送请求→处理请求→返回结果),与传统的C/S架构请求流程并无不同。
Docker daemon架构图:
Docker Server架构图:
1、Docker Server相当于C/S架构的服务端。功能为接受并调度分发DockerClient发送的请求。接受请求后,Docker Server通过路由与分发调度,找到相应的Handler来执行请求。
2、在Docker的启动过程中,通过包gorilla/mux创建了一个mux.Router来提供请求的路由功能。在Golang中gorilla/mux是一个强大的URL路由器以及调度分发器。该mux.Router中添加了众多的路由项,每一个路由项由HTTP请求方法(PUT、POST、GET或DELETE)、URL、Handler三部分组成。
3、创建完mux.Router之后,Docker将Server的监听地址以及mux.Router作为参数来创建一个httpSrv=http.Server{},最终执行httpSrv.Serve()为请求服务。
4、在Docker Server的服务过程中,Docker Server在listener上接受Docker Client的访问请求,并创建一个全新的goroutine来服务该请求。在goroutine中,首先读取请求内容并做解析工作,接着找到相应的路由项并调用相应的Handler来处理该请求,最后Handler处理完请求之后回复该请求。
1、Docker Engine是Docker架构中的运行引擎,同时也Docker运行的核心模块。它扮演DockerContainer存储仓库的角色,并且通过执行Job的方式来操纵管理这些容器。
2、在Docker Engine数据结构的设计与实现过程中,有一个Handler对象。该Handler对象存储的都是关于众多特定Job的Handler处理访问。举例说明:DockerEngine的Handler对象中有一项为:{“create”:daemon.ContainerCreate,},则说明当名为”create”的Job在运行时,执行的是daemon.ContainerCreate的Handler。
1、一个Job可以认为是Docker架构中Docker Engine内部最基本的工作执行单元。Docker可以做的每一项工作,都可以抽象为一个Job。例如:在容器内部运行一个进程,这是一个Job;创建一个新的容器,这是一个Job。Docker Server的运行过程也是一个Job,名为Serve Api。
2、Job的设计者,把Job设计得与Unix进程相仿。比如说:Job有一个名称、有参数、有环境变量、有标准的输入输出、有错误处理,有返回状态等。
1、DockerRegistry是一个存储容器镜像的仓库(注册中心),可理解为云端镜像仓库。按Repository来分类,dockerpull按照[repository]:[tag]来精确定义一个具体的Image。
2、在Docker的运行过程中,Docker Daemon会与Docker Registry通信,并实现搜索镜像、下载镜像、上传镜像三个功能,这三个功能对应的Job名称分别为:“search”、”pull”与“push”。
3、Docker Registry可分为公有仓库(Docker Hub)和私有仓库。
Repository:
1、已下载镜像的保管者(包括下载的镜像和通过Dockerfile构建的镜像)。
2、一个Repository表示某类镜像的仓库(例如:Ubuntu),同一个Repository内的镜像用Tag来区分(表示同一类镜像的不同标签或版本)。一个Registry包含多个Repository,一个Repository包含同类型的多个Image。
3、镜像的存储类型有Aufs、Devicemapper、Btrfs、Vfs等。其中CentOS系统7.x以下版本使用Devicemapper的存储类型。
4、同时在Graph的本地目录中存储有关于每一个的容器镜像具体信息,包含有:该容器镜像的元数据、容器镜像的大小信息、以及该容器镜像所代表的具体rootfs。
GraphDB:
1、已下载容器镜像之间关系的记录者。
2、GraphDB是一个构建在SQLite之上的小型数据库,实现了节点的命名以及节点之间关联关系的记录。
Driver是Docker架构中的驱动模块。通过Driver驱动,Docker可以实现对Docker容器执行环境的定制。即Graph负责镜像的存储,Driver负责容器的执行。
Graphdriver:
Graphdriver架构图
1、Graphdriver主要用于完成容器镜像的管理,包括存储与获取。
2、存储:dockerpull下载的镜像由Graphdriver存储到本地的指定目录(Graph中)。
3、获取:dockerrun(create)用镜像来创建容器的时候由Graphdriver到本地Graph中获取镜像。
Networkdriver:
Networkdriver架构图
Networkdriver的用途是完成Docker容器网络环境的配置,其中包括:
Execdriver:
Execdriver架构图
1、Execdriver作为Docker容器的执行驱动,负责创建容器运行命名空间、容器资源使用的统计与限制、容器内部进程的真正运行等。
2、现在Execdriver默认使用Native驱动,不依赖于LXC。
Libcontainer架构图:
1、Libcontainer是Docker架构中一个使用Go语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的API。
2、Docker可以直接调用Libcontainer来操纵容器的Namespace、Cgroups、Apparmor、网络设备以及防火墙规则等。
3、Libcontainer提供了一整套标准的接口来满足上层对容器管理的需求。或者说Libcontainer屏蔽了Docker上层对容器的直接管理。
Docke rContainer架构
1、DockerContainer(Docker容器)是Docker架构中服务交付的最终体现形式。
2Docker按照用户的需求与指令,订制相应的Docker容器:
index:GET /v1/repositories/(namespace)/(repo_name)/images 【docker pull调用】
关键名词解释:
index服务(Open API):主要提供镜像索引以及用户认证的功能。
当下载一个镜像的时候,首先会去index服务上做认证,然后index服务查找镜像所在的registry的地址,具体流程如下:
UnionFS( 联合文件系统):Union文件系统(UnionFS )是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtualfilesystem)。Union文件系统是Docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
另外,不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。
Docker 中使用的 AUFS(AnotherUnionFS)就是一种联合文件系统。 AUFS 支持为每一个成员目录(类似 Git 的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限, 同时 AUFS 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。
Docker 目前支持的联合文件系统种类包括 AUFS, btrfs, vfs 和 DeviceMapper。
特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。
base 镜像简单来说就是不依赖其他任何镜像,完全从0开始建起,其他镜像都是建立在他的之上,可以比喻为大楼的地基,docker镜像的鼻祖。
base 镜像有两层含义:(1)不依赖其他镜像,从 scratch 构建;(2)其他镜像可以之为基础进行扩展。
所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。
docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统就是UnionFS。
典型的 Linux 启动到运行需要两个FS,bootfs + rootfs:
在Docker镜像的最底层是bootfs,然后是rootfs。
bootfs(boot file system):Docker镜像的最底层是bootfs,用户是不能对这层作任何修改,主要包含bootloader和kernel,bootloader 主要是引导加载kernel,Linux刚启动时会加载bootfs文件系统。
rootfs (root file system):在 bootfs 上一层是 rootfs,我们也称之为 base image layer(FROM centos)。
我们以nginx为例:
下载完成以后依次执行下面命令:
#创建downloadimage,用于储存docker的image
mkdir -p /usr/local/downloadimage
#切换目录
cd /usr/local/downloadimage
#将docker的nginx镜像保存为nginx.tar文件
docker save nginx > nginx.tar
保存完成之后我们打开nginx.tar文件,其结构如下所示:
然后我们可以打开manifest.json文件,查看其结构:
manifest :描述文件,主要存在于registry中作为docker镜像的元数据文件,在pull、push、 save、load中作为镜像结构和基础信息的描述文件。
那么layer之间的层级是怎么界定的呢?每个layer文件夹下都有一个json的文件,层级关系信息都隐藏在这个信息里面。
从图上可以看出,每个layer都是通过json文件中的parent去信息去查找自己的上一层信息的,因此,我们可以得出nginx镜像的整体层级关系如下所示:
上一小节我们知道了docker镜像分层原理,docker的每一层layer合起来就是一个镜像,那么假如有两个image使用了同一个layer,那么这个layer会重复下载吗?答案是不会的,docker镜像下载的流程如下:
docker run:创建一个新的容器并运行一个命令;
docker start/stop/restart:启停容器;
docker kill:杀掉运行中的容器;
docker rm:删除一个或多个容器;
docker pause/unpause:暂停或恢复容器中的所有进程;
docker create:创建一个新的容器但不启动它;
docker update:更新一个或多个容器的配置;
Docker主要就是借助 Linux 内核技术Namespace来做到隔离的,Linux Namespaces机制提供一种资源隔离方案。PID,IPC,Network等系统资源不再是全局性的,而是属于某个特定的Namespace。每个namespace下的资源对于其他namespace下的资源都是透明,不可见的,因此在操作系统层面上看,就会出现多个相同pid的进程。系统中可以同时存在两个进程号为0,1,2的进程,由于属于不同的namespace,所以它们之间并不冲突。而在用户层面上只能看到属于用户自己namespace下的资源,例如使用ps命令只能列出自己namespace下的进程。这样每个namespace看上去就像一个单独的Linux系统。
目前,Linux 内核实现了6种 Namespace:
docker的底层是通过CGroup控制的。
CGroup:Control Groups的缩写,是Linux内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(如cpu memory i/o等等)的机制。
Cgroup作用:控制程序对资源的占用。
CGroup技术可以被用来在操作系统底层限制物理资源,起到Container的作用。图中每一个JVM进程对应一个ContainerCgroup层级,通过CGroup提供的各类子系统,可以对每一个JVM进程对应的线程级别进行物理限制,这些限制包括CPU、内存等等许多种类的资源。那么,docker是如何来定义容器使用的资源呢?
需求:要求容器 CPU 使用权重为512
/sys/fs/cgroup 为资源限制的目录
1. cat /sys/fs/cgroup/cpu/cpu.shares //查看宿主机CPU的权重
2. docker run -d dc833dc45d8f //运行一个容器或者进入交互式容器
docker exec -it 17f274c2ca9d /bin/bash
3. cat /sys/fs/cgroup/cpu/cpu.shares //查看容器中CPU的权重 发现和宿主机一样
4. exit //退出容器
docker stop id
docker rm id
5. docker run -d -c 512 dc833dc45d8f
6. cat /sys/fs/cgroup/cpu/cpu.shares //查看修改后容器中CPU的权重
结论:
CPU 和容器的 CPU权重都是 一样的 1024
如果不进行修改 CPU 的权重,那么容器与宿主机对 CPU 的权重都是默认的1024,这样分配是不合理的,因为宿主机与容器对 CPU 的权重一样,因而导致,它们在对 CPU 资源出现抢占的情况下,其可使用的 CPU 资源是 1:1
若限制容器为512,宿主机还是 1024,那么其CPU 使用权重的比例就变成了 2:1,可以通过上面对容器进行限制:
总结
使用 Cgroup,可更好地根据任务和用户分配硬件资源(CPU、内存、交换空间、写入速度)。
time dd if=/dev/zero of=a.txt bs=1M count=200 oflag=direct
容器中执行
因网络原因,每次执行效果不大一样
宿主机执行
因网络原因,每次执行效果不大一样
限速
可以看到,如果不对其进行限制,那么会使用宿主机最大的写速度,那么怎么限制呢
docker run -d --name czbk1 --device-write-bps /dev/sda:40MB dc833dc45d8f
进入到容器内部验证
time dd if=/dev/zero of=a.txt bs=1M count=200 oflag=direct
这样的话,就可以将容器的写入速度限制在了 40mb 左右
当多个容器任务运行时,很难计算CPU的使用率,为了使容器合理使用CPU资源,可以通过 --CPU-shares选项设置容器按比例共享CPU资源,这种方式还可以实现cpu使用率的动态调整
例如:运行三个新的容器A,B,C,占用cpu资源为1:1:2。
docker run --name A -itd --cpu-shares 1024 centos /bin/bash
docker run --name B -itd --cpu-shares 1024 centos /bin/bash
docker run --name C -itd --cpu-shares 2048 centos /bin/bash
如果又一个容器D需要更多的cpu资源,则可以将其 --cpu--shares的值设置为4096,那么ABCD的cpu资源比例为1:1:2:4。
Docker 安装完成后缺省配置有三个网络类型
使用 Docker network ls 可以查看当前系统中的 Docker 网桥
#查看docker的网桥
docker network ls
Docker 内置这三个网络,运行容器时,你可以使用该 --network 标志来指定容器应连接到哪些网络。
bridge(默认):此模式会为每一个容器分配、设置 IP等,并将容器连接到一个docker0 虚拟网桥
docker inspect 1d6f1cb322b3
none Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置 IP 等
#创建一个使用none 网络的容器
docker run -d --name "none-network" --network=none 941109e2896d
缺点是:无法联网
优点是:封闭的网络能很好的保证容器的安全性,适用于企业开发
应用场景:生成密钥
相当于Vmware中的桥接模式,与宿主机在同一个网络中,但没有独立IP地址。
容器将不会虚拟出自己的网卡,也不会配置自己的IP等,而是使用宿主机的IP和端口。
#创建一个使用none 网络的容器
docker run -d --name "host-network" --network=host 941109e2896d
使用 host 模式的容器可以直接使用宿主机的 IP 地址与外界通信,容器内部的服务端口也可以使用宿主机的端口
缺点:网络的隔离性不好
什么是Containers?
bridge 也是所有 container 的缺省网络
docker network inspect bridge
ifconfig 可以查看 docker0 网卡的地址为 inet 172.18.0.
注意:如果没有ifconfig,可以使用ip addr或者是执行yum install net-tools命令安装ifconfig命令
用户定义的网络
docker network create --subnet=172.128.0.0/16 czbkNetwork-bas
在 IP 地址后加上 "/" 符号以及 1-32 的数字,其中 1-32 的数字表示子网掩码中网络标识位的长度
自定义网络对应的网卡
每一个自创建的网络,在主机上都有一个 "br-" 开头;这和缺省 bridge 网络一样,只不过 bridge 不以 br- 开头,而自定义的网络使用 br- 开头罢了:
ifconfig
自定义网络czbkNetwork对应的网卡br-413b460a0fc8
1、如何知道自定义网络下有哪些用户容器?
docker inspect czbkNetwork
2、如何知道自定义网络(czbkNetwork)对应的网卡是那个?
docker network inspect czbkNetwork
我们可以看到网关地址是 172.188.0.1 那么 172.188.0.1 是谁呢,前面我们已经知道了,就是自定义网卡的地址
ifconfig
结论:网卡br-413b460a0fc8 是属于czbkNetwork网络的
3、Docker 容器相互访问以及容器是如何访问互联网的
也就是说默认情况下容器会使用docker0这个网桥,两个容器都使用这个网桥的话,就可以直接互相访问了。
然后我们Docker的容器里面访问互联网的时候,其实也是使用docker0网卡转换到宿主机网卡上,然后访问互联网。
4、互联网怎么访问 Docker?
互联网通过Docker暴露的端口访问Docker。