Docker Swarm 之 服务(Service)

摘要

Service 与 Task

什么是 Service?

  • Service 是用户定义的服务抽象,一个 Service 表示你希望在 Swarm 集群中运行的某个“应用”。

  • 它定义了你要运行的容器镜像、启动命令、副本数量、网络配置、环境变量、端口映射等信息。

  • 可以类比成 Kubernetes 中的 Deployment,代表的是“期望状态”。

什么是 Task?

  • Task 是 Service 的实际执行实例,Swarm 会根据 Service 的配置生成 Task。

  • Service中的每个副本对应一个 Task,每一个 Task 代表一个要在某个节点上运行的容器。

  • Task 的状态由 Swarm 管理,它负责启动、调度、重启等生命周期操作。

  • 当某个 Task 崩溃,Swarm 会自动重新调度一个新的 Task 来替代它。

  • Task 是不可变的,一旦创建不能修改,更新 Service 会创建新的 Task。

示例

  • 创建一个名为 nginx 的 Service,并指定镜像为 nginx:latest,并设置副本数为 3。

1
docker service create --name nginx --replicas 3 nginx:latest
  • 这里创建了一个名为 nginx 的 Service,并设置了副本数为 3,即Swarm会创建3个Task来完成这个任务,每个 Task 最终会对应一个具体的nginx容器。

项目 Service Task
定义 用户定义的服务配置 服务配置生成的执行单元
数量关系 一个 Service 包含多个 Task 一个 Task 属于一个 Service
状态 描述“期望状态” 代表“实际状态”
生命周期 可以更新 不可变,更新意味着重新创建
管理者 由用户管理 完全由 Swarm 调度和管理

Service 相关命令

命令 中文说明
create 创建一个新的服务
inspect 显示一个或多个服务的详细信息
logs 获取服务或任务的日志
ls 列出所有服务
ps 列出一个或多个服务的任务(Task)
rm 删除一个或多个服务
rollback 回滚服务的配置更改
scale 扩缩一个或多个可复制服务的副本数量
update 更新服务配置

docker service create: 创建服务

  • 常用参数说明

