5603 字
28 分钟
Docker Swarm 入门:多机集群与滚动更新
2026-11-15

先说清楚:什么时候单机不够用了#

前面十几篇里,我们的服务都跑在一台机器上:一份 Compose 文件、一台服务器、一个 docker compose up -d 搞定。这套打法简单、省心,但它有两个天花板,撞上任何一个,单机就真的不够用了。

第一个是可用性。单机意味着单点:服务器宕机、Docker 守护进程崩溃、机房断网,服务就整体消失。Watchtower 更新容器时那几秒的重启窗口可以忍,但机器本身挂掉几小时这种事,如果服务是对外营业的,就忍不了。更现实的场景是:你想给服务器打补丁、换内核、重启 Docker,单机上无路可退,只能挑深夜停机维护。

第二个是横向扩容。单机的 CPU 和内存就是上限。某个服务火了、流量翻了十倍,你在单机上能做的只有把 replicas 写大——可副本再多也挤在同一台机器上抢资源,机器本身就是瓶颈。真正需要的是把副本分散到多台机器上,机器不够了再加一台。

撞上这两堵墙,就需要”集群编排”:让多台机器组成一个整体,你只声明”我要这个服务跑 3 个副本”,系统自己决定每个副本落在哪台机器上,某台机器挂了,副本自动在别的机器上补回来。

做这件事的工具,名气最大的是 Kubernetes,但它概念多、组件重,学习曲线陡峭。其实 Docker 自己就内置了一个编排器——Swarm。它就藏在你已经装好的 Docker 里,不用额外安装任何软件,一条命令就能把单机 Docker 变成集群节点。Swarm 的定位就是:用你已经会的 Compose 知识,平滑地跨到多台机器上。本篇我们用三台机器把集群组起来,跑服务、玩自愈、做滚动更新,最后把 Swarm、单机 Compose、Kubernetes 三者的选型边界讲清楚。

WARNING

Swarm 是 Docker 的内置功能,docker swarm 系列命令在装好 Docker Engine 的机器上直接可用。本文的三台机器请提前各自按系列第一篇的方法装好 Docker,并确保它们之间网络互通(同机房、同内网最好)。

核心概念:先把词对上号#

Swarm 的概念不多,一张表对完号,后面实战就不会晕。

概念人话解释
节点(Node)集群里的一台机器,装着 Docker 并加入了集群
经理节点(Manager)集群的”大脑”,保存集群状态、调度副本、接受你的命令;它自己也能干活跑容器
工作节点(Worker)纯”干活的”,只负责运行分给它的容器,不参与决策
服务(Service)你声明的期望状态:“我要 nginx 镜像跑 3 个副本,暴露 80 端口”
任务(Task)服务拆出来的单个副本,落到某个节点上变成一个实际容器
副本(Replicas)同一个服务要维持的任务数量,少了 Swarm 自动补,多了自动删
栈(Stack)一组相关服务的集合,用一份 Compose 文件整体部署、整体管理
overlay 网络跨机器的虚拟网络,不同机器上的容器像在同一个局域网里互相访问
路由网格(Routing Mesh)在任意节点访问服务的端口,都能被转到某个健康副本上,不用关心副本具体在哪

这里面最重要的是理解**“服务”和”容器”的区别**。以前你操作的是容器:docker run 出来一个,死了就是死了。现在你操作的是服务:你只负责声明”应该是什么样”,Swarm 负责让现实不断向声明靠拢——副本死了它补、节点挂了它迁移。这种”声明式”的思维方式,是所有编排系统的共同点,先在这里适应,后面看 Kubernetes 也不会陌生。

组集群实战:三台机器变一台”大机器”#

假设三台机器的内网 IP 分别是:

机器内网 IP计划角色
node1192.168.1.11Manager
node2192.168.1.12Worker
node3192.168.1.13Worker

第一步:在 node1 上初始化集群#

在 node1 上执行:

Terminal window
docker swarm init --advertise-addr 192.168.1.11

--advertise-addr 是告诉集群其他节点”用这个地址来找我”,多网卡机器上必须写对,写错了后面加入会连不上。执行成功后输出大致如下:

Swarm initialized: current node (xxxx) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-xxxxxx 192.168.1.11:2377

这一刻,node1 就成了集群的第一个 Manager,集群诞生了。输出里那条现成的 join 命令带着令牌(token),就是给其他机器入伙用的”邀请码”。

