菜鸟学docker(二)

Docker中文文档

Image 镜像

介绍

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


挂载的含义:什么是挂载?mount的用处在哪?

  1. 提一句Windows下,mount挂载,就是给磁盘分区提供一个盘符(C,D,E,…)。比如插入U盘后系统自动分配给了它I:盘符其实就是挂载,退优盘的时候进行安全弹出,其实就是卸载unmount。
  2. Linux下,不像Windows可以有C,D,E,多个目录,Linux只有一个根目录/。在装系统时,我们分配给linux的所有区都在/下的某个位置,比如/home等等。
  3. 提问者插入了新硬盘,分了新磁盘区sdb1。它现在还不属于/。
  4. 我们虽然可以在一些图形桌面系统里找到他的位置,浏览管理里面的文件,但在命令行却不知怎么访问它的目录,比如无法使用cd或者ls。也无法在编程时指定一个目录对它操作。
  5. 这时提问者使用了 mount /dev/sdb1 ~/Share/ ,把新硬盘的区sdb1挂载到工作目录的~/Share/文件夹下,之后访问这个~/Share/文件夹就相当于访问这个硬盘2的sdb1分区了。对/Share/的任何操作,都相当于对sdb1里文件的操作。
  6. 所以Linux下,mount挂载的作用,就是将一个设备(通常是存储设备)挂接到一个已存在的目录上。访问这个目录就是访问该存储设备。
  7. linux操作系统将所有的设备都看作文件,它将整个计算机的资源都整合成一个大的文件目录。我们要访问存储设备中的文件,必须将文件所在的分区挂载到一个已存在的目录上,然后通过访问这个目录来访问存储设备。挂载就是把设备放在一个目录下,让系统知道怎么管理这个设备里的文件,了解这个存储设备的可读写特性之类的过程。
  8. 我们不是有/dev/sdb1 吗,直接对它操作不就行了?这不是它的目录吗?
  9. 这不是它的目录。虽然/dev是个目录,但/dev/sdb1不是目录。可以发现ls/dev/sdb1无法执行。/dev/sdb1,是一个类似指针的东西,指向这个分区的原始数据块。mount前,系统并不知道这个数据块哪部分数据代表文件,如何对它们操作。
  10. 插入CD,系统其实自动执行了 mount /dev/cdrom /media/cdrom。所以可以直接在/media/cdrom中对CD中的内容进行管理。

pic2

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

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

获取镜像

可以使用 docker pull 命令来从仓库获取所需要的镜像。
下面的例子将从 Docker Hub 仓库下载一个 Ubuntu 12.04 操作系统的镜像。

1
2
3
4
5
6
7
8
9
$ sudo docker pull ubuntu:12.04
Pulling repository ubuntu
ab8e2728644c: Pulling dependent layers
511136ea3c5a: Download complete
5f0ffaa9455e: Download complete
a300658979be: Download complete
904483ae0c30: Download complete
ffdaafd1ca50: Download complete
d047ae21eeaf: Download complete

下载过程中,会输出获取镜像的每一层信息。

该命令实际上相当于 $ sudo docker pull registry.hub.docker.com/ubuntu:12.04 命令,即从注册服务器registry.hub.docker.com 中的 ubuntu 仓库来下载标记为 12.04 的镜像。

有时候官方仓库注册服务器下载较慢,可以从其他仓库下载。 从其它仓库下载时需要指定完整的仓库注册服务器地址。例如

1
2
3
4
5
6
7
8
9
$ sudo docker pull dl.dockerpool.com:5000/ubuntu:12.04
Pulling dl.dockerpool.com:5000/ubuntu
ab8e2728644c: Pulling dependent layers
511136ea3c5a: Download complete
5f0ffaa9455e: Download complete
a300658979be: Download complete
904483ae0c30: Download complete
ffdaafd1ca50: Download complete
d047ae21eeaf: Download complete

完成后,即可随时使用该镜像了,例如创建一个容器,让其中运行 bash 应用。

1
2
sudo docker run -t -i ubuntu:12.04 /bin/bash
root@fe7fc4bd8fc9:/#

列出本地镜像

使用 docker images 显示本地已有的镜像。

1
2
3
4
5
6
7
8
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu 12.04 74fe38d11401 4 weeks ago 209.6 MB
ubuntu precise 74fe38d11401 4 weeks ago 209.6 MB
ubuntu 14.04 99ec81b80c55 4 weeks ago 266 MB
ubuntu latest 99ec81b80c55 4 weeks ago 266 MB
ubuntu trusty 99ec81b80c55 4 weeks ago 266 MB
...

在列出信息中,可以看到几个字段信息
来自于哪个仓库,比如 ubuntu
镜像的标记,比如 14.04
它的 ID 号(唯一)
创建时间
镜像大小
其中镜像的 ID 唯一标识了镜像,注意到 ubuntu:14.04 和 ubuntu:trusty 具有相同的镜像 ID,说明它们实际上是同一镜像。

TAG 信息用来标记来自同一个仓库的不同镜像。例如 ubuntu 仓库中有多个镜像,通过 TAG 信息来区分发行版本,例如 10.04、12.04、12.10、13.04、14.04 等。例如下面的命令指定使用镜像 ubuntu:14.04 来启动一个容器。

1
$ sudo docker run -t -i ubuntu:14.04 /bin/bash

如果不指定具体的标记,则默认使用 latest 标记信息。

创建镜像

创建镜像有很多方法,用户可以从 Docker Hub 获取已有镜像并更新,也可以利用本地文件系统创建一个。

修改已有镜像

先使用下载的镜像启动容器。

1
2
$ sudo docker run -t -i training/sinatra /bin/bash
root@0b2616b0e5a8:/#

注意:记住容器的 ID,稍后还会用到。

在容器中添加 json 和 gem 两个应用。

1
root@0b2616b0e5a8:/# gem install json

当结束后,我们使用 exit 来退出,现在我们的容器已经被我们改变了,使用 docker commit 命令来提交更新后的副本。

1
2
$ sudo docker commit -m "Added json gem" -a "Docker Newbee" 0b2616b0e5a8 ouruser/sinatra:v2
4f177bd27a9ff0f6dc2a830403925b5360bfe0b93d476f7fc3231110e7f71b1c

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

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

1
2
3
4
5
$sudo docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
training/sinatra latest 5bc342fa0b91 10 hours ago 446.7 MB
ouruser/sinatra v2 3c59e02ddd1a 10 hours ago 446.7 MB
ouruser/sinatra latest 5db5f8471261 10 hours ago 446.7 MB

之后,可以使用新的镜像来启动容器

1
2
$ sudo docker run -t -i ouruser/sinatra:v2 /bin/bash
root@78e82f680994:/#

我们可以看到这里修改镜像是与类的概念不同,这里是通过容器来修改镜像的,而我们的类修改是不会这样的。

利用 Dockerfile 来创建镜像

