☰
Docker Swarm 全生命周期管理:从集群初始化到退役的关键实践
2026/9/26 16:54:38 网站建设 项目流程

说实话,这几年聊 Docker Swarm 的人越来越少了,好像不扯两句 Kubernetes 都不好意思说自己搞容器。但你要是自己维护一个小团队、几台内网服务器,或者就想在一套不用装一堆第三方组件的环境里快速把容器编排跑起来,Docker Swarm 依然是那个开箱即用、心智负担最低的方案。我前前后后维护过几套 Swarm 集群,从最早 docker run 一个个起容器,到后来把所有核心业务都迁到 service 上,中间踩过的坑,基本都逃不开生命周期管理里最常见的那几关:节点初始化、服务发布、配置轮转、数据卷、故障恢复、备份升级。这篇文章就把 10 个我自己反复用、也反复教别人用的精要实践范例整理出来,从集群创建一直讲到退役,适合正在用 Docker Compose、想往编排上走一步的小团队运维,也适合已经上了 Swarm 但想系统补一遍操作细节的同学。

1. 底子打不好后面全是坑:节点初始化与集群创建

1.1 范例1:节点初始化与 Docker 引擎配置

很多人的 Swarm 集群是从一台台裸机或者云主机上直接apt install docker.io开始的,装完就docker swarm init,后面出问题才回头补配置。我的建议是,任何节点在加入集群之前,先把 Docker 引擎本身调好,否则后面服务起不来、日志撑爆磁盘、网络忽好忽坏,排查起来非常痛苦。

先看 daemon.json,这是 Swarm 生命周期里第一个关键文件。我常用的配置长这样:

