Docker 实战(五):Docker Swarm Mode

Docker Compose 中,我们可以在单台机器上操作多个相关联的 Docker 容器组成负载均衡集群。
那如果我们需要一个分布式的环境中,跨多台主机呢?
在 Docker 1.12 以上版本中,有个新的东西叫做 Swarm Mode
不同于之前版本的 Docker Swarm(还需要 pull swarm),Swarm Mode 已经集成在 Docker Engine 中。

主要特性

内置于 Docker Engine 的集群管理

可以直接用 Docker Engine CLI 来创建 Swarm 集群,并在该集群上部署服务。你不再需要额外的编排软件来创建或管理 Swarm 集群了。

去中心化设计

不同于在部署时就确定节点之间的关系, 新的 Swarm 模式选择在运行时动态地处理这些关系, 你可以用 Docker Engine 部署 manager 和 worker 这两种不同的节点。 这意味着你可以从一个磁盘镜像搭建整个 Swarm 。

声明式服务模型

Docker Engine 使用一种声明式方法来定义各种服务的状态。譬如,你可以描述一个由 web 前端服务,消息队列服务和数据库后台组成的应用。

服务扩缩

你可以通过 docker service scale 命令轻松地增加或减少某个服务的任务数。

集群状态维护

Swarm 管理节点会一直监控集群状态,并依据你给出的期望状态与集群真实状态间的区别来进行调节。譬如,你为一个服务设置了10个任务副本,如果某台运行着该服务两个副本的工作节点停止工作了,管理节点会创建两个新的副本来替掉上述异常终止的副本。 Swarm 管理节点这个新的副本分配到了正常运行的工作节点上。

跨主机网络

你可以为你的服务指定一个 overlay 网络。在服务初始化或着更新时,Swarm 管理节点自动的为容器在 overlay 网络上分配地址。

服务发现

Swarm 管理节点在集群中自动的为每个服务分配唯一的 DNS name 并为容器配置负载均衡。利用内嵌在 Swarm 中的 DNS 服务器你可以找到每个运行在集群中的容器。

负载均衡

你可以把服务的端口暴露给一个集群外部的负载均衡器。 在 Swarm 集群内部你可以决定如何在节点间分发服务的容器。

默认 TLS 加密

Swarm 集群中的节点间通信是强制加密的。你可以选择使用自签名的根证书或者来自第三方认证的证书。

滚动更新

docker service 允许你自定义更新的间隔时间, 并依次更新你的容器, docker 会按照你设置的更新时间依次更新你的容器, 如果发生了错误, 还可以回滚到之前的状态。

准备三台主机

你可以找三台物理主机,如果在单机环境做测试,可以使用 Docker Machine 创建三台 Docker 主机 manager、worker1、worker2。

1
2
3
$ docker-machine create -d virtualbox manager
$ docker-machine create -d virtualbox worker1
$ docker-machine create -d virtualbox worker2

在 Docker for Mac 中,已经不需要 VirtualBox,而是使用 HyperKit,所以这里需要先安装最新版本的 VirtualBox。

1
2
3
4
5
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
manager - virtualbox Running tcp://192.168.99.101:2376 v1.12.3
worker1 - virtualbox Running tcp://192.168.99.102:2376 v1.12.3
worker2 * virtualbox Running tcp://192.168.99.103:2376 v1.12.3

创建 Swarm

确认 manager 节点的 ip 地址

1
2
$ docker-machine ip manager
192.168.99.101

初始化 swarm

1
2
3
4
5
6
7
8
9
10
$ docker swarm init --advertise-addr 192.168.99.101
Swarm initialized: current node (3pniknvvlt9hb5bjpdnwkp5zr) is now a manager.

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

docker swarm join \
--token SWMTKN-1-3gxjh13dqeabze16assdjhng71gy4x7pvu2lqh8fdj22ttg02d-dnkw6tbxbh6so5f0ng7ukiysb \
192.168.99.101:2377

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

查看 manager token

1
2
3
4
5
6
$ docker swarm join-token manager
To add a manager to this swarm, run the following command:

docker swarm join \
--token SWMTKN-1-3gxjh13dqeabze16assdjhng71gy4x7pvu2lqh8fdj22ttg02d-dnkw6tbxbh6so5f0ng7ukiysb \
192.168.99.101:2377

查看 worker token

1
2
3
4
5
6
$ docker swarm join-token worker
To add a worker to this swarm, run the following command:

docker swarm join \
--token SWMTKN-1-3gxjh13dqeabze16assdjhng71gy4x7pvu2lqh8fdj22ttg02d-buqlcaugygyzizgewblrfdqr3 \
192.168.99.101:2377

