
1. 为什么我最终选了Docker Swarm而不是K8s1.1 一个真实项目的选型纠结去年下半年我接手了一个内部工具平台的改造任务。背景很简单原来那套东西跑在一台单机Docker上用docker-compose编排了五六个服务包括前端静态资源、两个后端API、一个定时任务worker、Redis和PostgreSQL。平时访问量不大一天也就几千次请求但问题是——只要那台机器出问题整个平台就挂了。更尴尬的是每次发版都要停服几十秒用户那边直接报错。需求很明确要搞成高可用集群至少三节点起步能滚动更新某个节点挂了服务不能断。预算方面公司给的是三台4核8G的云主机没有多余的资源去搞一套重型编排系统。摆在面前的选择其实就两个Kubernetes和Docker Swarm。K8s当然是行业标准生态完善但问题是——三台4核8G的机器跑K8s控制平面加工作负载资源开销太大了。etcd、apiserver、controller-manager、scheduler这些组件本身就要吃掉不少CPU和内存再加上CNI插件、Ingress Controller、监控组件留给业务容器的余量很紧张。而且团队里没有人真正在生产环境维护过K8s学习成本和运维成本都摆在那里。Docker Swarm就不一样了。它内置在Docker Engine里不需要额外安装控制平面组件manager节点和worker节点都是同一个Docker守护进程。资源开销小配置简单滚动更新、服务发现、负载均衡这些核心功能都有。虽然社区活跃度不如K8s但对于我们这种规模的项目来说够用且好用。最终我选了Swarm。跑了大半年中间经历过节点宕机、网络抖动、镜像更新失败等各种情况整体表现稳定。下面我把整个搭建过程、踩过的坑、以及一些实操经验完整地分享出来。1.2 Swarm和K8s的核心差异对比在动手之前有必要把两者的关键差异说清楚这样你在选型时能有个清晰的判断依据。对比维度Docker SwarmKubernetes安装复杂度内置在Docker中初始化一条命令需要单独部署控制平面组件多资源开销极低manager节点几乎不占额外资源较高控制平面本身就要消耗资源学习曲线平缓会Docker基本就会Swarm陡峭概念多配置复杂服务发现内置DNS轮询开箱即用需要CoreDNS配置相对复杂负载均衡内置ingress mesh自动分发需要Ingress Controller或LoadBalancer滚动更新原生支持参数直观支持但配置项更多存储卷支持本地卷和第三方驱动存储方案更丰富但配置复杂生态与社区相对小众但核心功能稳定生态庞大几乎什么都有适用规模中小规模集群几十个节点以内大规模集群上百节点也扛得住这张表不是要分出谁好谁坏而是帮你判断哪个更适合当前场景。如果你的团队规模不大、机器资源有限、又不想在编排系统上花太多精力Swarm是性价比很高的选择。反过来如果你需要精细的调度策略、丰富的生态工具、或者团队已经有K8s经验那直接上K8s更合适。1.3 高可用Web集群的核心诉求拆解回到项目本身“高可用Web集群”这个目标拆开来看其实包含了好几层意思。第一层是节点高可用。三台机器里任何一台挂了服务不能中断。这意味着每个服务至少要有两个副本分布在不同的节点上。Swarm的副本调度策略默认就是尽量分散但你需要确认它确实做到了。第二层是更新高可用。发版的时候不能停服用户无感知。Swarm的滚动更新功能可以做到这一点但需要合理配置更新间隔、并行度、失败回滚策略。第三层是入口高可用。用户访问的入口不能是单点。Swarm内置的routing mesh可以让任意节点的端口都接收请求并转发到健康的容器但如果你在前面加了Nginx或者HAProxy做反向代理那这个代理层本身也要考虑高可用。第四层是数据高可用。数据库和缓存不能因为节点故障丢数据。这部分Swarm本身不直接解决需要靠数据库自身的主从复制或者外部存储方案。把这四层想清楚之后整个架构的设计思路就清晰了。2. 集群搭建前的准备工作与节点规划2.1 机器规格与角色分配三台云主机的配置是一样的4核CPU、8G内存、100G SSD、Ubuntu 22.04 LTS。网络方面三台机器在同一个VPC内内网互通没有带宽瓶颈。角色分配上我让三台都作为manager节点。为什么不搞一主两从因为Swarm的manager节点之间通过Raft协议选举leader只要超过半数节点存活集群就能正常工作。三台manager意味着可以容忍一台故障。如果搞一主两从那两台worker不参与Raft投票主节点挂了整个集群就失去管理能力了。当然三台manager的代价是Raft共识写入需要多数节点确认理论上写入性能会比单manager略低。但对于我们这种管理操作频率很低的场景这点开销完全可以忽略。提示生产环境如果节点数量更多建议manager节点用奇数个3、5、7worker节点可以任意多。manager节点不要跑业务负载或者至少给业务容器设置资源限制避免管理组件被挤占。2.2 系统层面的初始化配置拿到机器之后有几项系统配置必须先做否则后面会出各种奇怪的问题。主机名设置。三台机器分别设成node-01、node-02、node-03方便识别。用hostnamectl set-hostname命令搞定然后记得在/etc/hosts里把三台机器的内网IP和主机名对应写上这样节点之间可以通过主机名互相访问。时间同步。集群节点之间的时间必须一致否则Raft协议会出问题。Ubuntu默认带了systemd-timesyncd确认一下服务是运行的就行。如果对时间精度要求高可以装chrony。内核参数调整。Docker Swarm需要一些内核模块和参数支持。主要是确保br_netfilter模块加载了net.bridge.bridge-nf-call-iptables和net.bridge.bridge-nf-call-ip6tables设成1。这些在安装Docker的时候通常会自动处理但手动确认一下更稳妥。防火墙规则。Swarm节点之间需要通信的端口不少包括2377集群管理、7946节点间通信TCP和UDP、4789overlay网络UDP。如果开了ufw或者iptables这些端口必须放行。我一开始就是忘了放行4789导致overlay网络死活不通排查了半天。Docker安装。用官方脚本安装最新稳定版就行。安装完之后确认docker info能正常输出并且Docker服务是开机自启的。2.3 初始化Swarm集群的完整命令准备工作做完之后在node-01上执行初始化命令docker swarm init --advertise-addr 10.0.1.10这里的10.0.1.10是node-01的内网IP。--advertise-addr参数很重要它告诉其他节点通过哪个地址来访问这个manager。如果不指定Docker可能会选错网卡导致其他节点连不上。执行成功后会输出一段join命令类似这样docker swarm join --token SWMTKN-1-xxxxx 10.0.1.10:2377把这条命令分别在其他两台机器上执行它们就以worker身份加入了。但我们要的是三manager所以还需要在node-01上执行docker swarm join-token manager这会输出manager的join token用这个token在node-02和node-03上重新加入。注意如果节点已经以worker身份加入了需要先docker swarm leave再用manager token重新join。三台都加入之后在任意一台manager上执行docker node ls应该能看到三个节点都是Ready状态并且都是Manager。注意manager节点的Raft共识要求多数派存活。三节点集群的quorum是2也就是说最多容忍一台manager故障。如果两台同时挂了集群就失去管理能力虽然已运行的服务不会立刻停止但你无法执行任何管理操作。所以三节点是最低配置有条件的话上五节点更稳。3. 服务编排与高可用架构设计3.1 整体架构分层说明整个平台的架构分成四层从外到内依次是入口层、应用层、缓存层、数据层。入口层负责接收用户请求做SSL终止和反向代理。我用了Traefik作为边缘路由器它原生支持Swarm的服务发现能自动感知后端服务的增减。Traefik本身以global模式部署也就是每个节点上都跑一个实例配合云厂商的负载均衡或者DNS轮询实现入口层的高可用。应用层就是那几个业务服务包括前端静态资源服务、两个后端API服务、一个定时任务worker。每个服务都设置至少两个副本通过Swarm的overlay网络互相通信。缓存层是Redis用主从模式部署一个master两个replica分别调度到不同节点上。Redis本身不是Swarm原生的集群方案但通过Swarm的placement constraints可以把不同角色固定到不同节点。数据层是PostgreSQL同样用主从流复制主库和从库分布在不同节点。数据持久化用本地卷加定期备份没有上分布式存储因为数据量不大本地SSD的性能和可靠性够用。3.2 用Stack文件定义整套服务Swarm的stack deploy命令支持compose文件格式这是它比K8s友好的地方之一——你不需要学一套全新的YAML规范会写docker-compose.yml基本就能上手。下面是我实际使用的stack文件的核心结构做了简化但保留了关键配置version: 3.8 services: traefik: image: traefik:v2.10 ports: - target: 80 published: 80 mode: host - target: 443 published: 443 mode: host volumes: - /var/run/docker.sock:/var/run/docker.sock:ro deploy: mode: global placement: constraints: - node.role manager update_config: parallelism: 1 delay: 10s order: start-first api: image: registry.example.com/api:latest deploy: replicas: 3 update_config: parallelism: 1 delay: 15s order: start-first failure_action: rollback rollback_config: parallelism: 1 delay: 10s restart_policy: condition: on-failure delay: 5s max_attempts: 3 window: 120s resources: limits: cpus: 1.0 memory: 1G reservations: cpus: 0.25 memory: 256M networks: - app-net healthcheck: test: [CMD, curl, -f, http://localhost:8080/health] interval: 15s timeout: 5s retries: 3 start_period: 30s这里有几个关键点值得展开说。update_config的order参数。默认是stop-first也就是先停旧容器再起新容器这会导致短暂的服务中断。改成start-first之后Swarm会先启动新容器等它健康检查通过后再停掉旧容器实现真正的零停机更新。healthcheck的重要性。没有健康检查的话Swarm只能判断容器进程是否在运行无法知道服务是否真正可用。加上healthcheck之后滚动更新会等新容器健康后才继续负载均衡也会自动摘除不健康的容器。resources的limits和reservations。limits是硬限制超过会被OOM kill或者CPU限流。reservations是调度依据Swarm会根据这个值来决定把容器放到哪个节点。两个都设上既能防止单个服务吃光资源又能让调度更合理。3.3 滚动更新策略的参数计算滚动更新的几个参数需要根据实际情况算一下不能随便填。假设api服务有3个副本update_config里parallelism设为1delay设为15shealthcheck的interval是15s、retries是3、start_period是30s。那么一个新容器从启动到被判定为健康最坏情况需要30sstart_period 15s×3retries 75s。加上delay的15s每更新一个副本大约需要90s。3个副本全部更新完大约需要270s也就是4分半钟。这个时间可以接受。如果服务启动更快可以把start_period和delay调小。如果服务启动很慢比如需要预热缓存那start_period要相应加大。failure_action设为rollback意味着如果更新过程中有新容器启动失败Swarm会自动回滚到上一个版本。这个功能非常实用但要注意回滚本身也是一次滚动更新也需要时间。所以整个更新窗口要留足。实操心得我第一次配滚动更新的时候把parallelism设成了2结果两个新容器同时启动同时连数据库数据库连接池瞬间被打满两个新容器都没通过健康检查触发了回滚。后来改成parallelism: 1就稳了。所以并行度不是越大越好要考虑后端依赖的承受能力。4. 网络、存储与数据持久化实操4.1 Overlay网络的配置与排错Swarm的overlay网络是跨节点通信的基础。创建一个overlay网络很简单docker network create --driver overlay --attachable app-net--attachable参数允许独立的容器不是service也接入这个网络方便调试。overlay网络的底层原理是VXLAN隧道。每个节点上有一个或多个VXLAN隧道端点容器发出的数据包被封装在UDP包里通过物理网络传输到目标节点再解封装交给目标容器。这个过程对应用是透明的但会带来一定的网络开销。实测下来overlay网络的吞吐量大约是物理网络的70%到80%延迟增加0.5ms到1ms左右。对于大多数Web应用来说这个开销可以接受。如果overlay网络出问题排查顺序是这样的先确认4789/UDP端口是否放行再确认节点的--advertise-addr和--listen-addr是否正确然后用docker network inspect查看网络的详细状态最后可以在容器里用ping和tcpdump抓包分析。我遇到过一次overlay网络不通的情况原因是某台节点的内网IP变了云主机重启后DHCP重新分配但Swarm还记着旧IP。解决办法是在那台节点上执行docker swarm leave然后在manager上docker node rm掉它再重新join。4.2 数据持久化的三种方案对比Swarm本身对存储的支持比较基础主要有三种方案方案原理优点缺点适用场景本地卷数据存在节点本地磁盘性能最好配置简单容器漂移到其他节点后数据不可用单节点固定调度的服务绑定挂载挂载宿主机目录灵活可直接管理同样有节点绑定问题配置文件、日志目录第三方卷驱动通过插件对接外部存储数据可跨节点访问需要额外部署存储系统需要漂移的有状态服务我的选择是数据库和Redis用本地卷加placement constraints固定到特定节点通过主从复制保证数据安全。应用服务的日志用绑定挂载写到宿主机方便排查。配置文件用Docker Config这是Swarm原生的配置管理功能比挂载文件更安全。Docker Config的用法很简单echo config_content | docker config create my_config -然后在stack文件里引用services: api: configs: - source: my_config target: /app/config.yml mode: 0440Config内容会被加密存储在Raft日志里只有被授权的服务才能访问。更新Config需要创建新的Config对象然后更新服务引用这比直接改文件要规范得多。4.3 数据库主从复制的Swarm化部署PostgreSQL的主从复制在Swarm里部署核心思路是用placement constraints把主库和从库固定到不同节点用本地卷持久化数据用healthcheck监控复制状态。主库的stack配置大概是这样services: postgres-primary: image: postgres:15 volumes: - pg-primary-data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD_FILE: /run/secrets/pg_password POSTGRES_INITDB_ARGS: --data-checksums deploy: replicas: 1 placement: constraints: - node.labels.role database - node.labels.db-role primary restart_policy: condition: any secrets: - pg_password从库类似但db-role标签设为replica并且启动命令里要配置primary_conninfo指向主库。节点标签用docker node update --label-add命令添加docker node update --label-add roledatabase node-02 docker node update --label-add db-roleprimary node-02 docker node update --label-add roledatabase node-03 docker node update --label-add db-rolereplica node-03这样主库永远调度到node-02从库永远调度到node-03。如果node-02挂了主库容器不会漂移到其他节点因为没有其他节点满足标签条件但数据还在本地卷里。这时候需要手动介入把从库提升为主库或者修复node-02后重新拉起主库。注意Swarm不会自动做数据库故障转移这跟K8s的StatefulSet加Operator不是一回事。有状态服务的故障转移需要额外的逻辑要么用Patroni这类工具要么人工介入。对于数据量不大、可用性要求不是极高的场景人工介入是可以接受的。5. 常见故障排查与避坑经验5.1 服务无法启动的排查路径服务起不来是最常见的问题排查思路要系统化不能瞎试。第一步看服务状态docker service ps service_name --no-trunc。这个命令会显示服务的所有任务task及其状态包括失败原因。--no-trunc参数很重要不然错误信息会被截断。第二步看容器日志docker service logs service_name。如果容器根本没起来日志可能是空的这时候要看任务级别的日志。第三步看节点状态docker node ls和docker node inspect node_id。确认节点是Ready状态没有被标记为Drain或Down。第四步看资源情况docker node inspect node_id --format {{.Description.Resources}}。确认节点有足够的CPU和内存来满足服务的reservations。常见的失败原因有这么几类镜像拉取失败私有仓库认证问题、端口冲突host模式端口被占用、资源不足reservations无法满足、健康检查失败服务启动太慢或配置错误、挂载卷权限问题容器内用户没有写权限。我遇到最多的是镜像拉取失败。私有仓库需要登录但Swarm的节点不会自动继承你本地的docker login凭证。解决办法是在每个节点上单独执行docker login或者用docker stack deploy的--with-registry-auth参数把认证信息传递给所有节点。5.2 节点宕机后的服务恢复实录有一次node-03因为云厂商的宿主机故障宕机了我记录了整个恢复过程。宕机发生后大约30秒Swarm的manager节点检测到node-03失联把它标记为Down。node-03上的容器被重新调度到node-01和node-02。由于每个服务都有至少两个副本而且分布在不同节点所以服务本身没有中断只是容量暂时下降。但是有两个问题第一node-03上的PostgreSQL从库停了主库还在正常运行但复制中断了。第二node-03上的本地卷数据暂时不可访问虽然数据还在磁盘上但节点不在了就没法读。大约10分钟后云厂商修复了宿主机node-03重新上线。Swarm自动把它标记为Ready但不会自动把之前漂移走的容器迁回来。这时候需要手动执行docker service update --force service_name来触发重新调度或者等下一次滚动更新时自然平衡。PostgreSQL从库需要手动重新启动并且要确认它从主库同步了宕机期间的数据。因为配置了WAL归档从库启动后会自动追赶不需要人工干预。这次故障让我意识到几个事情副本数不能只设2因为一台挂了就只剩1个副本没有冗余了。后来我把关键服务的副本数调到了3。另外节点的健康检查间隔可以调短一些默认的检测时间有点长。5.3 滚动更新失败的常见原因速查表现象可能原因排查方法解决方案更新卡在pending资源不足新任务无法调度docker service ps查看任务状态减少reservations或增加节点新容器反复重启健康检查失败docker service logs查看日志调整healthcheck参数或修复应用更新后服务不可用端口冲突或网络配置错误docker network inspect检查网络修正端口映射或网络配置回滚也失败旧版本镜像被删除docker image ls确认镜像存在保留旧版本镜像或使用固定tag更新速度极慢parallelism太小或delay太大检查update_config参数适当增大parallelism或减小delay部分节点更新成功部分失败节点间镜像不一致在各节点执行docker images统一镜像仓库或预拉取镜像这张表是我在实际运维中总结出来的基本上覆盖了80%的滚动更新问题。每次更新前我都会先在小范围比如一个副本上验证确认没问题再全量更新。实操心得永远不要用latest标签。每次构建镜像都用具体的版本号或者git commit hash作为tag。这样回滚的时候能精确回到上一个版本不会出现“回滚了但不知道回滚到哪个版本”的情况。另外旧版本的镜像不要急着删至少保留最近三个版本给回滚留足余地。6. 监控、日志与日常运维要点6.1 轻量级监控方案的搭建Swarm本身提供了一些基本的监控信息docker service ls能看到副本状态docker node ls能看到节点状态但这些不够。我需要知道每个容器的CPU、内存、网络使用情况以及服务的响应延迟和错误率。完整的Prometheus加Grafana方案当然好但对于三台机器来说有点重。我选了一个折中方案用cAdvisor采集容器指标用node_exporter采集节点指标Prometheus负责存储Grafana负责展示。这套组合的资源开销不大每个节点上cAdvisor加node_exporter大概占200M内存左右。部署方式上cAdvisor和node_exporter都用global模式确保每个节点都有一个实例。Prometheus和Grafana用单副本加placement constraints固定到node-01。关键配置是Prometheus的抓取目标。因为Swarm的服务发现是动态的Prometheus需要配置dockerswarm_sd_configs来自动发现服务。这个配置稍微有点复杂但配好之后就能自动感知服务的增减不需要手动改配置文件。除了基础设施监控应用层面我也加了健康检查端点和基本的指标暴露。每个API服务都暴露一个/metrics端点返回请求数、错误数、响应时间等指标。Prometheus抓取这些指标Grafana里配了几个关键的告警规则错误率超过1%持续5分钟告警P99延迟超过500ms持续5分钟告警容器重启次数超过3次告警。6.2 日志收集与集中查看Swarm的docker service logs命令可以看服务日志但它有几个限制只能看当前存在的容器的日志容器被删除后日志就没了日志量大时输出很慢不支持搜索和过滤。我的方案是用Loki做日志聚合。Loki是Grafana Labs出的日志系统跟Prometheus的设计理念类似只索引标签不索引内容资源开销比ELK小很多。每个节点上跑一个Promtail负责采集容器日志并推送到Loki。Loki和Grafana部署在一起在Grafana里可以直接用LogQL查询日志。日志的标签设计很重要。我给每个服务打了service_name、node_id、container_id三个标签。查询的时候可以按服务名过滤也可以按节点过滤定位问题很方便。日志保留策略上我设了30天的保留期。超过30天的日志自动删除。对于审计相关的日志单独配置了更长的保留期但那些日志量不大存储成本可以接受。6.3 日常巡检清单与自动化脚本日常运维不能靠人盯着我写了一个巡检脚本每天早上跑一次把关键指标输出到文件有异常就发通知。脚本检查的内容包括节点状态是否都是Ready服务副本数是否达到期望值容器重启次数是否异常磁盘使用率是否超过80%证书是否临近过期备份是否成功完成。#!/bin/bash # swarm_health_check.sh echo Node Status docker node ls --format {{.Hostname}}\t{{.Status}}\t{{.Availability}} echo Service Replicas docker service ls --format {{.Name}}\t{{.Replicas}} echo Container Restart Counts for node in $(docker node ls -q); do docker node ps $node --format {{.Name}}\t{{.CurrentState}}\t{{.Error}} | grep -v Running done echo Disk Usage df -h / | tail -1 echo Certificate Expiry echo | openssl s_client -connect localhost:443 2/dev/null | openssl x509 -noout -dates这个脚本很粗糙但够用。关键是坚持每天看发现异常及时处理不要等小问题变成大故障。实操心得巡检脚本的输出不要只看有没有报错还要看趋势。比如磁盘使用率从60%慢慢涨到75%虽然还没到告警阈值但按照这个速度两周后就会满。提前清理日志或者扩容比等到磁盘满了再处理要从容得多。7. 性能调优与资源限制的实战参数7.1 CPU和内存限制的合理设置Swarm的resources配置里limits和reservations的设置有讲究。limits是硬上限超过会被限制或杀掉。CPU的limit用cpus表示比如1.0表示一个核心。内存的limit用memory表示比如1G。reservations是调度预留Swarm在调度时会确保节点有足够的reservations资源。我的设置原则是reservations设为正常负载的50%左右limits设为正常负载的150%到200%。比如一个API服务正常跑起来占0.3个CPU、300M内存那reservations设0.25 CPU、256M内存limits设1.0 CPU、1G内存。这样设置的好处是调度时按reservations算三台机器能塞下更多服务运行时按limits限制防止某个服务异常时吃光节点资源。但要注意内存的limit如果设得太低容器会被OOM kill。Java应用尤其要注意JVM的堆内存要留出足够的余量给堆外内存和元空间。我一般给Java容器的内存limit是JVM最大堆的1.5倍。7.2 副本数与调度策略的平衡副本数不是越多越好。副本多了资源占用成倍增加而且服务间的协调开销也会变大。副本少了高可用性又不够。我的经验是无状态服务至少3个副本这样能容忍一个节点故障同时滚动更新时还有2个副本在服务。有状态服务根据数据量和一致性要求来定数据库一般1主2从Redis一般1主2从。调度策略上Swarm默认的spread策略会把副本尽量分散到不同节点。但如果你有特殊的节点标签比如某些节点是SSD、某些是HDD可以用placement preferences来引导调度。deploy: placement: preferences: - spread: node.labels.disk-type这个配置会让副本尽量分散到不同disk-type的节点上。不过Swarm的preferences是软约束不保证一定满足。硬约束用constraints但constraints太严格会导致副本无法调度。7.3 网络性能优化的几个关键参数Overlay网络的性能可以通过几个参数来优化。第一个是--opt encryptedfalse。默认情况下overlay网络的流量是不加密的如果你手动开启了加密会有额外的CPU开销。内网环境且安全要求不高的话保持默认不加密就行。第二个是MTU设置。VXLAN封装会增加50字节的开销所以overlay网络的MTU应该比物理网络小50。如果物理网络MTU是1500overlay网络应该设成1450。MTU不匹配会导致大包分片性能下降。docker network create --driver overlay --opt com.docker.network.driver.mtu1450 app-net第三个是避免不必要的端口发布。用ingress模式发布端口时所有节点都会监听该端口并做负载均衡。如果只需要特定节点接收流量用host模式发布端口减少ingress mesh的开销。这些参数调整之后实测API的P99延迟从45ms降到了32ms吞吐量提升了大约20%。对于延迟敏感的应用这些优化值得做。8. 从单机到集群的迁移路径与回退方案8.1 迁移前的兼容性检查清单从单机docker-compose迁移到Swarm集群不是把文件改个格式就完事了。有几项兼容性必须提前确认。第一检查所有服务的端口映射。单机环境下用ports: 8080:8080没问题Swarm里如果多个副本在同一节点端口会冲突。要么改成ingress模式让Swarm自动分配要么用host模式加placement constraints确保每个节点只有一个副本。第二检查卷挂载。单机环境下的相对路径挂载在Swarm里可能找不到因为工作目录不一样。全部改成绝对路径或者用Docker Config/Secret。第三检查环境变量。单机环境下.env文件很方便Swarm里需要用docker stack deploy --env-file或者把变量写进stack文件。第四检查依赖关系。docker-compose的depends_on在Swarm里不生效因为Swarm是最终一致性模型不保证启动顺序。需要用healthcheck和重试逻辑来替代。第五检查网络模式。单机环境默认的bridge网络在Swarm里不可用必须用overlay网络。我当初迁移的时候就是忘了检查端口映射结果api服务起了3个副本但只有1个能起来另外2个因为端口冲突一直重启。后来改成ingress模式才解决。8.2 灰度迁移的实操步骤迁移不能一刀切要灰度进行。第一步先在Swarm集群上部署一套完整的staging环境用同样的stack文件但服务名加个-staging后缀端口用不同的。验证所有服务能正常启动和通信。第二步把生产流量的一小部分比如5%切到staging环境观察一段时间。这一步需要入口层支持按权重分流Traefik的Weighted Round Robin功能可以做到。第三步逐步增加流量比例10%、25%、50%、100%。每增加一次观察至少30分钟确认错误率和延迟没有异常。第四步全量切换后保持旧环境运行至少24小时作为回退方案。确认新环境稳定后再下线旧环境。整个迁移过程花了我大约一周时间其中大部分时间是在观察和验证。虽然慢但稳。生产环境的迁移宁可慢一点也不要出事故。8.3 回退方案的设计与演练回退方案不能只是写在文档里必须实际演练过。我的回退方案分两级。第一级是服务级回退如果某个服务的新版本有问题用docker service rollback service_name回退到上一个版本。这个操作很快通常一两分钟就能完成。第二级是环境级回退如果整个Swarm集群有问题把流量切回旧的单机环境。旧环境一直保持着热备状态数据库通过逻辑复制保持同步。切换只需要改一下DNS或者负载均衡的配置几分钟就能完成。回退演练我每季度做一次。演练内容包括模拟服务更新失败触发自动回滚模拟节点宕机触发服务漂移模拟整个集群不可用触发环境切换。每次演练都会发现一些问题比如回滚后配置没有同步、DNS缓存导致切换延迟等。这些问题在真正出事之前发现并解决比出事时手忙脚乱要好得多。实操心得回退方案的关键不是技术而是决策流程。谁有权决定回退回退的触发条件是什么回退后谁来验证这些问题要在平时就明确下来写成checklist。出事的时候人是慌的照着checklist做比临时想要靠谱得多。9. 我在这套集群上踩过的坑与最终体会9.1 那些让我半夜爬起来处理的故障第一个坑是Raft日志膨胀。Swarm的manager节点会把所有集群状态变更记录在Raft日志里时间长了日志会越来越大。默认情况下Docker不会自动清理旧的Raft日志导致磁盘慢慢被占满。我遇到过一次磁盘使用率到95%告警排查发现是Raft日志占了30多G。解决办法是定期执行docker system prune和手动清理/var/lib/docker/swarm/raft目录下的旧快照。后来我加了一个定时任务每周清理一次。第二个坑是overlay网络的ARP缓存问题。容器频繁创建删除后节点上的ARP缓存会积累大量失效条目导致网络性能下降。这个问题比较隐蔽表现为服务间调用偶尔超时但重启容器就好了。后来我在节点上加了定时清理ARP缓存的脚本问题就没再出现过。第三个坑是镜像仓库的速率限制。我们用的公共镜像仓库对匿名拉取有速率限制滚动更新时多个节点同时拉镜像很容易触发限制导致拉取失败。解决办法是搭建内网镜像仓库做缓存所有节点从内网仓库拉镜像既快又不受限制。9.2 如果重新来过我会怎么调整如果让我重新搭一遍这套集群有几个地方我会做得不一样。第一manager节点和worker节点分开。虽然三台都是manager在资源上没问题但管理操作和业务负载混在一起排查问题时不够清晰。如果资源允许我会用两台低配机器做manager三台高配机器做worker。第二一开始就上Traefik或者Nginx Ingress而不是用Swarm的ingress模式发布端口。Swarm的ingress mesh虽然方便但功能有限不支持路径路由、SSL终止、限流等高级功能。后来我加Traefik的时候又花时间调整了端口发布方式。第三监控和日志从第一天就搭好。我一开始觉得集群规模小不需要监控结果出了问题只能靠猜。后来补上监控之后排查效率提升了很多。监控不是等规模大了才需要而是从第一天就需要。第四把所有的配置都代码化。包括stack文件、节点标签、网络配置、密钥管理全部放在Git仓库里。这样重建集群的时候只需要拉代码、执行脚本不需要凭记忆手动操作。我后来把整个集群的搭建过程写成了Ansible playbook重建一次只需要20分钟。9.3 这套方案适合什么规模的团队最后说一下适用边界。这套基于Docker Swarm的高可用Web集群方案适合的是这样的场景团队规模在10人以内没有专职的运维或平台工程师机器数量在3到10台之间服务数量在5到20个之间对可用性有要求但不需要99.99%那种级别。如果你的团队更大、服务更多、可用性要求更高那K8s或者云厂商的托管容器服务可能是更好的选择。Swarm的优势在于简单但简单也意味着功能有限。当你的需求超出Swarm的能力范围时迁移到K8s是必然的。但就我个人的体验来说在合适的场景下Swarm的性价比非常高。它让我用很少的精力维护了一套稳定运行的高可用集群把更多时间花在了业务开发上。技术选型没有绝对的对错只有适不适合。想清楚自己的需求选一个能解决问题的方案然后把它用好比盲目追求新技术要实在得多。