很多人对 Docker 的认知停在“跑个容器、打打镜像”的阶段,一旦业务量上来,要跨多台机器部署、做滚动更新、保证服务不中断,就会立刻撞上单机 Docker 的天花板。这时候摆在面前的两条主流路线,一条是 Kubernetes,另一条就是 Docker Swarm。而 Swarm 恰恰是我在生产环境里用得最久、也最省心的容器编排方案。这篇文章我不会去写那些官网文档里已经有的操作清单,而是把 Swarm 的原理、选型逻辑、生产环境实操和这三年里踩过的坑串起来讲一遍,适合那些“Docker 命令已经熟练、正准备把服务搬到多机集群上”的读者参考。
Swarm 的核心价值说起来很简单:它把多台 Docker 主机变成一台逻辑上的“大主机”,你不再需要关心容器具体落在哪台机器上,只需要声明“我要跑几个副本、用什么镜像、开放哪个端口”,剩下的调度、网络、服务发现全部交给集群自己处理。相比 Kubernetes 那套复杂到需要专业团队维护的体系,Swarm 的入口门槛低很多,功能却完全覆盖了小型生产环境的核心需求。下面我从头开始拆解。
1. 为什么我在2024年还在选Swarm而不是一上来就K8s
1.1 单机Docker的天然瓶颈
先用最直白的场景说明问题。你在一台服务器上用 docker run -d -p 8080:80 nginx 跑起来一个 Web 服务,一切正常。但流量翻倍后你想再起一个副本,就得手动指定不同端口:-p 8081:80,然后在前置 Nginx 里配置负载均衡指向这两个端口。这还只是“两个副本”的情况,如果副本扩展到五六个、其中一台服务器宕机、或者需要灰度发布新版本,单机 Docker 没有任何原生的手段帮你做调度、故障转移和健康检查,所有工作都落在你的自动化脚本和运维操作上。
更要命的是服务发现。容器重新创建后 IP 会变,单机 Docker 里你只能依赖 --link 或者外部注册中心去维护一份动态的地址表。当服务数量从一两个变成几十个,这种手工维护的成本会指数级上升,最终变成事故来源。我自己就经历过一次凌晨扩容事故:脚本里硬编码了旧容器 IP,扩容后一半请求打到已经不存在的地址上,页面报错持续了快二十分钟,排查下来问题极蠢,但根源就是单机模式缺少统一的服务发现机制。
Swarm 解决的就是这一层问题。它在集群层面维护了一份覆盖所有节点的状态,容器落在哪台机器、现在是什么状态、应该暴露什么服务,全部由控制面统一管理。你在任何一台 Manager 节点上执行 docker service 命令,操作的都是整个集群,而不是单独一台机器。
1.2 Swarm与K8s的取舍:我的真实选型逻辑
Kubernetes 生态确实更庞大,功能扩展性也更强,但“功能强”和“适合你”是两码事。Swarm 最突出的优势是自带于 Docker Engine 中,不需要额外安装一套管理组件,也不需要处理证书体系、etcd 部署、CNI 插件选型这些前置复杂度。如果你团队里没人专门负责基础设施,Swarm 是那种“一个懂 Docker 的人半小时就能把集群拉起来”的方案。
我经历过的项目里,选择 Swarm 一般是这几个前提:业务规模没有达到需要多集群、多租户、复杂灰度策略的程度;团队运维力量薄弱,没有专职的 K8s 管理员;需要快速交付,不希望基础设施本身变成瓶颈。这三个条件都满足的情况下,用 K8s 属于给自己造轮子。当然 Swarm 也有明确不擅长的场景,比如需要细粒度的 RBAC、复杂的自定义 CRD、大规模多集群管理,这种情况还是老老实实上 K8s,不要硬撑。
我觉得选型最忌讳的是“别人都在用所以我也要上”。容器编排工具的迁移成本很高,中途再切比一开始选对要痛苦得多。Swarm 和 K8s 不是简单的优劣关系,更像“摩托车”和“重型卡车”:拉人和跑短途,摩托车灵巧省油;拉几十吨货上高速,还是得卡车。关键是你得知道自己要拉多少货。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Swarm的底层工作机制:从节点到调度的心跳与共识
2.1 节点角色:Manager与Worker的真实分工
Swarm 集群中每台机器称为一个节点(Node),节点分为 Manager 和 Worker 两种角色。Manager 负责集群控制面,处理服务创建、更新、节点状态维护等 API 请求;Worker 只运行容器,并向 Manager 上报心跳和任务状态。第一次执行 docker swarm init 的节点自动成为 Manager,之后加入的节点默认是 Worker。
通常生产环境会配置三到五个 Manager 节点,其余全部作为 Worker。这不是拍脑袋定的,因为 Manager 节点需要参与 Raft 共识投票,奇数个节点才能避免平票问题。三节点 Manager 可以容忍一台宕机,五节点容忍两台,再多收益就不明显反而增加同步开销。Worker 节点则可以根据负载横向扩展,数量没有严格限制。
一个容易忽略的细节是:Manager 节点默认也承担 Worker 角色,可以运行任务容器。如果你希望 Manager 只做控制面,不跑业务容器,需要给节点打标签并设置约束条件。我在一个项目里就吃过亏,三个 Manager 上各自跑着一个日志采集容器,日志量大的时候把 Manager 的磁盘 IO 吃满,Raft 心跳出现延迟,整个集群状态都变得不稳定。后来强制业务容器只调度到 Worker 节点,问题才缓解。
2.2 服务与任务:声明式状态如何驱动实际容器
Swarm 里最重要的抽象是 Service(服务)和 Task(任务)。Service 是用户声明的期望状态:镜像、副本数、网络、端口、环境变量、更新策略等。Task 是 Swarm 根据 Service 状态创建出的调度单元,每个 Task 对应一个容器实例。
整个系统是典型的声明式驱动:你在 docker service create 里告诉 Swarm“我希望有 3 个 nginx 副本”,Swarm 的编排器就会持续对比期望状态和实际状态。如果实际运行的任务少于 3,编排器会创建新的 Task 并调度到合适节点;如果某个节点宕机导致任务消失,编排器也会自动在其他可用节点上补建,整个过程不需要人工介入。
这种模型带来的好处非常直观:部署行为变成可声明、可版本化的配置,而不是一堆顺序执行的 shell 命令。每次执行 docker service update 其实就是提交了一次期望状态变更,Swarm 自己计算需要保留哪些任务、重建哪些任务。理解这个机制之后,你会明白 Swarm 的“自动化容错”并不是什么黑魔法,只是一个比较朴素的状态对齐循环。
2.3 Raft一致性:Swarm高可用的基石
Swarm 集群的控制面状态需要所有 Manager 达成一致,这正是 Raft 共识算法的作用。简单来说,Raft 会把所有 Manager 上的集群状态日志复制到多数派节点,只有多数派确认写入,这次变更才算成功。默认情况下 Swarm 的所有日志是加密存储在各节点的 Docker 数据目录下的,我在实际运维中很少手动去动这些文件,但理解复制机制对排查故障很有帮助。
有一次集群中出现 Manager 节点磁盘满的情况,我执行 docker service ls 时命令长时间卡住。原因是当前连接的那个 Manager 与集群中的其他 Manager 失去多数派通信,无法确认最新状态,所以 API 查询也无法返回。这种情况处理起来不能直接重启 Docker,而是要先清理磁盘空间、恢复节点间网络连通,再观察 docker node ls 的输出是否恢复正常。
Raft 的另一个影响是:Manager 节点的时间必须保持同步,否则心跳和日志的时间戳会出现偏差,严重的会莫名出现 leader 切换。我所有 Swarm 节点都配了 chrony 做 NTP 同步,这一步在初期配置时很容易被忽略,但非常关键。
2.4 服务发现的实现逻辑:内置DNS与VIP
容器编排必须解决“服务在哪个 IP 上”这个问题。Swarm 的做法是为每个 Service 分配一个虚拟 IP(VIP),同时集群内置 DNS 服务把服务名解析到这个 VIP。客户端访问服务名时,流量到达 VIP 后由内核层面的 IPVS 规则转发到具体的某个 Task 容器上。
这意味着你不再需要手工维护服务地址。比如一个后端服务名为 api,前端服务在代码里直接用 http://api:8080 就能访问到后端的某个可用副本。后端扩容缩容、容器重建导致 IP 变化,前端完全感知不到。这个机制和 K8s 的 ClusterIP 思路类似,但实现上轻量得多。
需要注意 Swarm 服务发现默认作用于同一个 overlay 网络。如果你创建了两个不同的 overlay 网络,且服务没有同时接入它们,那么在网络 A 中的服务名在网络 B 里是解析不到的。这个细节在微服务拆分比较细的时候特别容易踩,后面我会专门讲到排查过程。
3. 三台服务器从零组集群:初始化、加节点、跑通服务的完整过程
3.1 环境准备与前置条件
这里以三台 Ubuntu 22.04 服务器为例,IP 分别为 192.168.1.10、192.168.1.11、192.168.1.12,前两台作为 Manager,第三台作为 Worker。所有节点先安装好 Docker Engine,版本尽量保持一致,我自己习惯统一用最新的稳定版,避免版本差异导致的行为不一致。
需要确保防火墙放行几个关键端口:TCP 2377 用于集群管理通信,TCP/UDP 7946 用于节点间通信,UDP 4789 用于 VXLAN overlay 网络数据面。如果这三类端口不通,集群虽然能初始化,但节点加入、跨主机容器通信都会出各种诡异问题。初始化前最好先验证一下节点间延迟和连通性,可以用 nc -vz 192.168.1.11 2377 这类命令测试端口,时间成本很低但能省掉后面一大截排查时间。
另外,建议把 Docker 数据目录放到独立磁盘,避免系统盘写满拖垮整个宿主。我在生产机上通常将 Docker root 配置为 /data/docker,并单独挂载高性能磁盘。这种操作要在初始化集群之前完成,因为 Swarm 状态也存放在这个目录里,后期迁移非常麻烦。
3.2 初始化集群与添加节点
在第一台服务器上执行集群初始化命令:
bash复制docker swarm init --advertise-addr 192.168.1.10
--advertise-addr 指定当前节点对外通告的地址,多网卡机器上必须显式指定,否则 Swarm 可能会选了错误的网卡,导致其他节点无法连接。初始化成功后终端会输出一段 docker swarm join 命令,包含一个用于加入 Manager 节点的 token 和完整地址,这个 token 有有效期,过期后可以用 docker swarm join-token manager 重新获取。
接着在第二台服务器上执行相同的 join 命令,把第二台节点加入进来。执行完 docker node ls 可以看到前两个节点都是 Manager 状态。第三台服务器如果作为 Worker,需要先从 Manager 上获取 Worker 的 token:
bash复制docker swarm join-token worker
拿到输出里的完整命令后,在第三台服务器上执行加入操作。加入完成后回到 Manager 上执行 docker node ls,此时应该看到三台节点,其中两个是 Leader/Reachable 的 Manager,一个是 Worker。到这里一个最简集群就搭好了,整个过程不到十分钟,这是我很喜欢 Swarm 的直接原因。
3.3 部署第一个服务:从nginx到完整应用栈
创建一个三副本的 nginx 服务并暴露端口:
bash复制docker service create \
--name web \
--replicas 3 \
--publish 8080:80 \
nginx:1.25
创建完成后可以用 docker service ps web 查看每个任务落在哪个节点上。可以看到 Swarm 会把三个副本尽量分散调度到不同节点,实现最基本的负载均衡和容错。此时任意一个节点宕机,剩下两个节点上的容器依然提供服务,同时编排器会在其他可用节点上补建副本。
如果是一个多服务的应用栈,我建议用 docker stack deploy 配合 Compose 文件来管理。Compose 文件里定义好各个服务、网络、依赖关系和端口映射,然后一条命令完成整个栈的部署:
bash复制docker stack deploy -c docker-compose.yml myapp
Swarm 会识别 Compose 文件里的 deploy 字段,比如 replicas、resources、update_config、restart_policy。需要注意的是,Compose 文件里的 version、services、networks 是兼容的,但 build 指令在 Swarm 模式下不可用,必须使用已经构建好的镜像。
3.4 滚动更新与回滚操作
生产环境发版最关心的就是不能中断服务。Swarm 的滚动更新策略在 docker service update 中体现得很直观:--update-parallelism 指定每次并行更新的副本数,--update-delay 指定每批更新之间的等待时间。例如:
bash复制docker service update \
--image nginx:1.26 \
--update-parallelism 1 \
--update-delay 10s \
web
这个命令会让 web 服务每次只更新一个副本,并等待 10 秒确认状态正常后再更新下一个。如果任务启动失败,Swarm 会停止更新并保留当前已更新的任务。你可以用 docker service ps web 查看每个任务的状态,判断是哪个节点上的更新出了问题。
回滚同样是一条命令的事:docker service rollback web。回滚会恢复到上一次部署时的镜像和配置。我在实际发版中已经形成习惯:每次更新前先记录当前的服务配置快照(docker service inspect --format '{{json .Spec}}' web > spec_backup.json),虽然 Swarm 自带回滚,但这个文件能在极端情况下帮你更清晰地对比变更内容,排查问题。
4. 生产环境绕不开的Ingress网络与数据持久化问题
4.1 ingress网络与路由网格
Swarm 默认创建的 ingress 网络承载了端口发布功能。当你用 --publish 8080:80 发布服务端口时,所有节点的 8080 端口都会监听流量,无论容器实际运行在哪台节点上。流量进入某个节点的 8080 端口后,会沿着 ingress 网络转发到实际运行该服务的 Task 容器上。这个能力被称为 Routing Mesh,是 Swarm 对外提供统一入口的基础。
举个例子,集群有三台节点,web 服务有两个副本分别落在节点 A 和节点 B。客户端访问任意一台节点(A、B、C)的 8080 端口,都能访问到 web 服务,哪怕节点 C 上没有运行任何 web 容器。这就意味着你可以在集群前面放一个简单的负载均衡器,把流量平均分发到所有节点,而不必关心具体容器位置。这个设计极大简化了入口层的配置。
实际使用时要留意端口冲突:同一个宿主机上的多个服务发布端口不能重复使用。比如服务 A 用 8080:80,服务 B 就不能在集群里再发布 8080 端口了,因为每个节点的 8080 都被 ingress 占用了。解决方式是用不同的宿主机端口映射,或者把有冲突的服务放到不同的独立集群。
4.2 数据持久化:volumes与bind mounts的选择
Swarm 里的容器默认是无状态的,容器删除后数据就丢了。需要持久化的数据库、文件存储等服务,必须显式挂载卷。在服务定义里可以用两种方式:bind mount 和 named volume。
bind mounts 直接映射宿主机目录,需要指定节点上的绝对路径:
bash复制docker service create \
--name mysql \
--mount type=bind,source=/data/mysql,target=/var/lib/mysql \
--constraint node.labels.db==true \
mysql:8.0
但 bind mount 有个天然问题:不同节点的路径可能不一致,而且 Swarm 不会自动把数据复制到调度目标节点。如果任务被调度到另一台没有 /data/mysql 目录的节点,启动就必然失败。我通常在 Worker 节点上打标签(如 node.labels.db=true),并用 --constraint 约束数据库类服务只调度到指定节点,从而保证数据目录一定存在。
named volume 是 Docker 管理的卷,跨节点时需要借助共享存储驱动(如 NFS、CIFS)才能做到真正共享。对于需要多副本同时读写同一份数据的场景,我的建议是不要用 Swarm 自带的本地卷,直接使用外部分布式存储配合 volume driver,或者把有状态服务单一副本化,配合宿主机目录做定期备份,简单可靠。
4.3 多环境配置与secret管理
生产环境跟测试环境的差异往往体现在环境变量、连接地址、凭证上。Swarm 的 secret 功能可以安全地保存敏感信息。创建 secret 最简单的做法:
bash复制echo "dbpass123" | docker secret create db_pass -
然后在 service 中指定使用该 secret:
bash复制docker service create \
--name api \
--secret db_pass \
--env DB_PASS_FILE=/run/secrets/db_pass \
your-registry/api:1.0
secret 在 Swarm 中是以内存文件系统挂载到 /run/secrets/ 目录下的,宿主机文件系统和镜像层里不会明文出现。相比把密码写死在环境变量或镜像构建参数里,这个方式至少让敏感信息不再随镜像分发。需要注意 secret 只能被 Swarm 服务使用,不能在普通容器里直接读取。
关于多环境配置,我更推荐用同一套镜像配合不同 Compose 文件中的环境变量来区分,而不是为每个环境重新构建镜像。镜像内容的差异越少,测试环境和生产环境的一致性就越高,发布时也越不容易出现“测试通过、生产炸了”的情况。
5. 真实踩坑记录:心跳超时、跨节点访问失败与更新卡死的排查链路
5.1 坑一:Manager节点过载导致的心跳超时
有一次线上集群突然出现大量服务重启,docker service ls 输出断续,部分节点状态显示为 Down。我第一时间怀疑网络问题,但在节点上执行与 Swarm 无关的命令都正常,而且节点间的 ping 很流畅。后来登录各 Manager 查看负载才发现,问题源自日志采集容器大量占用磁盘 IO,导致 Manager 节点上的 Docker 守护进程无法及时处理心跳和 Raft 同步。
排查链路是这样的:先缩小范围确定受影响的节点,用 docker node ls 看状态异常集中在哪些节点;接着登录异常节点看系统指标,发现 iowait 极高,定位到日志容器写盘无损;最后调整策略,把日志采集容器配置 --constraint node.role==worker 强制移动到 Worker 节点,并给 Manager 节点打上污点性质的约束,确保控制面资源不被业务挤占。
这个案例说明 Swarm 的心跳机制非常依赖节点稳定的 CPU 和 IO 资源。生产环境中 Manager 节点的资源预留很重要,不能因为看到管理器上还有空闲内存就顺手扔几个业务容器上去。控制面一抖动,影响的是整个集群,而不仅是一个容器。
5.2 坑二:服务无法跨节点访问
另一个高频问题出现在多网络场景。我的项目里有前端和前端两套服务,分别接入不同的 overlay 网络,结果前端通过服务名调用后端时偶发超时,再到后来直接解析不到。查 docker service inspect 发现两个服务虽然逻辑上属于同一个应用栈,但我创建 stack 时没有显式声明共同网络,导致它们各自待在不同的 overlay 网络中,内置 DNS 自然无法跨网络解析。
解决方式是在 Compose 文件中显式声明一个 overlay 网络,让需要互相通信的服务都接入这个网络。如果已经部署完成,可以用 docker network connect 手动给服务添加网络,但更推荐直接规范 Compose 文件,让所有需要互通的服务加入同一个网络,减少隐式网络隔离造成的困惑。
值得补充的是 --attachable 参数。overlay 网络默认只能由 Swarm 服务使用普通容器无法加入,调试时会很不方便。创建 overlay 网络时加上 --attachable,后面就可以手动 docker run --network my-overlay 起一个临时容器去测试网络里的服务名解析和连通性。排查阶段的利器。
5.3 坑三:滚动更新卡住的情况
滚动更新卡住是另一个常见现象。表现为 docker service update 命令执行后一直处于更新中,部分副本停在 New 状态迟迟不启动。排查发现镜像拉取过慢是首要原因,新版本镜像体积大且节点上没有缓存,每个节点都要重新拉取,拉取超时就会卡住。
查看更新卡住的关键命令是 docker service ps web,关注 ERROR 列。如果提示镜像拉取超时,解决方案是先在节点上手动 docker pull 新镜像,确认拉取成功后再触发更新。或者配置镜像拉取的重试和超时参数:在 service 的更新配置里,用 --update-failure-action rollback 让失败后自动回滚,避免服务长时间停留在半更新状态。
还有一次卡住是因为新版本容器启动后健康检查失败。Swarm 默认在任务启动后会给一点时间确认状态,但如果你定义了 --health-cmd 健康检查,且检查命令本身有 bug,比如检查的端口没暴露到容器内,那么新副本永远无法进入运行状态,更新自然卡住。这种情况建议先去掉健康检查或者修正检查命令,让更新恢复到正常节奏。
5.4 一个典型案例的完整排查过程
综合展示一次完整排查:某天线上订单服务多个副本全部处于启动失败状态,用户无法下单。我先执行 docker service ps order,看到多个任务反复重启,错误信息是“port is already allocated”。这说明服务宿主机端口被占用了。再进一步查看 docker service ls 和各个容器端口,发现有一个废弃的 order-old 服务没有下线,它占用着同一宿主机端口。
处理动作很简单:移除废弃服务 docker service rm order-old,然后手动触发一次重新部署。不到一分钟订单服务恢复。整个过程真正花时间的不是操作,而是从“容器启动失败”这个表象逐层定位到“端口冲突”这个根因。这类排查经验很难在文档里学到,只有多踩坑才能建立起“看现象-缩小范围-定位根因”的直觉。
6. 上线之后的运维日常:监控、备份与版本升级
6.1 Swarm集群的监控指标
集群跑起来后,监控要覆盖三层:节点层关注 CPU、内存、磁盘 IO、网络流量;编排层关注节点状态、服务副本数是否有偏差、任务是否异常重启;应用层关注容器内业务指标,如请求延迟、错误率、队列积压。
节点和编排层我直接用 cAdvisor + Prometheus + Grafana,cAdvisor 暴露容器指标,Prometheus 通过 node exporter 采集宿主机指标,再用一个简单的 exporter 调用 Docker API 获取 Swarm 节点状态和服务副本数。这样 Grafana 一张大屏就能同时看到节点负载和集群健康情况。告警规则至少要覆盖:节点 Down、服务副本数小于期望、任务重启次数突增、证书过期时间。
如果你不想维护一整套监控栈,Docker 自带的 docker events 也能在出问题时快速查看集群事件流。但生产环境还是建议上正式的采集系统,因为事件流只能用于事后排查,没法在异常发生时主动告警。
6.2 备份恢复策略
Swarm 集群的状态数据包括 Raft 日志、服务定义、网络定义、secret 等,全部存放在 Manager 节点的 Docker 数据目录中。备份的核心思路是对 Manager 节点做一致性快照,然后离线保存。Docker 没有为 Swarm 提供一键导出全部状态的命令,我通常的做法是定期对一台 Manager 节点做文件级快照,同时导出所有服务定义:
bash复制docker service ls --format "{{.Name}}" | while read s; do
docker service inspect "$s" > "backup/${s}.json"
done
这个备份文件能帮你重新创建服务,但不会恢复容器数据。所以真正重要的数据还得靠 volume 层备份,比如数据库用 mysqldump、文件存储用 rsync 到远程。恢复的时候,先在新集群执行 docker swarm init,再创建一个 Swarm 相关的空状态,然后逐步导入服务定义。好在我们平时很少真正走到恢复这一步,但备份可不能因此不做,尤其是 secret 和证书,丢了会很麻烦。
6.3 版本升级的节奏与个人经验
Docker Engine 升级是 Swarm 运维中最需要谨慎的操作。升级一台 Manager 节点时,它会先停止 Docker 服务,这段时间该节点无法参与 Raft 投票,如果只有三个 Manager,另外两个必须保持正常。所以我通常的升级顺序是:先升级 Worker 节点,逐个排除;再升级非 Leader 的 Manager;最后升级 Leader。每次升级中间留出观察时间,同时监控集群状态。
整体上,Swarm 的上手成本确实很低,但生产环境里真正决定体验的是细节:资源预留、网络规划、端口冲突、备份策略。这套东西我用下来最真实的感受是,Swarm 不会替你解决所有问题,但它把你从“逐台服务器手工操作”的泥潭里拉了出来,让你能把注意力放到业务本身。如果你正处在单机 Docker 到多机集群的过渡期,我建议从 Swarm 开始,先把集群的调度、网络和服务发现跑熟,再决定是否需要更重的编排系统。