使用 docker commit 来扩展一个镜像比较简单,但是不方便在一个团队中分享。我们可以使用 docker build来创建一个新的镜像。为此,首先需要创建一个 Dockerfile,包含一些如何创建镜像的指令。

新建一个目录和一个 Dockerfile

1
2
3
$ mkdir sinatra
$ cd sinatra
$ touch Dockerfile

Dockerfile 中每一条指令都创建镜像的一层,例如:

1
2
3
4
5
6
# This is a comment
FROM ubuntu:14.04
MAINTAINER Docker Newbee <newbee@docker.com>
RUN apt-get -qq update
RUN apt-get -qqy install ruby ruby-dev
RUN gem install sinatra

Dockerfile 基本的语法是

  • 使用#来注释
  • FROM 指令告诉 Docker 使用哪个镜像作为基础
  • 接着是维护者的信息
  • RUN开头的指令会在创建中运行,比如安装一个软件包,在这里使用 apt-get 来安装了一些软件

编写完成 Dockerfile 后可以使用 docker build 来生成镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ sudo docker build -t="ouruser/sinatra:v2" .
Uploading context 2.56 kB
Uploading context
Step 0 : FROM ubuntu:14.04
---> 99ec81b80c55
Step 1 : MAINTAINER Newbee <newbee@docker.com>
---> Running in 7c5664a8a0c1
---> 2fa8ca4e2a13
Removing intermediate container 7c5664a8a0c1
Step 2 : RUN apt-get -qq update
---> Running in b07cc3fb4256
---> 50d21070ec0c
Removing intermediate container b07cc3fb4256
Step 3 : RUN apt-get -qqy install ruby ruby-dev
---> Running in a5b038dd127e
Selecting previously unselected package libasan0:amd64.
(Reading database ... 11518 files and directories currently installed.)
Preparing to unpack .../libasan0_4.8.2-19ubuntu1_amd64.deb ...
Setting up ruby (1:1.9.3.4) ...
Setting up ruby1.9.1 (1.9.3.484-2ubuntu1) ...
Processing triggers for libc-bin (2.19-0ubuntu6) ...
---> 2acb20f17878
Removing intermediate container a5b038dd127e
Step 4 : RUN gem install sinatra
---> Running in 5e9d0065c1f7
. . .
Successfully installed rack-protection-1.5.3
Successfully installed sinatra-1.4.5
4 gems installed
---> 324104cde6ad
Removing intermediate container 5e9d0065c1f7
Successfully built 324104cde6ad

其中 -t 标记来添加 tag,指定新的镜像的用户信息。 “.” 是 Dockerfile 所在的路径(当前目录),也可以替换为一个具体的 Dockerfile 的路径。

可以看到 build 进程在执行操作。它要做的第一件事情就是上传这个 Dockerfile 内容,因为所有的操作都要依据 Dockerfile 来进行。 然后,Dockfile 中的指令被一条一条的执行。每一步都创建了一个新的容器,在容器中执行指令并提交修改(就跟之前介绍过的 docker commit 一样)。当所有的指令都执行完毕之后,返回了最终的镜像 id。所有的中间步骤所产生的容器都被删除和清理了。

  • 注意一个镜像不能超过 127 层

此外,还可以利用 ADD 命令复制本地文件到镜像;用 EXPOSE 命令来向外部开放端口;用 CMD 命令来描述容器启动后运行的程序等。例如

1
2
3
4
5
6
# put my local web site in myApp folder to /var/www
ADD myApp /var/www
# expose httpd port
EXPOSE 80
# the command to run
CMD ["/usr/sbin/apachectl", "-D", "FOREGROUND"]

现在可以利用新创建的镜像来启动一个容器。

1
2
$ sudo docker run -t -i ouruser/sinatra:v2 /bin/bash
root@8196968dac35:/#

还可以用 docker tag 命令来修改镜像的标签。

1
2
3
4
5
6
$ sudo docker tag 5db5f8471261 ouruser/sinatra:devel
$ sudo docker images ouruser/sinatra
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ouruser/sinatra latest 5db5f8471261 11 hours ago 446.7 MB
ouruser/sinatra devel 5db5f8471261 11 hours ago 446.7 MB
ouruser/sinatra v2 5db5f8471261 11 hours ago 446.7 MB

从本地文件系统导入

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

比如,先下载了一个 ubuntu-14.04 的镜像,之后使用以下命令导入:

1
sudo cat ubuntu-14.04-x86_64-minimal.tar.gz  |docker import - ubuntu:14.04

然后查看新导入的镜像。

1
2
3
docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu 14.04 05ac7c0b9383 17 seconds ago 215.5 MB

上传镜像

用户可以通过 docker push 命令,把自己创建的镜像上传到仓库中来共享。例如,用户在 Docker Hub 上完成注册后,可以推送自己的镜像到仓库中。

存出和载入镜像

存出镜像

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

1
2
3
4
5
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu 14.04 c4ff7513909d 5 weeks ago 225.4 MB
...
$sudo docker save -o ubuntu_14.04.tar ubuntu:14.04

载入镜像

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

1
$ sudo docker load --input ubuntu_14.04.tar


1
$ sudo docker load < ubuntu_14.04.tar

这将导入镜像以及其相关的元数据信息(包括标签等)。

移除本地镜像

如果要移除本地的镜像,可以使用 docker rmi 命令。注意 docker rm 命令是移除容器。

1
2
3
4
5
$ sudo docker rmi training/sinatra
Untagged: training/sinatra:latest
Deleted: 5bc342fa0b91cabf65246837015197eecfa24b2213ed6a51a8974ae250fedd8d
Deleted: ed0fffdcdae5eb2c3a55549857a8be7fc8bc4241fb19ad714364cbfd7a56b22f
Deleted: 5c58979d73ae448df5af1d8142436d81116187a7633082650549c52c3a2418f0

  • 注意:在删除镜像之前要先用 docker rm 删掉依赖于这个镜像的所有容器。

镜像的实现原理

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

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

Docker容器

启动容器

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

因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。

新建并启动

所需要的命令主要为 docker run。

例如,下面的命令输出一个 “Hello World”,之后终止容器。

1
2
$ sudo docker run ubuntu:14.04 /bin/echo 'Hello world'
Hello world

这跟在本地直接执行 /bin/echo ‘hello world’ 几乎感觉不出任何区别。

下面的命令则启动一个 bash 终端,允许用户进行交互。

1
2
$ sudo docker run -t -i ubuntu:14.04 /bin/bash
root@af8bae53bdd3:/#

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

在交互模式下,用户可以通过所创建的终端来输入命令,例如

1
2
3
4
root@af8bae53bdd3:/# pwd
/
root@af8bae53bdd3:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

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

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

    启动已终止容器

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

容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 ps 或 top 来查看进程信息。

1
2
3
4
root@ba267838cc1b:/# ps
PID TTY TIME CMD
1 ? 00:00:00 bash
11 ? 00:00:00 ps

可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。

守护态运行

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

