单机 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)tasksdocker service inspect <svc> 能看到 "VIP"
scheduler 根据约束/资源/标签把 task 绑定到具体 node产生 N 个 task 对象(ID 形如 serviceName.1.xxx
每个被选中节点的 dockerd 收到 task 指派通过 gRPC 推下去
节点本地 containerd 开始 create → start 容器docker ps 能看到容器名格式:serviceName.1.xxx
若定义了 configssecrets,agent 会把它们挂载进容器/var/lib/docker/swarm/secrets/ 下出现 tmpfs
若定义了 ports,所有 worker 节点上会创建 ingress 负载规则(IPVS/iptables)iptables -t mangle -L 能看到 DOCKER-INGRESS
若副本数不足,scheduler 持续 reconcile;节点宕机会重新调度docker service ps <svc> 可观察 ShutdownRunning

执行完后:

  • 你的业务容器已经在各节点跑起来
  • 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的策略里面)

接口网段作用
eth010.0.0.0/24overlay 网络 → 供 同一 overlay 里的服务互相访问(VIP/任务 IP)
eth1172.18.0.0/16docker_gwbridge → 供容器 出网(访问外网、宿主机、192.168.x.x)
eth210.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.让Swarmingress网络(即不使用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执行器,自动发现服务,然后使用分片广播并行执行任务