第二步:其余机器加入#

在 node2、node3 上分别执行输出里给的那条命令:

Terminal window
docker swarm join --token SWMTKN-1-xxxxxx 192.168.1.11:2377

成功后会看到 This node joined a swarm as a worker.。如果令牌忘了,也不用慌,在 Manager 上随时能重新查看:

Terminal window
# 查看 worker 加入令牌
docker swarm join-token worker
# 查看 manager 加入令牌
docker swarm join-token manager

两种令牌权限不同:拿 worker 令牌加入的只能当 Worker,拿 manager 令牌加入的会成为新的 Manager。join 令牌等于集群的入伙凭证,泄露了任何人都能把机器加进你的集群,务必当密码保管。怀疑泄露时用 docker swarm join-token --rotate worker 一键轮换,旧令牌立即作废。

第三步:验证集群状态#

回到 Manager(node1)上:

Terminal window
docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
a1b2c3 * node1 Ready Active Leader 27.x
d4e5f6 node2 Ready Active 27.x
g7h8i9 node3 Ready Active 27.x

三台都是 Ready,node1 是 Leader,集群就组好了。注意:所有 docker service、docker stack、docker node 管理命令只能在 Manager 上执行,在 Worker 上敲会直接报错,这是故意的——Worker 不持有集群状态,无权发号施令。

经理节点为什么要奇数个#

Manager 之间靠 Raft 协议同步集群状态,任何决策都需要”多数派”同意。这就决定了 Manager 数量的选择不是越多越好,而是要看容错:

Manager 数量决策需要的多数能容忍挂掉的 Manager 数评价
110单点,仅适合尝鲜和测试
220和 1 台没区别,还更脆弱,不要用
321小集群的甜点配置
532中等规模生产可用
743上限附近,再多同步成本大于收益

关键结论:2 台 Manager 是最差配置——挂任意一台就凑不齐多数派,整个集群无法调度,还不如 1 台。所以生产环境要么 3 台、要么 5 台,永远是奇数。还有一个常见误区要避开:Manager 也能跑容器,但节点多了以后,建议用下面的命令把 Manager 设成只调度不干活,避免管理进程和业务容器抢资源:

Terminal window
docker node update --availability drain node1

drain(排空)会把该节点上的副本迁走并标记为不再接收新副本,这个命令在后面维护单台机器时也非常好用。

WARNING

本文为了演示方便只设了 1 个 Manager,真实生产请至少 3 个。单 Manager 时它一旦宕机,正在运行的容器不会停(Worker 会继续跑已有副本),但集群无法再调度、无法补副本、无法更新服务——所以只是”暂时死不了”,不是高可用。

第一个服务:亲眼看一次自愈#

在 Manager 上创建第一个服务:

Terminal window
docker service create --name web --replicas 3 --publish published=8080,target=80 nginx:alpine

参数逐个看:--name web 服务名;--replicas 3 要维持 3 个副本;--publish published=8080,target=80 把容器内的 80 端口通过路由网格发布到集群所有节点的 8080 端口。注意新式写法推荐 published=...,target=... 的长格式,比老式的 -p 8080:80 更明确(短格式当然也能用)。

查看服务与副本分布:

Terminal window
docker service ls
docker service ps web
NAME MODE REPLICAS IMAGE PORTS
web replicated 3/3 nginx:alpine *:8080->80/tcp
ID NAME IMAGE NODE CURRENT STATE
xxxx web.1 nginx:alpine node1 Running
yyyy web.2 nginx:alpine node2 Running
zzzz web.3 nginx:alpine node3 Running

REPLICAS 3/3 表示期望 3 个、实际 3 个,状态健康。Swarm 自动把三个副本均匀摊到了三台机器上。现在,在浏览器或用 curl 访问任意一台机器的 8080 端口:

Terminal window
curl http://192.168.1.13:8080

都能打开 nginx 的欢迎页——哪怕你访问的这台机器上根本没有空闲副本在监听,路由网格也会把请求转到有副本的节点。这就是集群的感觉:三台机器对外像一台。

接下来见证自愈。去 node2 上,把 web.2 对应的容器强行删掉,或者干脆直接在 node2 上执行 docker stop 停掉那个容器。然后回 Manager 上再看:

Terminal window
docker service ps web