例如下面的命令会在后台运行容器。

1
2
$ sudo docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
1e5535038e285177d5214659a068137486f96ee5c2e85a4ac52dc83f2ebe4147

容器启动后会返回一个唯一的 id,也可以通过 docker ps 命令来查看容器信息。

1
2
3
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1e5535038e28 ubuntu:14.04 /bin/sh -c 'while tr 2 minutes ago Up 1 minute insane_babbage

要获取容器的输出信息,可以通过 docker logs 命令。

1
2
3
4
5
$ sudo docker logs insane_babbage
hello world
hello world
hello world
. . .

终止容器

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

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

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

1
2
3
4
sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba267838cc1b ubuntu:14.04 "/bin/bash" 30 minutes ago Exited (0) About a minute ago trusting_newton
98e5efa7d997 training/webapp:latest "python app.py" About an hour ago Exited (0) 34 minutes ago backstabbing_pike

处于终止状态的容器,可以通过 docker start 命令来重新启动。

此外,docker restart 命令会将一个运行态的容器终止,然后再重新启动它。

进入容器

在使用 -d 参数时,容器启动后会进入后台。 某些时候需要进入容器进行操作,有很多种方法,包括使用docker attach 命令或 nsenter 工具等。

attach 命令

docker attach 是Docker自带的命令。下面示例如何使用该命令。

1
2
3
4
5
6
7
$ sudo docker run -idt ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia
$sudo docker attach nostalgic_hypatia
root@243c32535da7:/#

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

nsenter 命令

安装

nsenter 工具在 util-linux 包2.23版本后包含。 如果系统中 util-linux 包没有该命令,可以按照下面的方法从源码安装。

1
2
3
$ cd /tmp; curl https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz | tar -zxf-; cd util-linux-2.24;
$ ./configure --without-ncurses
$ make nsenter && sudo cp nsenter /usr/local/bin

使用

nsenter 可以访问另一个进程的名字空间。nsenter 要正常工作需要有 root 权限。 很不幸,Ubuntu 14.04 仍然使用的是 util-linux 2.20。安装最新版本的 util-linux(2.24)版,请按照以下步骤:

1
2
3
4
$ wget https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz; tar xzvf util-linux-2.24.tar.gz
$ cd util-linux-2.24
$ ./configure --without-ncurses && make nsenter
$ sudo cp nsenter /usr/local/bin

为了连接到容器,你还需要找到容器的第一个进程的 PID,可以通过下面的命令获取。

1
PID=$(docker inspect --format "{{ .State.Pid }}" <container>)

通过这个 PID,就可以连接到这个容器:

1
$ nsenter --target $PID --mount --uts --ipc --net --pid

下面给出一个完整的例子。

1
2
3
4
5
6
7
8
9
$ sudo docker run -idt ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia
$ PID=$(docker-pid 243c32535da7)
10981
$ sudo nsenter --target 10981 --mount --uts --ipc --net --pid
root@243c32535da7:/#

导出和导入容器

导出容器

如果要导出本地某个容器,可以使用 docker export 命令。

1
2
3
4
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7691a814370e ubuntu:14.04 "/bin/bash" 36 hours ago Exited (0) 21 hours ago test
$ sudo docker export 7691a814370e > ubuntu.tar

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

导入容器快照

可以使用 docker import 从容器快照文件中再导入为镜像,例如

1
2
3
4
$ cat ubuntu.tar | sudo docker import - test/ubuntu:v1.0
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
test/ubuntu v1.0 9d37a6082e97 About a minute ago 171.3 MB

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

1
$sudo docker import http://example.com/exampleimage.tgz example/imagerepo
  • 注:用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。

删除容器

可以使用 docker rm 来删除一个处于终止状态的容器。 例如

1
2
$sudo docker rm  trusting_newton
trusting_newton

如果要删除一个运行中的容器,可以添加 -f 参数。Docker 会发送 SIGKILL 信号给容器。

数据卷volumes

数据卷

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

  • 数据卷可以在容器之间共享和重用
  • 对数据卷的修改会立马生效
  • 对数据卷的更新,不会影响镜像
  • 卷会一直存在,直到没有容器使用
  • 数据卷的使用,类似于 Linux 下对目录或文件进行 mount。

    创建一个数据卷

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

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

1
$ sudo docker run -d -P --name web -v /webapp training/webapp python app.py

  • 注意:也可以在 Dockerfile 中使用 VOLUME 来添加一个或者多个新的卷到由该镜像创建的任意容器。

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

    使用 -v 标记也可以指定挂载一个本地主机的目录到容器中去。
1
$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py

上面的命令加载主机的 /src/webapp 目录到容器的 /opt/webapp 目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,如果目录不存在 Docker 会自动为你创建它。

  • 注意:Dockerfile 中不支持这种用法,这是因为 Dockerfile 是为了移植和分享用的。然而,不同操作系统的路径格式不一样,所以目前还不能支持。

Docker 挂载数据卷的默认权限是读写,用户也可以通过 :ro 指定为只读。

1
2
$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp:ro
training/webapp python app.py

加了 :ro 之后,就挂载为只读了。

挂载一个本地主机文件作为数据卷

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

1
$ sudo docker run --rm -it -v ~/.bash_history:/.bash_history ubuntu /bin/bash

这样就可以记录在容器输入过的命令了。

  • 注意:如果直接挂载一个文件,很多文件编辑工具,包括 vi 或者 sed —in-place,可能会造成文件 inode 的改变,从 Docker 1.1 .0起,这会导致报错误信息。所以最简单的办法就直接挂载文件的父目录。

    数据卷容器

    如果你有一些持续更新的数据需要在容器之间共享,最好创建数据卷容器。

数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的。

首先,创建一个命名的数据卷容器 dbdata:

1
$ sudo docker run -d -v /dbdata --name dbdata training/postgres echo Data-only container for postgres

然后,在其他容器中使用 —volumes-from 来挂载 dbdata 容器中的数据卷。

1
2
$ sudo docker run -d --volumes-from dbdata --name db1 training/postgres
$ sudo docker run -d --volumes-from dbdata --name db2 training/postgres

还可以使用多个 —volumes-from 参数来从多个容器挂载多个数据卷。 也可以从其他已经挂载了数据卷的容器来挂载数据卷。

1
$ sudo docker run -d --name db3 --volumes-from db1 training/postgres
  • 注意:使用 —volumes-from 参数所挂载数据卷的容器自己并不需要保持在运行状态。

如果删除了挂载的容器(包括 dbdata、db1 和 db2),数据卷并不会被自动删除。如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时使用 docker rm -v 命令来指定同时删除关联的容器。 这可以让用户在容器之间升级和移动数据卷。具体的操作将在下一节中进行讲解。

利用数据卷容器来备份、恢复、迁移数据卷

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

备份

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

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

恢复

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

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

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

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

使用网络

外部访问容器

