
1. 为什么我最终选了 Docker Swarm 而不是 K8s1.1 一个真实的需求场景去年下半年我接手了一个内部工具平台的部署改造。背景很简单一套 Web 服务日活不高但要求 7x24 在线之前跑在一台单机上用 systemd 管着几个进程每次发版靠脚本 kill 掉再拉起来中间有十几秒的服务中断。业务方虽然嘴上不说但监控群里偶尔冒出来的 502 报警截图大家都心知肚明。需求拆下来其实就三条第一服务不能有明显中断滚动更新是底线第二节点挂了要能自动把容器调度到别的机器上第三运维成本要低团队里没有专职的容器平台工程师谁都能上手改配置、看状态。摆在面前的选择无非就是 Docker Swarm 和 Kubernetes。K8s 生态确实强大但它的学习曲线和运维复杂度对于这个体量的项目来说属于典型的杀鸡用牛刀。我算过一笔账上 K8s 意味着至少要维护 etcd、apiserver、controller-manager、scheduler 这一整套控制面再加上 CNI 网络插件、Ingress Controller、监控栈光是 YAML 文件就能堆出几十个。而 Swarm 是 Docker 引擎内置的docker swarm init一条命令就能起一个集群节点加入只需要一个 token。最终我选了 Swarm。不是说它比 K8s 好而是在这个具体场景下它的投入产出比最高。这篇文章就把我从零搭这套高可用 Web 集群的完整过程拆开讲包括架构设计、参数选择、踩过的坑以及那些文档里不会写的实操细节。1.2 Swarm 的架构到底是怎么回事很多人对 Swarm 的印象停留在Docker 自带的那个编排工具但具体它怎么工作的说不清楚。我用一个类比来解释Swarm 集群就像一家连锁餐厅Manager 节点是店长Worker 节点是厨师。Manager 节点负责决策——哪个菜容器放到哪个厨师节点那里做现在有几个订单服务副本需不需要加人。Worker 节点只管执行收到任务就干活。Manager 之间通过 Raft 协议保持一致这意味着如果有 3 个 Manager挂掉 1 个集群照样运转挂掉 2 个就歇菜了。所以生产环境 Manager 数量必须是奇数3 个是性价比最高的选择。这里有个关键点容易被忽略Manager 节点默认也可以跑任务。也就是说如果你只有 3 台机器全做成 Manager它们既管调度又跑容器。小集群这样没问题但如果 Manager 负载高了Raft 心跳可能延迟影响调度决策。我的做法是给 Manager 打上availabilitydrain标签让它们只做管理不跑业务容器业务全部调度到 Worker 上。Swarm 的调度粒度是 Service一个 Service 定义了你想要的状态——用哪个镜像、跑几个副本、暴露哪个端口、怎么更新。Swarm 会持续对比期望状态和实际状态不一致就自动纠正。这就是声明式编排的核心你告诉它你要什么它负责达成而不是你一步步指挥它怎么做。1.3 高可用的三个层次高可用这个词被用烂了但在 Swarm 集群里它其实对应三个不同层次的东西得分开看。第一层是服务高可用。同一个 Service 跑多个副本分散在不同节点上。一个容器挂了Swarm 在几秒内会在别的节点重新拉起一个。这解决的是单容器进程崩溃的问题。第二层是节点高可用。一台物理机或虚拟机宕机上面的容器全部消失Swarm 检测到节点失联后会把这些容器重新调度到健康节点上。这解决的是单机故障。第三层是入口高可用。所有外部流量打进来的入口不能是单点。Swarm 内置了 Routing Mesh路由网格每个节点都监听服务发布的端口流量可以在任意节点进入然后转发到实际运行容器的节点。这意味着你前面挂一个负载均衡器指向所有节点就行不需要关心容器具体在哪台机器上。这三层缺一不可。我见过有人只做了副本数 3但入口只有一个节点结果那个节点一挂整个服务就不可达了。所以设计的时候要把这三层都想清楚。2. 集群搭建前的准备工作与关键决策2.1 机器规划与网络设计我用了 5 台虚拟机配置如下表。这个规模对于中小型 Web 集群足够用了再大就得考虑更多 Manager 和网络分段。角色数量配置说明Manager32C4G只做管理不跑业务容器Worker24C8G跑 Web 服务和缓存负载均衡11C2G独立机器跑反向代理网络方面所有节点必须在同一个二层网络里能互相通信这是 Swarm 的硬性要求。Swarm 节点之间需要开放的端口包括2377集群管理、7946节点间通信TCP 和 UDP、4789VXLAN overlay 网络UDP。如果机器之间有防火墙这三个端口必须放行否则节点加不进来或者 overlay 网络不通。注意4789 端口是 UDP 的很多防火墙默认只配 TCP 规则这个坑我踩过表现是节点能加入集群但跨节点容器互相 ping 不通。主机名也要提前规划好因为 Swarm 用主机名标识节点。我统一用node-01到node-05这种命名方便在docker node ls里一眼看出是哪台。主机名改完记得改/etc/hosts确保每台机器都能解析所有节点的主机名这能避免很多莫名其妙的通信问题。2.2 Docker 安装与系统参数调优Docker 的安装本身不复杂但有几个系统参数必须调否则集群跑起来会出各种诡异问题。首先是vm.max_map_count默认值 65530 对于容器密集的场景偏低调到 262144。然后是文件描述符限制Docker daemon 和容器都需要大量的 fd我在/etc/security/limits.conf里把 nofile 调到 65536。还有net.ipv4.ip_forward1这是容器网络转发的基础必须开启。# 系统参数调优 cat /etc/sysctl.conf EOF vm.max_map_count 262144 net.ipv4.ip_forward 1 net.bridge.bridge-nf-call-iptables 1 EOF sysctl -p # 文件描述符限制 cat /etc/security/limits.conf EOF * soft nofile 65536 * hard nofile 65536 EOFDocker 的安装我建议用官方仓库而不是系统自带的版本系统自带的往往版本偏旧一些新特性用不了。安装完成后编辑/etc/docker/daemon.json配置日志驱动和存储驱动。日志驱动我用的json-file并限制了单个日志文件大小和数量不然容器跑久了日志能把磁盘撑爆。{ log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 }, storage-driver: overlay2, live-restore: true }live-restore这个参数值得说一下。开启后Docker daemon 重启时容器不会跟着停这对于需要升级 Docker 版本的场景非常有用能避免升级过程中的服务中断。2.3 初始化集群的正确姿势集群初始化在第一个 Manager 节点上执行。这里有个关键参数--advertise-addr必须指定为其他节点能访问到的 IP不能是 127.0.0.1否则其他节点加不进来。docker swarm init --advertise-addr 10.0.1.11 --default-addr-pool 10.20.0.0/16--default-addr-pool指定了 overlay 网络的地址池默认是 10.0.0.0/8这个范围可能和你的内网网段冲突。我就遇到过内网用了 10.0.x.x 网段结果 overlay 网络和物理网络打架容器跨节点通信直接失败。所以提前规划一个不冲突的地址池很重要我用的 10.20.0.0/16。初始化完成后会输出加入集群的命令包含 token。Manager 的加入 token 和 Worker 的加入 token 不一样别搞混了。token 可以用docker swarm join-token manager和docker swarm join-token worker随时查看。在另外两台 Manager 上执行加入命令注意要加--advertise-addr指定自己的 IP。三个 Manager 都加入后用docker node ls应该能看到三个节点都是 Ready 状态其中一个 Leader两个 Reachable。Worker 节点加入就简单了用 worker token 执行 join 命令即可。全部加入后我给 Manager 节点打上 drain 标签docker node update --availability drain node-01 docker node update --availability drain node-02 docker node update --availability drain node-03这样业务容器就只会调度到 Worker 节点上了。3. 核心服务的编排与部署实操3.1 用 Stack 文件定义整个应用Swarm 部署服务有两种方式docker service create一条条命令敲或者用docker stack deploy配合 compose 文件。生产环境我强烈建议用后者因为整个应用的状态都写在一个文件里可以版本控制可以 review可以一键重建。我用的 compose 文件版本是 3.8这是目前 Swarm 模式支持最完整的版本。整个文件定义了 Web 服务、缓存服务、网络和配置。下面我拆开讲每个部分的设计考量。version: 3.8 services: web: image: registry.internal/app:1.0.0 ports: - target: 8080 published: 80 protocol: tcp mode: ingress deploy: replicas: 4 update_config: parallelism: 1 delay: 10s order: start-first failure_action: rollback rollback_config: parallelism: 1 order: stop-first restart_policy: condition: on-failure delay: 5s max_attempts: 3 window: 120s placement: constraints: - node.role worker preferences: - spread: node.id resources: limits: cpus: 1.0 memory: 512M reservations: cpus: 0.25 memory: 128M healthcheck: test: [CMD, curl, -f, http://localhost:8080/health] interval: 15s timeout: 5s retries: 3 start_period: 30s networks: - app-net environment: - REDIS_HOSTcache - LOG_LEVELinfo3.2 副本数与调度策略的计算replicas: 4这个数字不是拍脑袋定的。我有 2 个 Worker 节点每个节点上跑 2 个 Web 容器。为什么不是 3 或 5因为 Swarm 的spread调度策略会尽量把副本均匀分布到各节点4 个副本在 2 个节点上正好每个 2 个。如果设成 3分布就是 21那个跑 1 个的节点资源利用率低而且一旦跑 2 个的节点挂了剩下 1 个节点要扛 3 个副本压力不均。副本数的下限是 2因为 1 个副本没有高可用可言。上限取决于节点资源我每个容器限制 1 核 512MWorker 是 4C8G理论上每个节点能跑 8 个但还要留资源给系统和其他服务所以 2 个是稳妥的。placement里的constraints确保容器只调度到 Worker 节点preferences里的spread: node.id让 Swarm 尽量均匀分布。这两个配合使用既限定了范围又保证了均衡。3.3 滚动更新的参数怎么调update_config是滚动更新的核心配置每个参数都影响更新时的服务可用性。parallelism: 1表示每次只更新 1 个副本。为什么不一次更新多个因为如果新版本有 bug一次更新多个会导致大面积故障。一次一个出问题影响面最小。delay: 10s是更新完一个副本后等 10 秒再更新下一个。这 10 秒是给新容器启动和健康检查用的。如果健康检查的start_period是 30 秒那 delay 至少要大于这个值否则新容器还没通过健康检查下一个更新就开始了可能导致所有旧容器都被替换掉而新容器还没就绪。order: start-first是关键。默认是stop-first意思是先停旧容器再起新容器这中间有一个副本不可用。改成start-first后先起新容器等它健康了再停旧容器实现零中断。代价是需要额外的资源来同时运行新旧容器。failure_action: rollback表示更新失败自动回滚。配合rollback_config使用回滚时用stop-first因为要尽快恢复到已知good状态。3.4 健康检查的设计细节健康检查看起来简单但设计不好会引发连锁问题。我的检查是curl -f http://localhost:8080/health这个/health端点需要应用自己实现返回 200 表示健康。interval: 15s是检查间隔timeout: 5s是单次检查超时retries: 3是连续失败 3 次才判定不健康。这意味着从容器出问题到被判定不健康最长需要 153 53 60 秒。这个时间窗口内负载均衡可能还在往这个容器转发流量所以应用本身要做好优雅关闭。start_period: 30s是启动宽限期这期间健康检查失败不计入 retries。对于启动慢的应用这个值要调大否则容器还没启动完就被判定不健康然后被杀掉陷入重启循环。实操心得健康检查端点不要检查太多依赖。我见过有人把数据库连接也放进健康检查结果数据库抖动一下所有 Web 容器全被判定不健康整个服务雪崩。健康检查只检查本进程能否正常响应即可依赖问题让应用自己处理。3.5 Overlay 网络与入口流量网络部分我定义了一个 overlay 网络app-netWeb 和缓存服务都接在这个网络上它们之间可以用服务名互相访问。Swarm 内置了 DNS 解析cache这个服务名会自动解析到缓存容器的 IP。入口流量走的是ingress模式的端口发布。published: 80表示所有 Swarm 节点都监听 80 端口流量从任意节点进入后通过 Routing Mesh 转发到实际运行容器的节点。这就是前面说的入口高可用。但这里有个性能问题Routing Mesh 的转发会经过一层 IPVS有一定的性能损耗。对于高并发场景可以用host模式发布端口这样端口只在实际运行容器的节点上监听流量不经过额外的转发。代价是负载均衡器需要知道哪些节点上有容器配置会复杂一些。我的场景并发不高用 ingress 模式足够了。负载均衡器我用的是独立的一台机器跑反向代理后端指向 3 个 Manager 节点的 80 端口。为什么指向 Manager 而不是 Worker因为 Routing Mesh 是所有节点都参与的Manager 也能转发流量。而且 Manager 通常负载低转发性能更稳定。4. 高可用验证与故障演练4.1 容器故障自愈验证集群搭好了不能只看docker service ls显示 4/4 就完事得实际验证故障自愈能力。我做的第一个测试是手动杀掉一个 Web 容器。在容器所在节点执行docker kill container-id然后立刻用watch docker service ps web观察。大概 3 到 5 秒后Swarm 检测到副本数不足开始在一个节点上拉起新容器。新容器启动到健康检查通过大约 20 秒这期间服务副本数是 3但因为有负载均衡用户无感知。这里有个细节Swarm 默认的restart_policy是condition: any意思是容器退出就重启。我改成了on-failure因为如果是正常退出exit code 0说明是计划内的停止不需要重启。配合max_attempts: 3和window: 120s如果容器在 120 秒内连续失败 3 次Swarm 就放弃重启把它标记为 Failed。这能防止一个必然崩溃的容器无限重启消耗资源。4.2 节点宕机演练容器故障是小打小闹节点宕机才是真正考验。我直接关掉了一台 Worker 节点shutdown -h now。Swarm 的节点失联检测大约需要 30 到 60 秒取决于心跳超时配置。失联后节点状态变成Down上面的容器被标记为Shutdown然后 Swarm 开始在健康节点上重新调度这些容器。因为我的副本数是 4分布在 2 个节点上各 2 个挂掉一个节点后剩下 2 个副本Swarm 会在健康节点上再拉起 2 个恢复到 4 个。整个过程从节点关机到副本数恢复大约花了 90 秒。这 90 秒内服务是可用的只是容量减半。如果流量大可能会变慢但不会中断。注意节点宕机后重新上线上面的容器不会自动恢复因为 Swarm 已经把副本调度到别处了。重新上线的节点是空的需要等下次调度或者手动 rebalance。可以用docker service update --force web强制重新平衡。4.3 滚动更新实战与回滚更新服务用docker service update --image registry.internal/app:1.0.1 web或者改 compose 文件后docker stack deploy。我更喜欢后者因为文件是版本化的。更新过程中用docker service ps web能看到新旧容器交替的过程。start-first策略下你会看到新容器先变成 Running然后旧容器才变成 Shutdown。整个过程服务副本数始终不低于 4。如果新版本有问题健康检查会失败failure_action: rollback触发自动回滚。我故意部署了一个健康检查会失败的版本测试观察到的行为是新容器启动后健康检查连续失败 3 次被判定不健康Swarm 停止更新并开始回滚把旧版本容器重新拉起来。整个过程大约 2 分钟服务始终可用。手动回滚用docker service rollback web它会回到上一个版本。但注意Swarm 只保留最近一次的历史版本再往前就回不去了。所以重要的版本变更前最好在镜像仓库里保留旧版本镜像。4.4 常见问题速查表下面这张表是我在实际运维中整理的问题排查速查表覆盖了大部分会遇到的坑。现象可能原因排查命令解决方法节点加入失败2377 端口不通telnet manager-ip 2377检查防火墙规则跨节点容器不通4789 UDP 未放行docker network inspect app-net放行 VXLAN 端口服务副本数上不去资源不足或约束不满足docker service ps web查看 Pending 原因更新卡住健康检查一直失败docker service logs web检查应用日志和健康端点入口 502容器未就绪或端口不对curl node-ip:80检查 published 端口和容器端口节点状态 Down节点失联或 daemon 挂了docker node ls检查节点网络和 Docker 服务overlay 网络冲突地址池与内网重叠docker network inspect重建集群指定新地址池这张表里的每一条我都是实际遇到过的。特别是 overlay 网络冲突那条当时排查了很久因为现象是部分节点通信正常部分不正常很迷惑。后来用docker network inspect看到 overlay 网段是 10.0.0.0/8而内网正好有 10.0.x.x 的机器路由冲突了。重建集群时指定--default-addr-pool 10.20.0.0/16后问题消失。5. 生产环境才懂的运维经验5.1 日志与监控的落地方式Swarm 环境下看日志比单机麻烦因为容器可能在任何节点上。docker service logs web能聚合所有副本的日志但只保留最近的而且容器重建后日志就没了。生产环境必须把日志收集到中心化存储。我的做法是每个节点跑一个日志收集容器挂载/var/lib/docker/containers目录把容器日志文件采集走。这样不管容器怎么调度日志都不会丢。应用日志统一输出到 stdout由 Docker 的 json-file 驱动写到磁盘再由采集器读取。监控方面Swarm 暴露了每个节点的 metrics 接口但更实用的是监控 Service 级别的状态。我写了一个简单的脚本定时docker service ls和docker node ls把副本数、节点状态推送到监控系统。副本数不等于期望值就告警节点状态不是 Ready 就告警。这两个指标能覆盖 90% 的异常情况。5.2 资源限制与预留的平衡resources里的limits和reservations是两个容易混淆的概念。limits是硬上限容器用超了会被限制CPU 被 throttle内存超了被 OOM Kill。reservations是调度依据Swarm 调度时会确保节点有足够的预留资源。我设的limits: cpus 1.0, memory 512Mreservations: cpus 0.25, memory 128M。这意味着调度时每个容器只占 0.25 核 128M 的额度但运行时最多能用 1 核 512M。这样一台 4C8G 的 Worker 理论上能调度 16 个容器按预留算但实际运行 4 个就比较满了。这个比例怎么定我的经验是 reservations 设为 limits 的 25% 到 50%。太低会导致节点超卖严重高峰期所有容器抢资源太高则资源利用率低。对于 Web 服务这种有波峰波谷的25% 到 30% 比较合适。实操心得内存的 limits 一定要设而且要比应用实际用量高 20% 到 30%。我见过没设内存限制的容器内存泄漏后把整个节点拖垮其他容器跟着遭殃。设了限制后最多是这个容器被杀不影响别人。5.3 配置与密钥的管理应用配置和敏感信息不要硬编码在镜像里也不要用环境变量明文写在 compose 文件里。Swarm 提供了 config 和 secret 两种对象。config 用于非敏感配置比如 nginx 配置文件、应用配置文件。secret 用于密码、密钥等敏感信息。它们都存储在 Swarm 的 Raft 日志里secret 还会加密存储并且只挂载到需要它的容器里其他容器看不到。echo my-password | docker secret create db_password - docker config create nginx_conf ./nginx.conf在 compose 文件里引用services: web: secrets: - db_password configs: - source: nginx_conf target: /etc/nginx/nginx.conf secrets: db_password: external: true configs: nginx_conf: external: truesecret 挂载到容器内的/run/secrets/db_password应用从这个文件读取。注意 secret 是挂载到内存文件系统的不会写到磁盘相对安全。5.4 备份与灾难恢复Swarm 集群的备份主要是备份 Raft 数据它包含了整个集群的状态——服务定义、网络、配置、密钥。备份方法是在 Manager 节点上执行docker swarm ca --rotate # 这个不是备份是轮换CA证书真正的备份是停掉 Docker daemon 后打包/var/lib/docker/swarm目录。但生产环境不能随便停 daemon所以更实际的做法是把 compose 文件、config 文件、secret 的创建脚本都纳入版本控制灾难时用这些文件重建集群。Raft 数据备份作为最后手段定期在维护窗口做。我个人的做法是所有编排文件放 Git 仓库secret 的值放密码管理器集群重建脚本化。这样即使整个集群没了也能在半小时内从零重建。5.5 版本升级的稳妥路径Docker 版本升级在 Swarm 集群里要一个一个节点来不能同时升。顺序是先升 Worker 再升 ManagerManager 里先升非 Leader 再升 Leader。升级前先docker node update --availability drain把节点上的容器迁走等容器都调度到别处后再停 Docker 升级。升级完--availability active恢复。因为有live-restore其实不 drain 也行但 drain 更稳妥。Manager 节点的升级要特别注意Raft 集群在升级期间要保持多数派可用。3 个 Manager 的情况下一次只能停 1 个停 2 个就失去多数派集群无法调度。所以升级 Manager 要一个一个来等一个完全恢复再加入下一个。6. 这套方案适合谁不适合谁6.1 什么场景下 Swarm 是甜点区用了大半年下来我觉得 Swarm 最适合这几类场景。第一是中小规模的 Web 服务副本数在 3 到 20 之间节点数在 3 到 10 之间。这个规模下 Swarm 的简洁性优势明显学习成本低运维负担小。第二是团队里没有专职容器平台工程师的情况。Swarm 的概念少命令直观后端开发自己就能改配置发版不需要专门学一套复杂的体系。第三是对稳定性要求高于对弹性要求的情况。Swarm 的调度策略相对保守不会频繁迁移容器适合那些启动慢、迁移成本高的应用。6.2 什么时候该考虑更重的方案如果出现下面这些信号说明 Swarm 可能不够用了该考虑更强大的编排系统。需要精细的自动扩缩容根据 CPU、内存、自定义指标自动调整副本数。Swarm 不支持自动扩缩容只能手动改副本数或者写脚本。需要复杂的发布策略比如金丝雀发布、蓝绿发布、A/B 测试。Swarm 只有滚动更新做不了流量切分。需要多租户隔离不同团队共享集群但资源要严格隔离。Swarm 的命名空间和权限控制比较弱。需要丰富的生态集成比如服务网格、Operator、CRD。这些是 K8s 的强项Swarm 基本没有。我的建议是先用 Swarm等它真的成为瓶颈了再迁移。不要为了以后可能用到而提前上复杂的方案那只会增加当下的负担。迁移的成本没有想象中那么高因为容器镜像、compose 文件这些资产是通用的迁移主要是改编排层的定义。6.3 我踩过的几个印象深刻的坑最后分享几个具体的坑都是文档里不会写但实际会遇到的。第一个是时间同步。Swarm 的 Raft 协议对时间敏感节点之间时间差太大可能导致选举异常。我遇到过一台机器 NTP 没配好时间差了 5 分钟结果它一直无法成为 Leader集群偶尔抖动。所有节点必须配 NTP 同步。第二个是磁盘空间。Docker 的镜像和日志很占空间特别是频繁更新时旧镜像不会自动删除。我写了个定时任务清理 dangling 镜像和停止的容器docker system prune要慎用会删掉所有未使用的资源包括你可能还需要的数据卷。第三个是 DNS 解析。Swarm 内置的 DNS 在容器重启后 IP 会变应用如果缓存了 IP 就会连不上。正确做法是每次连接都重新解析服务名或者用连接池并设置合理的过期时间。这个坑在缓存服务上特别明显Web 容器缓存了缓存容器的 IP缓存容器一重启Web 就连不上了。第四个是健康检查的副作用。健康检查本身会消耗资源如果检查太频繁或者检查逻辑太重会给应用增加额外负担。我一开始把 interval 设成 5 秒结果健康检查的请求占了总请求量的 10%。后来调到 15 秒就合理了。这套集群到现在跑了快一年经历过节点宕机、容器崩溃、版本回滚服务可用性保持在 99.9% 以上。对于这个体量的项目来说Swarm 交出了一份合格的答卷。如果你也在纠结选型希望这篇实操记录能帮你少走点弯路。