你会看到 web.2 的旧任务变成 Shutdown/Failed,紧接着一个新的任务被创建并调度起来,REPLICAS 很快回到 3/3。整个过程你什么都没做。你声明了”3 个副本”,Swarm 就永远让现实等于 3 个,这就是编排的全部魔法。极端一点,你把 node2 整台机器关机,它的副本会被重新调度到 node1、node3 上补齐——单机时代那种”机器挂了服务就没了”的日子结束了。

玩够了删掉服务:

Terminal window
docker service rm web

stack 部署:把 Compose 文件搬上集群#

单个服务用命令创建还行,真实项目是一组服务加网络加卷,还是得靠文件。这就是 stack:它吃的正是你已经写熟的 Compose 文件,部署命令是 docker stack deploy。

先看一份能直接用于 stack 的 Compose 文件(保存为 stack.yml):

services:
web:
image: nginx:alpine
ports:
- "8080:80"
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
max_attempts: 3

和普通 Compose 文件九成相同,区别集中在新增的 deploy 段:replicas 是副本数,update_config 控制滚动更新的节奏,restart_policy 控制副本失败后的重启策略。这些字段的思路在系列的 Compose 进阶篇里已经铺垫过,细节可以回去对照看:Docker Compose 进阶:环境变量、健康检查与多环境。

部署:

Terminal window
docker stack deploy -c stack.yml myapp

myapp 是栈名,栈内所有服务、网络、卷的名字都会自动加上 myapp_ 前缀,避免多栈之间撞名。查看与管理:

Terminal window
docker stack ls # 所有栈
docker stack services myapp # 栈里的服务
docker stack ps myapp # 栈里的全部任务
docker stack rm myapp # 整个栈一键拆除

和 docker compose up 的区别,心里要有数#

stack 用的是同一份文件格式,但执行者换成了 Swarm,两边行为有几处关键不同:

对比项docker compose updocker stack deploy
运行范围单台机器整个集群多台机器
副本与调度容器就在本机按 replicas 分散到各节点
build: 字段现场构建镜像被忽略,镜像必须提前构建好推到仓库
depends_on控制启动顺序被忽略,服务各自独立启动
deploy 段单机下基本被忽略(Swarm 模式专用)完整生效:副本、更新、重启、资源、放置约束
容器名可用 container_name 指定由 Swarm 自动生成,不可指定
更新方式up 时重建变化的容器声明式收敛 + 滚动更新

最需要记住的是 build: 被忽略这一条:Swarm 节点是多台,每台现场构建既慢又不一致,所以stack 部署的镜像必须先构建好并推送到所有节点都能拉取的镜像仓库(Docker Hub、私有 Registry 或 Harbor 都行),文件里只写镜像名和标签。本地没推仓库就 deploy,副本会在拉不到镜像的节点上反复失败。

滚动更新:换版本不停服#

服务跑起来只是开始,真正体现编排价值的是更新。假设 nginx 出了新版本,要把镜像从 nginx:1.27-alpine 升到 nginx:1.28-alpine。

先看用命令怎么滚动更新一个已有服务:

Terminal window
docker service update \
--image nginx:1.28-alpine \
--update-parallelism 1 \
--update-delay 10s \
web

--update-parallelism 1 表示一次只更新 1 个副本,--update-delay 10s 表示每批更新之间停 10 秒观察。更新过程是:停掉一个旧副本 → 在原地(或别的节点)拉起新版副本 → 等它 Running → 隔 10 秒 → 再动下一个。3 个副本分三批换完,全程服务始终有副本在对外响应,零停机。如果用的是 stack 管理,更新更简单:改掉 stack.yml 里的镜像标签和 update_config,重新执行一遍 docker stack deploy -c stack.yml myapp,Swarm 会自己算出差异并滚动收敛。

观察更新进度:

Terminal window
docker service ps web
docker service inspect --pretty web

更新到一半发现新版有 bug?一条命令回滚到上一个版本:

Terminal window
docker service rollback web

Swarm 保存了服务的上一份规格,回滚同样是滚动执行的——旧版本副本逐个替换回来。注意 rollback 只能回退一步,它不是版本历史系统;真正的版本管理靠你的镜像标签和 Git 里的 Compose 文件,这也是为什么 stack 文件要进版本库。

WARNING

滚动更新零停机的前提是服务本身无状态、且新旧版本能短暂共存(比如数据库表结构变更要兼容新旧两个版本)。有状态的强一致服务别指望靠滚动更新蒙混过关,下一节细说。

数据与状态:Swarm 的现实短板#