容器中可以运行一些网络应用,要让外部也可以访问这些应用,可以通过 -P 或 -p 参数来指定端口映射。

当使用 -P 标记时,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。

使用 docker ps 可以看到,本地主机的 49155 被映射到了容器的 5000 端口。此时访问本机的 49155 端口即可访问容器内 web 应用提供的界面。

1
2
3
4
$ sudo docker run -d -P training/webapp python app.py
$ sudo docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bc533791f3f5 training/webapp:latest python app.py 5 seconds ago Up 2 seconds 0.0.0.0:49155->5000/tcp nostalgic_morse

同样的,可以通过 docker logs 命令来查看应用的信息。

1
2
3
4
$ sudo docker logs -f nostalgic_morse
* Running on http://0.0.0.0:5000/
10.0.2.2 - - [23/May/2014 20:16:31] "GET / HTTP/1.1" 200 -
10.0.2.2 - - [23/May/2014 20:16:31] "GET /favicon.ico HTTP/1.1" 404 -

-p(小写的)则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的格式有ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort。

映射所有接口地址

使用 hostPort:containerPort 格式本地的 5000 端口映射到容器的 5000 端口,可以执行

1
$ sudo docker run -d -p 5000:5000 training/webapp python app.py

此时默认会绑定本地所有接口上的所有地址。

映射到指定地址的指定端口

可以使用 ip:hostPort:containerPort 格式指定映射使用一个特定地址,比如 localhost 地址 127.0.0.1

1
$ sudo docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

映射到指定地址的任意端口

使用 ip::containerPort 绑定 localhost 的任意端口到容器的 5000 端口,本地主机会自动分配一个端口。

1
$ sudo docker run -d -p 127.0.0.1::5000 training/webapp python app.py

还可以使用 udp 标记来指定 udp 端口

1
$ sudo docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

查看映射端口配置

使用 docker port 来查看当前映射的端口配置,也可以查看到绑定的地址

1
2
$ docker port nostalgic_morse 5000
127.0.0.1:49155.

注意:

  • 容器有自己的内部网络和 ip 地址(使用 docker inspect 可以获取所有的变量,Docker 还可以有一个可变的网络配置。)
  • -p 标记可以多次使用来绑定多个端口
    例如
    1
    $ sudo docker run -d -p 5000:5000  -p 3000:80 training/webapp python app.py

容器互联

容器的连接(linking)系统是除了端口映射外,另一种跟容器中应用交互的方式。

该系统会在源和接收容器之间创建一个隧道,接收容器可以看到源容器指定的信息。

自定义容器命名

连接系统依据容器的名称来执行。因此,首先需要自定义一个好记的容器命名。

虽然当创建容器的时候,系统默认会分配一个名字。自定义命名容器有2个好处:

  • 自定义的命名,比较好记,比如一个web应用容器我们可以给它起名叫web
  • 当要连接其他容器时候,可以作为一个有用的参考点,比如连接web容器到db容器
    使用 —name 标记可以为容器自定义命名。
    1
    $ sudo docker run -d -P --name web training/webapp python app.py

使用 docker ps 来验证设定的命名.

1
2
3
$ sudo docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aed84ee21bde training/webapp:latest python app.py 12 hours ago Up 2 seconds 0.0.0.0:49154->5000/tcp web

也可以使用 docker inspect 来查看容器的名字

1
2
$ sudo docker inspect -f "{{ .Name }}" aed84ee21bde
/web

注意:容器的名称是唯一的。如果已经命名了一个叫 web 的容器,当你要再次使用 web 这个名称的时候,需要先用docker rm 来删除之前创建的同名容器。

在执行 docker run 的时候如果添加 —rm 标记,则容器在终止后会立刻删除。注意,—rm 和 -d 参数不能同时使用。

容器互联

使用 —link 参数可以让容器之间安全的进行交互。

下面先创建一个新的数据库容器。

1
$ sudo docker run -d --name db training/postgres

删除之前创建的 web 容器

1
$ docker rm -f web

然后创建一个新的 web 容器,并将它连接到 db 容器

1
$ sudo docker run -d -P --name web --link db:db training/webapp python app.py

此时,db 容器和 web 容器建立互联关系。

—link 参数的格式为 —link name:alias,其中 name 是要链接的容器的名称,alias 是这个连接的别名。

使用 docker ps 来查看容器的连接

1
2
3
4
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
349169744e49 training/postgres:latest su postgres -c '/usr About a minute ago Up About a minute 5432/tcp db, web/db
aed84ee21bde training/webapp:latest python app.py 16 hours ago Up 2 minutes 0.0.0.0:49154->5000/tcp web

可以看到自定义命名的容器,db 和 web,db 容器的 names 列有 db 也有 web/db。这表示 web 容器链接到 db 容器,web 容器将被允许访问 db 容器的信息。

Docker 在两个互联的容器之间创建了一个安全隧道,而且不用映射它们的端口到宿主主机上。在启动 db 容器的时候并没有使用 -p 和 -P 标记,从而避免了暴露数据库端口到外部网络上。

Docker 通过 2 种方式为容器公开连接信息:

  • 环境变量
  • 更新 /etc/hosts 文件
    使用 env 命令来查看 web 容器的环境变量
1
2
3
4
5
6
7
8
9
$ sudo docker run --rm --name web2 --link db:db training/webapp env
. . .
DB_NAME=/web2/db
DB_PORT=tcp://172.17.0.5:5432
DB_PORT_5000_TCP=tcp://172.17.0.5:5432
DB_PORT_5000_TCP_PROTO=tcp
DB_PORT_5000_TCP_PORT=5432
DB_PORT_5000_TCP_ADDR=172.17.0.5
. . .

其中 DB_ 开头的环境变量是供 web 容器连接 db 容器使用,前缀采用大写的连接别名。

除了环境变量,Docker 还添加 host 信息到父容器的 /etc/hosts 的文件。下面是父容器 web 的 hosts 文件

1
2
3
4
5
$ sudo docker run -t -i --rm --link db:db training/webapp /bin/bash
root@aed84ee21bde:/opt/webapp# cat /etc/hosts
172.17.0.7 aed84ee21bde
. . .
172.17.0.5 db

这里有 2 个 hosts,第一个是 web 容器,web 容器用 id 作为他的主机名,第二个是 db 容器的 ip 和主机名。 可以在 web 容器中安装 ping 命令来测试跟db容器的连通。

1
2
3
4
5
6
root@aed84ee21bde:/opt/webapp# apt-get install -yqq inetutils-ping
root@aed84ee21bde:/opt/webapp# ping db
PING db (172.17.0.5): 48 data bytes
56 bytes from 172.17.0.5: icmp_seq=0 ttl=64 time=0.267 ms
56 bytes from 172.17.0.5: icmp_seq=1 ttl=64 time=0.250 ms
56 bytes from 172.17.0.5: icmp_seq=2 ttl=64 time=0.256 ms

