Docker Swarm生产环境实战:从选型到排障的完整指南
如果你问我在生产环境用 Docker Swarm 调度容器是什么体验我的第一反应不是“组件怎么配”而是“怎么让它少惹事”。Swarm 的风评这几年有点两极分化有人觉得它配置简单、升级方便有人觉得它功能太弱、生态没法跟另一个编排平台比。但真实的生产经历是——在一个规模不大、业务以无状态 Web 服务为主、团队没有专职平台运维的背景下Swarm 反而往往比一套完整编排系统更合适。这篇文章就用一线运维视角把 Swarm 用进生产的整套思路拆开选型、建集群、写服务编排、做滚动更新、处理存储与安全、以及最让人头疼的故障排查。适合刚上手集群、或者已经跑了 Swarm 但心里没底的人。1. 生产环境选型为什么我留下 Swarm 而不是硬上 K8s1.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/TCPTCP集群管理、节点加入、Raft 通信7946/TCP UDPTCP/UDP节点间 gossip 网络、容器网络发现4789/UDPUDPoverlay 网络的 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 zoneaz1 node1 docker node update --label-add zoneaz2 node2然后在 stack 文件里给服务加约束services: api: deploy: placement: constraints: - node.labels.zoneaz1 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那会把一个坏版本推满全集群。参数推荐值作用parallelism1 或 2每批更新的副本数影响发布速度和风险delay10s-30s批次间隔给监控留观察窗口orderstart-first先起新再停旧减少流量中断failure-actionpause失败自动暂停避免坏版本扩散monitor30s批次健康观察时间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_web8或者重新编辑 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: addr10.0.5.20,rw,nfsvers4 device: :/export/dataNFS 方案成本低、配置快适合日志、静态资源这类对 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 node6. 可观测性与故障排查技巧实录6.1 日志、指标与事件Swarm 集群可观测性三件套Swarm 自带的可观测性非常基础生产集群必须自己补齐。日志这块docker service logs能看单个服务的日志但只覆盖仍在集群历史里的任务而且默认 json-file 日志驱动在容器重启、节点重启时容易丢日志。生产环境我建议把日志驱动切到 syslog 或 fluentd统一收集到集中式日志平台。如果沿用默认驱动至少要保证日志被及时采集走别让容器日志只躺在宿主机上。指标监控通常用 Prometheus Grafana 的组合装一套。每台节点部署 node-exporter 采集主机资源再给节点上跑一个 cAdvisor 容器采集容器级指标。重点盯几个指标节点的 CPU、内存余量副本总数与实际 ready 数是否一致更新任务卡住的数量。Grafana 里加一个“Swarm 集群总览”面板这些指标十分钟能配好排障时能帮你快速定位问题方向。Swarm 事件也是排障的利器。执行docker events --filter scopeswarm --since 1h可以看到集群里所有服务的创建、更新、删除、健康检查失败等事件比翻日志直观得多。生产环境我通常跑一个后台任务把 swarm 事件转发到日志平台留作变更审计和故障回放。6.2 我踩过的五个典型故障下面这五个问题是我在维护集群过程中真实遇到过的频率高、迷惑性强值得单独拿出来说。现象常见原因检查命令处理思路服务一直 Pending资源不足或约束不满足docker service ps svc看调度器提示扩容节点或调整 constraints更新卡在新副本 Preparing健康检查没通过或 start_period 太短docker service ps svc --no-trunc调大 start_period检查健康检查端点容器频繁重启日志却正常limit-memory 设置过小被 OOMdocker 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 会稳定到让你几乎感觉不到它的存在。这种“感觉不到存在”才是一个调度器在生产环境里最好的状态。