Docker概述和介绍

Docker概述

Docker 最初是 dotCloud 公司创始人 Solomon Hykes 在法国期间发起的一个公司内部项目,它是基于 dotCloud 公司多年云服务技术的一次革新,并于 2013 年 3 月以 Apache 2.0 授权协议开源,主要项目代码在 GitHub 上进行维护。Docker 项目后来还加入了 Linux 基金会,并成立推动 开放容器联盟(OCI)。

Docker 自开源后受到广泛的关注和讨论,至今其 GitHub 项目 已经超过 5 万 4 千个星标和一万多个 fork。甚至由于 Docker 项目的火爆,在 2013 年底,dotCloud 公司决定改名为 Docker。Docker 最初是在 Ubuntu 12.04 上开发实现的;Red Hat 则从 RHEL 6.5 开始对 Docker 进行支持;Google 也在其 PaaS 产品中广泛应用 Docker。

Docker 项目的目标是实现轻量级的操作系统虚拟化解决方案。 Docker 的基础是 Linux 容器(LXC)等技术。

在 LXC 的基础上 Docker 进行了进一步的封装,让用户不需要去关心容器的管理,使得操作更为简便。用户操作 Docker 的容器就像操作一个快速轻量级的虚拟机一样简单。

下面的图片比较了 Docker 和传统虚拟化方式的不同之处,可见容器是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,而传统方式则是在硬件层面实现。

Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroup,namespace,以及 AUFS 类的 Union FS 等技术,对进程进行封装隔离,属于 操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 开始,则进一步演进为使用 runC 和 containerd。

  • runc 是一个 Linux 命令行工具,用于根据 OCI容器运行时规范 创建和运行容器。

  • containerd 是一个守护程序,它管理容器生命周期,提供了在一个节点上执行容器和管理镜像的最小功能集。

Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。

下面的图片比较了 Docker 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。

使用 Docker的好处

  • 更高效的利用系统资源

由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销,Docker 对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。

更快速的启动时间

传统的虚拟机技术启动应用服务往往需要数分钟,而 Docker 容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。

  • 一致的运行环境

开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 「这段代码在我机器上没问题啊」 这类问题。

  • 持续交付和部署

对开发和运维(DevOps)人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。