用 ping 来测试db容器,它会解析成 172.17.0.5。 * 注意:官方的 ubuntu 镜像默认没有安装 ping,需要自行安装。

用户可以链接多个父容器到子容器,比如可以链接多个 web 到 db 容器上。

Dockerfile介绍

基本结构

Dockerfile 由一行行命令语句组成,并且支持以 # 开头的注释行。

一般的,Dockerfile 分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# This dockerfile uses the ubuntu image
# VERSION 2 - EDITION 1
# Author: docker_user
# Command format: Instruction [arguments / command] ..

# Base image to use, this must be set as the first line
FROM ubuntu

# Maintainer: docker_user <docker_user at email.com> (@docker_user)
MAINTAINER docker_user docker_user@email.com

# Commands to update the image
RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# Commands when creating a new container
CMD /usr/sbin/nginx

指令

指令的一般格式为 INSTRUCTION arguments,指令包括 FROM、MAINTAINER、RUN 等。

FROM

格式为 FROM 或FROM :

第一条指令必须为 FROM 指令。并且,如果在同一个Dockerfile中创建多个镜像时,可以使用多个 FROM 指令(每个镜像一次)。

MAINTAINER

格式为 MAINTAINER ,指定维护者信息。

RUN

格式为 RUN 或 RUN [“executable”, “param1”, “param2”]。

前者将在 shell 终端中运行命令,即 /bin/sh -c;后者则使用 exec 执行。指定使用其它终端可以通过第二种方式实现,例如 RUN [“/bin/bash”, “-c”, “echo hello”]。

每条 RUN 指令将在当前镜像基础上执行指定命令,并提交为新的镜像。当命令较长时可以使用 来换行。

CMD

支持三种格式

  • CMD [“executable”,”param1”,”param2”] 使用 exec 执行,推荐方式;
  • CMD command param1 param2 在 /bin/sh 中执行,提供给需要交互的应用;
  • CMD [“param1”,”param2”] 提供给 ENTRYPOINT 的默认参数;
    指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会被执行。

如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD 指定的命令。

EXPOSE

格式为 EXPOSE […]。

告诉 Docker 服务端容器暴露的端口号,供互联系统使用。在启动容器时需要通过 -P,Docker 主机会自动分配一个端口转发到指定的端口。

ENV

格式为 ENV 。 指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持。

例如

1
2
3
4
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

ADD

格式为 ADD

该命令将复制指定的 到容器中的 。 其中 可以是Dockerfile所在目录的一个相对路径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)。

COPY

格式为 COPY

复制本地主机的 (为 Dockerfile 所在目录的相对路径)到容器中的

当使用本地目录为源目录时,推荐使用 COPY。

VOLUME

格式为 VOLUME [“/data”]。

创建一个可以从本地主机或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等。

WORKDIR

格式为 WORKDIR /path/to/workdir。

为后续的 RUN、CMD、ENTRYPOINT 指令配置工作目录。

可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如

1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

则最终路径为 /a/b/c。
ONBUILD
格式为 ONBUILD [INSTRUCTION]。

配置当所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令。

例如,Dockerfile 使用如下的内容创建了镜像 image-A。

1
2
3
4
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

如果基于 image-A 创建新的镜像时,新的Dockerfile中使用 FROM image-A指定基础镜像时,会自动执行ONBUILD 指令内容,等价于在后面添加了两条指令。

1
2
3
4
5
FROM image-A

#Automatically run the following
ADD . /app/src
RUN /usr/local/bin/python-build --dir /app/src

使用 ONBUILD 指令的镜像,推荐在标签中注明,例如 ruby:1.9-onbuild。

创建镜像

编写完成 Dockerfile 之后,可以通过 docker build 命令来创建镜像。

基本的格式为 docker build [选项] 路径,该命令将读取指定路径下(包括子目录)的 Dockerfile,并将该路径下所有内容发送给 Docker 服务端,由服务端来创建镜像。因此一般建议放置 Dockerfile 的目录为空目录。也可以通过 .dockerignore 文件(每一行添加一条匹配模式)来让 Docker 忽略路径下的目录和文件。

要指定镜像的标签信息,可以通过 -t 选项,例如

1
$ sudo docker build -t myrepo/myapp /tmp/test1/

Docker Compose

使用一个 Dockerfile 模板文件可以定义一个单独的应用容器,如果需要定义多个容器就需要服务编排。服务编排这里需要用到Docker Compose 。

Dockerfile 可以让用户管理一个单独的应用容器;而 Compose 则允许用户在一个模板(YAML 格式)中定义一组相关联的应用容器(被称为一个 project,即项目),例如一个 Web 服务容器再加上后端的数据库服务容器等。

Docker-Compose 是 Docker 的一种编排服务,是一个用于在 Docker 上定义并运行复杂应用的工具,可以让用户在集群中部署分布式应用。

通过 Docker-Compose 用户可以很容易地用一个配置文件定义一个多容器的应用,然后使用一条指令安装这个应用的所有依赖,完成构建。Docker-Compose 解决了容器与容器之间如何管理编排的问题。
pic4

  • 服务 (service) :一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
  • 项目 (project) :由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。

Compose 项目由 Python 编写,实现上调用了 Docker 服务提供的 API 来对容器进行管理。因此,只要所操作的平台支持 Docker API,就可以在其上利用 Compose 来进行编排管理。

快速上手

没有什么比来一个小例子练练手更好的学习方法了,我们以官网上的简单示例来看看 docker compose 的使用方法。
我们设计这么一个场景,使用 Python 启动一个 Web 服务,输出一个hello()方法,每次访问的时候在 Redis 缓存中进行计数,并且将统计的结果打印到页面中。

第一步,创建 Python 服务

创建项目路径:

1
2
mkdir composetest
cd composetest

在目录下创建app.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import time

import redis
from flask import Flask


app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)


def get_hit_count():
retries = 5
while True:
try:
return cache.incr('hits')
except redis.exceptions.ConnectionError as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)


@app.route('/')
def hello():
count = get_hit_count()
return 'Hello World! I have been seen {} times.\n'.format(count)

if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)

在这个例子中, redis 使用了容器内的网络默认端口是6379。这段 Python 程序的内容就是,启动后连接 Redis 并且输出 hello()方法,当每次访问的时候累计访问次数并且将结果放回到页面。
在同目录下创建requirements.txt文件,添加项目依赖的python包:

1
2
flask
redis

第二步,创建 Dockerfile

我们来写一个 Dockerfile 来定义 Docker 镜像,此镜像包含了 Python 的依赖包和 Python 环境。
同样在此目录下,我们创建一个 Dockerfile 文件。

1
2
3
4
5
FROM python:3.4-alpine
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

这段代码表示:

  • 使用基础镜像 Python 3.4
  • 将当前目录映射到镜像/code目录下
  • 设置工作目录为 /code
  • 安装 Python 依赖包
  • 启动 app.py 程序
第三步,使用 Compose 文件定义一个服务

在当期目录下,我们创建一个 docker-compose.yml 文件,内容如下:

1
2
3
4
5
6
7
8
version: '2'
services:
web:
build: .
ports:
- "5000:5000"
redis:
image: "redis:alpine"

这个 Compose 文件定义了两个服务, 一个 Pyhon Web 服务和 redis 服务。

  • Pyhon Web 服务:使用 Dockerfile 构建了当前镜像。将 Web 容器内部的5000端口映射到 host 的5000端口;并将 Web 容器与 redis 容器连接。
  • redis服务:该容器直接由官方的 redis 镜像创建。
    第四步,使用 Compose 编译启动应用
    使用命令docker-compose up启动
    version: ‘2’
    services:
    web:
    build: .
    command: python app.py
    ports:
    • “5000:5000”
      volumes:
    • .:/code
      redis:
      image: “redis:alpine”

启动成功之后,在浏览器访问:http://ipaddress:5000/ ,返回如下:
Hello World! I have been seen 1 times.

刷新再次访问返回
Hello World! I have been seen 2 times.

Docker Compose常用命令

使用docker-compose up -d在后台启动服务

1
2
3
[root@localhost composetest]# docker-compose up -d
Starting composetest_web_1 ...
Starting composetest_web_1 ... done

使用docker-compose ps命令查看启动的服务

1
2
3
4
5
[root@localhost composetest]# docker-compose ps
Name Command State Ports
-------------------------------------------------------------------------------------
composetest_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
composetest_web_1 python app.py Up 0.0.0.0:5000->5000/tcp

使用docker-compose stop停止服务。

1
2
3
[root@localhost composetest]# docker-compose stop
Stopping composetest_web_1 ... done
Stopping composetest_redis_1 ... done

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#查看帮助
docker-compose -h

# -f 指定使用的 Compose 模板文件,默认为 docker-compose.yml,可以多次指定。
docker-compose -f docker-compose.yml up -d

#启动所有容器,-d 将会在后台启动并运行所有的容器
docker-compose up -d

#停用移除所有容器以及网络相关
docker-compose down

#查看服务容器的输出
docker-compose logs

#列出项目中目前的所有容器
docker-compose ps

#构建(重新构建)项目中的服务容器。服务容器一旦构建后,将会带上一个标记名,例如对于 web 项目中的一个 db 容器,可能是 web_db。可以随时在项目目录下运行 docker-compose build 来重新构建服务
docker-compose build

#拉取服务依赖的镜像
docker-compose pull

#重启项目中的服务
docker-compose restart

#删除所有(停止状态的)服务容器。推荐先执行 docker-compose stop 命令来停止容器。
docker-compose rm

#在指定服务上执行一个命令。
docker-compose run ubuntu ping docker.com

#设置指定服务运行的容器个数。通过 service=num 的参数来设置数量
docker-compose scale web=3 db=2

#启动已经存在的服务容器。
docker-compose start

#停止已经处于运行状态的容器,但不删除它。通过 docker-compose start 可以再次启动这些容器。
docker-compose stop

docker-machine

docker-machine是解决docker运行环境问题。docker技术是基于Linux内核的cgroup技术实现的,那么问题来了,如果在非Linux平台上使用docker技术需要依赖安装Linux系统的虚拟机。docker-machine就是docker公司官方提出的,用于在各种平台上快速创建具有docker服务的虚拟机的技术。你可以把它理解为virtualbox或者vmware,最开始在win7上用得比较多,但是win10开始自带了hyper-v虚拟机,已经不再需要docker-machine了,docker可以直接运行在安装了Linux系统得hyper-v上。

为什么要用docker machine

Docker Machine 使你能够在各种 Linux 上配置多个远程 Docker 宿主机。 此外,Machine 允许你在较早的 Mac 或 Windows 系统上运行 Docker,如上一主题所述。 Docker Machine 有这两个广泛的用例。

我有一个较旧的桌面系统,并希望在 Mac 或 Windows 上运行 Docker

如果你主要在不符合新的 Docker for Mac 和 Docker for Windows 应用程序的旧 Mac 或 Windows 笔记本电脑或台式机上工作,则需要 Docker Machine 来在本地“运行Docker”(即Docker Engine)。在 Mac 或 Windows box 中使用 Docker Toolbox 安装程序安装 Docker Machine 将为 Docker Engine 配置一个本地的虚拟机,使你能够连接它、并运行 docker 命令。

Docker Machine最主要的两个作用

  • 使用 Docker Machine 方便在不同的环境中使用 Docker ,比如:Win/Mac
  • 使用 Docker Machine 方便在云环境下批量部署 Docker环境,比如:私有云,公有云批量安装Docker环境

    Docker machine 常用命令

    //创建虚拟机
    docker-machine create [OPTIONS] [arg…]

//移除虚拟机
docker-machine rm [OPTIONS] [arg…]

//登录虚拟机
docker-machine ssh [arg…]

//docker客户端配置环境变量
docker-machine env [OPTIONS] [arg…]

//检查机子信息
docker-machine inspect

//查看虚拟机列表
docker-machine ls [OPTIONS] [arg…]

//查看虚拟机状态
docker-machine status [arg…] //一个虚拟机名称

//启动虚拟机
docker-machine start [arg…] //一个或多个虚拟机名称

//停止虚拟机
docker-machine stop [arg…] //一个或多个虚拟机名称

//重启虚拟机
docker-machine restart [arg…] //一个或多个虚拟机名称

docker-swarm

docker-swarm是解决多主机多个容器调度部署得问题。swarm是基于docker平台实现的集群技术,他可以通过几条简单的指令快速的创建一个docker集群,接着在集群的共享网络上部署应用,最终实现分布式的服务。swarm技术相当不成熟,很多配置功能都无法实现,只能说是个半成品,目前更多的是使用Kubernetes来管理集群和调度容器。

在用学习容器编排之前,所有操作本地进行的,docker cli 连接是一台的docker host,不管是docker run 还是docker container 都是在一台机器上,但是实际的生产环境下,一个应用很复杂他部署在一台机器上满足不了我们的需求,都是通过集群的方式来解决问题的。

目前,Swarm 是 Docker 社区提供的唯一一个原生支持 Docker 集群管理的工具。它可以把多个 Docker 主机组成的系统转换为单一的虚拟 Docker 主机,使得容器可以组成跨主机的子网网络。

**Docker Swarm 是一个为 IT 运维团队提供集群和调度能力的编排工具。用户可以把集群中所有 Docker Engine 整合进一个「虚拟 Engine」的资源池,通过执行命令与单一的主 Swarm 进行沟通,而不必分别和每个 Docker Engine 沟通。在灵活的调度策略下,IT 团队可以更好地管理可用的主机资源,保证应用容器的高效运行。


pic8

