单机 Docker Swarm 创建多副本服务
准备
docker以及相关的配套软件
操作步骤
docker-compose.yaml
services:
you-cats:
image: you-cats:20251027
restart: on-failure:3
deploy:
replicas: 3
endpoint_mode: vip
volumes:
- /opt/springboot/YouCats/application-docker.yml:/application.yml
- /opt/springboot/YouCats/log4j2-docker.xml:/log4j2.xml
- /opt/logs/YCM:/opt/logs/YCM
- /opt/webapps:/opt/webapps
ports:
- "17000:8032"
启动与删除命令
1、启动操作
# 初始化
docker swarm init
# 启动
docker stack deploy -c docker-compose.yaml your_stack_name
命令解释
① docker swarm init —— 把 Docker 引擎变成“集群大脑”
| 步骤 | 动作 | 落地点 |
|---|---|---|
| ① | 生成 256-bit 集群根 CA 证书 | /var/lib/docker/swarm/certificates/swarm-root-ca.crt |
| ② | 生成 manager 节点证书(CN=swarm-manager) | 同上目录 |
| ③ | 创建 Raft 数据库目录 | /var/lib/docker/swarm/raft/ 里出现 raft.db |
| ④ | 启动内置的 Raft 存储插件(swarm-raftkit) | 监听 127.0.0.1:4242(仅本机) |
| ⑤ | 启动 dockerd 内置的 swarmkit goroutine | 成为 manager + leader |
| ⑥ | 创建默认 overlay 网络 ingress(用于路由网格) | docker network ls 能看到 swarm 作用域的 ingress |
| ⑦ | 输出 SWMTKN 令牌 | 通过 openssl 把根 CA 哈希+随机密钥拼成 token |
| ⑧ | 本机自动打上 node 标签 | docker node ls 能看到自己状态 Ready、角色 Leader |
执行完后:
- 当前节点既是 manager 也是 worker
- 2377 端口开始监听,等待其他 manager/worker 加入
- 集群 ID、根 CA、join-token 全部持久化到本地磁盘
② docker stack deploy -c docker-compose.yaml your_stack_name —— 把“声明式 YAML”变成“真实容器”
| 阶段 | 动作 | 落地点/命令 |
|---|---|---|
| ① | client 端把 compose 文件解析成 Stack spec | 本地完成 |
| ② | 调用 Docker API POST /services/create | 发到 manager 2375/2377 |
| ③ | manager 把 spec 存进 Raft 日志(写盘 + 复制) | raft.db 新增一条 |
| ④ | allocator 为每个 service 分配 虚拟 IP (VIP) 及 tasks | docker service inspect <svc> 能看到 "VIP" |
| ⑤ | scheduler 根据约束/资源/标签把 task 绑定到具体 node | 产生 N 个 task 对象(ID 形如 serviceName.1.xxx) |
| ⑥ | 每个被选中节点的 dockerd 收到 task 指派 | 通过 gRPC 推下去 |
| ⑦ | 节点本地 containerd 开始 create → start 容器 | docker ps 能看到容器名格式:serviceName.1.xxx |
| ⑧ | 若定义了 configs、secrets,agent 会把它们挂载进容器 | /var/lib/docker/swarm/secrets/ 下出现 tmpfs |
| ⑨ | 若定义了 ports,所有 worker 节点上会创建 ingress 负载规则(IPVS/iptables) | iptables -t mangle -L 能看到 DOCKER-INGRESS 链 |
| ⑩ | 若副本数不足,scheduler 持续 reconcile;节点宕机会重新调度 | docker service ps <svc> 可观察 Shutdown → Running |
执行完后:
- 你的业务容器已经在各节点跑起来
- overlay 网络、VIP、iptables 规则、secrets/configs 全部就绪
- 整个集群持续“自愈合”:挂掉节点、缩容扩容、滚动更新都由 Swarm 自动完成
可能会出现下面的问题:
Error response from daemon: could not choose an IP address to advertise since this system has multiple addresses on interface enp4s0 (2409:8a60:1e85:*:*:*:*:* and 2409:8a60:1e85:*:*:*:*:*) - specify one with --advertise-addr
这个错误是 Docker Swarm 初始化时不知道用哪个 IP 地址 导致的,因为你机器上有 多个全局单播 IPv6 地址。
解决思路:显式告诉 Swarm 用哪一个地址(或干脆用 IPv4 地址)。
docker swarm leave --force # 如果之前初始化失败,先清掉
# 例如用 IPv4 地址 192.168.1.100,是部署服务的宿主机地址
docker swarm init --advertise-addr 192.168.1.100
# 或者指定 IPv6 地址(用中括号括起来)
docker swarm init --advertise-addr [2409:8a60:1e85:*:*:*:*:*]
# 成功出现以下日志
Swarm initialized: current node ... is now a manager.
...
# 继续部署
docker stack deploy -c docker-compose.yaml your_stack_name
2、以下是停止或删除操作
# 1. 停栈
docker stack rm youcats
# 2. 可选:把 swarm 也拆掉(回到普通 compose 模式)
docker swarm leave --force
# 3. 可选:清理留下的卷,不影响宿主机持久化卷
docker volume prune
# 缩容到0副本,但是不删除栈
docker service scale youcats_you-cats=0
滚动更新
1.使用docker service update ✅推荐
# 更新指定服务的镜像版本
docker service update --image your-registry/your-app:new-version backend_your-service-name
# 示例:如果您的服务名为 backend_web
docker service update --image myregistry/app:v2.0.0 backend_web
2.强制滚动更新
# 强制更新(即使没有配置变更)
docker service update --force backend_your-service-name
# 结合镜像更新和强制重启
docker service update --image myregistry/app:v2.0.0 --force backend_web
3.更新docker-compose.yaml并重新部署 ✅推荐
# 1. 修改 docker-compose.yaml 中的镜像版本
# 2. 重新部署(Docker Swarm 会进行滚动更新)
docker stack deploy -c docker-compose.yaml backend
4.配置滚动更新参数
version: '3.8'
services:
your-app:
image: your-registry/your-app:latest
deploy:
replicas: 3
update_config:
parallelism: 1 # 每次更新1个实例
delay: 10s # 每个实例更新间隔10秒
failure_action: rollback
order: start-first # 先启动新实例,再停止旧实例
rollback_config:
parallelism: 1
delay: 5s
failure_action: pause
order: stop-first
然后使用下面命令更新
docker stack deploy -c docker-compose.yaml backend
5.批量更新多个服务
# 获取所有服务
docker service ls --filter label=com.docker.stack.namespace=backend -q
# 批量更新所有服务
for service in $(docker service ls --filter label=com.docker.stack.namespace=backend -q); do
docker service update --image myregistry/app:v2.0.0 $service
done
6.高级更新策略
6.1.蓝绿部署式更新
# 1. 先更新部分实例
docker service update --image myregistry/app:v2.0.0 --update-parallelism 1 backend_web
# 2. 等待验证
sleep 60
# 3. 更新剩余实例
docker service update --image myregistry/app:v2.0.0 --update-parallelism 2 backend_web
6.2.金丝雀发布
# 1. 先更新1个实例
docker service update --image myregistry/app:v2.0.0 --update-parallelism 1 backend_web
# 2. 监控这个实例
docker service logs -f backend_web.1
# 3. 如果正常,更新所有实例
docker service update --image myregistry/app:v2.0.0 --update-parallelism 3 backend_web
自动化脚本实例
#!/bin/bash
# update-service.sh
STACK_NAME="backend"
SERVICE_NAME="web"
NEW_IMAGE="myregistry/youcats:v1.5.0"
echo "开始更新服务 $SERVICE_NAME..."
echo "新镜像版本: $NEW_IMAGE"
# 更新服务
docker service update --image $NEW_IMAGE ${STACK_NAME}_${SERVICE_NAME}
# 等待更新完成
echo "等待更新完成..."
while true; do
RUNNING=$(docker service ps ${STACK_NAME}_${SERVICE_NAME} --filter "desired-state=running" --format "table {{.CurrentState}}" | grep -c "Running")
TOTAL_REPLICAS=$(docker service inspect ${STACK_NAME}_${SERVICE_NAME} --format "{{.Spec.Mode.Replicated.Replicas}}")
if [ "$RUNNING" -eq "$TOTAL_REPLICAS" ]; then
echo "所有实例更新完成!"
break
fi
echo "更新中... ($RUNNING/$TOTAL_REPLICAS 个实例运行中)"
sleep 10
done
echo "服务更新完成!"
健康检查脚本
#!/bin/bash
# health-check.sh
SERVICE="${STACK_NAME}_${SERVICE_NAME}"
TIMEOUT=300 # 5分钟超时
INTERVAL=10
echo "执行健康检查..."
end=$((SECONDS+TIMEOUT))
while [ $SECONDS -lt $end ]; do
# 检查所有实例是否健康
UNHEALTHY=$(docker service ps $SERVICE --format "table {{.CurrentState}}" | grep -v "Running" | grep -v "\\-\\-" | wc -l)
if [ "$UNHEALTHY" -eq 0 ]; then
echo "所有实例健康!"
exit 0
fi
echo "等待实例健康... ($UNHEALTHY 个不健康实例)"
sleep $INTERVAL
done
echo "健康检查超时!"
exit 1
回滚操作,快速回滚
# 回滚到上一个版本
docker service update --rollback backend_web
# 或者指定回滚到特定版本
docker service update --image myregistry/youcats:v1.4.0 backend_web
网络放通
新启用的ingress出口IP网段(也就是docker_gwbridge网段172.18.0.0/16)必须加入iptables里面,否则无法访问到宿主机的一些服务。(本人将该网段加入了ufw的策略里面)
| 接口 | 网段 | 作用 |
|---|---|---|
| eth0 | 10.0.0.0/24 | overlay 网络 → 供 同一 overlay 里的服务互相访问(VIP/任务 IP) |
| eth1 | 172.18.0.0/16 | docker_gwbridge → 供容器 出网(访问外网、宿主机、192.168.x.x) |
| eth2 | 10.0.1.0/24 | 另一 overlay 子网(ingress 或别的 attachable network) |
容器内看默认路由
ip route | grep default
另外,如果前期设置了DOCKER_USER链,那么也需要将docker_gwbridge网段加入DOCKER_USER网络策略链中,否则无法通过网关访问外网地址。
访问服务
使用docker swarm启动的多副本服务,是通过ingress做了一层负载均衡操作的,而实际在配置compose.yaml时又配置了端口暴露,那么我们只需要访问宿主机的地址加路径即可,如17000:8032就使用了宿主机的17000端口映射了容器的8032端口,所以使用宿主机IP和端口ip:17000/path即可访问服务,ingress会自动轮询多个服务。
需要注意
docker stack+deploy.replicas不能和network_mode: bridge混用;
ports:在 bridge 模式下只会随机落在某一个副本所在节点,负载均衡/灰度发布都做不到,也看不到3个实例各自独立的IP。
要想「3个副本都能被均匀访问」,必须:
1.让Swarm用ingress网络(即不使用network_mode: bridge);
2.或者自己建overlay网络+上层反向代理(Traefik、Nginx、HAProxy、K8s Ingress等)。
XXL-JOB分片广播多服务副本
新建entrypoint.sh,使用gateway地址作为执行器地址,不然XXL-JOB无法访问
#!/bin/sh
# ① 动态注入环境变量,只对当前的shell有效,切换后需要重新export
XXL_JOB_EXECUTOR_IP=$(ip route get 1 | awk '{print $7;exit}')
# 去除空格 %% 后,## 前
XXL_JOB_EXECUTOR_IP=${XXL_JOB_EXECUTOR_IP%% }
XXL_JOB_EXECUTOR_IP=${XXL_JOB_EXECUTOR_IP## }
export XXL_JOB_EXECUTOR_IP
HOST_NAME=$(hostname)
HOST_NAME=${HOST_NAME%% }
HOST_NAME=${HOST_NAME## }
export HOST_NAME
# ② 把变量打到控制台,方便排查
echo ">>> 容器真实 IP 是 ${XXL_JOB_EXECUTOR_IP}"
echo ">>> 容器主机名是 ${HOST_NAME}"
# ③ 用 exec 把 Java 进程变成 1 号进程,信号可透传
exec java ${JAVA_OPTS} -Dfile.encoding=UTF-8 -jar app.jar "$@"
新建dockerfile
# 定义父镜像
FROM jdk:17-tools
# 将jar包添加到容器
COPY YouCats.jar /YouCats.jar
# 将entrypoint.sh添加进容器
COPY entrypoint.sh /entrypoint.sh
#暴露容器端口为8032 Docker镜像告知Docker宿主机应用监听了8032端口
EXPOSE 8032
# 赋权
RUN chmod +x /entrypoint.sh
# 执行entrypoint,会最先执行
ENTRYPOINT ["/entrypoint.sh"]
构建镜像
docker build -t image-name:version -f dockerfile-path .
程序配置,其中使用${}包裹的都是在执行entrypoint中提前注入的环境变量,可直接使用
xxl:
job:
enabled: true
admin:
address: http://192.168.1.5:8080/xxl-job-admin
timeout: 300
accessToken: ...
executor:
appName: ycm-pro-docker
address:
ip: ${XXL_JOB_EXECUTOR_IP}
logPath: /opt/logs/YCM/docker/xxl-${HOST_NAME}
logRetentionDays: 30
配置XXL-JOB执行器,自动发现服务,然后使用分片广播并行执行任务
评论