cat > /etc/docker/daemon.json <<EOF { "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "labels": ["usage=prod", "region=cn-east"], "live-restore": true } EOF systemctl restart docker

这里每个字段都不是随手写的。cgroupdriver用 systemd 是因为现代 Linux 发行版都跑 systemd,保持驱动一致能避免资源统计对不上;日志驱动限制max-size=10m、max-file=3是为了防止容器把 / 写满,这个坑我踩过太多次了,一个服务疯狂打日志,两天就把磁盘干满,整个集群全部受影响。上面这组配置意味着每个容器最多保留 30MB 本地日志,量大的服务记得另接日志采集。labels是给节点打固定标签的,后面做任务调度约束要用。live-restore最容易被忽略,它允许 Docker daemon 重启时,节点上正在跑的容器不跟着一起挂掉——集群升级、改配置文件时全靠它兜底。

网络层面,Swarm 需要固定放行几个端口,如果节点之间有防火墙,漏掉一个就会导致集群初始化成功但任务调度异常。核心端口如下:

端口协议用途
2377TCP集群管理通信、manager 间 Raft 同步
7946TCP/UDP节点间心跳与 gossip
4789UDPoverlay 网络 VXLAN 封装

还有一个很多人忽略的点:不要轻易关闭 Docker 自己维护的 iptables 规则。有些安全加固脚本会顺手把 FORWARD 链设为 DROP,容器间的 overlay 网络立刻就不通。如果你确实需要自己管理防火墙,务必在改动前后用docker service create --publish 端口 服务名起一个测试服务验证一下,再跑业务迁移。

1.2 范例2:集群初始化与节点加入

引擎配置好之后,第一次初始化集群要做的决定其实比你想的多。执行 init 的时候,--advertise-addr是我最推荐的参数,尤其是多网卡机器:

docker swarm init \ --advertise-addr 10.0.0.11 \ --task-history-limit 5

为什么一定写--advertise-addr?因为节点上有多个 IP 的时候,Swarm 会猜一个,一旦猜错,比如选了 Doker 内部虚拟网卡的地址,其他节点 join 进来之后 overlay 网络就是通的,但 manager 之间的 Raft 心跳会莫名其妙中断。提前把业务网段的 IP 写清楚,省掉后面一大轮排查。

初始化完,拿到 join 命令的方式也要养成习惯。别去翻终端历史记录里那串 token,你应该随时能用下面的命令重新获取:

# 在 manager 节点上 docker swarm join-token worker docker swarm join-token manager

如果你怀疑 token 泄露了,或者某个节点离开后想彻底踢掉,直接轮换:

docker swarm join-token --rotate worker

节点 join 进来之后,docker node ls能看到整体状态。一个新手常犯的错误是不管 manager 数量,觉得两台机器每台都是 manager 很稳。实际上 Swarm 的 Raft 过半机制决定了奇数个 manager 最安全:3 个 manager 允许同时挂 1 个,5 个允许挂 2 个;反而是 2 个 manager 的配置最危险,挂掉任何一个都凑不够过半,整个集群会进入只读甚至瘫痪状态。所以我的原则是:要么 1 个 manager 保底,要么 3 个 manager 起步,生产环境尽量别用双 manager。

初始化时还可以顺手开启 autolock。docker swarm init --autolock或者之后用docker swarm update --autolock=true,集群会把敏感数据加密,重启 daemon 后需要 unlock key 才能交给 manager 相关节点。第一次用的时候大概率会不习惯,总得去翻 key,但安全性确实上了一个台阶。这个 key 建议打印出来放到线下密码箱里,别只存在服务器上。

2. 服务的创建到滚动更新,参数就是命根子

2.1 范例3:创建服务时就把参数想清楚

很多人把 Swarm 当成"远程版 docker run",创建服务时只写镜像名和端口,后面出了问题又用 docker service update 慢慢补。实际经验是,创建服务时多花两分钟把参数定全,后面能省出好几个通宵。我常用的"标准服务"模板长这样:

docker service create \ --name web \ --replicas 5 \ --constraint node.labels.role==web \ --limit-memory 1g \ --limit-cpu 0.8 \ --reserve-memory 256m \ --restart-condition any \ --update-delay 15s \ --update-parallelism 1 \ --update-order start-first \ --rollback-monitor 20s \ --health-cmd "curl -f http://localhost/health || exit 1" \ --health-interval 10s \ --publish published=80,target=8080,mode=ingress \ --with-registry-auth \ nginx:1.25

逐个说说关键参数。--limit-memory和--limit-cpu是上限,--reserve-memory是预留,这两个一起配合,调度器才会合理地分配任务密度。只写 limit 不写 reserve 的后果是,Swarm 会按最宽裕的情况把服务都堆到同一台机器上,机器内存有 1G 它敢给你堆 10 个 1G 的任务,直到 OOM 全部重启。

--update-order start-first是个特别容易被忽略的细节。默认先停旧任务再起新任务,短暂中断期在更新时是能实际感知到的;改成 start-first 后,Swarm 会先在目标节点上把新任务跑起来,确认正常后再把旧的撤掉。对零停机有要求的服务,这个参数比什么花哨流量切换都直接。

健康检查我建议在创建时就用参数写明,因为 Swarm 的滚动更新、任务调度都依赖它来判断"任务是否可用",没有健康检查的服务,更新时只能靠发呆等超时。上面模板里写的--health-cmd是容器内执行的命令,你根据自己应用实际的路由来改,关键是返回码 0 才算健康。

--with-registry-auth是给私有镜像仓库用的,添加这个参数以后,Swarm 会把节点的仓库认证信息同步给其他节点,避免任务被调度到别的机器上时因为拉不下来镜像而反复重启。特别是镜像仓库开启权限认证之后,忘了这个参数,服务就会一直卡在 pulling 状态。

最后说发布模式。mode=ingress是默认的,流量先进 Swarm 内部的 ingress 网络,再由调度器转发给对应任务,好处是任何节点上的端口都能访问到整个服务;mode=host则是只绑定任务实际所在节点的端口,适合端口冲突敏感的场景。日常业务用 ingress 就够了。

2.2 范例4:滚动发布与自动回滚

服务上线之后,日常最频繁的操作就是更新镜像。用 docker service update 之前,我一般会用上一节创建服务时那组参数做一次完整更新,这样行为是确定的:

docker service update \ --image registry.example.com/web:v2 \ --update-delay 15s \ --update-parallelism 2 \ --update-failure-action rollback \ web

--update-delay的意思是每批任务更新完成后,等 15 秒再更新下一批。这 15 秒不只是礼貌性等待,而是给健康检查留出确认窗口,让问题暴露在最小影响范围内。--update-parallelism控制每批同时更新的任务数,2 表示每次动 2 个副本,机器多的集群可以调大一点。--update-failure-action rollback是自动回滚的开关,一旦新任务启动失败或健康检查不过,Swarm 会按旧镜像把服务恢复回更新前状态。

如果更新完之后你想手动回到上一版本,一条命令就够了:

docker service rollback web

回滚时有几个实际问题要提醒。第一,镜像 tag 一定要保留旧的,别因为"都用 latest 了还要什么旧 tag"这种想法把原来版本覆盖掉。我之前就干过这种事:更新后发现有 bug,想回滚,结果镜像仓库里 v1 已经被清理了,只能现场重新构建,业务停了一个多小时。第二,回滚操作也是滚动执行,不是瞬间全部换回来,所以同样会有过渡时间,发布窗口期一定要预留够。第三,如果创建服务时带上了健康检查,更新时 Swarm 会等新任务通过状态检查后再继续下一批;没有健康检查的话,这个自动确认机制就不存在了。

这里分享一个我踩过的大坑:有一次更新时把--update-failure-action写成了continue,新版本因为一个配置错误一直连接不上数据库,结果服务 ps 列表里任务状态全是 running,但线上流量全是 502。我盯了十分钟才反应过来是健康检查没配。所以现在的习惯是,所有面向用户的 web 服务,创建时必须带 healthcheck,更新时 failure action 也必须写 rollback,宁可在监控里多看到一次回滚记录,也不要在业务不可用的状态下干着急。

3. 配置、密钥和有状态服务,生产环境绕不开的三座山

3.1 范例5:config 和 secret 的正确用法

很多入门者把数据库密码、应用配置直接写进服务的环境变量里,图省事。这个习惯在 Swarm 环境里要改,因为docker inspect能直接把服务定义和运行时环境变量一起查出来,等于把敏感信息明文暴露给任何有节点权限的人。Swarm 原生提供的 secret 机制就是为了解决这个问题。

先看创建方式:

printf "mysecretdb123" | docker secret create db_pass_20260110 -

用命令行标准输入创建的好处是不会在 shell 历史记录里留下密码内容。secret 创建后,在服务里引用:

docker service create \ --name api \ --secret source=db_pass_20260110,target=db_pass,mode=0400 \ --publish 8080:8080 \ registry.example.com/api:v1

容器运行时,secret 会以 tmpfs 文件的形式挂载到/run/secrets/db_pass,应用从这里读密码,而不是从环境变量里拿。mode 权限位设成 0400 是保证只有容器内 root 能读。

secret 本身是"不可变"的,创建后不能修改内容,更新只能先创建新版本再换引用。所以我的命名习惯是带日期后缀:db_pass_20260110。轮换流程也固定为四步:创建新版本 secret、用 docker service update 把服务切到新 secret、确认业务正常、删掉旧 secret。每个 swarn 集群上我都配了一个简单的发布检查清单,每次轮换按清单走,出错概率直线下降。

config 的用法和 secret 几乎一样,区别只在内容不敏感、可以大家都看得见。比如改 nginx 配置:

docker config create nginx_conf_20260110 ./nginx.conf docker service create \ --name nginx \ --config source=nginx_conf_20260110,target=/etc/nginx/nginx.conf \ nginx:1.25

config 同样不能原地改,所以要养成配置文件名带版本号的习惯,更新配置本质上是"创建新 config、更新服务引用、清理旧 config"。这套流程看起来比直接改挂载文件麻烦,但它保证了每个任务拿到的配置版本完全一致,不会出现有的节点是旧配置、有的节点是新配置的灵异现象。

3.2 范例6:有状态服务与数据卷

Swarm 对无状态服务非常友好,任务挂了就换个节点重新拉起,数据在镜像里、在配置里,丢了也无所谓。但数据库这类有状态服务就麻烦得多,因为数据得持久化。很多人第一反应是"直接挂个本地卷",结果任务被调度走之后发现数据不跟着走。

先看本地卷的正确写法:

docker service create \ --name postgres \ --constraint node.labels.db==postgres \ --publish 5432:5432 \ --mount type=volume,src=pgdata,dst=/var/lib/postgresql/data \ --env-file ./postgres.env \ postgres:14

这里必须想明白一个关键机制:本地卷pgdata是在任务实际运行的节点上创建的,如果 Swarm 把 postgres 任务重新调度到另一台节点,新节点上没有这个卷,容器会带着空数据启动,等于数据库瞬间被"格式化"了。所以有状态服务我用--constraint node.labels.db==postgres把任务固定在这台机器上,配合节点标签,让调度器明白这个任务只能去那台机器。

把任务固定到一个节点之后,这台机器就成了整个集群里的单点,所以必须配套备份策略。我一般每天凌晨做一次逻辑备份,把 SQL dump 传到独立存储上。还有一个选择是 NFS 或其他共享存储,这样任务调度到任何节点都能挂到同一份数据:

docker service create \ --name shared-storage-test \ --mount type=volume,src=nfsdata,dst=/data,volume-driver=local,\ volume-opt=type=nfs,volume-opt=device=:/data,\ volume-opt=o=addr=10.0.0.20,rw,nfsvers=4 \ nginx:1.25

NFS 方案的好处是数据不绑死在单台机器上,坏处是 NFS 服务器本身又成了单点,而且网络存储的 IO 稳定性直接决定数据库性能。我对 Swarm 上跑数据库的最终建议是:小规模业务用"节点固定 + 逻辑备份"的简单方案,规模大了就别硬用原生卷,直接上云数据库或者专用的高可用方案,别在 Swarm 里硬撑多副本数据库,那才是真的给自己挖坑。

4. 高可用、监控与排障,别等翻车才想起

4.1 范例7:故障转移与 Raft quorum 的维护

Swarm 自己会处理任务级的故障转移:某个 worker 节点宕机后,调度器会把这个节点上的任务重新调度到其他满足约束的健康节点上。前提是你的服务不是有状态服务,前面提过,有状态服务靠固定节点,宕了就是宕了,只能通过恢复流程救回来。

维护高可用的第一习惯是保持节点 availability 状态符合预期。比如要重启某台机器,先把它变成 drain:

docker node update --availability drain node2

drain 的效果是:Swarm 会把这个节点上的任务平滑疏散到其他节点,同时不会再给它分配新任务。等机器维护完成,再切回 active:

docker node update --availability active node2

我见过有人在生产环境直接重启节点,完全不管 availability 状态,结果节点起回来后上面的任务因为状态错乱反复重启,业务时断时续。记住,凡是计划内的维护,都必须先 drain 再动手。

manager 节点的高可用又是另一套逻辑。Swarm 的 manager 之间通过 Raft 选主和同步数据,必须有过半节点在线才能继续正常工作。3 个 manager 挂 1 个没问题,5 个挂 2 个也没问题,但超过这个数就会导致集群进入不稳定状态,所有管理命令都会超时。这里最危险的场景就是两台 manager 跑生产,挂一台就只剩一台,不够过半,整个集群直接"失联",连 service ps 都查不了。

如果你真的遇到了 manager 大面积宕机、quorum 丢失的情况,官方提供的最后手段是在幸存节点上重新初始化:

docker swarm init --force-new-cluster --advertise-addr 10.0.0.11

注意,这条命令会以当前节点的数据重建一个单 manager 集群,相当于"急救模式",原来的 worker 节点基本都要重新加入。它绝不是在集群还健康时用来重置的常规操作,误用了等于自断双臂。我提醒团队里的成员,这条命令就是消防斧,只有确认 quorum 无法恢复时才允许碰。

日常高可用要做的还有一件小事:每季度检查一下docker node ls,看看有没有节点状态变成 Down、有没有 manager 长期处于 Reachable 之外的状态。这个习惯我坚持了好几年,每次检查都发现不了问题,但有一次真的抓到了一台内存有故障的节点长期处于半死状态,如果不是提前发现,下一次滚动更新可能就出大事。

4.2 范例8:监控与日志收集

Swarm 自带的排障命令是你最先应该用的,别一上来就搭监控平台。任务起不来,第一件事查任务状态和历史:

docker service ps web --no-trunc

这个命令的输出会显示每个任务的当前状态、上一状态、是正常退出还是异常退出、在哪个节点运行、启动尝试次数。服务反复重启时,从这里能看到任务是被 OOM 杀了还是被健康检查弹掉的,比对着 CPU 曲线猜答案高效得多。

日志查看也有专门的姿势:

docker service logs --tail 200 --follow web

--tail限制条数,--follow实时跟踪,排障时够了。如果需要永久留存日志,我建议把 Docker daemon 默认的 json-file 日志驱动换成 GELF 或者 Fluentd。GELF 的配置大概这样:

{ "log-driver": "gelf", "log-opts": { "gelf-address": "udp://10.0.0.30:12201" } }

这里的 10.0.0.30 就是你日志采集服务的地址。实际经验是,先在少数服务上切换日志驱动测试,确认日志格式没问题再大面积铺开,不然日志量暴涨会直接把采集端打爆。

资源监控方面,如果不想上整套 Kubernetes 的监控体系,用 Prometheus 加 node-exporter 就足够 Swarm 用了。node-exporter 以 global 模式部署,保证每个节点都会跑一个:

docker service create \ --name node-exporter \ --mode global \ --publish 9100:9100 \ --mount type=bind,src=/proc,dst=/host/proc \ --mount type=bind,src=/sys,dst=/host/sys \ --mount type=bind,src=/,dst=/rootfs \ --mode global \ prom/node-exporter:latest

--mode global是"每台节点部署一个任务"的调度模式,非常适合指标采集这类 agent 型工作负载。node-exporter 抓的数据主要是节点的 CPU、内存、磁盘、网络,配合 Grafana 面板就能满足日常容量观察。容器层面的指标可以再加一个 cAdvisor,不过对大多数小集群来说,node-exporter 加 docker stats 就够了,不要为了监控而监控,维护监控系统本身也是成本。

5. 备份、升级和退役,给集群一个体面的结局

5.1 范例9:集群备份与恢复

很多人的备份思路是"定期给数据库 dump",这在单机 Docker 时代够用,但 Swarm 集群要备份的远不止数据库。Swarm 的集群状态——包括 manager 的 Raft 数据、节点信息、服务定义——都存放在/var/lib/docker/swarm目录下。没了它,你的集群就丢了灵魂,所有服务定义都要手动重建。

我习惯的备份流程很简单:

systemctl stop docker tar -czf /backup/swarm-$(date +%F).tar.gz /var/lib/docker/swarm systemctl start docker

为什么先停 Docker?因为正在运行的 daemon 可能在内存里有尚未落盘的 Raft 数据,直接复制目录容易得到不一致的备份。在 manager 节点上短暂停 Docker 对业务的影响很小,服务任务不会立即受管理命令影响,但做备份时我一般选在业务低谷期,并且一台一台地备份,不要同时停多个 manager。

服务定义本身也应该备份下来,这样即使集群彻底没了,至少还能按原样重建:

for s in $(docker service ls -q); do docker service inspect "$s" > "svc-${s}.json" done

恢复流程比备份复杂一点,我建议在离线机器上先演练一遍,不然真到故障时手忙脚乱。大致步骤是:安装 Docker、停 daemon、删掉默认的/var/lib/docker/swarm、把备份的目录放回去、启动 daemon,然后用docker swarm init --force-new-cluster重新收敛出一个可用集群。等集群恢复后,再用 docker service create 逐个重建服务,或者直接解析之前 inspect 出来的 JSON 文件重新创建。

这里有三个容易翻车的细节。第一,备份文件不要留在节点本地,要定期同步到对象存储或另一台独立服务器上,不然节点硬盘挂了等于备份全丢。第二,镜像本身也要留存,备份 Raft 数据不等于备份镜像层,镜像仓库的镜像一旦被清理,重建服务时照样拉不到。第三,验证备份最好的方式不是某天真的出事了才试,而是每两个月在一台新机器上跑一遍恢复流程,我和团队就是靠着这种演练,在真正面对一次 manager 全部宕机的故障时才没有手忙脚乱。

5.2 范例10:节点维护、升级与集群退役

前面提过节点维护的核心操作是 drain。这里再补一个细节:如果你要维护的是 manager 节点,还有一个额外的工序——搞清楚谁是最新的 Raft leader:

docker node ls

输出里 Leader 那一列能看清当前 leader。维护顺序最好是先动 follower,再动 leader,每次只动一台。为什么?因为 leader 一旦 down 掉,集群要重新选主,Raft 数据要重新同步,多台 manager 同时维护等于把整个集群踢出 quorum,风险完全不可控。

升级 Docker 版本也是生命周期里的固定环节。Swarm 集群升级的推荐顺序是:先升级非 leader 的 manager,再升级 leader,最后升级 worker 节点。每个节点升级期间,该节点的 daemon 会重启,如果章节 1.1 里你配了 live-restore,正在运行的容器不会中断。这也是我为什么反复强调 daemon.json 里那项配置的原因——很多团队升级 Docker 版本时出现业务闪断,排查来排查去发现就是少了 live-restore。

如果要彻底退掉一个 worker 节点,流程是三步:

# 在 worker 节点上 docker swarm leave # 回到 manager 节点上,确认节点已经消失 docker node rm worker1

如果这个节点是 manager,先降级再离开:

docker node demote node2

然后在 node2 上执行docker swarm leave,最后在活跃 manager 上docker node rm node2。记住,不要在只剩两个 manager 的集群里随便移除一个 manager,除非你做好了单 manager 运行的准备。

集群退役这个事也值得认真处理。很多临时集群用完就扔,节点上的任务停了,但服务定义、网络、卷都还在。标准的清理顺序是:

# 停止所有服务 docker service ls -q | xargs docker service rm # 将节点退出集群 docker swarm leave --force # 清理残留资源和卷 docker system prune -a --volumes

docker system prune会把停用的容器、不再使用的网络、无主的镜像和数据卷全部清理掉。最后一步最容易忘的是清理主机上的 /var/lib/docker 目录和对应的防火墙规则,不然重装 Docker 时总会遇到端口被占或者网络冲突的奇怪问题。集群退役不是按一下删除键就万事大吉,数据备份、凭据销毁、资源回收都要按顺序来,这才是"善始善终"。

最后再分享一个我自己坚持了很久的习惯:每个 Swarm 集群我都会建一个 README 文档,把节点 IP、标签含义、备份位置、升级顺序、上次演练恢复的日期都写进去。别嫌麻烦,等你凌晨三点被服务告警叫醒的时候,会发现这些记录比任何监控面板都管用。Swarm 的难点从来不在某个高级特性,而在于把节点、服务、数据、备份这些基础环节一样样做对——这才是全生命周期管理的真实含义。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询