到处都使用容器带来的困扰

  1. 怎么去管理这么多容器?
  2. 怎么能方便的横向扩展?
  3. 如果容器down了,怎么能自动恢复?
  4. 如何去更新融起而不影响业务?
  5. 怎么去调度容器的创建?
  6. 保护隐私数据?
    pic5

    Swarm的架构

  7. swarm集群的架构
  8. 节点下面有角色:Worker Manager
  9. Manager 是整个warm集群的大脑,为了避免单点的故障,我们的大脑至少有2个,状态的同步通过raft协议进行同步。raft协议可以确保多个Manager之前是同步的。

分布式系统之于单机系统,优势之一就是有更好的容错性。


  • 比如,一台机器上的磁盘损坏,数据丢失,可以从另一台机器上的磁盘>恢复(分布式系统会对数据做备份)
  • 比如,集群中某些机器宕机,整个集群还可以对外提供服务
    这是如何做到的?比较容易的一个想法就是备份(backup)。一个系统的工作模是:接受客户端的command,系统进行处理,将处理的结果返回给客户端。由此可见,系统里的数据可能会因为command而变化。
    实现备份的做法之一就是复制状态机(Repilcated State Machine,RSM),它有一个很重要的性质——确定性(deterministic):
  • 如果两个相同的、确定性的状态从同一状态开始,并且以相同的顺序获得相同的输入,那么这两个状态机将会生成相同的输出,并且结束在相同的状态
    也就是说,如果我们能按顺序将command作用于状态机,它就可以产生相同的状态和相同的输出
    那么一个状态机如何实现呢?如下图所示(来自raft协议):
    pic6

上图中,每个RSM都有一个replicated log,存储的是来自客户端的commands。每个RSM中replicate log中commads的顺序都是相同的,状态机按顺序处理replicate log中的command,并将处理的结果返回给客户端。由于状态机具有确定性,因此每个状态机的输出和状态都是相同的。
上图中有一个模块——Consensus Module刚刚没有提及。这个模块用于保证每个server上Log的一致性!


  • 如果不做任何保障,直接将commad暴力写入,一旦服务器宕机或者出现什么其他故障,就会导致这个Log丢失,并且无法恢复。而出现故障的可能性是很高的,这就导致系统不可用
  • raft就是Consensus Module的一个实现
    因此,raft是一致性协议,是用来保障servers上副本一致性的一种算法。
  1. worker是通过gossip的网络结构进行同步
  2. Service 和Replicas,这里的Service在docker compose中的Service是一样的。

pic7

相关概念

节点

运行 Docker 的主机可以主动初始化一个 Swarm 集群或者加入一个已存在的 Swarm 集群,这样这个运行 Docker 的主机就成为一个 Swarm 集群的节点 (node) 。节点分为管理 (manager) 节点和工作 (worker) 节点。

管理节点用于 Swarm 集群的管理,docker swarm 命令基本只能在管理节点执行(节点退出集群命令 docker swarm leave 可以在工作节点执行)。一个 Swarm 集群可以有多个管理节点,但只有一个管理节点可以成为 leader,leader 通过 raft 协议实现。

工作节点是任务执行节点,管理节点将服务 (service) 下发至工作节点执行。管理节点默认也作为工作节点。你也可以通过配置让服务只运行在管理节点。下图展示了集群中管理节点与工作节点的关系。

服务和任务

任务 (Task)是 Swarm 中的最小的调度单位,目前来说就是一个单一的容器。 服务 (Services) 是指一组任务的集合,服务定义了任务的属性。服务有两种模式:

  • replicated services 按照一定规则在各个工作节点上运行指定个数的任务。
  • global services 每个工作节点上运行一个任务

两种模式通过 docker service create 的 –mode 参数指定。下图展示了容器、任务、服务的关系。

创建Swarm集群

初始化集群

查看虚拟主机,现在没有

1
2
docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS

使用 virtualbox 创建管理节点

1
2
3
docker-machine create --driver virtualbox manager1
#进入管理节点
docker-machine ssh manager1

执行 sudo -i 可以进入Root 权限
我们使用 docker swarm init 在 manager1 初始化一个 Swarm 集群。

1
2
3
4
5
6
7
8
docker@manager1:~$ docker swarm init --advertise-addr 192.168.99.100
Swarm initialized: current node (j0o7sykkvi86xpc00w71ew5b6) is now a manager.

To add a worker to this swarm, run the following command:

docker swarm join --token SWMTKN-1-47z6jld2o465z30dl7pie2kqe4oyug4fxdtbgkfjqgybsy4esl-8r55lxhxs7ozfil45gedd5b8a 192.168.99.100:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

如果你的 Docker 主机有多个网卡,拥有多个 IP,必须使用 –advertise-addr 指定 IP。 执行 docker swarm init 命令的节点自动成为管理节点。

命令 docker info 可以查看 swarm 集群状态:

1
2
3
4
5
6
7
8
9
10
11
Containers: 0
Running: 0
Paused: 0
Stopped: 0
...snip...
Swarm: active
NodeID: dxn1zf6l61qsb1josjja83ngz
Is Manager: true
Managers: 1
Nodes: 1
...snip...

命令 docker node ls 可以查看集群节点信息:

1
2
3
docker@manager1:~$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
1ipck4z2uuwf11f4b9mnon2ul * manager1 Ready Active Leader

退出虚拟主机

1
docker@manager1:~$ exit

增加工作节点

上一步初始化了一个 Swarm 集群,拥有了一个管理节点,在 Docker Machine 一节中我们了解到 Docker Machine 可以在数秒内创建一个虚拟的 Docker 主机,下面我们使用它来创建两个 Docker 主机,并加入到集群中。

创建虚拟主机worker1

创建主机

1
$ docker-machine create -d virtualbox worker1

进入虚拟主机 worker1

1
$ docker-machine ssh worker1

加入 swarm 集群

1
2
3
4
5
docker@worker1:~$ docker swarm join \
--token SWMTKN-1-47z6jld2o465z30dl7pie2kqe4oyug4fxdtbgkfjqgybsy4esl-8r55lxhxs7ozfil45gedd5b8a \
192.168.99.100:2377

This node joined a swarm as a worker.

退出虚拟主机

1
docker@worker1:~$ exit

同理对工作节点2

查看集群

进入管理节点:

1
docker-machine ssh manager1

宿主机子上查看虚拟主机

1
2
3
4
5
docker@manager1:~$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
manager1 * virtualbox Running tcp://192.168.99.100:2376 v17.12.1-ce
worker1 - virtualbox Running tcp://192.168.99.101:2376 v17.12.1-ce
worker2 - virtualbox Running tcp://192.168.99.102:2376 v17.12.1-ce

在主节点上面执行 docker node ls 查询集群主机信息

1
2
3
4
5
docker@manager1:~$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
1ipck4z2uuwf11f4b9mnon2ul * manager1 Ready Active Leader
rtcpqgcn2gytnvufwfveukgrv worker1 Ready Active
te2e9tr0qzbetjju5gyahg6f7 worker2 Ready Active

这样我们就创建了一个最小的 Swarm 集群,包含一个管理节点和两个工作节点。