必须把话说在前面:Swarm 天生适合无状态服务,有状态服务在它上面很难受。

原因在于卷(volume)的本质。Docker 的本地卷是绑在某台具体机器上的:一个数据库服务的副本今天落在 node2,它的卷就在 node2 的硬盘里;明天 node2 宕机,Swarm 在 node3 上把这个副本补起来——可 node3 上没有那份数据,新副本面对的是一个空卷,等于数据”丢”了。对 nginx 这类无状态服务这完全不是问题,副本之间一模一样、随起随灭;但对 MySQL、PostgreSQL 这类服务,这是灾难。

业界的通行处理方式有三条,按推荐程度排序:

方案做法适用情况
数据库不进 Swarm数据库单独部署(单机、主从或云数据库),Swarm 只跑无状态的应用层绝大多数中小项目,首选
放置约束钉住节点用 deploy.placement.constraints 把有状态服务固定在某台机器,卷不漂移接受”这台机器挂了该服务就停”的妥协时
分布式存储上 Ceph、GlusterFS、NFS 或云盘做共享卷有专门运维能力的大型场景

放置约束的写法长这样,把服务钉在 node2 上:

deploy:
placement:
constraints:
- node.hostname == node2

钉住之后它就不再漂移,本质上是”借 Swarm 的壳、行单机之实”,可用性回到了单机水平,但至少更新和管理统一了。我的建议很明确:应用层放心上 Swarm,数据库这类有状态核心先留在集群之外,等规模真的逼到那一步,再考虑 Kubernetes 生态里的 StatefulSet 和专业存储方案。

secrets 与 configs:敏感信息别再写进环境变量#

在单机 Compose 时代,密码经常直接写在 .env 或环境变量里,多机集群下这更危险——环境变量在 docker service inspect 里一览无余。Swarm 内置了 secrets(机密)和 configs(普通配置)两个机制:它们由 Manager 加密保存(静态加密于 Raft 日志中),只在运行时以文件形式挂进需要它的容器,路径是 /run/secrets/<名字>,且只存在于内存文件系统里,不落盘。

secrets 最小例子#

Terminal window
# 从标准输入创建 secret,不在命令行和 shell 历史里留明文
printf 'myS3cretPass' | docker secret create db_pass -
# 创建服务时挂载
docker service create --name db \
--secret db_pass \
-e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_pass \
mysql:8

注意这里给应用的是 MYSQL_ROOT_PASSWORD_FILE 这个文件路径而不是密码本身——主流数据库镜像都支持用 _FILE 后缀的环境变量从文件读密码。应用读到的仍是密码,但密码从未出现在环境变量、镜像和 Compose 文件里。

configs 最小例子#

configs 是同一套机制的不加密版,用来分发 nginx 配置这类普通文件,改配置不用重做镜像:

Terminal window
docker config create nginx_conf ./nginx.conf
docker service create --name web \
--config source=nginx_conf,target=/etc/nginx/nginx.conf \
nginx:alpine

配置更新时创建新版本 config 再 docker service update --config-rm / --config-add 滚动替换即可。两者的小结:密码、令牌、证书私钥用 secret;普通配置文件用 config;都不要再塞环境变量和镜像层。

选型:Swarm、Compose、Kubernetes 怎么选#

最后把三者的边界一次说清:

维度单机 ComposeDocker SwarmKubernetes
机器数量1 台2 到几十台几台到成千上万台
学习成本最低,你已经会了低,复用 Compose 知识高,概念与组件成体系
高可用无副本自愈 + 多 Manager副本自愈 + 更细的控制面冗余
功能丰富度基础够用:滚动更新、secrets、overlay 网络极强:自动伸缩、丰富的调度与生态
有状态支持卷在本机,直观弱,需绕开或钉节点StatefulSet + 存储生态,完整但复杂
运维负担几乎为零很轻需要专人或托管服务
生态现状活跃仍在维护,功能稳定但生态在收缩事实标准,生态庞大

我的建议分三档:

  • 一台机器能装下、停机几分钟能接受:继续单机 Compose,别折腾。这仍是个人项目和小型服务的最佳解。
  • 有了两三台机器、要高可用和不停机更新、团队不想再学一套新系统:选 Swarm,它的每一分复杂度都花在了刀刃上。你前面十几篇积累的镜像、网络、Compose 知识全部直接复用,一个下午就能上线。
  • 规模奔着几十台以上、需要自动伸缩、复杂调度、接第三方生态(监控、日志、服务网格全家桶):直接 Kubernetes,不要在 Swarm 上硬撑。需要说明的是,Swarm 本身仍由 Docker 公司维护、没有停止更新,“Swarm 已死”的说法并不准确;但新功能与社区生态的重心确实在 Kubernetes 一边,选型时要把这点算进长期账。