这里可以自由选择 manager 还是 worker 类型的节点,示例中以 worker 为例。

查看节点

1
2
3
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
3pniknvvlt9hb5bjpdnwkp5zr * manager Ready Active Leader

进入节点

这里有 2 种办法进入 worker1,先新开一个终端窗口

1
$ eval $(docker-machine env worker1)

或者

1
$ docker-machine ssh worker1

这里建议打开 3 个终端窗口,分别执行 $ eval $(docker-machine env ) 进入 manager、worker1、worker2 三台主机。

进入 worker1 后执行 查看 worker token 下的那段命令

1
2
3
$ docker swarm join \
--token SWMTKN-1-3gxjh13dqeabze16assdjhng71gy4x7pvu2lqh8fdj22ttg02d-buqlcaugygyzizgewblrfdqr3 \
192.168.99.101:2377

同样的方式进入 worker2 执行上述相同的命令。

回到 manager 查看节点

1
2
3
4
5
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
1s6uw7ew22xhvy5trv2ejnncd worker2 Ready Active
3pniknvvlt9hb5bjpdnwkp5zr * manager Ready Active Leader
beh2b7riuqv7oz5nhbhuvmr0t worker1 Ready Active

部署服务

在创建 3 个节点完成后,现在可以在上边部署服务。
现在还是回到 manager 节点

创建服务

1
2
$ docker service create --replicas 1 --name helloworld alpine ping docker.com
doq2uzfwm3c5fukhksv50ewsf

name 指定容器名字
replicas 只复制一个实例
alpine ping docker.com 定义一个 Alpine Linux container 并执行 ping docker.com 命令。

查看服务实例

1
2
3
$ docker service ls
ID NAME REPLICAS IMAGE COMMAND
doq2uzfwm3c5 helloworld 1/1 alpine ping docker.com

检查服务

1
2
3
4
5
6
7
8
9
10
11
12
13
$ docker service inspect --pretty helloworld
ID: doq2uzfwm3c5fukhksv50ewsf
Name: helloworld
Mode: Replicated
Replicas: 1
Placement:
UpdateConfig:
Parallelism: 1
On failure: pause
ContainerSpec:
Image: alpine
Args: ping docker.com
Resources:

查看服务运行在哪个节点

1
2
3
$ docker service ps helloworld
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
c81lt2t8ride9ioduqv86i0v7 helloworld.1 alpine manager Running Running 7 minutes ago

这里要关注 DESIRED STATE 和 LAST STATE 两个状态。

在服务所在节点查看进程,这里是 manager

1
2
3
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
458df9db3e9a alpine:latest "ping docker.com" 10 minutes ago Up 10 minutes helloworld.1.c81lt2t8ride9ioduqv86i0v7

服务扩展

还在是 manager 节点,通过命令,我们可以改变集群中的节点实例。

1
$ docker service scale <SERVICE-ID>=<NUMBER-OF-TASKS>

在这里,我们把 helloworld 实例扩展到 5 个

1
2
$ docker service scale helloworld=5
helloworld scaled to 5

查看服务实例运行节点分布

1
2
3
4
5
6
7
$ docker service ps helloworld
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
c81lt2t8ride9ioduqv86i0v7 helloworld.1 alpine manager Running Running 20 minutes ago
b98r4ypet6b9doiof1bb0aq68 helloworld.2 alpine worker1 Running Running 5 seconds ago
20hkvi1i3ihur0x17a989qvpy helloworld.3 alpine manager Running Running 8 seconds ago
472p0ykqnwtjvmo10bx1kutz8 helloworld.4 alpine worker2 Running Running 2 seconds ago
dfmx6i4aunk27m8xstavncwxa helloworld.5 alpine worker1 Running Running 2 seconds ago

可以看到,manager 和 worker1 有 2 个实例,worker2 有 1 个实例。

到 manager 上执行

1
2
3
4
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e90f1e77d7aa alpine:latest "ping docker.com" About a minute ago Up About a minute helloworld.3.20hkvi1i3ihur0x17a989qvpy
458df9db3e9a alpine:latest "ping docker.com" 22 minutes ago Up 22 minutes helloworld.1.c81lt2t8ride9ioduqv86i0v7

到 worker1 上执行

1
2
3
4
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
806eb838fd86 alpine:latest "ping docker.com" 2 minutes ago Up 2 minutes helloworld.5.dfmx6i4aunk27m8xstavncwxa
faa01a019dcf alpine:latest "ping docker.com" 2 minutes ago Up 2 minutes helloworld.2.b98r4ypet6b9doiof1bb0aq68

