在生产环境跑容器,编排工具选型是个绕不开的话题。不少团队在 Docker 原生生态里摸爬滚打一阵后,会发现单机 docker run 和 docker-compose 撑不起高可用和动态扩缩容的需求,于是把目光投向 Kubernetes。但对很多中小团队来说,直接上 K8s 的学习成本、运维复杂度和资源开销都偏高。这时候 Docker Swarm 反而成了那个“刚刚好”的选项——它内嵌在 Docker 引擎里,不需要额外部署控制面组件,用 docker stack deploy 就能把 compose 文件变成生产级服务。这篇文章不聊理论,直接聚焦生产环境里 Swarm 的完整落地路径,从架构设计到调度策略,再到避坑经验,一次性讲清楚。
1. Docker Swarm 在生产环境的真实定位与选型逻辑
先说清楚一个容易被误解的点:Swarm 并不是 Kubernetes 的廉价平替,它在生产环境里有着自己非常明确的适用边界。Docker Swarm 的核心优势是“运维极简”,整个控制面由 Docker 引擎自带,不依赖 etcd 这类外部组件,也没有 CNI、CSI 那一整套插件生态。对于服务数量在几十到上百个、流量模型比较稳定的业务,Swarm 的调度能力完全够用,而且故障域很容易理解——就是 manager 节点和 worker 节点的关系。
1.1 Swarm 适合什么类型的生产负载
根据我带过的几个生产项目复盘,Swarm 在以下场景里表现得相当可靠:
- 微服务架构中的无状态 API 层,比如网关、用户服务、订单服务,需要副本数扩展和滚动更新
- 定时任务和异步消费者,比如用 alpine 镜像打包的 Python 爬虫、Node.js 的队列 worker
- 内部工具链平台,比如 CI 的构建节点、日志采集器、监控 exporter
- 有状态服务的“轻量级”处理,比如单实例数据库的故障转移(注意是故障转移,不是水平扩展)
不适合用 Swarm 的场景也很清晰:需要细粒度水平扩展的有状态存储(例如 Cassandra、ClickHouse)、需要自定义调度策略的机器学习训练任务、以及需要服务网格级流量治理的大型微服务系统。判断标准很简单——如果你的服务需要根据 CPU 指标做 HPA,或者需要金丝雀发布到 1% 流量,Swarm 的抽象层级确实不够用。
1.2 为什么“简单”在生产环境反而是优势
很多团队忽略了运维心智负担这个隐性成本。Kubernetes 的 Deployment、Service、Ingress、RBAC、Namespace 这些概念,每个都是一条学习曲线。而 Swarm 的抽象只有四种:Node、Service、Task、Network。任何熟悉 docker run 的工程师都能在半天内上手 swarm mode。生产环境里出故障时,能快速定位问题比拥有无穷尽的功能更重要。Swarm 的 service inspect 输出非常直白,etcd 脑裂这类控制面问题在 Swarm 里几乎不存在,因为 Raft 共识组就是 manager 节点本身。
1.3 生产环境选型前的几个硬性检查点
在决定用 Swarm 之前,最好先过一遍这几个问题。如果答案都是肯定的,那 Swarm 会是一个让你睡得着觉的选择。
- 团队是否已经有 Docker 使用经验,但对 K8s 运维不熟悉
- 应用是否以 12 因素为原则设计,配置可以从环境变量注入
- 是否能够接受“平台功能够用但不丰富”的现状
- 服务数量是否在可预估范围内增长,而不是爆发式增长
| 对比维度 | Docker Swarm | Kubernetes |
|---|---|---|
| 部署复杂度 | Docker 引擎内置,开箱即用 | 需要 kubeadm 或云托管,组件多 |
| 学习曲线 | 低,复用 compose 概念 | 高,概念体系庞大 |
| 功能丰富度 | 核心调度、服务发现、滚动更新 | 自动扩缩容、Operator、自定义 CRD |
| 故障恢复速度 | 秒级重新调度 | 秒级重新调度,但控制面更复杂 |
| 运维成本 | 低,适合小团队 | 高,需要专门平台团队 |
2. 生产级 Swarm 集群的部署与节点规划
部署 Swarm 本身不难,难的是从一开始就把集群的架构搭对。生产环境的集群规划,决定了后续半年你是在享受自动化还是在救火。我见过不少团队把三个 manager 和十几个 worker 全放在同一个物理机架上,然后交换机一断电整个集群全挂。节点规划要从故障域、网络拓扑、硬件规格三个维度去考虑。
2.1 节点角色划分与数量计算公式
Swarm 的 manager 节点负责三件事:维护集群状态、调度任务、提供 API 入口。worker 节点只负责运行容器。生产环境里 manager 节点数量必须是奇数,这是 Raft 共识算法的硬性要求。公式是:存活节点数大于总节点数的一半才能选出 leader。所以三个 manager 允许挂一个,五个允许挂两个。
硬件规格上,manager 节点不需要太强,2 核 4G 就够撑住几百个服务的心跳和调度开销,但磁盘建议用 SSD——因为 Raft 日志的持久化对 IO 延迟敏感。worker 节点按业务负载配置,但我强烈建议统一规格,这样调度器在 spread 策略下能获得最均衡的分布效果。如果 worker 规格参差不齐,就需要用资源约束配合 placement 约束来避免任务堆积在小规格节点上。
2.2 初始化集群的完整命令与参数释义
初始化时有两个参数非常关键。advertise-addr 决定了其他节点通过什么地址找到你,如果你有多网卡,务必显式指定内网 IP,否则 Swarm 可能选错网卡导致跨节点通信失败。autolock 参数用于开启 manager 节点的密钥自动锁定,防止 etcd 备份泄露后集群被恶意接管。
# 在第一个 manager 节点上执行 docker swarm init --advertise-addr 192.168.1.10 --autolock # 输出会包含 worker 和 manager 的 join token docker swarm join-token worker docker swarm join-token manager拿到 token 后,在 worker 节点执行:
docker swarm join --token SWMTKN-1-xxxxx 192.168.1.10:2377这里要说个细节:2377 是 Swarm 集群通信端口,2376 是 Docker API 的 TLS 端口。如果你开启了 TLS 认证(生产环境强烈建议),join 的时候还要指定 CA 证书。默认情况下 Swarm 的管理流量是 AES 加密的,但如果你把 Docker API 暴露到了非安全网络,必须配置 iptables 和 TLS,否则等于把集群的 root 权限送给别人。
2.3 为 manager 节点开启自动锁与备份恢复策略
autolock 开启后,manager 节点重启时会要求输入解锁密钥,这个密钥只出现一次,务必存到密码管理器里。备份方面,最重要的不是容器数据,而是 Raft 日志——它包含了集群的全部期望状态。
# 备份 manager 节点的 Raft 数据目录 tar czf swarm-backup.tar.gz /var/lib/docker/swarm # 恢复时先停掉 docker 服务 systemctl stop docker rm -rf /var/lib/docker/swarm tar xzf swarm-backup.tar.gz -C /var/lib/docker/ systemctl start docker实操下来,恢复备份后一般还需要重新 init 并 join 其他节点。这个操作建议在维护窗口做,因为恢复过程中服务会短暂不可用。
注意:不要在 worker 节点上执行 docker service ls,这需要 manager 的权限。生产环境建议用 docker context 来切换管理端点,避免混淆操作目标。
3. 核心调度机制与服务部署实操
Swarm 的调度器把“服务的期望状态”分解为“任务的部署动作”。理解这个抽象是掌握 Swarm 的关键。你定义的是服务,也就是一个抽象的逻辑单元,比如“my-api”;Swarm 通过调度器把这个服务具体化为多个 task,即具体运行中的容器实例,并确保它们分布在合适的节点上。无论某个节点挂了还是容器崩了,调度器都会自动在新节点拉起 task,让集群状态收敛到你定义的期望值。
3.1 创建服务时最容易被忽略的调度参数
docker service create 有很多参数,但有几个对生产环境影响极大,值得逐一深究。
第一个是 --replicas,这个不用多说。第二个是 --placement-pref,它控制调度器的分布偏好。默认的 spread 策略会把任务尽量散开,这对高可用来说是好事,但如果你有成本优化需求,可以用 --placement-pref spread=node.labels.zone 让任务在不同可用区间分布,或者反着来,让任务尽量集中以节省跨区流量。
# 创建带约束的服务,只在 labeled 为 gpu=true 的节点运行 docker service create \ --name mnist-inference \ --constraint node.labels.gpu == true \ --replicas 2 \ --reserve-cpu 2 \ --reserve-memory 2g \ registry.example.com/ai/mnist:v1.2第三个参数是 --reserve-cpu 和 --reserve-memory,它们的名字有点误导。reserve 不是限制上限,而是告诉调度器“这个任务至少需要这么多资源”。调度器在放置任务时会排除掉剩余资源不足的节点,但单个任务的资源使用上限由 --limit-cpu 和 --limit-memory 控制。生产环境中我见过很多因为只设 reserve 不设 limit 导致节点内存被打爆的案例,两者必须搭配使用。
3.2 滚动更新策略的参数调优经验
滚动更新是生产环境最高频的操作。Swarm 的默认更新行为是“先启动新任务,再停止旧任务”。这个顺序本身没问题,但如果新版本启动就崩溃,Swarm 会反复重试,导致服务雪崩。以下是我在生产环境稳定运行的更新配置模板:
docker service create \ --replicas 6 \ --update-order start-first \ --update-delay 10s \ --update-failure-action rollback \ --update-max-failure-ratio 0.2 \ --restart-condition any \ --restart-delay 5s \ myapp/web:v2.0update-delay 是每批任务更新间的等待时间,我一般设 10 秒,给健康检查留出余地。update-max-failure-ratio 是允许的失败比例,超过 0.2 就触发回滚。update-failure-action 建议直接设成 rollback,不要用 pause——pause 会让服务停在半更新状态,排查问题反而更麻烦。
3.3 手动扩缩容与节点维护的最佳实践
扩缩容直接执行:
docker service scale my-api=10但真正考验调度能力的是节点维护场景。比如某台 worker 要重启内核,正确步骤是:
# 1. 将节点置为 drain 状态 docker node update --availability drain worker-03 # 2. 等待已运行任务迁移完成,检查服务状态 docker service ls docker service ps my-api # 3. 节点维护完后重新激活 docker node update --availability active worker-03drain 操作会将该节点上所有 task 平滑迁移到其他节点。这里有个容易被坑的点:drain 迁移是“停止旧任务并调度新任务”,期间可能出现短暂的副本数下降。如果你的服务有严格的容量要求,建议在维护窗口操作,或者先临时 scale 上去,等迁移完成后再 scale 回来。
4. 网络与存储:生产环境绕不开的两个深水区
网络和存储是 Swarm 生产环境里最容易出“灵异事件”的地方。很多服务在测试环境一切正常,一上生产就间歇性超时、数据丢失,最后定位下来都是网络或存储配置的问题。
4.1 Overlay 网络的底层行为与排错思路
Swarm 默认的 ingress 网络是外部负载均衡入口,service 发布端口时流量会经过 ingress 网络。而用户自定义的 overlay 网络,则是服务间内部通信的通道,支持 DNS 解析。生产环境我强烈建议为每个业务创建独立的 overlay 网络,而不是全部扔在默认网络里,这样能隔离广播域和减少 DNS 解析的干扰。
# 创建自定义 overlay 网络 docker network create --driver overlay --attachable --subnet 10.30.0.0/24 prod-netattachbale 参数允许非 swarm 服务也接入这个网络,对调试非常有帮助。排错时最常用的命令是 docker network inspect 和登录到容器里 ping 服务名。如果跨节点 ping 不通,先查防火墙是否放行了 4789 端口(VXLAN 端口)。
4.2 存储选型:为什么本地 volume 是默认首选
Swarm 内置的 volume 驱动只支持本地存储,跨节点的数据共享需要自己接 NFS 或者云盘。根据我的生产经验,存储方案的选择要遵循一条原则:能不用共享存储就不用,因为共享存储往往成为性能瓶颈和单点故障。
对于无状态服务,直接使用容器可写层或 tmpfs 就够了,重启丢数据无所谓。对于有状态服务,优先用本地 volume 加节点约束,保证任务始终调度到同一节点:
docker service create \ --name postgres \ --constraint node.hostname == worker-db-01 \ --mount type=volume,source=pgdata,target=/var/lib/postgresql/data \ postgres:164.3 解决“跨节点数据共享”的 NFS 配置细节
如果确实需要跨节点共享数据(比如多个 worker 并发写一个上传目录),NFS 是最简单的方案。配置时需要注意挂载参数:
docker service create \ --name file-server \ --mount type=volume,volume-driver=local,device=:/srv/nfs/share,o=addr=192.168.50.10,rw,src=share,target=/data \ nginx:alpine这里踩过最大的坑是 NFS 的硬挂载和软挂载问题。生产环境必须使用硬挂载加 intr 参数,否则 NFS 服务端短暂不可用时,客户端进程会卡在不可中断的 IO 状态,docker 想删容器都删不掉。另外,NFS 性能对网络延迟极其敏感,确保 NFS 服务端和 worker 节点在同一内网且网卡带宽足够。
5. 服务发现、配置管理与外部流量接入
一个部署在 Swarm 里的应用要真正服务用户,必须解决三个问题:服务之间怎么找到对方、配置和密钥怎么管理、外部流量怎么进来。
5.1 Swarm 内置 DNS 与服务间调用的坑
Swarm 内置的 DNS 解析规则是:同一 overlay 网络内的服务可以直接用服务名互相访问。不同网络之间不行。还有一个隐藏规则:如果服务没有 attach 到某个 overlay 网络,那么 DNS 解析会失败。所以创建服务时务必要加 --network 参数。
docker service create --name api \ --network prod-net \ --replicas 3 \ myapp/api:v1.0从 API 服务里访问数据库服务,直接写 redis:6379 就行。如果服务间需要跨网络访问,要先把两个网络都 attach 到同一个服务上。实际使用中我发现,频繁跨网络的服务间调用,性能会下降 10%-20%,因为每次请求都要经过额外的封包解包。所以网络设计上尽量把有调用链关系的服务放在同一个 overlay 网络里。
5.2 docker secret 与 docker config 的正确打开方式
很多从 docker-compose 过渡上来的工程师,习惯把数据库密码写进环境变量。这在生产环境是致命伤——环境变量在 docker inspect 里直接可见。正确做法是用 docker secret:
echo "s3cr3t-db-pass" | docker secret create db_pass -创建服务时引用:
docker service create \ --name api \ --secret db_pass \ --secret source=redis_pass,target=redis_password \ myapp/api:v1.0secret 会以文件形式挂载到 /run/secrets/ 目录。应用代码里读文件而不是读环境变量。这里有个小技巧:target 参数可以把 secrets 重命名为应用期望的文件名,避免改代码。
docker config 和 secret 类似,但适合存储非敏感配置,比如 nginx.conf 或 logback.xml。两者本质上都是通过 Swarm 管理节点加密分发,只不过 secret 在创建后不可编辑,config 可以 update。
5.3 外部流量接入 Swarm 的最佳路径
生产环境接入外部流量的方案,我推荐用 Nginx 或 Traefik 反代到 Swarm 服务。Swarm 的 ingress 负载均衡虽然自带,但它的能力止步于 L4,无法做基于 Host 或 Path 的路由。用反代可以获得完全的流量控制能力。
一个稳定的架构是:在 manager 节点上部署 Nginx 容器,配置 upstream 指向 Swarm 服务的 DNS 名称,然后 publish 端口到宿主机。或者用 Traefik 配合 Docker 服务发现,自动从 Swarm 获取服务列表并配置路由规则。
# traefik.yml 中的服务发现配置 providers: docker: endpoint: "unix:///var/run/docker.sock" swarmMode: true exposedByDefault: false在发布服务时加上标签:
docker service create \ --name web \ --label traefik.enable=true \ --label traefik.http.routers.web.rule=Host(`app.example.com`) \ --label traefik.http.services.web.loadbalancer.server.port=8080 \ myapp/web:v1.0这种模式下,Nginx/Traefik 容器必须运行在 manager 节点上(因为要访问 docker socket),为了避免管理器被流量打爆,建议把反代容器固定到一台独立的 worker 节点,通过 docker context 远程连接管理。
6. 生产环境的典型故障与排查实录
讲再多理论,都不如把实际故障摆出来分析一遍有说服力。下面是我在生产环境里真实遇到过的四类典型问题,每一个都踩过坑,也都总结出了明确的排查路径。
6.1 服务一直显示 updating 卡死怎么办
症状:docker service ls 显示服务状态长时间停留在 updating。原因通常是某个节点上的旧容器无法停止,或者新容器无法启动。排查步骤如下:
# 找到具体卡住的任务 docker service ps my-api # 查看任务状态停留在什么阶段 docker inspect --format '{{.Status.State}}' <task-id>最常见的原因是节点磁盘满了,镜像无法拉取。df -h 看一眼就能确认。另一种情况是镜像仓库认证过期,导致新节点拉不到镜像。Swarm 不会自动重试拉取失败的镜像,需要手动 docker pull 触发一次,然后服务会自动恢复。
6.2 滚动更新后服务陷入 CrashLoopBackOff
CrashLoopBackOff 意味着新版本容器启动后就崩,反复被调度器拉起。Swarm 的 restart-condition any 会一直重试,导致资源被无效容器占用。这时候第一反应不是去查看哪台节点有问题,而是应该直接叫停更新,把服务回滚到上一个稳定版本:
docker service rollback my-apirollback 操作会恢复到最后一次部署的镜像版本。但注意,rollback 只回滚镜像和配置,不回滚数据——如果你的新版本有数据库迁移,回滚后还需要手动处理数据兼容。
6.3 服务跨节点通信间歇性超时
这个问题排查起来要比前两个更隐蔽。现象是服务 A 访问服务 B,偶尔超时 3-5 秒。定位后发现,原因是 overlay 网络的 VXLAN 数据包在跨节点时被交换机丢弃或者触发了反噬风暴。解决办法是:
- 检查所有节点的防火墙,放行 4789/7946 端口
- 确保 MTU 一致性,docker 默认 overlay 网络的 MTU 是 1450,如果宿主机网卡 MTU 是 1500,两者不匹配会引发分片丢失
# 在每台节点上确认 MTU ip link show docker_gwbridge另外,很多云厂商虚拟机默认启用了网卡的 TCP 校验和卸载,会导致 VXLAN 包校验失败。这个时候需要关闭 tx-checksumming:
ethtool -K eth0 tx off rx off这是我调过最诡异的一个问题,症状和网络抖动一模一样,但根因完全不一样。
6.4 节点 drain 后任务迁移不符合预期
有时执行 docker node update --availability drain 后,任务并没有像预期那样全部迁移走。原因往往是服务设置了 node.labels 相关约束,而其他节点不满足这些约束,导致调度器无法重新部署。此时检查:
docker service ps my-api --filter desired-state=Running如果新任务一直处于 Pending,说明没有节点满足约束。解决办法是临时放宽约束,或者先解除对应节点的 drain 状态。生产环境里最省心的做法是:给每台 worker 设置 zone 标签,调度约束尽量用 zone 而不是 hostname,这样单节点故障后任务能迁移到同 zone 的其他节点。
7. 服务治理与生命周期管理的进阶实践
Swarm 能做的不仅仅是部署和扩展。把它用熟了之后,你会发现一些高级玩法能显著提升生产环境的安全性和操作效率。
7.1 通过节点标签实现环境隔离与差异化调度
节点标签是 Swarm 调度规则的基础。除了前面提到的 zone 标签,我还习惯用 role 标签区分不同职能的节点:
docker node update --label-add role=api-worker node-01 docker node update --label-add role=db-worker node-02这样创建服务时就能用约束限制调度目标:
docker service create \ --constraint node.labels.role == api-worker \ --replicas 5 \ myapp/api:v2.17.2 使用 docker stack 管理整套应用生命周期
当服务数量多起来后,一条条 docker service create 就太繁琐了。docker stack deploy 可以像 docker-compose 一样,用一个 compose 文件描述整个应用栈,一次性部署或更新。
# stack.yaml version: "3.9" services: api: image: myapp/api:v2.1 environment: DB_HOST: postgres networks: - prod-net secrets: - db_pass deploy: replicas: 5 placement: constraints: - node.labels.role == api-worker update_config: order: start-first failure_action: rollback postgres: image: postgres:16 volumes: - pgdata:/var/lib/postgresql/data networks: - prod-net deploy: replicas: 1 placement: constraints: - node.labels.role == db-worker networks: prod-net: driver: overlay volumes: pgdata: secrets: db_pass: external: true执行部署:
docker stack deploy -c stack.yaml myappstack 的好处是幂等更新,改配置后重新 deploy 只会变更差异部分。另外,用 stack 管理多个环境(dev/staging/prod)也方便,只需要切换 docker context 然后 deploy 不同的 compose 文件片段。
7.3 日志与监控的轻量级闭环方案
Swarm 本身不带日志和监控,但可以非常轻量地补齐。日志我用的是 Loki + Promtail,每个 worker 节点跑一个 Promtail 容器,直接把 docker 容器日志采集到 Loki 里。premetheus 则通过 node-exporter 暴露节点指标,CAdvisor 暴露容器指标,然后用 Prometheus 抓取。
# Promtail 服务示例 docker service create \ --name promtail \ --constraint node.role == worker \ --mount type=bind,source=/var/log,destination=/var/log \ --mount type=bind,source=/var/lib/docker/containers,destination=/var/lib/docker/containers,readonly \ --mount type=bind,source=/var/run/docker.sock,destination=/var/run/docker.sock \ grafana/promtail:2.9.0 \ -config.file=/etc/promtail/promtail.yaml监控告警方面,配置好 Prometheus 的 alertmanager 规则,磁盘使用率超过 85%、容器频繁重启、节点 CPU 持续跑满,其实就可以做到生产环境的可观测闭环。这套方案所有组件都跑在 Swarm 集群里,不引入额外的外部依赖。
生产环境没有银弹,Swarm 也不是万能的。但它给那些不想被 K8s 运维绑架的团队提供了一个足够务实、足够透明的底座。我个人的体会是,把核心精力放在业务容器化改造和集群网络设计上,把集群本身当成一个不折腾的“黑盒”,遇到问题通过 docker service ps 和 docker service logs 总能快速定位。最后分享一个小技巧:每次做变更前,先执行 docker service inspect 把当前配置留档,出任何问题都能快速对比回滚。这套流程用顺了之后,你会发现 Swarm 的稳定性和可预期性,比很多“功能强大”的平台都要让人心安。