如果你问我在生产环境用 Docker Swarm 调度容器是什么体验,我的第一反应不是“组件怎么配”,而是“怎么让它少惹事”。Swarm 的风评这几年有点两极分化:有人觉得它配置简单、升级方便,有人觉得它功能太弱、生态没法跟另一个编排平台比。但真实的生产经历是——在一个规模不大、业务以无状态 Web 服务为主、团队没有专职平台运维的背景下,Swarm 反而往往比一套完整编排系统更合适。这篇文章就用一线运维视角,把 Swarm 用进生产的整套思路拆开:选型、建集群、写服务编排、做滚动更新、处理存储与安全、以及最让人头疼的故障排查。适合刚上手集群、或者已经跑了 Swarm 但心里没底的人。
1. 生产环境选型:为什么我留下 Swarm 而不是硬上 K8s
1.1 它适合什么样的业务,不适合什么样的业务
先说结论:Swarm 从来不是万能的,但它在一个很具体的场景里非常能打。我见过不少团队在业务规模还不到几十个容器的时候,就基于“大家都在用”的理由上一套完整编排系统,结果光维护控制面、网络插件、准入控制器就把两三个人拖进去了。而 Swarm 的优势恰恰在于朴素——它不试图把集群变成一个小型操作系统,只是给 Docker 加了一套原生的调度协议。
适合 Swarm 的典型画像是:容器数量在几十到几百的规模;绝大多数服务是无状态的 Web、API、批处理任务;团队已经熟悉 Docker 基础操作;没有专职的容器平台团队;希望用最少的学习成本把“单机跑容器”升级成“集群跑服务”。这种情况下,Swarm 的收敛速度、交付速度、排障成本,客观说比复杂编排平台更容易让人接受。
不太适合的场景也明确:服务类型混杂、需要异构资源精细调度的;对多集群联邦、跨集群容灾有强需求;团队重度依赖云原生生态里那些特定组件,希望在集群里跑各种复杂 operator 的。Swarm 没有内置的扩展机制,调度器也相对单纯,强行把复杂场景塞进来,最后只会把业务方的耐心耗光。选型的第一步不是比功能列表,而是判断你手上这份工作的真实复杂度。
1.2 从“跑容器”到“调度容器”:Swarm 真正改变的是什么
单机 Docker 时代,你的运维模型是一台一台机器分别处理,每台机器上有哪些容器,靠脚本和人情记忆。到了 Swarm 这里,管理单位从“容器”变成了“服务”:服务声明期望状态是 3 个副本、内存上限、健康检查、更新策略,调度器负责把这个期望状态收敛成实际状态。这个模型变化才是“调度”二字的本质。
拿最常见的场景举例。业务容器崩了,单机 Docker 是不会主动帮你拉起来的;Swarm 会根据 restart_policy 在一个节点上重启它。节点整体宕机呢?单机 Docker 无解,Swarm 会把该节点上未完成的任务在另一台健康节点上重新创建。服务需要发布新版本,Swarm 可以按下发顺序启动新副本、等待健康检查通过,再把旧副本停掉。听起来都是小事,但在生产环境里,这些“小事”如果靠人肉值班去做,就是事故的温床。
这也是我常跟团队打的比方:单机 Docker 像是你自己跑腿送快递,Swarm 则是一个车队调度系统。你只告诉系统“这批货要送 50 件、不能送到某片区、每批送 5 件、送完检查一下再送下一批”,系统自己安排车辆和路线。你的职责是给规则,不是盯车轮。
1.3 选型之前必须想清楚的三个问题
踩过几次坑之后,我觉得上 Swarm 之前真正该问的问题不是“它会不会比 K8s 差”,而是下面三个:
损失一个节点,业务能不能扛住?如果答案是“不能扛”,那 Swarm 管理节点的高可用只能保证调度端不挂,服务副本能不能扛住流量取决于你留了多少冗余副本。先把业务侧的冗余设计好,再上集群。
服务有没有状态?Swarm 对有状态服务的支持比较原始。数据库、消息队列这类服务,如果数据落在容器本地,调度器一调度到别的节点数据就丢了。所以在选型阶段,要先把有状态的部分剥离出去,或者非常清楚地接受它的存储限制。
团队愿不愿意用 stack 文件作为发布入口?Swarm 发布的核心是把 compose 文件稍加改造,变成 stack 文件,然后一次 deploy 上线整套服务。如果团队习惯登录服务器手敲 docker run,上 Swarm 的第一周会很不适应。这不是技术门槛,是操作习惯的门槛。
这三个问题如果都能给出明确答案,Swarm 在你这里大概率是个正确的选择。
2. 生产集群搭建:一次把高可用打牢
2.1 节点角色划分与规模设计:3 个管理节点起步
Swarm 集群有两种角色:管理节点参与 Raft 共识,负责维持集群状态、调度任务;工作节点只负责跑容器。很多人刚开始会把集群搭成一个管理节点加一堆工作节点,测试没问题,但一旦管理节点宕机,整个集群的调度能力就停了,已运行的服务虽然还活着,却没人管它们的生老病死。
生产环境我最低建议是3 个管理节点。Raft 共识要求多数派存活才能继续工作,3 个节点允许挂 1 个,5 个节点允许挂 2 个。为什么不是 2 个?2 个节点最多容忍 1 个故障,但一旦出现网络分区,两个节点各执一词,谁也成不了多数派,反而容易出现假死状态,不如 3 个稳。管理节点之间要保证网络质量,最好放在同一个低延迟区域,别搞跨地域管理节点,否则 Raft 日志同步延迟会直接影响任务调度。
规模设计上没有银弹,但可以给一个落地参考:业务高峰期并发容器数在 50 个以下,3 管理 + 3 工作节点比较舒服;50 到 150 个容器,可以加到 5 个工作节点;再往上,优先看瓶颈是 CPU 还是内存,再按资源余量扩。管理节点上尽量不跑业务容器,可以让调度器把管理节点标记成不可调度,避免集群状态同步和业务负载互相抢资源。
2.2 初始化与加入集群:完整命令与参数解读
集群初始化其实是一条命令的事,但参数里藏着生产环境的关键细节。假设第一台管理节点内网 IP 是 10.0.1.10,我一般这样初始化:
docker swarm init \ --advertise-addr 10.0.1.10 \ --listen-addr 0.0.0.0:2377--advertise-addr是告诉其他节点“你通过这个地址找我”,在多网卡机器上必须显式指定,否则可能选错网卡导致节点间通信失败。--listen-addr是本地监听地址,默认 0.0.0.0:2377 没问题,但如果机器上有多个网卡且只想局域网访问,就把这里改成具体 IP。
初始化完成后,用下面的命令查看加入令牌:
docker swarm join-token worker docker swarm join-token manager新节点执行输出的 docker swarm join 命令即可。这里有个容易被忽略的点:manager token 和 worker token 是要分开管理的,谁拿到 manager token 谁就能管理整个集群。生产环境里,manager token 只给负责集群管理的人,其他人一律用 worker token 加入。
硬件条件允许的话,建议一开局就把管理节点配置成带自动锁模式:
docker swarm init --autolock启用后,管理节点重启时要输入解锁密钥才会重新参与集群。这个功能能防止有人偷走管理节点的磁盘数据后离线解密 Raft 日志,代价是每次重启都要人工介入。密钥一定放密码管理库里备份,别存在本地文本文件里,否则管理节点重启时你就只能干瞪眼。
2.3 端口、防火墙与集群安全基线
Swarm 正常工作只需要三个端口,但每一个都很关键:
| 端口 | 协议 | 用途 |
|---|---|---|
| 2377/TCP | TCP | 集群管理、节点加入、Raft 通信 |
| 7946/TCP + UDP | TCP/UDP | 节点间 gossip 网络、容器网络发现 |
| 4789/UDP | UDP | overlay 网络的 VXLAN 数据面 |
安全基线第一条:2377 端口只能是集群内网访问,绝对不要暴露到公网。默认的加入令牌如果泄露,任何人都能把机器加入集群,成为集群节点然后尝试横向移动。实际运维中,我会在云平台安全组和节点本机防火墙两层都做限制,只允许管理节点所在网段访问 2377。
第二条:定期轮换 join token。长期不轮换的 token 就像一个永远有效的万能钥匙,内部人员流动、供应商离职都可能带着它离开。轮换命令很简单:
docker swarm join-token --rotate worker docker swarm join-token --rotate manager第三条:节点间通信默认是加密的,Swarm 会自动签发 TLS 证书并定期轮换。但证书轮换参数建议显式设置,默认的 90 天在某些长期不重启的集群里可能触发意外,我一般会用--cert-expiry 2160h把证书周期调到 90 天以上,同时配套监控提醒。
3. 服务编排:调度器其实有一套自己的“路由规则”
3.1 Service 是包装后的容器:用 stack 文件作为服务“宪法”
很多新手把 service 理解成“多副本的容器”,这个理解不够准确。service 是 Swarm 中最小的调度单元,它把镜像、网络、存储、环境变量、健康检查、更新策略、重启策略打包成一个声明式对象。调度器拿到这个对象后,负责让集群状态向声明的状态收敛。
生产实践中,我强烈建议用 stack 文件管理服务,而不是一条条敲 docker service create。stack 文件本质上是 compose 文件的集群版本,可以放进 Git 做版本管理,发布前后的 diff 一目了然。一个标准的生产 stack 文件长这样:
version: "3.8" services: web: image: registry.example.com/web:1.4.2 deploy: replicas: 3 update_config: parallelism: 1 delay: 15s order: start-first resources: limits: cpus: "2" memory: 2G reservations: cpus: "0.5" memory: 1G restart_policy: condition: on-failure healthcheck: test: ["CMD", "curl", "-fs", "http://localhost/healthz"] interval: 15s timeout: 5s retries: 3 start_period: 30s networks: - appnet networks: appnet: driver: overlay这份文件里有几个生产关键点:镜像地址使用明确的版本 tag;副本数由 replicas 声明;资源限量和预留分开配置;健康检查挂在服务级而不是容器级。发布时一条命令搞定:
docker stack deploy -c docker-stack.yml prod集群里的状态和文件里的期望不一致时,Swarm 会自动尝试收敛。可以这么说,stack 文件就是你与调度器之间的合同,所有临时手改都容易让合同失控。
3.2 reserve 和 limit:调度器判断放不放新副本的核心依据
绝大多数服务排障问题,根源出在资源声明写得太随意。Swarm 调度器在决定把任务放到哪个节点时,优先看每个节点的可用资源,也就是所有任务的 reservations 总和没有超过节点容量。它不会看你某个容器实际用了多少内存,只看你在配置里预留了多少。
所以这里必须理解两个词的差异:reservations是向调度器申请的“保底资源”,调度器把它当作能不能调度新副本的依据;limits是容器运行时的最大资源上限,超了会被内核用 OOM Kill 杀掉。典型问题是很多人只写了 limit 没写 reserve,结果调度器认为所有节点都很空闲,把一大堆任务塞到同一台机器上,运行后实际内存超限,容器频繁被杀,表现为服务反复重启。
我处理这类问题有一套固定做法:先看单容器稳定运行时的平均内存和 CPU,以此为基准把 reservations 设成峰值的 80%,limits 设为峰值的 1.5 到 2 倍。同时每个节点至少预留 10% 到 15% 的资源容量给系统进程和日志收集,绝不做 100% 填充。比如节点总内存 8G,我最多允许所有服务的 reservations 总和在 7G 以内,这样即使某一个容器出现内存泄漏,系统还有余量能扛一段时间,而不是立刻触发雪崩。
3.3 constraints 与 placement:把服务调度到它该去的节点
Swarm 的调度路由规则有两条:约束是“必须满足”,放置偏好是“尽量满足”。用标签给节点打属性,然后制定规则,表达力远超很多人想象。
例如给不同可用区的节点打标签:
docker node update --label-add zone=az1 <node1> docker node update --label-add zone=az2 <node2>然后在 stack 文件里给服务加约束:
services: api: deploy: placement: constraints: - node.labels.zone==az1 preferences: - spread: node.labels.zone这样配置的意思是:api 服务必须跑在标签为 az1 的节点上;当同一个服务多个副本时,Swarm 会尽量把副本分散到不同 zone,避免被一个可用区的故障团灭。约束语义是强制的,如果满足条件的节点不够,新副本会一直停在 Pending 状态,这时去查 docker service ps 就能看到调度器给出的提示。
还有一个我经常用的节点角色约束:管理节点不跑业务容器。可以在管理节点上设置docker node update --availability drain <manager_node>,或者把业务服务统一加上约束node.role != manager。这是纯经验之谈:管理节点负载一旦被业务容器打满,Raft 心跳都会变慢,整个集群的调度稳定性立刻下降一个档次。
4. 滚动更新、回滚和弹性伸缩怎么控制
4.1 滚动更新的三个关键参数
Swarm 的滚动更新模型不复杂,但参数组合直接影响发布时是否流量受损。核心参数有三个:并行度、间隔、顺序。
--update-parallelism控制同时更新的副本数,我一般设 1 或 2。设大确实发布快,但假设新版本有隐藏 Bug,瞬间一半副本进入新版本,回滚成本就高了。--update-delay是两个批次之间的等待时间,给监控系统留出观察窗口。--update-order有两种:start-first先启动新副本,新副本通过健康检查后再停旧副本;stop-first先停旧副本再启动新副本,这种方式会导致服务短暂减少,一般只在资源紧张时用。
发布流程中的失败处理同样关键,我用过多次的组合是:--update-failure-action pause。意思是任何一批更新失败,自动暂停后续批次,把集群停在一个部分新、部分旧的状态,留给你人工判断。生产环境绝不建议continue,那会把一个坏版本推满全集群。
| 参数 | 推荐值 | 作用 |
|---|---|---|
| parallelism | 1 或 2 | 每批更新的副本数,影响发布速度和风险 |
| delay | 10s-30s | 批次间隔,给监控留观察窗口 |
| order | start-first | 先起新再停旧,减少流量中断 |
| failure-action | pause | 失败自动暂停,避免坏版本扩散 |
| monitor | 30s | 批次健康观察时间 |
4.2 健康检查:谁是“新版本可以上线”的裁判
滚动更新优雅不优雅,很大程度取决于健康检查写得对不对。健康检查在 Swarm 里既是容器的存活探针,也是更新的成功判据。新一轮副本启动后,Swarm 会等待副本进入 healthy 状态,再决定是否继续下一批。
健康检查参数要特别留意start_period。这个参数是给应用启动留的宽限期,期间健康检查失败不会触发重启。Java 应用启动可能 40 秒,天然慢;如果你不设 start_period,它会在还没起来时就被判定失败,反复重启,更新流程卡在 Preparing 状态。
我还见过一种情况:健康检查命令写的是curl -f http://localhost/,但应用根路径返回 200 其实是欢迎页,真正的业务线程池已经耗尽,接口全都超时。健康检查测的应该是应用的核心健康端点,最好是独立的/healthz,里面检查数据库连接、缓存连接等关键依赖,而不是应用进程还活着就一直告诉我“健康”。
4.3 扩容缩容与回滚:运维动作要有纪律
弹性伸缩在 Swarm 里直接一条命令搞定:
docker service scale prod_web=8或者重新编辑 stack 文件里的 replicas 再 deploy。手动 scale 适合临时扩容,比如大促、活动流量;长期容量调整我还是建议回到 stack 文件里改,保持 Git 记录一致。
回滚同样简单:
docker service rollback prod_web这条命令会把服务恢复到上一次成功的配置和镜像版本。但注意,它回滚的是镜像 tag 对应的配置,不是镜像内容。如果新版本有问题但镜像 tag 没变,只是内容变了,那这次“回滚”覆盖不到问题。所以生产规范的第一个原则是:每次发布必须换新 tag,不允许覆盖同一个 tag。
运维纪律上还有一条:不要在凌晨大促前临时docker service update --force重启所有副本。--force不会重新拉取新配置,只是强行重启,它解决不了配置错误,只会放大风险。所有变更先在测试集群验证,再上生产,这套动作比任何参数都值钱。
5. 生产环境绕不开的存储、配置与安全
5.1 数据持久化:bind mount 和 local volume 撑不起生产
Swarm 的调度是分布式的,数据持久化一定不能绑定到某个节点上。很多第一次上集群的人习惯用 bind mount 把宿主机目录挂进容器,这在单机环境没问题,但在集群里,当容器被调度到另一个节点时,数据目录不会跟着走,于是“容器起来了,数据没了”。
Swarm 内置的 local volume 同样有这个问题,数据只存在写入的节点上。生产环境里,需要共享数据的服务通常用网络存储解决,最朴素的做法是 NFS。stack 文件里这样写:
volumes: data: driver: local driver_opts: type: nfs o: addr=10.0.5.20,rw,nfsvers=4 device: ":/export/data"NFS 方案成本低、配置快,适合日志、静态资源这类对 IOPS 要求不高的数据。但它有单点风险,NFS 服务器本身上了规模后性能会成为瓶颈。对数据库这种高 IO 状态服务,Swarm 原生存储方案并不理想,我的建议是:超过单实例容量或写入频次要求高的数据库,不要轻易放在 Swarm 里跑,独立部署或走托管数据库方案,让 Swarm 专注无状态服务。
5.2 Secret 与 Config:把敏感信息挡在镜像外面
生产环境最不该出现的操作是把数据库密码写进 Dockerfile 或者环境变量里。Swarm 提供 secret 机制,专门用来安全分发敏感信息。
创建方式:
echo "your-db-password" | docker secret create db_password -stack 文件里挂载:
services: web: image: registry.example.com/web:1.4.2 secrets: - db_password secrets: db_password: external: true容器里通过/run/secrets/db_password读取。secret 会加密保存在 Raft 日志中,只分发到被调度到对应副本的节点,不进入镜像层,也不进入环境变量。普通配置信息用 config 机制,用法类似但不会被严格加密。这里有个容易踩的坑:secret 更新后,必须重新部署服务才会生效,运行中的容器不会自动拿到新值。
我见过一个团队把 secret 文件直接提交进 Git 仓库,虽然用的是 Swarm 的 secret 机制,但源头泄露了,等于白搭。正确做法是 secret 文件与代码仓库分离,只把文件名提交,内容通过密码库或 CI 系统的受保护变量在部署时注入。
5.3 安全配置中的常见误区和节奏
除了前面提到的端口和 token,还有几个安全细节容易被人忽略。
管理节点的 Docker API 默认可能监听在本地 socket 上,如果为了方便监控,把 API 绑定到可以被外部访问的地址,又没有启用 mTLS,等于把管理接口暴露给内网所有人。生产环境建议保留默认 Unix socket,需要远程管理时用 SSH 隧道进到管理节点再操作。
镜像供应链同样要管。生产集群经常遇到“镜像 tag 是 latest”这种写法,latest 的好处是很爽,坏处是每次调度新副本时拉到的镜像可能是不同的,而且无法复现问题。所有生产镜像必须包含不可变版本号,配合私有镜像仓库做统一管理。
另外要重视节点补丁。Swarm 控制面跑在容器和宿主机内核之上,宿主机内核的漏洞,不会因为docker service ls一切正常而消失。给工作节点定期重启打补丁,最好用的是 drain 节点再重启,避免容器被强制中断:
docker node update --availability drain <node> # 重启节点 docker node update --availability active <node>6. 可观测性与故障排查技巧实录
6.1 日志、指标与事件:Swarm 集群可观测性三件套
Swarm 自带的可观测性非常基础,生产集群必须自己补齐。日志这块,docker service logs能看单个服务的日志,但只覆盖仍在集群历史里的任务,而且默认 json-file 日志驱动在容器重启、节点重启时容易丢日志。生产环境我建议把日志驱动切到 syslog 或 fluentd,统一收集到集中式日志平台。如果沿用默认驱动,至少要保证日志被及时采集走,别让容器日志只躺在宿主机上。
指标监控通常用 Prometheus + Grafana 的组合装一套。每台节点部署 node-exporter 采集主机资源,再给节点上跑一个 cAdvisor 容器采集容器级指标。重点盯几个指标:节点的 CPU、内存余量,副本总数与实际 ready 数是否一致,更新任务卡住的数量。Grafana 里加一个“Swarm 集群总览”面板,这些指标十分钟能配好,排障时能帮你快速定位问题方向。
Swarm 事件也是排障的利器。执行:
docker events --filter scope=swarm --since 1h可以看到集群里所有服务的创建、更新、删除、健康检查失败等事件,比翻日志直观得多。生产环境我通常跑一个后台任务,把 swarm 事件转发到日志平台,留作变更审计和故障回放。
6.2 我踩过的五个典型故障
下面这五个问题是我在维护集群过程中真实遇到过的,频率高、迷惑性强,值得单独拿出来说。
| 现象 | 常见原因 | 检查命令 | 处理思路 |
|---|---|---|---|
| 服务一直 Pending | 资源不足或约束不满足 | docker service ps <svc> | 看调度器提示,扩容节点或调整 constraints |
| 更新卡在新副本 Preparing | 健康检查没通过或 start_period 太短 | docker service ps <svc> --no-trunc | 调大 start_period,检查健康检查端点 |
| 容器频繁重启,日志却正常 | limit-memory 设置过小被 OOM | docker inspect <container>查 ExitCode 137 | 提高 limits 或修复内存泄漏 |
| 管理节点挂掉后集群还能撑,但发布失败 | Raft 多数派不足 | docker node ls看管理节点状态 | 恢复节点,确保管理节点为奇数 |
| overlay 网络跨节点不通 | 防火墙封了 4789/UDP | 节点间抓包排查 | 放开 4789/UDP 端口 |
第一个故障的具体表现是:docker service ps显示任务一直是 Pending,切到具体任务详情才看到一行字“no suitable node(没有满足条件的节点)”。这种情况常见于 constraints 写得太死,例如只允许一个标签的节点,但那个节点因为维护被 drain 了,于是服务永远调度不起来。排查思路不是加副本数,而是先看约束和节点状态。
第二个故障非常容易误判:服务更新一直不往下走,停在新副本启动阶段,看起来像“资源不够”。实际查下来,往往是健康检查的start_period设置过短。应用启动要 45 秒,你设了 10 秒,副本永远通过不了健康检查,更新自然卡住。调整start_period到应用启动耗时的 1.5 倍左右,问题立刻解决。
另外关于 overlay 网络,值得一提的是跨节点通信默认走 VXLAN,依赖 4789/UDP 端口。很多云平台安全组教程只放行了 TCP 端口,把 UDP 漏了,结果就是单机内通信正常,跨节点服务访问超时。排查这类问题,先看防火墙规则,再在节点上抓包验证,别上来就怀疑应用代码。
6.3 排障心法:先看状态,再动服务
我见过太多人排障第一反应是“把这个服务重启一下”。在 Swarm 里,重启一个容器只是掩盖问题,不会让期望状态收敛到哪里去。正确的排障顺序应该是:先看docker service ps了解任务生命周期,再看docker service logs看应用日志,最后才决定是否需要干预。
举个例子,某服务出现副本频繁重启,直接docker service update --force重启,看似暂时恢复,但过一小时又复发。用docker service ps一看,发现任务状态在“Failed”和“Starting”之间反复切换,docker inspect显示 ExitCode 是 137,立刻能锁定是内存限制问题。整个定位过程不到三分钟,而盲目重启可能让你重复踩同一坑。
另一个心法是:不要在管理节点上手动执行docker service update改参数,除非你完全清楚 stack 文件的状态。原因为手改后的配置会覆盖 stack 文件声明的配置,下次执行 stack deploy 时新旧配置不一致,容易产生意想不到的收敛行为。我的做法是任何参数变更都先改 Git 里的 stack 文件,走常规 deploy 流程,保留可追溯的变更记录。
最后分享一个我个人反复验证过的经验:Swarm 集群里,90% 的诡异问题都不是 Swarm 本身的问题,而是资源声明不当、健康检查不准、镜像 tag 混乱、防火墙端口不全这些“外围问题”。把外围问题的基础打牢,Swarm 会稳定到让你几乎感觉不到它的存在。这种“感觉不到存在”,才是一个调度器在生产环境里最好的状态。