
大家刚开始接触 Docker Swarm 时多半会围着docker service create和docker service scale这两个命令打转觉得“能起服务、能扩副本”就算会用了。但做了一段时间运维以后你会发现命令只是表面真正决定集群生死的是更外围那些事初始化时节点怎么选、端口怎么开、证书 token 怎么管更新发布能不能在出问题时快速回滚节点要维护了怎么优雅下线集群不想要了怎么安全拆除。这篇文章我就按“全生命周期管理”这条线把我实际用过的 10 个 Docker Swarm 精要实践范例整理出来。从docker swarm init开始一直到docker swarm leave覆盖服务部署、扩缩容、滚动更新、数据持久化和节点维护这些核心场景适合已经跑熟单机 Docker、正打算往多节点集群走的开发者也适合正在维护小规模生产集群的运维朋友。1. 集群奠基初始化前把端口、角色和 advertise-addr 一次想清楚1.1 范例1初始化前先规划节点角色与通信端口别在三台机器上直接敲 init我见过不少朋友把服务器买回来之后直接就是一句docker swarm init然后发现 manager 之间经常互相找不到或者工作节点加了半天加不进来。问题多半出在初始化前没有把节点角色和端口规划好。先说节点角色。Docker Swarm 把节点分成 manager 和 worker 两类manager 负责集群状态维护、服务调度和 API 接入worker 主要跑业务容器。生产环境我习惯用 3 个或 5 个 manager取奇数是为了 raft 选主时不容易平票如果只是本地实验或测试学习1 个 manager 就够不要为了“看起来专业”硬凑 3 台机器毕竟多一个节点就多一份维护成本。worker 节点数量根据业务规模来可以先从 2 个起步后面需要再加。然后是端口。以下这三个端口必须提前在安全组或防火墙里放行否则节点之间没法通信端口协议用途2377tcp集群管理消息、节点加入、raft 通信7946tcp/udp节点之间的发现与心跳4789udpoverlay 网络 VXLAN 数据面这里有个很容易被忽略的细节如果你既跑了 Swarm 又跑了 Docker 默认的docker0桥接网络节点之间跨主机容器通信走的是 4789 端口。云服务商的安全组如果只放行 tcp 不放开 udp经常出现“服务能创建但跨节点访问不通”的怪问题。我踩过一次排查了半天最后发现是安全组把 4789/udp 漏了。初始化命令是这样的docker swarm init --advertise-addr 10.0.0.11--advertise-addr我强烈建议手动指定尤其在有多个网卡的机器上。如果不指定Docker 可能会选一个带有默认路由的地址有时候会选到内网口有时候会选到公网口导致其他节点通过这个地址连不上你。判断标准很简单这个地址必须是你从其他节点能直接访问到的地址。我的习惯是先在每台机器上执行ip addr看一遍网卡确认好固定的内网 IP 再做初始化。初始化完成后立刻执行docker node ls看看自己节点的状态确认当前节点角色是MANAGERHostname 和 Availability 都对。这一步花不了十秒钟但能帮你避免后面一堆“群龙无首”的问题。1.2 范例2用 join-token 把工作节点带进集群记得定期轮换 token第一个节点初始化完之后往集群里加节点就是标准操作了。worker 节点加入的命令不是手写的而是从 manager 上获取docker swarm join-token worker这条命令会输出完整的一行docker swarm join --token xxx命令你复制到工作节点执行就行。manager 节点加入则用docker swarm join-token manager。这里有个我认为很关键的习惯每次加完节点如果 token 打印在屏幕上被别人看到过或者你需要批量添加一批临时节点批量加完以后最好轮换一次 tokendocker swarm join-token --rotate workertoken 轮换不会影响已在集群中的节点只会让旧的 token 失效。这样即使之前有人悄悄记录过 worker token也没办法再往你集群里混入新节点了。我在批量加入节点时还有个习惯加完节点立刻给节点打上标签。标签不是必须的但它对后面做服务调度约束非常有用。比如有三个节点是负责跑数据库的可以这样打标签docker node update --label-add roledb node-b等以后创建服务的时候--constraint node.labels.roledb就能把数据库类服务精确地调度到这些节点上。这个动作如果你在集群早期就做后面维护成本会低很多如果等到服务已经满天飞了再回头补标签那就只能慢慢调整了。2. 服务上线部署思维要从 docker run 切换到 docker service2.1 范例3副本数、健康检查与 ingress 网络三件套共同决定服务是否“真正能用”把服务跑起来的命令很简单docker service create \ --name web \ --replicas 3 \ --publish published8080,target80 \ nginx:1.26-alpine这条命令的本质和docker run不一样你不用自己决定容器在哪台机器上跑Swarm 在集群里帮你调度。副本数 3 意味着它会尽量把 3 个容器分布到不同的节点上这本身就是一层高可用。--publish published8080,target80里published8080是对集群对外暴露的端口target80是容器内的端口。当你在有多个节点的 Swarm 集群里访问任意一个节点的 8080 端口时请求都会通过 ingress 网络被转发到某个健康副本上这就是 Swarm 路由网格的基本逻辑。如果你不想走路由网格而是希望每个节点只暴露本节点的副本可以用--publish modehost这个我一般只在特殊场景用普通 Web 服务还是默认的 ingress 模式更省心。服务创建之后必须尽快加上健康检查。没有健康检查的服务Docker 只能根据容器进程是否存活来判断它好不好进程还活着但业务已经 500 了Swarm 是不知道的。健康检查参数可以直接写在service create里docker service create \ --name web \ --replicas 3 \ --health-cmd wget -qO- http://localhost/ || exit 1 \ --health-interval 10s \ --health-timeout 5s \ --health-retries 3 \ --publish published8080,target80 \ nginx:1.26-alpine注意这里我用的是wget因为很多精简镜像是没有 curl 的。健康检查命令必须选镜像里真实存在的工具否则状态会一直显示starting等于没配。健康检查的意义和普通进程存活还不一样它是给 Swarm 做“任务是否健康”判断用的。健康检查失败到一定次数服务会被重新调度这在后面的更新和自愈场景里非常关键。另外一个容易忽略的点是如果服务需要多个容器之间互相通信记得把它们放到同一个 overlay 网络里。自定义网络可以这样建docker network create -d overlay --attachable mynet之后创建服务时加一个--network mynet服务之间直接通过服务名互相访问即可这样不用关心容器 IP 会变。记住默认的 ingress 网络只是给外部流量做路由用的内部服务之间的调用还是建议单独分一个 overlay 网络逻辑更清晰。2.2 范例4扩缩容不只是一条 scale 命令placement 约束和资源预留才是关键要扩容最简单的是docker service scale web5或者用docker service update --replicas 5 web两者效果基本一样。但如果你的集群节点配置差异很大盲目扩缩容很容易出问题。比如 3 台机器中有一台是 2C4G 的老机器其他两台是 8C16G你把一个很吃内存的服务扩到 10 副本调度器可能优先把任务堆到配置差的机器上最后大家都不稳。解决这个问题的两个工具是 placement 约束和资源预留。placement 约束用来指定“这个服务可以跑在哪些节点上”格式是docker service update \ --constraint-add node.labels.roleapi \ --constraint-add node.role!manager \ webnode.role!manager是我特别喜欢用的一个约束意思是业务容器不落在 manager 节点上。manager 节点本身承担了 raft 通信和集群调度任务再把重业务压上去集群一抖动 manager 也会跟着抖。生产环境我一般都会给业务服务加上这个约束。资源预留通过在创建或更新服务时指定这些参数docker service update \ --reserve-cpu 0.25 \ --reserve-memory 256m \ --limit-cpu 1.5 \ --limit-memory 1g \ web其中reserve是调度器在决定把任务放到哪个节点时参考的预留值limit是容器运行时真正受到限制的 cgroup 上限。很多人以为设置了 limit 就够了其实如果没有 reserve调度器可能把一堆 limit 加起来远超节点的真实容量最后节点 load 爆表设置了 reserve 后调度器在放任务时会主动避开那些已经被预留得差不多的节点。一个相对安全的做法是先根据监控数据估算单个副本的正常占用reserve 设为正常值limit 设为正常值的 1.5 到 2 倍。还有一类服务希望每个节点都跑一个副本比如日志采集器或监控 exporter用--mode global更合适docker service create \ --name node-exporter \ --mode global \ prom/node-exporter这种模式下不需要指定--replicasSwarm 会自动保证每个可用的节点上都跑一个任务新节点加入集群时也会自动补齐。理解了副本模式和 global 模式的区别扩缩容的时候思路就会清晰很多。3. 版本发布滚动更新要有节奏回滚要有完整排查链路3.1 范例5滚动更新的核心不是换镜像而是 update-parallelism、delay 和 orderSwarm 的滚动更新机制我觉得设计得挺巧妙的它更新的不是整个服务而是服务里的一个个任务。你要发布新版本只需要docker service update \ --image nginx:1.27-alpine \ --update-parallelism 1 \ --update-delay 30s \ --update-order start-first \ --update-failure-action pause \ web逐项解释一下这些参数的实际意义--update-parallelism 1表示每次只更新一个副本。如果设置成 2 或 3则每一批同时更新多个副本。对生产环境来说从 1 开始最稳妥等确认没问题再考虑调大。--update-delay 30s表示每更新完一个副本等 30 秒再更新下一个。这个时间窗口给了你观察日志和监控的余地。如果副本健康检查需要 10 秒delay 至少要比健康检查周期长否则上一个还没确认健康下一个已经开始更新了。--update-order start-first表示先启动新副本等新副本进入运行状态后再停掉旧副本。这个模式能最大限度降低服务中断时间代价是更新期间节点上会同时存在新旧两个容器对资源的要求会翻倍。如果集群资源本来就很紧张可以改用默认的stop-first先停旧的再起新的但会有短暂的服务不可用窗口。默认值其实是stop-first这一点很多人记反了我特别强调一下。--update-failure-action pause表示如果某一批任务更新失败Swarm 会自动暂停更新不会继续往下滚。这个我强烈建议保留不要图省事改成 continue否则一个坏镜像能被滚到所有副本上。更新过程中观察状态用一条命令就够docker service ps web你会看到每个任务当前处于什么状态是Running、Starting还是Failed同时右侧会显示上一次的更新状态。如果某一行的状态里出现UpdateState: paused说明更新被暂停了需要赶紧去查日志。3.2 范例6发布翻车后的快速回滚链路不要一上来就 rollback回滚是发布动作的安全网。标准命令是docker service update --rollback web这条命令会把你服务镜像等一系列配置回滚到上一次发布前的状态而且会按照服务配置里的更新策略分批执行。如果 rollback 能顺利触发Swarm 会自动把任务恢复到旧版本整个过程不需要你手动去改 image。但我的经验是回滚之前先做三轮排查不要一看到服务有问题就直接 rollback。原因是有些问题不是新版本本身引入的而是节点资源不足、网络不通、或者配置被改坏了直接回滚解决不了根因。我的一般排查链路是这样的第一步看任务状态。执行docker service ps web看哪些任务卡在Starting或者Failed这些任务集中在哪些节点上。如果所有失败都集中在一个节点那大概率是那个节点本身有问题而不是镜像问题。第二步看服务日志。执行docker service logs --since 30m web重点看报错是拉取镜像失败、端口占用还是容器启动后进程崩溃。镜像失败通常是私有仓库的问题端口占用往往是因为节点上有一个残留容器占了同一个端口。第三步用docker inspect看单个任务的详细信息。先通过docker service ps web --no-trunc拿到失败任务的容器 ID然后docker inspect container-id看State段里的ExitCode和Error。ExitCode 1多半是应用启动逻辑问题ExitCode 137则很可能是内存不足被杀。排查完之后再决定如果确认是新版本代码问题执行docker service update --rollback web如果只是某个节点有问题先处理节点比如docker node update --availability drain 出问题节点再考虑要不要回滚。我遇到过一个很典型的场景新镜像没问题但其中一个 worker 节点的磁盘被日志写满了所有任务都拉不下镜像这时候去 rollback 只会把旧的也推不上来正确操作是先把节点 drain 掉再手动清理磁盘。回滚完成后记得再执行一遍docker service ps web确认所有任务都回到 Running 状态。如果回滚也失败说明你必须手动指定旧镜像重来一遍了先docker service ps web查到上一个可用镜像的版本然后docker service update --image nginx:1.26-alpine web这种情况下我建议把--update-failure-action pause保留至少失败时能暂停留给你一个稳定的现场。4. 数据与配置容器可以随时漂移但状态不能跟着丢4.1 范例7本地卷不会随容器“漂移”跨节点共享要提前想清楚Swarm 里的任务是分布在不同节点上的这带来一个很现实的问题容器可以随时被调度到另一台机器上但容器里的数据怎么搬家先明确一个基本点如果创建服务时没有挂载任何卷容器内的所有写入都会在容器删除时一并消失。Swarm 里为了高可用是会重新调度任务的一旦任务换到别的节点原来的容器被删掉数据就没了。所以上生产环境必须给有状态的服务挂载持久化存储。最简单的挂载方式docker service create \ --name app \ --mount typevolume,sourceappdata,target/data \ --replicas 1 \ your-image这里创建了一个名为appdata的 Docker volume挂载到容器/data。它的内容包括在 manager 中以 raft 方式同步的 volume metadata但实际数据卷本体仍然只存在创建它的那个节点上。这一点非常关键这并不代表数据会在集群内自动复制或跟随服务迁移。如果你只有一个副本并且这个副本被重新调度到另一个节点数据不会跟着走。所以更可靠的做法是给有状态服务加上 placement 约束把它固定在有数据的节点上docker service update \ --constraint-add node.labels.roledb \ app同时在该节点上打好roledb的标签。这样即便任务需要重启它也只会在这个节点上重启不会漂到别的机器上去。那如果想要一个真正能在多节点间共享的存储呢我的建议是不要自己去造分布式的轮子直接用外部的 NFS 或云厂商的共享文件存储。创建服务时用绑定挂载方式指向 NFS 目录docker service create \ --mount typebind,source/mnt/sharedstorage/target/data \ --replicas 3 \ app但要注意一点多个副本同时写同一个共享目录应用本身必须支持并发写。如果不支持共享存储反而会引入数据损坏问题。我见过有人把 MySQL 数据目录放在 NFS 上还开了 3 个副本结果数据还没跑几天就坏了。数据库这类应用更适合采用“单副本加持久化节点约束”的模式不要把多副本写到共享存储上。4.2 范例8Secret 和 Config 是 Swarm 里的“配置单”不要把它们写进镜像配置管理和数据持久化同样重要。现在很多团队还习惯把数据库密码、API key 直接塞进镜像环境变量里镜像一旦推送到仓库就等于把秘密公开了。Swarm 原生提供的方案是 secret 和 config。创建 secret 很简单echo your-db-password | docker secret create db_password -创建服务时挂载进去docker service create \ --name app \ --secret sourcedb_password,target/run/secrets/db_password,mode0400 \ your-image挂载后密码会以文件形式出现在容器里的/run/secrets/db_password应用读取这个文件即可。secret 文件在容器内是以内存文件系统挂载的不会写入容器可写层。应用的配置文件如果是一些非敏感内容比如 nginx 配置、应用环境变量模板可以用 config 来管理docker config create app_config nginx.conf docker service create \ --name web \ --config sourceapp_config,target/etc/nginx/nginx.conf \ nginx:1.26-alpine这比把配置文件拷贝进镜像或者通过 bind mount 维护要干净得多。配置文件的任何修改都可以通过docker config create生成新版本然后更新服务引用。更新 secret 或 config 时Swarm 不会自动把新版本应用到已经运行的任务。你需要主动触发一次服务更新docker service update \ --secret-rm db_password \ --secret-add sourcedb_password_new,target/run/secrets/db_password,mode0400 \ app这里有个非常重要的坑secret 一旦更新Swarm 会通过滚动更新重启任务把新 secret 文件挂载进去。所以更新 secret 本身也是一次发布动作必须按前面说的更新节奏来不要以为只是改个配置就偷偷摸摸地做。5. 运维收尾节点维护、集群升级与下线清理5.1 范例9drain 是节点维护的唯一优雅方式别直接去 stop 容器当你需要重启某个节点、升级内核、或者更换硬件时千万不要直接去节点上停容器正确做法是先把节点标记为 draindocker node update --availability drain node-b执行之后docker node ls里这个节点的 Availability 会变成Drain。Swarm 会把该节点上的所有服务任务主动停止并在其他可用节点上重新创建。这个过程不是瞬间完成的新任务从拉镜像到健康检查通过需要时间所以你在维护窗口前要预估好这个迁移耗时。维护完成后把节点重新打开docker node update --availability active node-b但有一个点很多人不知道节点恢复 active 后之前被挪走的任务并不会自动搬回来。Swarm 只在任务失败或需要重新调度时才会把任务放到这个节点上不会主动做“重新平衡”。如果你希望恢复后把服务的一部分任务移回该节点可以手动触发一次服务更新docker service update --force web这会按照滚动更新策略把所有任务重新创建一遍调度器在放置时就会考虑这个刚恢复的节点。注意--force会把所有任务都重建生产环境要选在低峰期操作。如果某个节点已经彻底宕机了比如硬件损坏、系统无法引导那就不需要 drain 了。你可以直接docker node ls docker node rm node-id把该节点从集群中移除。docker node rm需要先确认节点状态是Down如果还是Ready会提示你没办法删除。这时要看情况如果它真的还活着先把它改成 drain 再来删如果它已经不可达等状态变成 Down 再删。5.2 范例10集群升级要分批次滚动彻底下线要清理干净Swarm 集群的升级本质上是 Docker Engine 的升级但集群节点因为角色不同升级顺序有讲究。我的建议是先升级 worker 节点再升级 manager 节点升级每个节点之前先 drain升级完成后重新 active再等它完全恢复正常再动下一个节点。如果集群里没有多余的 worker 节点来做漂移至少保证升级过程中不会有两个 manager 同时不可用否则可能影响 raft 选主。每次升级完成后用这些命令做常规确认docker node ls docker service ls docker service ps --format table {{.Name}}\t{{.CurrentState}}\t{{.Error}} web docker node psdocker node ps是我很常用的命令它能快速列出某个节点上所有任务的状态可以帮你确认节点升级完成后负载是否正常。日志方面单服务日志用docker service logs --tail 100 --follow web节点自身的 Docker 日志则看系统日志比如用journalctl -u docker -n 100。如果业务日志量很大建议把 Docker daemon 的 log driver 配置成集中式日志方案否则节点磁盘很快会被日志打满这也是很多集群“莫名其妙”任务失败的幕后黑手。最后说集群下线清理。如果你确定整个 Swarm 不再需要了不要只在一台机器上操作。步骤是这样先移除服务让容器都停止docker service rm $(docker service ls -q)然后在每个 worker 节点上执行docker swarm leave在 manager 节点上执行docker swarm leave --force--force是必须的因为 manager 节点离开集群时不带这个参数会被拒绝。最后一个 manager 离开后Swarm 集群状态就没了。但此时节点上可能还残留着/var/lib/docker/swarm目录里面有机密数据和 raft 日志。如果这台机器将来还要重新加入其他集群建议清掉sudo systemctl stop docker sudo rm -rf /var/lib/docker/swarm sudo systemctl start docker不清这个目录的话下次docker swarm init可能会因为状态残留而报错甚至会出现“看起来初始化成功了但节点列表怪怪的”的问题。我自己的习惯是每次在集群里增加或删除节点后都顺手执行一次docker swarm join-token --rotate worker把旧的加入凭证作废。这个动作既简单又能避免历史 token 在离职人员或测试脚本里留存带来的隐患。整体看下来Docker Swarm 的整套生命周期操作其实不复杂难的是一旦出事你能不能快速定位。我的心得是把docker service ps、docker node ls和docker service logs这三条命令练成肌肉记忆每次变更完先看状态再看日志最后再做决策。上面这 10 个范例基本覆盖了从初始化到拆集群的常见节点照着一步步走至少能保证你在集群的每个阶段都知道自己该做什么、不该做什么。