判断标准就一条:先看你的痛点是”单机不够用”还是”需要平台级能力”,前者 Swarm 绰绰有余,后者一步到位上 Kubernetes,中间反复横跳才是最贵的。

排障:最常见的四类问题#

节点状态 Down 或 Disconnected#

在 Manager 上 docker node ls 看到某节点不是 Ready:

  1. 先查那台机器本身:Docker 服务是否在跑(systemctl status docker)、机器是否宕机、磁盘是否写满(磁盘满会导致 Docker 异常)。
  2. 查节点间连通性:Manager 与 Worker 之间要互通 2377/TCP(集群管理)、7946/TCP+UDP(节点间发现)、4789/UDP(overlay 网络数据面),云服务器记得看安全组,很多人栽在这一步。
  3. 临时失联的节点恢复网络后会自动归队;确认机器已报废的,用 docker node rm -f <节点> 在 Manager 上强制清除,再决定是否补新机器。

swarm join 失败#

最常见三类报错与对策:

现象常见原因处理
connection refused / timeout2377 端口被防火墙或安全组拦了放行 2377/TCP 后重试
token 相关的 unauthorized令牌抄错或已轮换在 Manager 上 docker swarm join-token worker 重新取
node already part of a swarm这台机器以前加过别的集群先 docker swarm leave 退出旧集群再加入

另外 --advertise-addr 写错(写成了机器的另一张网卡地址)也会导致 join 时连不上 Manager init 时广播的地址,初始化时务必用其他节点真正能到达的那个 IP。

副本一直 Pending 或 Preparing 起不来#

服务层面查起:

Terminal window
docker service ps --no-trunc web
docker service inspect web

service ps 最后一列的错误信息最关键,逐个对:

  • Pending 且无错误信息:没有节点满足调度条件——副本数超过可用节点、放置约束写错、或者节点都被 drain 了,docker node ls 看 AVAILABILITY 列。
  • Preparing 反复重试:多半在拉镜像,镜像名或标签写错、仓库要登录、私有仓库没在每台节点上 docker login(Swarm 部署需要加 --with-registry-auth 把登录凭证分发给各节点)。
  • Rejected:节点资源不足或端口冲突,看错误信息里的具体描述。
  • 进一步深挖单个任务:docker service ps 找到任务所在节点,去那台机器上 docker logs 看对应容器的日志,排障方法与单机完全一致。

服务发布了端口却访问不通#

先确认路由网格工作正常:docker service ls 看 PORTS 列是否真的发布了;再查云安全组和系统防火墙是否放行了发布端口到所有节点(路由网格是任意节点都能进,所以每台都要放行);最后检查 overlay 数据面端口 4789/UDP 是否被拦,它不通时跨节点的转发会直接失效。

小结#

这一篇我们跨过了单机到集群的门槛:

  • Swarm 是 Docker 内置编排器,不用另装,先想清楚”可用性”和”横向扩容”两个单机天花板再上;
  • 核心概念就一组:你声明服务的期望状态,Swarm 用任务副本去逼近它,副本死了自动补,这就是自愈;
  • 经理节点用 Raft 多数派,数量只取奇数,3 台是小集群甜点,2 台是最差配置;
  • docker stack deploy 复用 Compose 文件,但 build: 会被忽略、镜像必须先推仓库,deploy 段在集群里才真正生效;
  • 滚动更新靠 --update-parallelism 与 --update-delay 控制节奏,出事一条 rollback 回退;
  • 有状态服务是 Swarm 的短板,数据库留在集群外、或用放置约束钉住节点,别让卷漂移;
  • 密码走 secrets、配置走 configs,别再进环境变量;
  • 选型记住一句话:单机痛点用 Swarm,平台级需求上 Kubernetes。

至此,从单机部署、自动更新、监控、安全,一路走到多机集群,这套 Docker 系列的骨架就完整了。

Docker Swarm 入门:多机集群与滚动更新
https://blog.anexns.com/posts/docker-swarm/
作者
Anexns
发布于
2026-11-15
许可协议
CC BY-NC-SA 4.0