部署服务

我们使用 docker service 命令来管理 Swarm 集群中的服务,该命令只能在管理节点运行。

新建服务

进入集群管理节点:

1
docker-machine ssh manager1

使用 docker 中国镜像

1
2
docker search alpine
docker pull registry.docker-cn.com/library/alpine

现在我们在上一节创建的 Swarm 集群中运行一个名为 helloworld 服务。

1
2
3
4
5
docker@manager1:~$ docker service create --replicas 1 --name helloworld alpine ping ityouknow.com
rwpw7eij4v6h6716jvqvpxbyv
overall progress: 1 out of 1 tasks
1/1: running [==================================================>]
verify: Service converged

命令解释:

  • docker service create 命令创建一个服务
  • —name 服务名称命名为 helloworld
  • —replicas 设置启动的示例数
  • alpine 指的是使用的镜像名称,ping ityouknow.com指的是容器运行的bash

使用命令 docker service ps rwpw7eij4v6h6716jvqvpxbyv 可以查看服务进展

1
2
3
docker@manager1:~$ docker service ps rwpw7eij4v6h6716jvqvpxbyv
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
rgroe3s9qa53 helloworld.1 alpine:latest worker1 Running Running about a minute ago

使用 docker service ls 来查看当前 Swarm 集群运行的服务。

1
2
3
docker@manager1:~$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
yzfmyggfky8c helloworld replicated 0/1 alpine:latest

监控集群状态

登录管理节点 manager1

1
docker-machine ssh manager1

运行 docker service inspect —pretty 查询服务概要状态,以 helloworld 服务为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
docker@manager1:~$  docker service inspect --pretty helloworld

ID: rwpw7eij4v6h6716jvqvpxbyv
Name: helloworld
Service Mode: Replicated
Replicas: 1
Placement:
UpdateConfig:
Parallelism: 1
On failure: pause
Monitoring Period: 5s
Max failure ratio: 0
...
Rollback order: stop-first
ContainerSpec:
Image: alpine:latest@sha256:7b848083f93822dd21b0a2f14a110bd99f6efb4b838d499df6d04a49d0debf8b
Args: ping ityouknow.com
Resources:
Endpoint Mode: vip

运行 docker service inspect helloworld 查询服务详细信息。

运行docker service ps 查看那个节点正在运行服务:

1
2
3
docker@manager1:~$  docker service ps helloworld
NAME IMAGE NODE DESIRED STATE LAST STATE
helloworld.1.8p1vev3fq5zm0mi8g0as41w35 alpine worker1 Running Running 3 minutes

在节点执行docker ps 查看容器的运行状态。

1
2
3
docker@worker1:~$   docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
96bf5b1d8010 alpine:latest "ping ityouknow.com" 4 minutes ago Up 4 minutes helloworld.1.rgroe3s9qa53lf4u4ky0tzcb8

这样的话,我们在 Swarm 集群中成功的运行了一个 helloworld 服务,根据命令可以看出在 worker1 节点上运行。

退出 Swarm 集群

如果 Manager 想要退出 Swarm 集群, 在 Manager Node 上执行如下命令:

1
docker swarm leave

就可以退出集群,如果集群中还存在其它的 Worker Node,还希望 Manager 退出集群,则加上一个强制选项,命令行如下所示:

1
docker swarm leave --force

在 Worker2 上进行退出测试,登录 worker2 节点

1
docker-machine ssh  worker2

执行退出命令

1
docker swarm leave

查看集群节点情况:

1
2
3
4
5
6
docker@manager1:~$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
j0o7sykkvi86xpc00w71ew5b6 * manager1 Ready Active Leader
xwv8aixasqraxwwpox0d0bp2i worker1 Ready Active
ij3z1edgj7nsqvl8jgqelrfvy worker2 Down Active
i31yuluyqdboyl6aq8h9nk2t5 worker3 Ready Active

可以看出集群节点 worker2 状态已经下线

也可以再次加入

1
2
3
4
docker@worker2:~$ docker swarm join \
> --token SWMTKN-1-47z6jld2o465z30dl7pie2kqe4oyug4fxdtbgkfjqgybsy4esl-8r55lxhxs7ozfil45gedd5b8a \
> 192.168.99.100:2377
This node joined a swarm as a worker.

再次查看

1
2
3
4
5
6
7
docker@manager1:~$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
j0o7sykkvi86xpc00w71ew5b6 * manager1 Ready Active Leader
xwv8aixasqraxwwpox0d0bp2i worker1 Ready Active
0agpph1vtylm421rhnx555kkc worker2 Ready Active
ij3z1edgj7nsqvl8jgqelrfvy worker2 Down Active
i31yuluyqdboyl6aq8h9nk2t5 worker3 Ready Active

可以看出集群节点 worker2 又重新加入到了集群中

重新搭建命令

使用 VirtualBox 做测试的时候,如果想重复实验可以将实验节点删掉再重来。

1
2
3
4
5
6
7
8
//停止虚拟机
docker-machine stop [arg...] //一个或多个虚拟机名称

docker-machine stop manager1 worker1 worker2
//移除虚拟机
docker-machine rm [OPTIONS] [arg...]

docker-machine rm manager1 worker1 worker2

停止、删除虚拟主机后,再重新创建即可。

Docker 三剑客的区别。

docker-machine
docker-machine是解决docker运行环境问题。docker技术是基于Linux内核的cgroup技术实现的,那么问题来了,如果在非Linux平台上使用docker技术需要依赖安装Linux系统的虚拟机。docker-machine就是docker公司官方提出的,用于在各种平台上快速创建具有docker服务的虚拟机的技术。你可以把它理解为virtualbox或者vmware,最开始在win7上用得比较多,但是win10开始自带了hyper-v虚拟机,已经不再需要docker-machine了,docker可以直接运行在安装了Linux系统得hyper-v上。

docker-compose
dcoker-compose主要是解决本地docker容器编排问题。一般是通过yaml配置文件来使用它,这个yaml文件里能记录多个容器启动的配置信息(镜像、启动命令、端口映射等),最后只需要执行docker-compose对应的命令就会像执行脚本一样地批量创建和销毁容器。

docker-swarm
docker-swarm是解决多主机多个容器调度部署得问题。swarm是基于docker平台实现的集群技术,他可以通过几条简单的指令快速的创建一个docker集群,接着在集群的共享网络上部署应用,最终实现分布式的服务。swarm技术相当不成熟,很多配置功能都无法实现,只能说是个半成品,目前更多的是使用Kubernetes来管理集群和调度容器。总结:如果你是在非Linux环境下考虑使用docker-compose,当然我更推荐使用hyper-v或者virtualbox。如果你需要同时操作多个容器,或者希望使用配置文件记录容器启动命令参数,那么推荐使用docker-compose。如果你需要在多台主机上部署docker容器,并对其进行调度,那么swarm是一种选择,当然更推荐Kubernetes。

-------------本文结束感谢您的阅读-------------