到 worker2 上执行

1
2
3
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f020134c72bb alpine:latest "ping docker.com" 2 minutes ago Up 2 minutes helloworld.4.472p0ykqnwtjvmo10bx1kutz8

删除服务

1
$ docker service rm helloworld

确认是否删除

1
2
3
$ docker service inspect helloworld
[]
Error: no such service: helloworld

滚动更新

这里我们会先部署 3 个 Redis 3.0.6 实例到 swarm 节点,然后再更新到 3.0.7。

1
2
3
4
5
6
$ docker service create \
--replicas 3 \
--name redis \
--update-delay 10s \
redis:3.0.6
0knduq4z4vae02wvc33vz5b0u

update-delay 实例之间的更新延时时间. 可以使用秒 s、分钟 m 或者 小时 h。例如 10m30s 就是延时 10分30秒。
默认情况同一时间更新一个实例。可以通过 –update-parallelism 配置同时更新的个数。

1
2
3
4
5
$ docker service ps redis
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
27mlfg8pqlvz9w4yky1q9fxm7 redis.1 redis:3.0.6 worker1 Running Running 2 seconds ago
ejlctv6j92caxapd7g2bll0bo redis.2 redis:3.0.6 manager Running Preparing 16 seconds ago
3l0nu4zt99kecwig1sfie1km9 redis.3 redis:3.0.6 worker2 Running Preparing 16 seconds ago

这里要关注 CURRENT STATE,上边的状态说明 worker1 实例已经 Running,但 manager 和 worker2 还在 Preparing。
到各自主机上用 docker ps 可以证明这一点。等待片刻,所有实例的 CURRENT STATE 都变成 Running。

看最后的 Running 时间,第二个节点晚了 7 分钟,第三个节点晚了 16 分钟,三个实例全部启动成功。

1
2
3
4
5
$ docker service ps redis
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
27mlfg8pqlvz9w4yky1q9fxm7 redis.1 redis:3.0.6 worker1 Running Running 18 minutes ago
ejlctv6j92caxapd7g2bll0bo redis.2 redis:3.0.6 manager Running Running 2 minutes ago
3l0nu4zt99kecwig1sfie1km9 redis.3 redis:3.0.6 worker2 Running Running 11 minutes ago

如果想看各个 redis 实例的启动日志,你可以 docker ps 拿到容器 ID,然后 docker logs CONTAINER_ID 看到 redis 的启动日志。
如果想知道在最后一个实例启动之前 16 分钟内三个实例发生了什么事情,你需要 docker-machine ssh NODE_NAME ,看 /var/log/docker.log 中的内容。

接下来,我们更新 redis 实例到 3.0.7

1
2
$ docker service update --image redis:3.0.7 redis
redis

因为之前已经在各个节点更新过 3.0.7 镜像,省略了下载新镜像的过程,所以这次更新在 2 分钟之内全部完成。

1
2
3
4
5
6
7
8
$ docker service ps redis
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
cf3stbcfz3zdmw5mgogya6amd redis.1 redis:3.0.7 manager Running Running 2 minutes ago
27mlfg8pqlvz9w4yky1q9fxm7 \_ redis.1 redis:3.0.6 worker1 Shutdown Shutdown 2 minutes ago
ewv52c73p0klbx6hmofxm72ti redis.2 redis:3.0.7 worker2 Running Running about a minute ago
ejlctv6j92caxapd7g2bll0bo \_ redis.2 redis:3.0.6 manager Shutdown Shutdown about a minute ago
4rcaexit4kupwcjrxdnjftgln redis.3 redis:3.0.7 worker1 Running Running about a minute ago
3l0nu4zt99kecwig1sfie1km9 \_ redis.3 redis:3.0.6 worker2 Shutdown Shutdown about a minute ago

拉掉节点

有时,比如计划维护时间,您需要将节点设置为不可用。 DRAIN 可用性防止节点从 swarm 管理器接收新任务。
它还意味着管理器停止在节点上运行的任务,并在具有 ACTIVE 可用性的节点上启动副本任务。

拉掉 worker1 节点

1
2
$ docker node update --availability drain worker1
worker1

这时看到 worker1 节点已经 Shutdown,并且在 worker2 上启动了一个新的 redis 实例。

