# Springcloud on Docker Swarm
**Repository Path**: jeffwang78/springcloud-on-docker-swarm
## Basic Information
- **Project Name**: Springcloud on Docker Swarm
- **Description**: 笔记:使用 Docker Swarm 部署 Spring cloud 微服务案例
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 8
- **Forks**: 2
- **Created**: 2022-11-22
- **Last Updated**: 2025-01-24
## Categories & Tags
**Categories**: Uncategorized
**Tags**: Docker, Swarm, SpringCloud, alibaba, sentinel
## README
# Springcloud on Docker Swarm
## 介绍
笔记:使用 Docker Swarm 部署 Spring cloud 微服务案例
## 前言
微服务的架构是基于使用资源换能力的思路设计的(SOA当然也是其核心思路),因此,在实际部署应用时会出现大量的集群式部署,这会带来两方面的挑战:
* 系统拓扑结构复杂
* 部署更新困难
而在开发阶段,也会给测试环境带来不小的困扰,如何有效配置、隔离个人测试环境呢?
对于开发环境,显而易见的答案是使用docker。而生产环境也是可以使用docker方便的构建、更新集群的。
本文以SpringCloud alibaba 体系为例,完成在Docker Swarm集群下的环境搭建。
## Part 1: Docker 基础
本章内容相当基础,有经验的读者可直接转到:[7.1.6 Demo 构建及启动](#c716),验证Demo项目的构建部署。
**本部分使用 Dockerfile/Compose 等文件均在 docker 目录下。**
### 1.1 环境
本文采用Windows WSL ubuntu虚拟机来搭建Docker环境。
### 1.2 Docker 安装
方便起见,使用 apt 安装 Docker。
首先需要增加Docker 的gpg Key:
```
$ sudo mkdir-p/etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o/etc/apt/keyrings/docker.gpg
```
然后添加Repository:
```
$ echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable"| sudo tee/etc/apt/sources.list.d/docker.list >/dev/null
```
可能需要提前安装:
```
$ sudo apt-get install \
ca-certificates \
curl \
gnupg \
lsb-release
```
之后安装Docker CE
```
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
```
参看版本:
```
$ docker -v
Docker version 20.10.21, build baeda1f
```
### 1.3 Docker 简单配置
使用 systemctl 或 service 启动 docker daemon :
```
$ sudo service docker start
```
若要使用国内源,编辑/etc/docker/deamon.json 文件:
```json
{
"registry-mirrors": [
"http://hub-mirror.c.163.com",
"https://registry.docker-cn.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
```
重启动后生效:
```
$ sudo service docker restart
```
检查是否生效:
```
$ sudo docker info | grep http
Registry: https://index.docker.io/v1/
http://hub-mirror.c.163.com/
https://registry.docker-cn.com/
https://docker.mirrors.ustc.edu.cn/
```
Ubuntu 必须使用 root 使用 docker 命令,每次使用 sudo 很麻烦,可以将当前用户添加到 docker 用户组中,即可正常使用 docker 。
```
$ sudo adduser your-user-name docker
$ groups # 显示是否添加成功
adm sudo netdev docker
```
添加组之后,重新登录即可生效。
### 1.3 Docker container
docker使用Container容器来运行应用。
可以将Container比作一个沙盒,运行于操作系统之上,且与隔离。
Docker 与 虚拟机的差别在于,虚拟机提供了一组虚拟化的硬件(cpu,磁盘等)。而docker仅提供了虚拟化的操作系统,因此,docker的成本远低于虚拟机。两者关系大概是:
```mermaid
graph BT
subgraph Host宿主机
hw[硬件]
os[操作系统]
end
app[应用程序]
subgraph VirtualMachine
vmhw[虚拟硬件]
vmos[操作系统]
end
subgraph Docker
vos[虚拟操作系统]
end
hw --> os
os --> vmhw
os --> vos
vmhw --> vmos --> app
vos --> app
os -.-> app
```
#### 1.3.1 运行container
使用docker 启动容器很简单、快速,以nginx为例:
```
$ docker run -d --name myweb -p 8080:80 nginx
e86eef6514efd6c18d958720f75c1b81eda67e2129380c97580472c126c9a401
$ curl localhost:8080
...
Welcome to nginx!
...
```
ngnix 已经在运行了。
看一下运行的 container 信息:
```
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e86eef6514ef nginx "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp, :::8080->80/tcp myweb
```
信息中的重点:
* CONTAINER ID: container 的唯一编号。是run之后那一大段的字符串的前12位。docker命令中使用它来代表container。
* IMAGE: 容器使用的镜像名称。镜像里包含一个沙盒系统和应用程序。
* COMMAND: 容器启动后,执行的命令。本例中,执行的是 nginx 启动命令。
* STATUS:容器状态,表示是否在运行。使用 docker stop 或 start 或 restart 命令控制。
* PORTS: 容器对外暴露的端口。0.0.0.0:8080->80/tcp表示 宿主机的 TCP 8080端口,代理 容器的 TCP 80 端口。也即,访问宿主机 8080端口都将被转交给 容器的 80端口。
* NAME: container 的名称。上文通过 --name 指定的助记符,使用名字可替代 container id.
* -d: --dettach 表示执行后,脱离该容器,也相当于将容器在后台启动。
简化的思路来看待container,可以认为它只是一个独立的应用程序在运行,至于运行哪个应用程序,这是由镜像里的内容来决定的。
docker 的启动、停止、重启动:
```
$ docker stop e86eef6514ef
e86eef6514ef
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e86eef6514ef nginx "/docker-entrypoint.…" 30 minutes ago Exited (0) 5 seconds ago myweb
$ docker start myweb
myweb
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e86eef6514ef nginx "/docker-entrypoint.…" 30 minutes ago Up 4 seconds 0.0.0.0:8080->80/tcp, :::8080->80/tcp myweb
$ docker restart myweb
myweb
```
可见,使用 name 或 id 均可控制 container。
#### 1.3.2 登录container
可以“登录”运行中的容器,进行一些检查、处理。
使用 docker exec 命令:
```
$ docker exec -i --tty myweb bash
# -i 交互式
# --tty 提供伪终端,这两项可连写为 -it
# myweb 容器名称
# bash 相当于 /bin/bash,启动容器上的shell。
root@e86eef6514ef:/# whoami
root
root@e86eef6514ef:/# hostname
e86eef6514ef
root@e86eef6514ef:/#
```
登录实际上是运行了conatiner上的 shell,并使用 -it 提供终端可以进行输入、输出。当然也可以执行其他命令。
#### 1.3.4 删除Conatiner
Container stop 之后,仍占用着存储空间,可以通过 rm 命令删除:
```
$ docker rm myweb
myweb
```
### 1.4 Container文件操作
#### 1.4.1 使用 Alpine
运行的Container是一个虚拟机,当然包含一个操作系统,及其上的应用程序。为了检验这一点,使用 Docker 推荐用于学习的 Alpine 操作系统作为示例。Alpine镜像只有8M,使用方便。
首先启动一个纯净的Alpine操作系统:
```
$ docker run -d -t --name alpine alpine
d97cb6d531474311a4a60b379ba98dc6dd76cb36492c1c0fda2e6b0f13c93532
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d97cb6d53147 alpine "/bin/sh" 7 seconds ago Up 6 seconds alpine
```
#### 1.4.2 安装JDK
apline 使用 apk 安装包。先连接至alpine容器,再执行安装命令:
```
docker exec -it alpine sh
/ # apk add openjdk8
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
...
(44/46) Installing openjdk8-jre-base (8.345.01-r0)
(45/46) Installing openjdk8-jre (8.345.01-r0)
(46/46) Installing openjdk8 (8.345.01-r0)
Executing busybox-1.35.0-r17.trigger
Executing fontconfig-2.14.0-r0.trigger
Executing mkfontscale-1.2.2-r0.trigger
Executing java-common-0.5-r0.trigger
Executing ca-certificates-20220614-r0.trigger
OK: 126 MiB in 60 packages
/ # exit
```
#### 1.4.3 安装Spring APP
JDK安装完毕后,将需要运行的Springboot jar包复制到容器内:
```
# 首先建立目录 /app/
$ docker exec alpine mkdir /app
# cp 复制文件至 containerID或NAME:目的
$ docker cp ansible-spring-example1-0.0.1.jar alpine:/app/
$ docker exec -it alpine sh
/ # ls -l app
total 17220
-rw-r--r-- 1 1000 1000 17629271 Nov 12 14:21 ansible-spring-example1-0.0.1.jar
```
可见,文件已经复制过去了。
#### 1.4.3 启动Spring APP
可以登录并使用 java 来启动,也可以通过exec命令来启动。如:
```
$ docker exec alpine java -jar /app/ansible-spring-example1-0.0.1.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.5)
2022-11-23 02:11:37.739 INFO 197 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-11-23 02:11:38.941 INFO 197 --- [ main] j.f.o.a.AnsibleSpringExample1Application : Started AnsibleSpringExample1Application in 4.867 seconds (JVM running for 5.898)
```
这里没有使用 -d 参数,因此,exec命令没有返回。添加 -d 后即可让Spring在容器的后台执行。
Springboot 启动在 8080端口,使用curl访问:
```
$ curl localhost:8080
curl: (7) Failed to connect to localhost port 8080: Connection refused
```
Springboot运行在 Container 中,其网络空间与宿主机隔离,未发布端口(即将Conatiner端口映射到宿主机端口上)时,无法从Container外部访问。
因此,需发布端口,但当前容器alpine已经无法追加端口,因此,需将alpine删除,并重新使用 -p 8080:8080 启动。
这里存在一个问题:**镜像只读属性**,即容器内的文件变化,并不会改变镜像内容,因此,在之前安装的openjdk8 Java 环境和spring app jar 文件都不会出现在新启动的容器中。
每次重复的执行安装、copy操作自然是太过麻烦,因此,为何不考虑将这些内容放入一个新的镜像里呢?
### 1.5 镜像制作
本节使用简单的命令来建立镜像。
#### 1.5.1 理解Dockerfile
回顾上一节,为了在容器中运行Spring APP,我们执行了下列操作:
1. 运行一个alpine操作系统:run ... alpine
2. 安装JDK: apk add openjdk8
3. 安装jar: docker cp .jar alpine:/app/
4. 进入/app/目录:cd /app/
5. 启动jar: java -jar ...
将这些步骤写成一个脚本,是不是就可以方便的运行这个应用了呢?
Docker 提供了类似的脚本机制来构建镜像:Dockerfile。
简单的讲,Dockerfile就是由上述命令组合而成,当然与之不同的是命令的格式。
#### 1.5.2 第一个 Dockerfile
在Java 项目目录建立一个文本文件Dockerfile:
```Dockerfile
# First docker file
# 1. 运行一个alpine操作系统:run ... alpine
FROM alpine
# 2. 安装JDK: apk add openjdk8
RUN apk add openjdk8
# 3. 安装jar: docker cp .jar /app/
COPY target/ansible-spring-example1-0.0.1.jar /app/
# 声明暴露 8080端口
EXPOSE 8080/tcp
# 4. 进入/app/目录:cd /app/
WORKDIR /app/
# 5. 启动jar: java -jar ...
ENTRYPOINT java -jar ansible-spring-example1-0.0.1.jar
```
最简单的 Dockerfile就像一个脚本,这里引入了五个关键字,也是最常用的关键字。通过与脚本命令的对比,很容易理解其含义。
#### 1.5.3 构建镜像
利用Dockerfile 构建的过程类似与执行了 Dockerfile中的各项命令,并将结果保存在“image镜像”中,构建结果可以复制、分发、运行。
使用 docker build命令进行构建:
```
$ docker build --tag springapp:0.1 ./
# docker build 自动在当前目录下寻找 Dorkerfile。
# 如需要指定 使用 -f some-docker-file-name
Sending build context to Docker daemon 35.4MB
Step 1/5 : FROM alpine
---> bfe296a52501
Step 2/5 : RUN apk add openjdk8
---> Running in 470fbb4d7ad3
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
...
(46/46) Installing openjdk8 (8.345.01-r0)
OK: 126 MiB in 60 packages
Removing intermediate container 470fbb4d7ad3
---> d202e956fd20
Step 3/5 : COPY target/ansible-spring-example1-0.0.1.jar /app/
---> 173eb3f32e41
Step 4/5 : WORKDIR /app/
---> Running in 469675eefc5b
Removing intermediate container 469675eefc5b
---> e72847f59a05
Step 5/5 : ENTRYPOINT java -jar ansible-spring-example1-0.0.1.jar
---> Running in 56d77a106ff5
Removing intermediate container 56d77a106ff5
---> d70f6f096ed9
Successfully built d70f6f096ed9
Successfully tagged springapp:0.1
```
上述命名构建了一个 名为 springapp, 版本号 0.1 的 镜像。
使用 image ls 来查看构建好的镜像 :
```
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
springapp 0.1 d70f6f096ed9 3 minutes ago 149MB
nginx latest 88736fe82739 7 days ago 142MB
alpine latest bfe296a52501 10 days ago 5.54MB
```
spring app 已经构建完成。
*注意:*
> 使用 maven 可直接构建镜像。
> Spring推荐将jar解压后打入image 这样可提高启动速度。
#### 1.5.4 使用镜像启动程序
与之前一样,使用spingapp:0.1镜像启动程序并发布8080端口:
```
$ docker run --name myapp -p 8080:8080 -d springapp:0.1
bcb68fbbd2588a85e62cb8ad16f95fc198d2b72630f8ecd058b1bf2fce1bc1fc
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bcb68fbbd258 springapp:0.1 "/bin/sh -c 'java -j…" 8 seconds ago Up 7 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp myapp
$ curl localhost:8080/info
config: value=Value 1, host=192.168.2.130
```
容器的好处在于可以启动多个应用实例,可尝试分别启动 myapp1,myapp2并将端口发布到宿主机的8081,8082端口。
#### 1.5.6 容器的自动重启动
使用容器对外提供服务时,如果容器程序意外退出,是否有办法自动重新启动呢?
容器启动时可以指定重启动的策略,包括:
|**Policy** | **Result**|
| --- | --- |
|no |Do not automatically restart the container when it exits. This is the default.
|on-failure[:max-retries] |Restart only if the container exits with a non-zero exit status. Optionally, limit the number of restart retries the Docker daemon attempts.|
|always |Always restart the container regardless of the exit status. When you specify always, the Docker daemon will try to restart the container indefinitely. The container will also always start on daemon startup, regardless of the current state of the container.|
|unless-stopped|Always restart the container regardless of the exit status, including on daemon startup, except if the container was put into a stopped state before the Docker daemon was stopped.|
下面是一个简单的示例:
```
$ docker run -d --restart=on-failure:5 --name test1 alpine sh -c "date && sleep 10 && exit 1"
```
这样 test1 当执行失败时,也就是 exit 1 之后,将进行5次重启。
检查重启情况:
```
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c5354bf97672 alpine "sh -c 'date && slee…" 20 seconds ago Up 9 seconds test1
..
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c5354bf97672 alpine "sh -c 'date && slee…" 34 seconds ago Up Less than a second test1
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c5354bf97672 alpine "sh -c 'date && slee…" 5 minutes ago Exited (1) 3 minutes ago test1
```
从 CREATED 时间 和 STATUS中 UP 时间 相对比可知,启动了多次,最后停止服务(重启动5次后)。
## Part 2: Docker Swarm
**SWARM**(蜂群),显而易见,这是一种集群技术。当需要一个或多个宿主机集群上部署多实例的应用时,Docker Swarm可以帮助快速实现这一目的,并且简洁易用。
### 2.1 Swarm 基本概念
#### 2.2.1 Nodes 节点
Node 就是加入Swarm集群中的宿主机。
Swarm 集群将Node分成两种角色:
* Manager: 管理节点,顾名思义,用于集群管理。其中有一台管理节点是集群Leader。(集群选举采用Raft,所以,manager node 数量必须是单数,否则会脑裂。推荐 3,5,7。)
* Worker: 工作节点。也即无法执行管理命令的节点。(但Swarm很灵活,可以轻松的改变节点角色)。
另外,管理节点也是可以运行任务的。因此,最简的Swarm集群是:1个管理节点。本文大部分demo均使用该模式运行。
*注意:Swarm各节点应配置时钟同步服务。*
#### 2.2.2 Services & Tasks
服务和任务是面向用户的核心概念。使用Swarm的目的就是发布服务和任务。
* Service:服务,是对Swarm中运行容器任务的定义(声明)。 Swarm 使用服务定义来创建容器、执行任务,监视和调度任务。
* Task: 任务,任务是指运行在Swarm中的容器。任务使用的镜像、命令、参数、副本数量、部署方式等均由服务定义。当服务提交到Swarm集群后,相应的任务就将执行。
#### 2.2.3 Load balancing
Swarm支持服务运行时的负载均衡,Swarm采用内置的DNS服务实现服务发现,并可通过DNSrr方式在集群内实现负载均衡。
在集群之外,任何已发布的服务,都可以通过集群宿主机进行访问,而不必关心任务实际运行地址。
### 2.2 Swarm 体验
为体验多宿主机环境,可以使用 play-with-docker.com 的服务。
#### 2.2.1 play-with-docker.com
Play-with-docker.com (简写为 PWD) 免费提供网页版虚拟机终端,可以在其上进行Docker 的各种测试包括Swarm。
使用PWD之前,需要注册用户,可以使用hub.docker的用户登录。登录之后,选择下方的 Start 按钮,会跳转至虚拟机终端界面。
PWD 提供了Swarm模板,可以一键创建 3 Manager 5 Worker 的 Swarm 集群。
本节多主机操作均在 PWD上完成。
*注意:*
> PWD 终端使用 Ctrl+Ins 复制, Ctrl+Shift+V进行粘贴。
#### 2.2.2 创建Swarm 集群
PWD中点击左侧 + ADD NEW INSTANE 启动一个host。在终端中创建一个Swarm集群:
```
$ docker swarm init
Error response from daemon: could not choose an IP address to advertise since this system has multiple addresses on different interfaces (192.168.0.8 on eth0 and 172.18.0.58 on eth1) - specify one with --advertise-addr
```
多网卡情况下,需要指定一个网卡或地址,如:
```
$ docker swarm init --advertise-addr eth0
Swarm initialized: current node (0hx30spp5mszfe6ytpck032c5) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-08zuzjjee7unrbek19fxi0aapiujqskhtpnie9qnusibddpn99-b5wzovk7p3tbr0niadkh1s4zm 192.168.0.8:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
```
再创建两个instance ,分别让他们作为Worker加入集群,将上面那行 join 命令复制,并在两台主机上执行。
```
[node2] (local) root@192.168.0.7 ~
$ docker swarm join --token SWMTKN-1-08zuzjjee7unrbek19fxi0aapiujqskhtpnie9qnusibddpn99-b5wzovk7p3tbr0niadkh1s4zm 192.168.0.8:2377
This node joined a swarm as a worker.
```
在第一台机器(Manager & Leader)查看集群节点信息:
```
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
0hx30spp5mszfe6ytpck032c5 * node1 Ready Active Leader 20.10.17
qku9a0sa9zj0ckpekz3b0wmlc node2 Ready Active 20.10.17
0aocykuthmwjspq1p9x69zwz6 node3 Ready Active 20.10.17
```
已有一个Manager 两个 Worker。
再添加两个Manager,构建 3Manager 2 Worker 的集群。
添加Manager节点有两种办法:
1. 使用join-token manager 加入:
```
$ docker swarm join-token manager
To add a manager to this swarm, run the following command:
docker swarm join --token SWMTKN-1-08zuzjjee7unrbek19fxi0aapiujqskhtpnie9qnusibddpn99-9dfl34rb3fg6abemllnll3iw1 192.168.0.8:2377
# 在新主机运行上面的命令即可
```
2. 将Worker提升为 Manager :
```
$ docker node promote node2
Node node2 promoted to a manager in the swarm.
# 当然也可以降级:
$ docker node demote node2
Manager node2 demoted in the swarm.
```
最后,可以直接使用PWD的模板功能,建立如下Swarm集群:
```
[manager1] (local) root@192.168.0.8 ~
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
jqzuxg0dju0z8qcmc2yvjk2xm * manager1 Ready Active Leader 20.10.17
r759xo0x5ewz4zkz3vv39e26d manager2 Ready Active Reachable 20.10.17
pzch463s4eyk3fcs15q75vb54 manager3 Ready Active Reachable 20.10.17
rqf3i5fcwetliead6knl4jego worker1 Ready Active 20.10.17
s8kai33smwyg7ilzz5gaezy24 worker2 Ready Active 20.10.17
```
#### 2.2.3 部署简单服务
同样,使用 nginx来作为样例部署一个简单的服务,在管理机上执行;
```
$ docker service create --name myweb -p 80:80 nginx
fwg499h27ex24z5r7hxmanlnj
overall progress: 0 out of 1 tasks
overall progress: 1 out of 1 tasks
1/1: running
verify: Service converged
```
创建了一个服务,名为 myweb,镜像为 nginx,发布端口为80:80。
确认是否在执行:
```
$ curl localhost
...
Welcome to nginx!
...
```
#### 2.2.4 服务副本
Swarm 允许在集群中同时运行多个副本,使用Swarm scale 命令:
```
$ docker service scale myweb=5
myweb scaled to 5
overall progress: 5 out of 5 tasks
1/5: running
2/5: running
3/5: running
4/5: running
5/5: running
verify: Service converged
```
查看一下服务任务的分布:
```
$ docker service ps myweb
$ docker service ps myweb
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
i5y2501bz7mw myweb.1 nginx:latest manager1 Running Running 12 minutes ago
598mgf71oht0 myweb.2 nginx:latest manager2 Running Running 30 seconds ago
qtev1qo00xgo myweb.3 nginx:latest worker2 Running Running 24 seconds ago
ceap6buum0be myweb.4 nginx:latest manager3 Running Running 21 seconds ago
g1ef5avrbppk myweb.5 nginx:latest worker1 Running Running 20 seconds ago
```
可见五个服务均匀分布在集群的五个节点上。
#### 2.2.5 负载均衡
为了理解swarm内部负载均衡的效果,首先将服务副本数缩减到2个,这样便于观察:
```
# 使用service update 命令,效果等同于 scale myweb=2
$ docker service update myweb --replicas 2
myweb
overall progress: 2 out of 2 tasks
1/2: running
2/2: running
verify: Service converged
[manager1] (local) root@192.168.0.8 ~
$ docker service ps myweb
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
i5y2501bz7mw myweb.1 nginx:latest manager1 Running Running 22 minutes ago
598mgf71oht0 myweb.2 nginx:latest manager2 Running Running 22 minutes ago
```
可见myweb的两个tasks分别运行在 manager1 和 manager 2 上。
在Manger2上查找并登录该容器:
```
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
43ce24c967ae nginx:latest "/docker-entrypoint.…" 35 minutes ago Up 35 minutes 80/tcp myweb.2.598mgf71oht0sxcnt6b6yxcb4
[manager2] (local) root@192.168.0.7 ~
$ docker exec -it 43ce24c967ae bash
root@43ce24c967ae:/# cd /usr/share/nginx/html/
root@43ce24c967ae:/usr/share/nginx/html# echo "THIS is Manager2" >> index.html
root@43ce24c967ae:/usr/share/nginx/html# exit
```
在manager2 执行两次 curl :
```
$ curl localhost
...