参数 说明 示例命令(含说明)
--name 指定服务名称 docker service create --name my-web nginx
→ 创建一个名为 my-web 的 nginx 服务
--replicas 设置副本数量(仅适用于 replicated 模式) docker service create --replicas 3 nginx
→ 启动 3 个 nginx 副本
--publish-p 映射端口(格式如 80:80 docker service create -p 8080:80 nginx
→ 将容器的 80 端口映射到主机 8080
--env-e 设置环境变量 docker service create -e ENV=prod nginx
→ 设置环境变量 ENV=prod
--mount 设置数据卷挂载 docker service create --mount type=bind,src=/data,target=/app nginx
→ 将主机的 /data 目录挂载到容器内 /app
--constraint 设置部署约束(如指定节点) docker service create --constraint 'node.labels.type == web' nginx
→ 仅部署在带标签 type=web 的节点上
--network 指定服务所属的网络(通常使用 overlay 网络) docker service create --network my-net nginx
→ 将服务连接到自定义网络 my-net
--detach-d 后台运行服务(默认行为) docker service create -d nginx
→ 后台创建服务,不阻塞终端,因为是默认行为,所以不加 -d 也是一样的,service不支持像 docker run 那样支持前台运行
--limit-cpu / --limit-memory 设置资源限制 docker service create --limit-cpu 0.5 --limit-memory 256M nginx
→ 每个任务最多使用 0.5 个 CPU 和 256MB 内存
--restart-condition 设置重启策略(如 on-failure、any、none) docker service create --restart-condition on-failure nginx
→ 仅当容器失败时自动重启
--mode 指定服务运行模式,支持:replicatedglobalreplicated-jobglobal-job docker service create --mode global nginx
→ 在集群每个节点上运行一个 nginx 实例
  • 服务运行模式(–mode)详解

模式名称 说明 使用场景示例
replicated 默认模式。用户指定需要运行多少个副本,Swarm 在合适的节点上调度这些副本。 典型的 Web 服务,如 nginx、Node.js、Java 应用等
global 每个可用节点只部署一个任务实例,不需要用户指定副本数。 系统级服务,如日志收集器(Fluentd)、监控代理(Prometheus node exporter)
replicated-job 在多个节点上按副本数运行一次性任务,任务完成后即退出。 数据处理、批处理任务,如转换文件或跑 ETL
global-job 所有节点上各运行一次的短暂任务,执行完毕即退出。 初始化脚本、每台机器上运行一次的数据清洗、初始化环境任务等

job 模式通常配合镜像中设定的入口命令使用,不适用于长期运行的服务。
replicated 和 global 模式适用于持续运行的服务,Swarm 会自动重启失败的任务。

  • 服务重启策略(–restart-condition)详解

含义说明
none 不重启任务。即使任务失败,也不会尝试恢复。适用于短生命周期的任务或测试服务。
on-failure 仅在任务异常失败时(exit code 非 0)自动重启。常用于可能偶发失败的服务。
any(默认) 无论任务如何退出(包括正常退出或失败),都会尝试重启。适用于持续运行服务。

情景总结

情景 none on-failure any
服务运行时崩溃(exit code ≠ 0) ❌ 不重启 ✅ 自动重启 ✅ 自动重启
服务正常结束(exit code = 0) ❌ 不重启 ❌ 不重启 ✅ 自动重启
持续运行型服务(如 nginx) ❌ 不推荐 可用 ✅ 推荐
一次性任务(如批处理、数据初始化) ✅ 推荐 可用 ❌ 不推荐

docker service create 使用示例

  • 创建一个名为 my-nginx 的服务,并指定 3 个副本,将 80 端口映射到主机的 80 端口,并使用 nginx 镜像

1
2
3
4
5
6
# 此时在浏览器中输入Swarm中任意节点的IP地址,即可访问到Nginx服务,即使任务没有被分配到这个节点,也能访问到Nginx服务,这就是Swarm的负载均衡功能
docker service create \
--name my-nginx \
--replicas 3 \
--publish 80:80 \
nginx
  • 创建一个名为 log-agent 的全局服务,并使用 fluentd 镜像,即每个节点都会运行一个 fluentd 容器

1
2
# global
docker service create --name log-agent --mode global fluentd
  • 创建一个名称为 web-app 的服务,并设置环境变量 NODE_ENV=production ,挂载 /data 目录到容器的 /app/data 目录,并设置 2 个副本,并且指定启动容器的命令为 node server.js

1
2
3
4
5
6
docker service create \
--name web-app \
--env NODE_ENV=production \
--mount type=bind,src=/data,target=/app/data \
--replicas 2 \
node:18 node server.js
  • 创建一个名称为 db 的服务,并指定运行在具有 role=db 标签的节点上,并且使用名为 db-data 的卷挂载数据

1
2
3
4
5
6
7
8
# 创建数据卷
docker volume create db-data
# 创建服务
docker service create \
--name db \
--constraint 'node.labels.role == db' \
--mount type=volume,source=db-data,target=/var/lib/mysql \
mysql:8
  • 使用 on-failure 策略,仅在失败时自动重启

1
2
3
4
docker service create \
--name unstable-worker \
--restart-condition on-failure \
my-worker-image
  • 指定网络,网络驱动类型为 overlay

1
2
3
4
5
6
7
8
9
10
11
# 先在manager节点上创建一个 overlay 网络(适用于 Swarm 模式)
docker network create --driver overlay my-overlay-net
# 创建驱动类型为 overlay 的网络,会立即同步所有 manager 节点,但不会同步到 worker 节点,只有当任务被分配到 worker 节点时,该网络才会同步到 worker 节点

# 创建服务并加入该网络
docker service create \
--name web-service \
--network my-overlay-net \
--replicas 5 \
nginx
# --network: 指定服务运行时连接到该网络,这样在同一个网络中的服务之间可以使用 服务名互相访问,实现服务发现和负载均衡。

小贴士

1
2
3
4
5
6
7
docker network ls
NETWORK ID NAME DRIVER SCOPE
6b7aadbbd180 bridge bridge local
5ddadf5d0608 docker_gwbridge bridge local
21c6f5b1bedd host host local
idx465x3jg68 ingress overlay swarm
a770c5ad4b13 none null local
  • 初始化Swarm集群后,会创建一个默认的 overlay 网络: ingress,如果我们创建服务时没有指定网络,那么服务就会加入 ingress 网络。
  • 但是这个默认的 ingress 网络并不能用于服务之间通过服务名称互相访问,但可以通过IP或Hostname访问服务。
  • 默认的 ingress 网络仅用于 ingress 负载均衡(即 -p 端口映射),不支持服务内部通信或 DNS 服务发现。
  • 另外,初始化Swarm集群后,还会创建一个默认的 bridge 网络: docker_gwbridge,负责连接 Swarm 集群的 Overlay 网络与宿主机网络,负责跨节点的流量转发
    • 当一个容器在 Overlay 网络里访问外部 IP,比如访问公网,流量最终通过 docker_gwbridge 网络出口出去。
    • 节点间 VXLAN 隧道的流量也会借助此网络桥接到宿主机的物理网络接口。

docker service ls: 列出所有服务

1
2
3
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
dmnztimv3pb8 my-nginx replicated 2/2 nginx:latest *:80->80/tcp

docker service inspect: 查看服务详情

1
docker service inspect my-nginx

docker service log: 查看服务日志

1
2
3
4
5
6
# 查看指定服务下所有任务的日志
docker service logs my-nginx
# 查看指定任务的日志,指定任务ID,不支持任务名称
docker service logs p4tats0f9npk
# 滚动查看日志
docker service logs -f my-nginx

docker service ps: 列出服务任务

1
2
3
4
docker service ps my-nginx
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
p4tats0f9npk my-nginx.1 nginx:latest manager1 Running Running 13 minutes ago
v7z383xy7dso my-nginx.2 nginx:latest manager2 Running Running 15 minutes ago

docker service scale: 扩容/缩容服务

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
# 启动一个服务
docker service create \
--name my-nginx \
--replicas 3 \
--publish 80:80 \
nginx
# 查看服务任务
docker service ps my-nginx
## 输出
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
p4tats0f9npk my-nginx.1 nginx:latest manager1 Running Running 14 seconds ago
v7z383xy7dso my-nginx.2 nginx:latest manager2 Running Running 2 minutes ago
w0sm5ixvi6eb my-nginx.3 nginx:latest worker2 Running Running 4 seconds ago

# 扩容到5个任务
docker service scale my-nginx=5
# 查看服务任务
docker service ps my-nginx
## 输出
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
p4tats0f9npk my-nginx.1 nginx:latest manager1 Running Running about a minute ago
v7z383xy7dso my-nginx.2 nginx:latest manager2 Running Running 3 minutes ago
w0sm5ixvi6eb my-nginx.3 nginx:latest worker2 Running Running about a minute ago
ksbeflwg3mcj my-nginx.4 nginx:latest worker1 Running Running less than a second ago
iw4zlm56n1x8 my-nginx.5 nginx:latest manager3 Running Running less than a second ago

# 缩容到2个人任务
docker service scale my-nginx=2
# 查看服务任务
docker service ps my-nginx
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
p4tats0f9npk my-nginx.1 nginx:latest manager1 Running Running 2 minutes ago
v7z383xy7dso my-nginx.2 nginx:latest manager2 Running Running 4 minutes ago


# 还可以使用如下命令进行扩缩容
docker service update --replicas=5 my-nginx

docker service update: 更新服务

  • 支持的参数

参数 说明 示例
--image 更新服务使用的镜像 --image nginx:1.25
--replicas 设置服务的副本数量(仅适用于 replicated 模式) --replicas 5
--env-add 添加环境变量 --env-add DEBUG=true
--env-rm 移除环境变量 --env-rm OLD_VAR
--publish-add 添加端口映射 --publish-add published=8080,target=80
--publish-rm 移除端口映射 --publish-rm 80
--mount-add 添加挂载 --mount-add type=bind,src=/data,dst=/data
--mount-rm 移除挂载 --mount-rm /data
--constraint-add 添加部署约束 --constraint-add 'node.labels.zone==east'
--constraint-rm 移除部署约束 --constraint-rm 'node.labels.zone==east'
--limit-cpu 设置 CPU 限制 --limit-cpu 0.5
--limit-memory 设置内存限制 --limit-memory 256M
--restart-condition 设置重启策略(none、on-failure、any) --restart-condition on-failure
--update-delay 设置任务更新之间的延迟 --update-delay 10s
--update-parallelism 设置并发更新任务的数量 --update-parallelism 2
--update-order 设置更新顺序(start-first 或 stop-first) --update-order start-first
--update-failure-action 更新失败后的动作(pause、continue、rollback) --update-failure-action rollback
--rollback 回滚到上一次成功配置 --rollback
  • 示例

1
2
3
4
5
6
7
8
9
10
docker service update \
--image nginx:1.25 \
--replicas 4 \
--env-add ENV=prod \
--limit-memory 512M \
--limit-cpu 1.0 \
--update-delay 10s \
--update-parallelism 2 \
--update-failure-action rollback \
my-nginx

docker service rollback: 回滚服务

  • 会将服务回滚到上一次成功部署的版本,包括镜像、环境变量、部署约束等。

1
2
# docker service rollback <service_name>
docker service rollback my-nginx

docker service rm: 删除服务

  • 删除服务会停止服务并删除服务。

1
2
# docker service rm <service_name>
docker service rm my-nginx

经验技巧

如何让任务运行在指定的节点上?

  • 创建服务时,可以使用 --constraint 参数指定节点的标签,使其运行在具有指定标签的节点上。

1
2
3
4
5
6
7
8
9
10
11
# 给结点加标签
docker node update --label-add env=prod worker1

# node.labels 是Swarm内置属性,表示节点的标签,这里指定节点标签为 env=prod
docker service create \
--name my-nginx \
--replicas 2 \
--constraint 'node.labels.env == prod' \
nginx:latest

# 这里要注意,运行任务后如果修改了node的标签,那么任务就会重新分配,分配是如果找不到符合标签的节点,就会运行失败。
  • 只能运行在管理节点上

1
2
3
4
5
# node.role 是 Swarm 的内置属性,表示节点的类型,值为 manager 或 worker。
docker service create \
--name manager-only-service \
--constraint 'node.role == manager' \
nginx
  • Swarm 内置属性

属性名 示例值 说明
node.id node.id == abcd1234 节点的唯一 ID(可用 docker node ls 查看)
node.hostname node.hostname == manager-1 节点主机名
node.role node.role == managernode.role == worker 节点在 Swarm 中的角色(管理/工作)
engine.labels.* engine.labels.disk == ssd Docker 引擎级别的标签(需手动设置)
node.platform.os node.platform.os == linux 节点操作系统类型
node.platform.arch node.platform.arch == x86_64 节点架构类型(如 arm64, x86_64

如何访问Service服务?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 启动一个service,3个副本,镜像为 whoami,这个镜像会返回当前访问的容器的ID,即返回的Hostname
docker service create --name whoami \
--replicas 3 \
-p 80:80 \
traefik/whoami

# 查看service服务运行在哪些节点上,看到这里只有 manager1\manager2\worker2 节点上运行,注意这里看到的ID是Task ID,并非容器ID
docker service ps whoami
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
u1elzp22hyvm whoami.1 traefik/whoami:latest manager2 Running Running 2 minutes ago
4cent1kfgghb whoami.2 traefik/whoami:latest worker2 Running Running 17 seconds ago
lu6u6j8ji0uq whoami.3 traefik/whoami:latest manager1 Running Running 40 seconds ago
# 如果希望查询某个Service的所有容器的ID,可以执行如下命令
docker service ps whoami -q | xargs docker inspect --format '{{.Status.ContainerStatus.ContainerID}}' | cut -c 1-12
a3fb63bde8e7
a14e30a02987
549610f3a1e9
  • 此时我们通过curl访问 Swarm 集群中的任意一个节点的IP,都可以访问到这个服务,比如 curl 10.211.55.12,这是 manager3 节点的 IP 地址,虽然这个服务并没有在 manager3 节点上运行,但是我们依旧可以访问到这个服务,不仅如此,每次运行命令返回的Hostname(就是容器ID)都会发生变化,其效果就是在各个运行的容器间轮询,这就是 Swarm 集群的负载均衡效果。

1
2
3
4
5
6
7
8
9
10
11
12
curl http://10.211.55.12
## 输出
Hostname: a3fb63bde8e7 # 容器ID
IP: 127.0.0.1
IP: ::1
IP: 10.0.0.36 # 容器IP,对接 br0
IP: 172.18.0.4 # 容器IP,对接 docker_gwbridge
RemoteAddr: 10.0.0.6:52964
GET / HTTP/1.1
Host: 10.211.55.12
User-Agent: curl/7.61.1
Accept: */*
  • 在集群内部访问服务,建议将所有服务运行在相同的network中,这样可以不同的service之间可以通过服务名称访问服务,在集群外部,可以通过nginx等代理访问服务。

  • 可以编写一个脚本方便查看service与container的运行关系,比如:docker_service_container

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/bin/bash
# filename: docker_service_container
# 用法提示
if [ -z "$1" ]; then
echo "用法: $0 <SERVICE_NAME>"
exit 1
fi
# service名称
SERVCIE_NAME=$1
# 所属network,默认 ingress,这里要注意一下,如果是自定义的overlay网络,只能获取到当前主机上的容器IP
NETWORK=$2
if [ -z "$2" ]; then
NETWORK=$(docker service inspect $SERVCIE_NAME --format '{{json .Endpoint.VirtualIPs}}' | jq '.[0].NetworkID' | sed 's/"//g')
fi

# NETWORK=${2:-ingress}

d1=$(echo -e "SERVICE-ID TASK-ID CONTAINER-ID NODE-ID" | awk '{printf "%-12s %-12s %-12s %-12s\n", substr($1,1,12), substr($2,1,12), substr($3,1,12), substr($4,1,12)}' ;\
docker service ps $SERVCIE_NAME --filter "desired-state=running" -q | xargs docker inspect --format '{{.ServiceID}} {{.ID}} {{.Status.ContainerStatus.ContainerID}} {{.NodeID}}' \
| awk '{printf "%-12s %-12s %-12s %-12s\n", substr($1,1,12), substr($2,1,12), substr($3,1,12), substr($4,1,12)}')

#echo "$d1"

d2=$(docker service ps $SERVCIE_NAME --filter "desired-state=running")

#echo "$d2"

nn1=$(awk '
NR==FNR && FNR > 1 {
id = $1
name = $2
node = $4
desired = $5
# 拼接 CURRENT STATE(从第6列开始的所有字段)
current = ""
for (i=6; i<=NF; i++) {
current = current $i " "
}
gsub(/ /, "-", current)
current = substr(current, 1, length(current)-1) # 去掉最后空格
info[id] = name "\t" node "\t" desired "\t" current
next
}
FNR==1 {
print $0 "\tTASK-NAME\tNODE\tDESIRED_STATE\tCURRENT_STATE"
next
}
{
print $0 "\t" info[$2]
}
' <(echo "$d2") <(echo "$d1") | tail -n +2 | column -t)

nn2=$(echo "CONTAINER-ID IP";docker network inspect ${NETWORK} -f '{{range $id, $container := .Containers}}{{slice $id 0 12}} {{$container.Name}} {{$container.IPv4Address}}{{println}}{{end}}' | grep $SERVCIE_NAME | awk -F " " '{print $1" "$3}')

awk '
NR==FNR { ip[$1]=$2; next }
{ print $0, ip[$3] }
' <(echo "$nn2") <(echo "$nn1") | column -t
  • 运行

1
2
3
4
5
6
7
8
chmod +x /usr/local/bin/docker-swarm-service
# 执行脚本
docker-swarm-service whoami
## 输出
SERVICE-ID TASK-ID CONTAINER-ID NODE-ID TASK-NAME NODE DESIRED_STATE CURRENT_STATE IP
snuhv0g2dm1q czisrjrp8k2t 9e4ce349971c kp2zerd28xgz whoami.1 manager1 Running Running-about-an-hour-ago 10.0.0.35/24
snuhv0g2dm1q tvbkwmnafedw 0d586021260d kp2zerd28xgz whoami.2 manager1 Running Running-about-an-hour-ago 10.0.0.36/24
snuhv0g2dm1q 815qp859a1f4 449d5e1a231d kp2zerd28xgz whoami.3 manager1 Running Running-about-an-hour-ago 10.0.0.15/24