1
2
3
4
5
6
7
8
9
$ docker service ps redis
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
cf3stbcfz3zdmw5mgogya6amd redis.1 redis:3.0.7 manager Running Running 13 minutes ago
27mlfg8pqlvz9w4yky1q9fxm7 \_ redis.1 redis:3.0.6 worker1 Shutdown Shutdown 13 minutes ago
ewv52c73p0klbx6hmofxm72ti redis.2 redis:3.0.7 worker2 Running Running 12 minutes ago
ejlctv6j92caxapd7g2bll0bo \_ redis.2 redis:3.0.6 manager Shutdown Shutdown 12 minutes ago
80zf0ykluvhhmydro7egm04iu redis.3 redis:3.0.7 worker2 Running Running 17 seconds ago
4rcaexit4kupwcjrxdnjftgln \_ redis.3 redis:3.0.7 worker1 Shutdown Shutdown 35 seconds ago
3l0nu4zt99kecwig1sfie1km9 \_ redis.3 redis:3.0.6 worker2 Shutdown Shutdown 13 minutes ago

恢复 worker1 节点

1
2
$ docker node update --availability active worker1
worker1

查看 worker1 节点状态已经 Active

1
2
3
4
5
6
7
8
$ docker node inspect --pretty worker1
ID: beh2b7riuqv7oz5nhbhuvmr0t
Hostname: worker1
Joined at: 2016-11-30 09:29:10.681020915 +0000 utc
Status:
State: Ready
Availability: Active
...

现在 worker1 可以接收新的任务了。

1
2
$ docker service scale redis=5
redis scaled to 5

可以看到在 worker1 上启动了 2 个新的 redis 实例

1
2
3
4
5
6
7
8
9
10
11
$ docker service ps redis
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
cf3stbcfz3zdmw5mgogya6amd redis.1 redis:3.0.7 manager Running Running 20 minutes ago
27mlfg8pqlvz9w4yky1q9fxm7 \_ redis.1 redis:3.0.6 worker1 Shutdown Shutdown 20 minutes ago
ewv52c73p0klbx6hmofxm72ti redis.2 redis:3.0.7 worker2 Running Running 19 minutes ago
ejlctv6j92caxapd7g2bll0bo \_ redis.2 redis:3.0.6 manager Shutdown Shutdown 19 minutes ago
80zf0ykluvhhmydro7egm04iu redis.3 redis:3.0.7 worker2 Running Running 7 minutes ago
4rcaexit4kupwcjrxdnjftgln \_ redis.3 redis:3.0.7 worker1 Shutdown Shutdown 7 minutes ago
3l0nu4zt99kecwig1sfie1km9 \_ redis.3 redis:3.0.6 worker2 Shutdown Shutdown 20 minutes ago
79dffeo1l7etwc731b5lgrgac redis.4 redis:3.0.7 worker1 Running Running 12 seconds ago
5eyb4lhp16m9n2ob2bkifpeqw redis.5 redis:3.0.7 worker1 Running Running 12 seconds ago

路由网络 routing mesh

Docker Swarm Mode 可以发布服务端口,使其可用于群外的资源。
所有节点都加入路由网络,路由网络使得每个节点都能够接受已发布端口上的连接,即使节点上没有任何服务正在运行。
路由网络将所有传入的请求路由到正在运行服务的节点上。

为了在群中使用入口网络,您需要在群集节点之间打开以下端口:

  • Port 7946 TCP/UDP
  • Port 4789 UDP

发布一个对外端口

发布端口使用以下命令

1
2
3
4
$ docker service create \
--name <SERVICE-NAME> \
--publish <PUBLISHED-PORT>:<TARGET-PORT> \
<IMAGE>

这里以 nginx 为例,为了演示,这里只用了 2 个 nginx 实例

1
2
3
4
5
6
$ docker service create \
--name my-web \
--publish 8080:80 \
--replicas 2 \
nginx
d3192apcq7hharl4kpzl0eqqg

确认服务已经启动

1
2
3
4
$ docker service ps my-web
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
a90s5hnb58dck8cxny4k2xgdk my-web.1 nginx worker1 Running Running 2 minutes ago
2148yi9va70eu27xl6nfi9une my-web.2 nginx worker2 Running Running about a minute ago

这时我们分别访问三个节点,都可以看到 nginx 首页。

架构如下(官方图)

如果你要发布一个新的端口

1
2
3
$ docker service update \
--publish-add <PUBLISHED-PORT>:<TARGET-PORT> \
my-web

配置一个负载均衡器

前边实现了节点中 service 的负载均衡。
我们可以在 Swarm Load Balance 之前再加一层负载均衡器,实现节点之间的负载均衡。
具体的实现这里就不再描述,可以自己编写 haproxy.cfg 和 Dockerfile 构建 Docker HAProxy 镜像。

架构如下(官方图)

-EOF-