使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile 来进行镜像构建,并结合 持续集成(Continuous Integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署。

  • 更轻松的迁移

由于 Docker 确保了执行环境的一致性,使得应用的迁移更加容易。Docker 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。

  • 更轻松的维护和扩展
    Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。

Docker 包括三个基本概念

镜像(Image)容器(Container)仓库(Repository)

镜像(Image)

在 Docker 的术语里,一个只读层被称为镜像,一个镜像是永久不会变的。

由于 Docker 使用一个统一文件系统,Docker 进程认为整个文件系统是以读写方式挂载的。 但是所有的变更都发生顶层的可写层,而下层的原始的只读镜像文件并未变化。由于镜像不 可写,所以镜像是无状态的

对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:18.04 就包含了完整的一套 Ubuntu 18.04 最小系统的 root 文件系统。

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

  • 分层存储

因为镜像包含操作系统完整的 root 文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 ISO 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。

每一个镜像都可能依赖于由一个或多个下层的组成的另一个镜像。我们有时说,下层那个 镜像是上层镜像的父镜像。

基础镜像:一个没有任何父镜像的镜像,谓之基础镜像。

镜像ID:所有镜像都是通过一个 64 位十六进制字符串 (内部是一个 256 bit 的值)来标识的。 为简化使用,前 12 个字符可以组成一个短ID,可以在命令行中使用。短ID还是有一定的 碰撞机率,所以服务器总是返回长ID。

** 镜像的实现原理 **

Docker 镜像是怎么实现增量的修改和维护的? 每个镜像都由很多层次构成,Docker 使用 Union FS 将这些不同的层结合到一个镜像中去。

通常 Union FS 有两个用途, 一方面可以实现不借助 LVM、RAID 将多个 disk 挂到同一个目录下,另一个更常用的就是将一个只读的分支和一个可写的分支联合在一起,Live CD 正是基于此方法可以允许在镜像不变的基础上允许用户在其上进行一些写操作。 Docker 在 AUFS 上构建的容器也是利用了类似的原理。

1、获取镜像:可以使用 docker pull 命令来从仓库获取所需要的镜像。如:docker pull ubuntu:11.04下载过程中,会输出获取镜像的每一层信息。

该命令实际上相当于 $ sudo docker pull registry.hub.docker.com/ubuntu:11.04 命令,即从注册服务器registry.hub.docker.com 中的 ubuntu 仓库来下载标记为 11.04 的镜像。如果不指定具体的标记,则默认使用 latest 标记信息。

2、列出本地镜像:docker images 显示本地已有的镜像。

3、修改已有镜像

  • 通过镜像运行容器并且进入到容器内部 [root@localhost ~]# docker run -t -i centos /bin/bash

在里面更新一些信息之后,退出。通过命令 docker commit来提交容器副本。

1
2
[root@localhost ~]# docker commit -m="has update" -a="hu" ba251f00e5ca hu-centos:v1
sha256:027d38945df46bf73cd70b6df730a037aa494c8324205ea72d0c381cf2980909

其中,-m 来指定提交的说明信息,跟我们使用的版本控制工具一样;-a 可以指定更新的用户信息;之后是用来创建镜像的容器的 ID;最后指定目标镜像的仓库名和 tag 信息。创建成功后会返回这个镜像的 ID 信息。

使用 docker images 来查看新创建的镜像。

  • 利用 Dockerfile 来创建镜像

Dockerfile 基本的语法是

1
2
3
4
5
使用#来注释
FROM 指令告诉 Docker 使用哪个镜像作为基础
接着是维护者的信息
RUN开头的指令会在创建中运行,比如安装一个软件包
编写完成 Dockerfile 后可以使用 docker build 来生成镜像。

后面具体说明使用文件build镜像。

  • 从本地文件系统导入

要从本地文件系统导入一个镜像,可以使用 openvz(容器虚拟化的先锋技术)的模板来创建: openvz 的模板下载地址为 templates 。

比如,先下载了一个 ubuntu-14.04 的镜像,之后使用以下命令导入:cat ubuntu-11.04-x86_64-minimal.tar.gz |docker import - ubuntu:11.04

4、上传镜像

通过 docker push 命令,把自己创建的镜像上传到仓库中来共享.

5、存出镜像

如果要导出镜像到本地文件,可以使用 docker save 命令。

$sudo docker save -o ubuntu_11.04.tar ubuntu:11.04

6、载入镜像

可以使用 docker load 从导出的本地文件中再导入到本地镜像库

$ sudo docker load –input ubuntu_11.04.tar($ sudo docker load < ubuntu_11.04.tar)\

7、移除本地镜像

如果要移除本地的镜像,可以使用 docker rmi 命令。

容器(Container)

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。

前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层。

容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。

数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。

启动容器

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。

docker run

运行并启动一个 bash 终端,允许用户进行交互。$ sudo docker run -t -i ubuntu:11.04 /bin/bash

其中,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。

当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:

1
2
3
4
5
6
7
检查本地是否存在指定的镜像,不存在就从公有仓库下载
利用镜像创建并启动一个容器
分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
从地址池配置一个 ip 地址给容器
执行用户指定的应用程序
执行完毕后容器被终止

可以利用 docker start 命令,直接将一个已经终止的容器启动运行。

守护态运行

更多的时候,需要让 Docker 容器在后台以守护态(Daemonized)形式运行。此时,可以通过添加 -d 参数来实现。

查日志:$ sudo docker logs CONTAINER_ID

终止容器

可以使用 docker stop 来终止一个运行中的容器。

此外,当Docker容器中指定的应用终结时,容器也自动终止。 例如对于上一章节中只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。

终止状态的容器可以用 docker ps -a 命令看到。

进入容器

使用docker attach 命令或 nsenter 工具等。

attach 命令:docker attach 是Docker自带的命令。如:$ sudo docker run -idt ubuntu

$sudo docker attach CONTAINER_ID

attach 命令有时候并不方便。当多个窗口同时 attach 到同一个容器的时候,所有窗口都会同步显示。当某个窗口因命令阻塞时,其他窗口也无法执行操作了。

导出容器

$ sudo docker export 7691a814370e > ubuntu.tar

这样将导出容器快照到本地文件。

导入容器快照

$ cat ubuntu.tar | sudo docker import - test/ubuntu:v1.0

也可以通过指定 URL 或者某个目录来导入,例如

删除容器:如果要删除一个运行中的容器,可以添加 -f 参数。

仓库(Repository)

一个 Docker Registry 中可以包含多个 仓库(Repository);每个仓库可以包含多个 标签(Tag);每个标签对应一个镜像。

通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。

私有 Docker Registry
除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。在 私有仓库 一节中,会有进一步的搭建私有 Registry 服务的讲解。

开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持 docker 命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。在官方的商业化版本 Docker Trusted Registry 中,提供了这些高级功能。

除了官方的 Docker Registry 外,还有第三方软件实现了 Docker Registry API,甚至提供了用户界面以及一些高级功能。比如,Harbor 和 Sonatype Nexus。

数据卷volumes

数据卷

数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:

1
2
3
4
数据卷可以在容器之间共享和重用
对数据卷的修改会立马生效
对数据卷的更新,不会影响镜像
卷会一直存在,直到没有容器使用

类似于 Linux 下对目录或文件进行 mount。

创建一个数据卷

如果你有一些持续更新的数据需要在容器之间共享,最好创建数据卷容器。数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的。

1
2
3
4
5
6
7
8
9
10
11
12
$ docker volume create my-vol
查看所有的 数据卷
$ docker volume ls
local my-vol
在主机里使用以下命令可以查看指定 数据卷 的信息
$ docker volume inspect my-vol
```
#### 启动一个挂载数据卷的容器

在用 docker run 命令的时候,使用 --mount 标记来将 数据卷 挂载到容器里。在一次 docker run 中可以挂载多个 数据卷。

下面创建一个名为 web 的容器,并加载一个 数据卷 到容器的 /webapp 目录。

$ docker run -d -P
–name web
# -v my-vol:/wepapp
–mount source=my-vol,target=/webapp
training/webapp
python app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#### 查看数据卷的具体信息

$ docker inspect web

#### 删除数据卷

$ docker volume rm my-vol


### 挂载主机目录

#### 挂载一个主机目录作为数据卷

使用 --mount 标记可以指定挂载一个本地主机的目录到容器中去。

$ docker run -d -P
–name web
# -v /src/webapp:/opt/webapp
–mount type=bind,source=/src/webapp,target=/opt/webapp
training/webapp
python app.py

1
2
3
-v 参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹,现在使用 --mount 参数时如果本地目录不存在,Docker 会报错。

Docker 挂载主机目录的默认权限是 读写,用户也可以通过增加 readonly 指定为 只读。

$ docker run -d -P
–name web
# -v /src/webapp:/opt/webapp:ro
–mount type=bind,source=/src/webapp,target=/opt/webapp,readonly
training/webapp
python app.py

1
2
3
#### 挂载一个本地主机文件作为数据卷

--mount 标记也可以从主机挂载单个文件到容器中

$ docker run –rm -it \

-v $HOME/.bash_history:/root/.bash_history \

–mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history
ubuntu:18.04
bash

root@2affd44b4667:/# history
1 ls
2 diskutil list



### 利用数据卷容器来备份、恢复、迁移数据卷
可以利用数据卷对其中的数据进行进行备份、恢复和迁移。

备份

首先使用 --volumes-from 标记来创建一个加载 dbdata 容器卷的容器,并从本地主机挂载当前到容器的 /backup 目录。命令如下:

$ sudo docker run --volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

容器启动后,使用了 tar 命令来将 dbdata 卷备份为本地的 /backup/backup.tar。

恢复

如果要恢复数据到一个容器,首先创建一个带有数据卷的容器 dbdata2。

$ sudo docker run -v /dbdata --name dbdata2 ubuntu /bin/bash


然后创建另一个容器,挂载 dbdata2 的容器,并使用 untar 解压备份文件到挂载的容器卷中。

$ sudo docker run --volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf
/backup/backup.tar