在互联网时代,企业的大部分业务都需要依赖于应用程序(Application)来运行的,应用如果出问题,整个业务就无法正常运转,这可能会导致公司亏损甚至倒闭。这种情况是真实发生过的,并且还不少见。
应用程序运行在服务器上,而在早期的技术环境下,每台服务器只能运行一个应用程序。比如,你有一个电商网站(应用程序)要运行,它就需要一台专门的服务器。因为在那时候,操作系统(像 Windows 或 Linux)没有相应的技术手段来保证在同一台服务器上安全稳定地运行多个应用。
因此当时的情况是,如果一个公司想要新增应用,IT 部门就得去买一台新的服务器。但问题是,没有人知道新应用具体需要多强的服务器来支撑其运行,IT 部门只能凭经验猜测。
为了避免服务器性能不够用,导致业务运行不畅,公司一般会选择买“更大更好的”服务器。
举个例子,如果电商网站一天大概会有 1000 个订单,IT 部门可能会担心高峰期流量太大,影响订单处理速度,所以会买一台可以处理 5000 个订单的服务器。这虽然能保证业务运行流畅,但绝大多数情况下,这台服务器只被利用了 5%-10% 的资源,造成了严重的资源浪费。
虚拟机
为了解决上述所说的资源浪费问题,虚拟机(VM)出现了。
虚拟机出现后,当业务部门需要增加应用的时候,IT部门无须采购新的服务器。取而代之的是,IT部门会尝试在现有的,并且有空闲性能的服务器上部署新的应用。
但是虚拟机也不是十全十美的,虚拟机最大的缺点就是依赖其专用的操作系统(OS)。OS会占用额外的CPU、RAM和存储,这些资源本可以用于运行更多的应用。
其次,虚拟机启动通常比较慢,并且可移植性比较差。
容器
再之后,容器出现了。
容器模型其实跟虚拟机模型相似,其主要的区别在于,容器的运行不会独占操作系统。实际上,运行在相同宿主机上的容器是共享一个操作系统内核的,这样就能够节省大量的系统资源(CPU、RAM以及存储)。
同时容器还具有启动快和便于迁移等优势。将容器从笔记本电脑迁移到云上,之后再迁移到数据中心的虚拟机或者物理机之上,都是很简单的事情。
但是容器技术很复杂,其复杂度也阻碍了其发展,直到Docker技术的出现,容器才开始流行起来。
Docker介绍
在谈到Docker时,主要是指Docker引擎。Docker引擎就是用于运行和编排容器的基础设施工具。其作用在于创建、管理和编排容器。让你更容易地使用容器。也就是说,Docker本身并不是容器,它是创建和管理容器的工具。
通过 Docker,开发者可以快速构建一个应用程序的容器,假设你有一个网页应用需要运行在某个服务器上。使用 Docker,你可以创建一个包含应用代码、运行时环境(如 Node.js)和所有库的容器。这个容器可以在开发机、测试机和生产环境中无缝运行,而不必担心每个环境的差异。
这样,Docker不仅简化了容器的使用,还提高了应用程序的移植性和可扩展性。
Docker引擎主要有两个版本:企业版(EE)和社区版(CE)。每个季度,企业版和社区版都会发布一个稳定版本。社区版本会提供4个月的支持,而企业版本会提供12个月的支持。
Docker安装
为了运行相关命令,需要一个运行Docker的主机。安装过程自行搜索资料。
安装完成后,通常登录Docker主机后的第一件事情是使用docker version命令检查Docker是否正在运行。
当命令输出中包含Client和Server的内容时,就可以进行命令的演示了。
Docker基本概念
在Docker中,有 三个主要概念:镜像、容器和仓库,需要我们好好理解,这对于后面Docker内容的学习至关重要。
对于刚接触的Docker朋友来说,可能一下子不能完全理解这3个概念,我们可将这三个概念这样理解:镜像类似于程序,而容器可以类似于进程,自然而然,仓库则类似于gitHub这样的代码托管仓库。
我们将编写好的应用程序如Springboot应用要想通过容器运行起来,首先需要制作出所需的Docker镜像(类似程序开发中源码编写),并将其推送到仓库中保存(类似将源码保存到gitHub等代码仓库),然后通过docker pull等指令将镜像从仓库中拉取下来docker run 执行生成容器(类似通过git clone源码然后编译执行程序)。
下面给出较为官方一点的解释:
-
镜像:镜像是一个只读的模板,包含了操作系统、应用程序、依赖库、配置文件等,用以创建 Docker 容器。我们把应用程序和配置依赖打包好形成一个可交付的运行环境 (包括代码、运行时需要的库、环境变量和配置文件等) ,这个打包好的运行环境就是 image 镜像文件。只有通过这个镜像文件才能创建 Docker 容器实例 (类似 Java 中 New 一个对象出来) 。
-
容器:容器是镜像的运行实例,通过docker run 镜像名称就可以基于指定的镜像运行容器。docker容器的本质是宿主机上的一个进程。
-
仓库:仓库则是用于存储镜像的地方,你可以将镜像上传到仓库,也可以下载其他人创建的镜像。
Docker引擎架构
Docker是CS架构,由多个组件组成,以下是Docker的主要组件:
-
Docker Daemon:它是Docker的核心组件,负责管理镜像、容器、网络和卷等资源,并将Docker API暴露给客户端。用户通过Docker client(Docker命令)与Docker daemon交互
-
Docker Client:它是与Docker Daemon通信的主要接口,可以通过命令行或API向Daemon发送请求。
-
Docker Registry:管理Docker镜像,用户可以上传或者下载上面的镜像,官方地址为https://registry.hub.docker.com/,也可以搭建自己私有的Docker registry
上述这些组件共同构成了Docker的核心功能,使得开发人员和系统管理员能够更加便捷地开发、部署和管理应用程序。
再谈Docker镜像
镜像和镜像仓库(Docker Registry)
镜像和仓库是密不可分的,因为Docker镜像存储在镜像仓库服务中,因此在介绍镜像后续知识之前,先简单了解下Docker仓库是有必要的.
Docker仓库分公共仓库和私有仓库,我们默认使用的就是docker hub公共仓库,公共仓库可以供任何人访问。
Docker Hub也分为官方仓库(Official Repository)和非官方仓库(Unofficial Repository)。
官方仓库中的镜像是由Docker公司审查的。这意味着其中的镜像会及时更新,由高质量的代码构成,这些代码是安全的,有完善的文档和最佳实践。
大部分流行的操作系统和应用在Docker Hub的官方仓库中都有其对应镜像。
但是在企业级应用环境中,我们不可能将企业的内部容器推送到公共镜像仓库中,我们需要搭建私有镜像仓库服务,搭建私有镜像的方式有很多,具体怎么搭建这里就不介绍了。
镜像仓库服务包含多个镜像仓库(Image Repository)。同样,一个镜像仓库中可以包含多个镜像。下图展示了包含3个镜像仓库的镜像仓库服务。
使用以下命令可以从镜像官方仓库中拉取镜像:
docker image pull <repository>:<tag>
拉取时如果没有在仓库名称后指定具体的镜像标签,则会拉取标签为latest的镜像。
从非官方仓库拉取镜像也是类似的,读者只需要在仓库名称面前加上Docker Hub的用户名或者组织名称。
下面的示例展示了如何从tu-demo仓库中拉取v2这个镜像,其中镜像的拥有者是Docker Hub账户nigelpoulton.
docker image pull nigelpoulton/tu-demo:v2
镜像命名规则
镜像名称的通用格式为:DOCKER_REGISTRY/repo/name:tag,各个字段具体含义如下:
- DOCKER_REGISTRY:企业统一的Docker Registry地址;
- repo:镜像仓库,用来管理某一类镜像;
- name:某个镜像的具体名称,一般的命名规则为:系统名称+系统版本+服务名+服务版本。例如:centos7.6-nginx-1.47。
- tag:某个镜像具体的标签。例如:2.0。
例如一个拉取到的镜像名字是registry.k8s.io/e2e-test-images/agnhost:2.39,根据DOCKER_REGISTRY/repo/name:tag规范,则它是在registry.k8s.io地址上的repo叫e2e-test-images、名字叫agnhost、版本是2.39的镜像。
docker 镜像分层结构
Docker 镜像是分层的,这意味着每个镜像都是由多个层(layers)叠加而成的。
-
基础层:每个镜像都有一个基础层,通常是一个操作系统的镜像(如 Ubuntu 或 Alpine)。这是所有其他层的起点。
-
应用层:在基础层之上,可以添加应用程序和依赖。例如,你可以在基础层上添加 Python 解释器、库文件等。
-
增量更新:每当你对镜像进行修改(如安装新的软件),Docker 会创建一个新的层,而不是直接修改原始层。这样,原始层不会被改变,避免了不必要的重复。
再次强调,所有的Docker镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上,创建新的镜像层。
举一个简单的例子,假如基于Ubuntu Linux 16.04创建一个新的镜像,这就是新镜像的第一层;如果在该镜像中添加Python包,就会在基础镜像层之上创建第二个镜像层;如果继续添加一个安全补丁,就会创建第三个镜像层。
在添加额外的镜像层的同时,镜像始终保持是当前所有镜像层的组合,理解这一点非常重要。下图中每个镜像层包含3个文件,而镜像包含了来自两个镜像层的6个文件。
下图展示了一个稍微复杂的三层镜像,在外部看来整个镜像只有6个文件,这是因为最上层中的文件7是文件5的一个更新版本。这种情况下,上层镜像层中的文件覆盖了底层镜像层中的文件。这样就使得文件的更新版本作为一个新镜像层添加到镜像当中。
上图中所有镜像层堆叠并合并,对外提供的是如下统一的视图:
这种分层结构使得 Docker 镜像更加灵活和高效:
- 多个镜像可以共享相同的基础层,减少存储需求。
- 其次修改或更新镜像时,只需重新构建顶部的层,速度更快。
镜像和容器
如果要启动一个容器,需要一个对应的镜像。
如果启动容器时,本地Docker主机上没有对应的镜像,那么就会自动从镜像仓库服务中拉取镜像。
常见的镜像仓库服务是Docker Hub,但是也存在其他镜像仓库服务。拉取操作会将镜像下载到本地Docker主机,然后就可以使用该镜像启动一个或者多个容器。
一旦容器从镜像启动后,二者之间就变成了互相依赖的关系,并且在镜像上启动的容器全部停止之前,镜像是无法被删除的(加-f 选项可以强制删除,但是不建议这么做)。尝试删除镜像而不停止或销毁使用它的容器,会导致错误。
镜像操作相关命令
下面介绍Docker中与镜像相关的操作命令。
docker search <镜像名称>
docker search命令用于搜索官方仓库镜像,如:
docker search还支持以下常用选项:
//只显示官方镜像
docker search --filter "is-official=true" nginx
//默认情况下,Docker返回25行结果,--limit参数用来修改返回结果的数量
docker search --limit 4 nginx
docker images
docker images(也可以写成docker image ls)命令用于列出本地Docker主机中的镜像,默认不加选项参数情况下,过滤掉中间映像层,如果加上-a选项,则会列出本地所有镜像,如:
可以看出,我的环境中已经下载了2个镜像:nginx、tomcat。
输出中每一列含义如下:
- REPOSITORY:镜像名称
- TAG:用于和镜像名称确定镜像。
- IMAGE ID:镜像ID,可用镜像ID唯一标志一个镜像。
- CREATED:镜像创建时间
- SIZE:镜像大小
docker pull <镜像名称>:<标签/>/镜像ID
docker pull 命令用于从仓库中拉取镜像到本地,可以通过TAG指定镜像版本,如果镜像名称后不加TAG则默认拉取的是latest版本的镜像。
下面是拉取nginx 1.14版本的镜像到本地:
docker rmi <镜像名称>:<标签/>/镜像ID
docker rmi命令(也可以写成docker image rm)可以通过镜像名称或镜像ID来删除本地镜像:
如果想要同时删除多个镜像,可以将它们的ID或者名称作为参数传入,每个镜像之间用空格分开,如:
docker rmi nginx tomcat mysql
注意:删除镜像前需确保没有正在运行该镜像创建的容器存在,如果你想删除的镜像有基于其启动的容器运行,而你又确实想删除它,那么可以加上参数-f:
//-f 强制删除镜像
docker rmi -f nginx
docker save
docker save用于将 Docker镜像 保存成 tar 包,一般和-o参数一起使用,表示打包到哪个文件。
docker load
docker load命令用于导入镜像 ,一般和参数-i使用表示从哪个文件导入镜像。
docker tag
docker tag 用于给镜像打标签,以下命令是给nginx:1.14再打一个标签为nginx:newtag,而且可以看出新打出来的标签对应的image id和nginx:1.14相同。
docker image inspect <镜像名称>:<标签/>/镜像ID
docker image inspect用于显示指定镜像的详细信息:
再谈容器
现在已经对镜像有所了解,是时候开始介绍容器了。
首先我们想一下现实生活中的容器是什么呢?
装东西用的都可以称为容器,比如“瓶子”、“箱子”、“水杯”、“集装箱”等等。
我们再想一下容器的作用是什么?说白了就是“装东西”,为了方便我们搬用。
比如把水装到瓶子里,我们只要拿好瓶子,就能轻轻松松的把水带到任何地方。
计算机世界里的容器概念也一样,它的作用也是“装东西”,只不过不是装水了,而是装:代码、环境、运行时、配置文件、系统文件、设置等等。
我们可以简单理解为:保证程序运行的对象都可以装到容器中。
Docker容器简介
如果学术一点介绍,容器就是镜像的运行时实例。我们可以从单个镜像上启动一个或多个容器。
下图为使用单个Docker镜像启动多个容器的示意图。
启动容器的简便方式是使用docker container run命令,其实命令中的container不要也可以。
docker container run <image>
# 或
docker run <image>
该命令可以携带很多参数,后面会详细介绍。
同样,还有其它一些命令用于管理容器:
# 停止正在运行的容器
docker container stop
# 启动容器
docker container start
# 删除容器
docker container rm
这些命令后面都会详细介绍。
容器vs虚拟机
容器和虚拟机都依赖于宿主机才能运行。宿主机可以是笔记本,是数据中心的物理服务器,也可以是公有云的某个实例。
虚拟机和容器最大的区别是容器更快并且更轻量级——与虚拟机运行在完整的操作系统之上相比,容器会共享其所在主机的操作系统/内核。
在下面的示例中,假设宿主机是一台需要运行4个业务应用的物理服务器。
虚拟机模型
在虚拟机模型中,首先要开启物理机并启动Hypervisor引导程序。
一旦Hypervisor启动,就会占有机器上的全部物理资源,如CPU、RAM、存储和NIC。Hypervisor接下来就会将这些物理资源划分为虚拟资源,并且看起来与真实物理资源完全一致。
然后Hypervisor会将这些资源打包进一个叫作虚拟机(VM)的软件结构当中。这样用户就可以使用这些虚拟机,并在其中安装操作系统和应用。
前面提到需要在物理机上运行4个应用,所以在Hypervisor之上需要创建4个虚拟机并安装4个操作系统,然后安装4个应用。
容器模型
服务器启动之后,所选择的操作系统(OS)如Linux会启动。
与虚拟机模型相同,OS也占用了全部硬件资源。在OS层之上,需要安装容器引擎(如Docker)。容器引擎可以获取系统资源,比如进程树、文件系统以及网络栈,接着将资源分割为安全的互相隔离的资源结构,称之为容器。
每个容器看起来就像一个真实的操作系统,在其内部可以运行应用。按照前面的假设,需要在物理机上运行4个应用。因此,需要划分出4个容器并在每个容器中运行一个应用。
两种模型的对比
虚拟机模型将底层硬件资源划分到虚拟机当中。每个虚拟机都是包含了虚拟CPU、虚拟RAM、虚拟磁盘等资源的一种软件结构。因此,每个虚拟机都需要有自己的操作系统来声明、初始化并管理这些虚拟资源。
但是操作系统本身是有其额外开销的。例如,每个操作系统都消耗一点CPU、一点RAM、一点存储空间等。
容器模型共享一个操作系统/内核。这意味着只有一个操作系统消耗CPU、RAM和存储资源。
此外容器并不是完整的操作系统,其启动要远比虚拟机快:因为容器内部并不需要内核,也就没有定位、解压以及初始化的过程——更不用提在内核启动过程中对硬件的遍历和初始化了。这些在容器启动的过程中统统都不需要!
最终结果就是,容器几乎可以在1s内启动wan’c。唯一对容器启动时间有影响的就是容器内应用启动所花费的时间。
简而言之,容器模型要比虚拟机模型简洁并且高效的原因了。
容器操作相关命令
下面介绍Docker中与容器操作相关的命令。
docker run
docker run命令用于创建一个新的容器并运行,docker run命令可以指定很多选项,常用的命令选项有:
- –name: 给容器取一个名字,如果不指定,则会自动随机分配一个名称
- –restart:指定容器停止后的重启策略.
- –network:指定网络
- -d:以后台模式运行
- -p 主机端口:容器端口:指定端口映射,将容器内服务的端口映射在宿主机的指定端口
- -it:使当前的终端连接到容器的Shell,并启动交互模式这使得您可以在容器内执行命令,相当于在您本地机器上操作一样。
- -v:绑定一个数据卷
- -e:设置环境变量,容器中可以使用该环境变量
- -m: 最多为容器分配多少内存,以防止其占用过多的系统资源。
- –rm=false:指定容器启动后自动删除容器,不能和-d选项共存
其中,–restart选项可以设置的取值如下:
no 默认策略,在容器退出时不重启容器
on-failure 在容器非正常退出时(退出状态非0),才会重启容器
on-failure:3 在容器非正常退出时重启容器,最多重启3次
always 意味着容器崩溃总是自动重启容器
unless-stopped 在容器退出时总是重启容器,但是不考虑在Docker守护进程启动时就已经停止了的容器
例如:
docker run --name my_container \
--restart always \
--network my_network \
-d \
-p 8080:80 \
-it \
-v /my/local/data:/app/data \
-e ENVIRONMENT=production \
-m 512m \
--rm \
nginx /bin/bash
这样就启动了一个名为my_container的nginx容器。
上面有些参数涉及到容器其它内容,不理解没关系,后面的文章都会详细介绍的。
docker ps
docker ps命令将会列出当前宿主机中所有运行中的容器。
如果加上参数-a,则会列出当前服务器中所有的容器,无论是否在运行。
如果加上参数-q,则仅列出CONTAINER ID 这一列字段。
docker ps
docker ps -a
docker ps -q
输出结果中每一列含义:
- CONTAINER ID:每个容器的唯一标识符号
- IMAGE:该容器是基于哪个镜像创建的
- COMMAND:运行容器时的命令
- CREATED:容器创建的时间
- STATUS:容器的运行状态
- PORTS:容器开放的端口信息
- NAME:容器的别名,在运行容器执行docker run 时可使用 --name进行指定。
docker ps有一个选项-f用以过滤输出。-f选项后接不同的参数以实现不同的过滤,常用的参数如下:
//根据容器名称过滤
docker ps -a -f name=xxx
//根据容器id过滤
docker ps -a -f id=xxx
//根据容器状态过滤
docker ps -a -f status=xxx
//根据容器连接的网络过滤
docker ps -a -f network=xxx
//根据容器的数据卷过滤
docker ps -a -f volume=xxx
还可以通过–formart {{.Names}}选项格式化输出列表
//输出容器的名称
docker ps -a --format {{.Names}}
//输出容器的ID
docker ps -a --format {{.ID}}
//输出启动容器的镜像的ID
docker ps -a --format {{.Image}}
//输出容器的启动的令
docker ps -a --format {{.Command}}
//输出创建容器的时间点
docker ps -a --format {{.CreatedAt}}
//输出容器开放的端口信息
docker ps -a --format {{.Ports}}
//输出容器的状态
docker ps -a --format {{.Status}}
//输出容器硬盘的大小
docker ps -a --format {{.Size}}
docker rm
docker rm 容器id(或容器名)
docker rm :删除一个或多个容器。docker rm命令只能删除处于终止或退出状态的容器,并不能删除还处于运行状态的容器。
如果需要强制删除一个正在运行的容器,需要加上选项-f:
如果需要删除环境中所有正在运行的容器,使用以下命令即可:
docker rm $(docker ps -q)
docker exec
通过docker exec命令可以进入容器内部进行操作,根据容器操作系统的不同,分为两种情况:
#有bash命令的linux系统:例如centos
docker exec -it 容器id(或容器名) /bin/bash
#没有bash命令的linux系统:例如alpine系统
docker exec -it 容器id(或容器名) sh
docker inspect 容器名/容器运行ID
docker inspect命令用于获取容器的详细信息如容器的状态,容器的网络信息,容器的卷信息等等。
docker inspect 容器id(或容器名)
docker stop 容器名/容器运行ID
docker stop命令用于停止正在运行的容器
docker restart 容器名/容器运行ID
docker restart命令用于启动已停止的容器。
docker create 容器名/容器运行ID
docker create命令用于新建一个容器但是并不启动,容器会处于初建状态(需要使用docker ps -a才能查看到该容器),如果需要启动需要再使用docker start命令启动。