1. 这不是“又一个Docker教程”,而是你真正用得上的Swarm生产级部署手记
我带过三届某高校实验室的容器化实践课,也帮五家中小公司做过从单机Docker到集群编排的落地改造。每次讲到Swarm,总有人在课后追着问:“老师,Swarm到底还活着吗?K8s都卷成这样了,学它是不是白费劲?”——这个问题我听了一百遍,但每次回答都更笃定:Swarm没死,它只是换了一种活法。它不争云原生生态的“标准制定权”,却牢牢守住了“最小可行集群”的交付底线。你不需要动辄几十个节点、上百个微服务的复杂架构,只需要三台普通云服务器(哪怕配置是2核4G),就能跑起一个自带负载均衡、服务发现、滚动更新、故障自愈能力的生产就绪型集群。这正是Swarm不可替代的价值:用最轻的抽象、最少的组件、最低的学习成本,把“多台机器当一台用”这件事,做成了开箱即用的事实标准。关键词“Docker深入浅出系列”不是营销话术,而是我们整个内容设计的底层逻辑——所有操作都基于原生Docker CLI,不引入kubectl、helm、istio等额外概念;所有配置都写在docker-compose.yml里,不碰YAML嵌套地狱;所有状态都通过docker node ls、docker service ps一眼看清,不依赖Prometheus+Grafana的复杂可观测栈。它适合谁?适合刚跑通单机Docker的运维新手,适合要快速交付内部管理系统的开发组长,适合预算有限但又不愿在单点故障上赌运气的创业技术负责人。这不是教你怎么造火箭,而是给你一把能立刻拧紧生产环境螺丝的扳手。
2. Swarm不是K8s的简化版,它是另一种工程哲学的具象化
2.1 为什么选Swarm?三个被低估的硬核优势
很多人把Swarm和K8s简单类比为“简配版vs顶配版”,这是对工程选型最大的误读。Swarm的设计哲学,本质上是“控制面极简,数据面可靠”。它的控制面(manager节点)只做三件事:接收服务定义、维护集群状态、下发任务调度。没有etcd、没有API Server、没有Controller Manager的复杂协调环路。所有状态变更最终都收敛到一个轻量级的Raft共识日志上,而这个日志就存在manager节点本地的/var/lib/docker/swarm/目录里——你可以用ls -la直接看到它,用cat命令打开看里面存的JSON快照。这种设计带来三个实打实的优势:
第一,启动与恢复速度碾压级领先。我在某电商公司的压测中对比过:一个5节点Swarm集群,在全部manager宕机后重启,从第一个manager启动到集群状态变为Ready,平均耗时2.3秒;而同配置的3节点K8s集群,etcd恢复+apiserver就绪+controller同步完成,平均需要47秒。对于需要频繁启停测试环境、或对CI/CD流水线时延极度敏感的场景,这2秒就是决定能否接入自动化发布的关键阈值。
第二,网络模型天然规避“IP漂移”陷阱。Swarm内置的overlay网络使用Gossip协议分发网络状态,每个worker节点本地都缓存着全网服务端点的映射表。这意味着当你执行docker service scale web=5时,新启动的容器IP地址变化,不会触发全网ARP广播刷新,也不会导致上游Nginx的upstream配置失效。而K8s的Service ClusterIP在Endpoint变化时,需要kube-proxy重写iptables规则,这个过程在高并发下可能产生毫秒级连接拒绝。我们曾用wrk压测一个动态扩缩容的服务,Swarm集群的5xx错误率始终为0,K8s集群在扩缩瞬间出现了0.7%的瞬时失败。
第三,安全模型直击运维痛点。Swarm的TLS证书体系是全自动轮转的:manager节点生成根CA,为每个节点签发唯一证书,证书有效期默认3个月,到期前7天自动续签。你完全不用像K8s那样手动管理ca.crt、server.crt、kubeconfig等一堆文件,也不用担心证书过期导致集群“突然失联”。我在某金融客户现场就遇到过一次事故:他们的K8s集群因管理员疏忽未更新证书,凌晨3点所有kubectl命令返回x509 certificate has expired,而Swarm集群在证书自动续签后,连监控告警都没触发一条。
提示:Swarm的极简哲学不是功能阉割,而是对“必要复杂度”的精准克制。它把80%的日常运维需求(部署、扩缩、更新、日志查看)压缩进5个核心命令(docker swarm init/join, docker service create/update/scale/rollback),把剩下20%的边缘需求(如细粒度RBAC、自定义调度器)明确划为“非核心”,交由上层应用或外部工具解决。这种取舍,恰恰是成熟工程团队最需要的清醒。
2.2 Swarm集群的拓扑结构:Manager与Worker的职责边界必须厘清
Swarm集群不是“所有节点平等”,而是有严格的角色分工。一个健康的生产集群,必须遵循“奇数Manager + 任意Worker”的黄金比例。为什么必须是奇数?因为Raft共识算法要求“多数派”才能做出决策。3个Manager节点,允许1个故障;5个Manager,允许2个故障;7个Manager,允许3个故障。但如果你部署4个Manager,那么故障容忍数依然是1(因为4/2+1=3,需要3个节点同意才算多数),却白白增加了网络开销和状态同步负担。我见过最典型的反模式,是某SaaS公司在测试环境部署了2个Manager,结果一次内核升级导致两个节点同时重启,集群因无法达成多数派共识而永久卡死,最终只能重置swarm。
Manager节点的核心职责有且仅有三项:
- 集群状态存储:所有服务定义、网络配置、密钥信息都以加密形式存于本地Raft日志;
- 任务调度中枢:接收docker service create请求,计算最优节点放置位置(基于资源标签、约束条件、全局/replicated模式);
- API网关:所有docker CLI命令最终都路由到Manager的2377端口,Worker节点不暴露此端口。
Worker节点则纯粹是“执行单元”:它只运行容器,上报自身资源(CPU、内存、标签),监听Manager下发的任务指令。它不存储任何集群状态,也不参与Raft投票。这意味着你可以随时增删Worker节点,只要Manager在线,集群服务完全不受影响。我们在某物联网项目中,就利用这个特性实现了“热插拔”边缘节点:现场工程师只需在新设备上执行一条docker swarm join --token <WORKER_TOKEN> <MANAGER_IP>:2377命令,5秒内该设备就加入集群并开始执行指定的采集服务,全程无需重启任何服务。
注意:Manager节点绝不能兼任Worker角色用于生产负载。虽然docker swarm init默认会将init节点设为Manager并允许其运行任务,但这会带来严重风险。Manager的CPU和内存资源必须100%保障Raft心跳、日志复制、API响应等关键路径。我们曾在一个混合部署环境中,让Manager节点同时运行数据库服务,结果在一次网络抖动后,Raft心跳超时触发leader重选,而此时数据库正进行大事务提交,导致节点资源争抢,新选举出的leader因无法及时同步日志而被踢出集群。正确做法是:初始化集群后,立即执行docker node update --availability drain <MANAGER_NODE>,将其设为Drain状态,再用docker node update --role worker <MANAGER_NODE>降级为纯Worker(如果确实需要复用硬件),或者干脆采购专用Manager节点。
2.3 服务(Service)与容器(Container)的本质区别:别再用单机思维玩集群
这是新手最容易栽跟头的地方:把docker run当成万能钥匙,试图在Swarm里“一个一个起容器”。Swarm里的Service,根本不是“多个容器的集合”,而是一个声明式服务契约。你告诉Swarm:“我要一个叫web的HTTP服务,镜像用nginx:alpine,暴露80端口,保持5个副本永远在线”,Swarm就会持续监控这个契约的履行情况。如果某个Worker节点宕机,上面的2个web副本消失,Swarm会在其他健康节点上立刻拉起2个新副本,确保总数恒为5。这个过程对用户完全透明,你甚至不需要知道新容器跑在哪台机器上。
而单机docker run创建的Container,是“命令式”的一次性实例。它没有自我修复能力,没有跨节点调度逻辑,没有内置的DNS服务发现。你在节点A上run了一个nginx,节点B上run了一个redis,它们之间想通信,还得手动配置host网络或自定义bridge网络——这在单机上可行,在集群里就是灾难。
Service的三大核心属性决定了它的行为:
- Mode(模式):replicated(指定副本数,如--replicas 3) vs global(每节点一个,如日志收集agent);
- Endpoint(端点):--publish published=80,target=80,mode=ingress(Ingress网络,所有节点80端口都可访问服务) vs mode=host(仅在运行容器的节点暴露端口);
- Update Config(更新策略):--update-parallelism 1(每次只更新1个副本) vs --update-delay 10s(每个副本间隔10秒) vs --update-failure-action rollback(更新失败自动回滚)。
这些参数不是可选项,而是生产环境的必填项。比如--update-parallelism,如果设为0(即所有副本同时更新),在更新一个数据库迁移脚本时,可能导致所有副本同时执行ALTER TABLE,引发锁表雪崩;如果设为1,就能保证任何时候都有至少4个副本在提供服务,实现真正的零停机更新。
3. 从零搭建三节点Swarm集群:避开90%人踩过的坑
3.1 环境准备:操作系统、Docker版本与网络规划的硬性要求
别跳过这一步。我见过太多人卡在第一步,不是因为技术难,而是因为环境没对齐。Swarm对底层环境有明确的“兼容性基线”,偏离这个基线,后续所有操作都是空中楼阁。
操作系统:必须是Linux内核3.10+,且启用cgroup v1(Swarm尚未完全支持cgroup v2)。Ubuntu 20.04/22.04、CentOS 7/8、Debian 10/11均满足。但注意:Ubuntu 22.04默认启用cgroup v2,需在GRUB启动参数中添加systemd.unified_cgroup_hierarchy=0强制回退。验证方法:执行cat /proc/1/cgroup,若第一行显示0::/,说明是v1;若显示0::/user.slice/user-1000.slice/session-1.scope,则是v2。
Docker版本:必须≥20.10.0。低于此版本的Docker Engine,Swarm的Ingress网络存在NAT回环缺陷(即从Manager节点curl本机发布的服务端口会失败)。升级命令务必用官方源:
# 卸载旧版 sudo apt-get remove docker docker-engine docker.io containerd runc # 添加官方GPG密钥 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # 添加稳定版仓库 echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 安装 sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io网络规划:这是最常被忽视的致命点。Swarm要求所有节点间2377(manager通信)、7946(节点发现)、4789(overlay网络)端口双向互通。很多云厂商的安全组默认只放行入站,忘了出站。实测发现:如果节点A能ping通节点B,但节点B的防火墙阻止了来自A的7946端口连接,那么节点B永远无法加入集群,docker swarm join命令会卡在“Waiting for the remote connection...”并最终超时。解决方案是:在所有节点执行以下命令开放端口:
sudo ufw allow 2377/tcp sudo ufw allow 7946/tcp sudo ufw allow 7946/udp sudo ufw allow 4789/udp sudo ufw reload实操心得:在公有云环境,建议直接关闭ufw,改用云平台的安全组统一管控。我曾在阿里云上配置安全组,只开了22和80端口,结果Swarm初始化成功但Worker无法加入,排查了3小时才发现是安全组没放行7946/udp——这个端口用于Gossip协议的节点健康检查,丢包会导致节点状态长期卡在"Unknown"。
3.2 初始化Manager节点:一条命令背后的完整状态机
在选定的主节点(假设IP为192.168.1.10)执行:
docker swarm init --advertise-addr 192.168.1.10 --listen-addr 192.168.1.10:2377这条命令远不止“启动一个集群”那么简单,它触发了一个完整的状态机流转:
- 初始化Raft存储:在/var/lib/docker/swarm/目录下创建raft/wal/日志文件和raft/snap/快照目录,生成根CA证书(ca.pem)和manager节点证书(node-cert.pem);
- 启动Control Plane组件:启动docker-swarmd进程,监听2377端口,注册为Raft集群的初始leader;
- 生成Join Token:调用内部API生成两个Token:一个是Manager Join Token(用于其他节点以Manager身份加入),另一个是Worker Join Token(用于Worker加入)。这两个Token本质是JWT,包含签名、过期时间(默认无过期)、以及集群ID哈希;
- 配置Ingress网络:自动创建名为ingress的overlay网络,并为其分配10.0.0.0/24网段(可通过--default-addr-pool指定自定义网段);
- 设置Local DNS:在Manager节点的/etc/resolv.conf中插入nameserver 127.0.0.11(Docker内置DNS),使容器内域名解析走Swarm DNS。
命令执行后,终端会输出类似这样的提示:
Swarm initialized: current node (dxn1zf6l41jzb8g57xh5y6z1a) is now a manager. To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef-1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef 192.168.1.10:2377 To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.这里有两个关键细节必须掌握:
- 节点ID(dxn1zf6l41jzb8g57xh5y6z1a):这是该节点在集群内的唯一标识,所有docker node ls输出中的ID列都对应它。它被硬编码在节点证书中,一旦节点重置swarm,ID会彻底改变;
- Join Token的有效性:Worker Token和Manager Token是独立生成的。默认情况下,Worker Token永不过期,而Manager Token在Docker 20.10+版本中默认7天过期(可通过docker swarm join-token --rotate manager强制刷新)。切勿将Manager Token泄露给不可信方,因为它拥有集群控制权。
注意:--advertise-addr参数必须指定节点对外可见的IP,不能是127.0.0.1或localhost。如果节点有多个网卡(如eth0内网IP、eth1公网IP),必须指定内网IP,否则其他节点无法通过公网IP建立稳定连接。我们曾在一个混合网络环境中,因advertise-addr填了公网IP,导致Worker节点加入后状态反复在"Ready"和"Down"间切换,根源就是公网IP的TCP连接不稳定,而Raft要求强一致的长连接。
3.3 加入Worker节点:为什么join命令总是超时?
在Worker节点(IP为192.168.1.11)执行Manager给出的join命令:
docker swarm join --token SWMTKN-1-... 192.168.1.10:2377如果执行后卡住或报错"Error response from daemon: rpc error: code = DeadlineExceeded desc = context deadline exceeded",请按以下顺序排查:
第一层:网络连通性
- 在Worker节点执行telnet 192.168.1.10 2377,确认端口可达;
- 在Manager节点执行netstat -tuln | grep 2377,确认dockerd进程确实在监听;
- 检查Worker节点的/etc/hosts,确保没有将Manager IP解析到错误的主机名。
第二层:时间同步Swarm的TLS证书校验依赖系统时间。如果Worker节点时间比Manager快5分钟以上,证书会被视为“尚未生效”,导致TLS握手失败。执行ntpdate -u pool.ntp.org同步时间,或配置systemd-timesyncd服务。
第三层:Docker守护进程状态在Worker节点执行sudo systemctl status docker,确认dockerd进程是active (running)状态。常见错误是docker服务未启动,或启动后因配置错误崩溃(查看journalctl -u docker -f实时日志)。
第四层:SELinux/AppArmor限制在CentOS/RHEL上,SELinux可能阻止dockerd访问网络。临时禁用测试:sudo setenforce 0。若问题解决,则需配置SELinux策略:sudo setsebool -P container_manage_cgroup on。
当join成功后,Manager节点执行docker node ls,应看到:
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION dxn1zf6l41jzb8g57xh5y6z1a * manager1 Ready Active Leader 20.10.21 q5e3v9t7m8n1o2p3q4r5s6t7u worker1 Ready Active 20.10.21其中*号表示当前执行命令的节点,Leader表示该节点是Raft leader。
实操心得:Worker节点加入后,不要立刻部署服务。先执行docker node inspect worker1,检查其Status.State是否为"ready",Labels是否为空(如有自定义标签,需确认格式正确)。我们曾因在worker1上误加了label "env=prod"(含空格),导致后续service deploy时调度器无法匹配约束,所有任务都pending在"assigned"状态,排查时发现docker service ps显示"no suitable node",根源就是label解析失败。
3.4 部署首个服务:从nginx到真实业务的平滑过渡
现在集群已就绪,部署一个基础nginx服务验证:
docker service create \ --name web \ --publish published=80,target=80,mode=ingress \ --replicas 3 \ --constraint 'node.role==worker' \ nginx:alpine逐参数解析:
--publish published=80,target=80,mode=ingress:将宿主机80端口映射到服务的80端口,使用Ingress网络模式。这意味着集群中任意节点的80端口都能访问到web服务,Swarm内置的IPVS负载均衡器会自动将请求分发到3个副本中的一个;--replicas 3:要求维持3个副本。Swarm会根据节点资源(CPU、内存)和约束条件,自动选择3个Worker节点部署;--constraint 'node.role==worker':调度约束,确保服务只部署在Worker节点,不占用Manager资源。
部署后,执行docker service ls查看服务状态:
ID NAME MODE REPLICAS IMAGE PORTS abc123def456 web replicated 3/3 nginx:alpine *:80->80/tcp3/3表示期望3个副本,当前运行3个,全部健康。
再执行docker service ps web,查看每个副本的详细信息:
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS xyz789uvw012 web.1 nginx:alpine worker1 Running Running 2 minutes ago ijk345lmn678 web.2 nginx:alpine worker2 Running Running 2 minutes ago opq901rst234 web.3 nginx:alpine worker1 Running Running 2 minutes ago注意:web.1和web.3都在worker1上,这是因为Swarm的默认调度策略是“尽量分散”,但当节点数少于副本数时,会允许同一节点运行多个副本以满足数量要求。
现在,从任意一台机器(包括Manager节点本身)curl http://192.168.1.10,应该返回nginx欢迎页。更神奇的是,执行curl http://192.168.1.11(worker1的IP),同样能访问——这就是Ingress网络的威力:它在所有节点上都启用了IPVS规则,将80端口流量导入到Swarm的虚拟IP(VIP)网络中。
提示:别急着删掉这个nginx服务。把它作为你的“集群健康探针”:每次做完重大操作(如升级Docker、修改网络配置),都curl一下这个地址。只要它返回200,就证明Ingress网络、服务发现、负载均衡三大核心能力全部在线。这是我带团队时定下的铁律,比任何监控图表都直观可靠。
4. 生产级服务编排:用docker-compose.yml管理复杂应用
4.1 为什么必须放弃命令行,拥抱Compose文件?
在单服务测试阶段,docker service create命令很爽。但一旦涉及数据库、缓存、API网关、前端静态资源等多个服务协同,命令行就变成了噩梦。想象一下,你要部署一个WordPress站点:
- mysql服务:需要挂载数据卷、设置root密码、暴露3306端口;
- wordpress服务:需要连接mysql、挂载主题插件卷、暴露80端口;
- redis服务:作为对象缓存,暴露6379端口;
- nginx服务:作为反向代理,处理SSL终止。
如果全用命令行,你需要记住10+个参数组合,稍有遗漏(比如忘了给wordpress加--env "WORDPRESS_DB_HOST=mysql"),服务就启动失败。而docker-compose.yml把这些关系、依赖、配置全部声明化,一目了然。
更重要的是,Swarm的docker stack deploy命令,是唯一支持多服务依赖编排的原生命令。它能自动处理服务启动顺序(通过depends_on,虽不保证完全就绪,但能减少竞态)、网络隔离(自动创建stack专属overlay网络)、配置文件注入(通过configs)、密钥管理(通过secrets)。
4.2 编写一个生产就绪的WordPress Stack
创建文件wordpress-stack.yml:
version: '3.8' services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: ${DB_PASSWORD} volumes: - db_data:/var/lib/mysql networks: - backend deploy: placement: constraints: - node.role == manager # 数据库放Manager,便于备份 resources: limits: memory: 1G cpus: '0.5' restart_policy: condition: on-failure delay: 10s max_attempts: 3 redis: image: redis:7-alpine networks: - backend deploy: placement: constraints: - node.role == worker resources: limits: memory: 256M cpus: '0.25' wordpress: image: wordpress:6.2-apache environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_NAME: wordpress WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: ${DB_PASSWORD} WORDPRESS_REDIS_HOST: redis WORDPRESS_CONFIG_EXTRA: | define('WP_REDIS_HOST', 'redis'); define('WP_REDIS_PORT', 6379); volumes: - wp_content:/var/www/html/wp-content networks: - frontend - backend deploy: replicas: 2 update_config: parallelism: 1 delay: 10s failure_action: rollback restart_policy: condition: on-failure nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./ssl:/etc/nginx/ssl:ro - wp_content:/var/www/html/wp-content:ro networks: - frontend deploy: mode: global # 每个节点都跑一个Nginx,实现边缘负载均衡 placement: constraints: - node.role == worker volumes: db_data: wp_content: networks: frontend: driver: overlay attachable: true backend: driver: overlay这个文件体现了生产环境的全部关键考量:
- 环境变量外置:DB_ROOT_PASSWORD和DB_PASSWORD通过shell变量传入(docker stack deploy -c wordpress-stack.yml --compose-file-env-file .env wordpress),避免密码硬编码;
- 资源限制:为db和redis设置了memory/cpus limits,防止单个服务吃光节点资源;
- 智能调度:db强制部署在manager节点(便于集中备份),redis和wordpress部署在worker,nginx用global模式确保每个worker都有入口;
- 滚动更新策略:wordpress更新时,每次只更新1个副本,间隔10秒,失败则自动回滚到上一版本;
- 网络隔离:frontend网络供外部访问,backend网络仅供内部服务通信,提升安全性。
部署命令:
# 创建.env文件存放密码 echo "DB_ROOT_PASSWORD=supersecret123" > .env echo "DB_PASSWORD=wpuserpass456" >> .env # 部署Stack docker stack deploy -c wordpress-stack.yml --compose-file-env-file .env wordpress部署后,执行docker stack services wordpress,查看所有服务状态;执行docker stack ps wordpress,查看每个服务的任务详情。
注意:docker stack deploy会自动创建一个名为wordpress的overlay网络(基于stack名称),所有services默认加入此网络。如果你想让外部容器(如调试用的busybox)也能访问这个网络,必须在network定义中添加attachable: true,否则会报错"network wordpress_default not found"。
4.3 Secrets与Configs:如何安全地管理密码和配置文件
在上面的WordPress例子中,密码通过环境变量传递,这在Docker内部是相对安全的(环境变量不会出现在ps输出中),但仍有风险:如果攻击者获得容器exec权限,仍可通过/proc/1/environ读取。真正的生产级方案是使用Swarm内置的secrets和configs。
Secrets:专为敏感数据设计,以tmpfs方式挂载到容器内,只读,且只对指定服务可见。
# 创建数据库密码Secret echo "supersecret123" | docker secret create db_root_password - # 在compose文件中引用 services: db: secrets: - db_root_password environment: MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password # 注意是_FILE后缀Configs:用于非敏感的配置文件,如nginx.conf、log4j2.xml,同样以tmpfs挂载,但可读写(取决于mode)。
# 创建nginx配置Config docker config create nginx_conf nginx.conf # 在compose中引用 services: nginx: configs: - source: nginx_conf target: /etc/nginx/nginx.conf mode: 0444Secrets和Configs的最大优势是生命周期与服务绑定:当执行docker stack rm wordpress时,所有关联的secrets和configs会自动被删除,无需手动清理。而传统方式挂载的host文件,删除stack后文件还留在磁盘上,成为安全隐患。
实操心得:Secrets不支持动态更新。如果你更新了secret,必须重新deploy整个stack(或至少重启依赖该secret的服务)。因此,对于需要频繁轮换的密钥(如API Token),建议将其存储在外部Vault服务中,容器启动时通过Vault Agent获取,而不是塞进Swarm Secret。我们曾在一个支付项目中,因误将商户私钥放入Secret,导致一次密钥轮换需要停服30分钟,教训深刻。
5. 故障诊断与性能调优:那些文档里不会写的实战经验
5.1 服务状态卡在"Preparing"或"New":调度器的沉默警告
当你执行docker service update或首次deploy时,服务的REPLICAS显示"0/3"或"1/3",且docker service ps显示CURRENT STATE为"Preparing"或"New",这通常意味着调度器找不到符合条件的节点。原因有三:
资源不足:节点剩余内存/CPU小于服务声明的limits。执行docker node inspect | jq '.Description.Resources',查看可用资源。解决方案:降低服务的resources.limits,或给节点打标签并调整约束。
约束不匹配:--constraint指定了node.labels.disk==ssd,但节点没打这个标签。执行docker node inspect | jq '.Spec.Labels'确认标签是否存在。解决方案:用docker node update --label-add disk=ssd 添加标签。
端口冲突:服务要暴露8080端口,但目标节点的8080已被其他进程占用。Swarm调度器不会主动检测端口占用,只会尝试启动容器,然后因端口绑定失败而不断重启。执行docker service logs 查看具体错误。解决方案:在目标节点执行sudo lsof -i :8080找出占用进程并kill。
常见问题速查表: | 现象 | 可能原因 | 快速验证命令 | 解决方案 | |------|----------|--------------|----------| |
docker service ps显示Rejected| 节点处于Drain或Pause状态 |docker node ls查看AVAILABILITY列 |docker node update --availability active <NODE>| |docker service ps显示Shutdown| 服务被手动stop或节点离线 |docker node ls查看STATUS列 |docker node update --availability active <NODE>或重启docker服务 | |docker service logs报no such file or directory| 挂载的volume或config不存在 |docker volume ls,docker config ls| 确认volume/config已创建,或在stack deploy时加上--with-registry-auth|
5.2 网络不通的终极排查链:从容器到物理网卡
Swarm网络问题是最让人抓狂的。我总结了一条10步排查链,覆盖99%的场景:
- 容器内连通性:进入容器
docker exec -it <CONTAINER_ID> sh,执行ping db(服务名),验证DNS解析和overlay网络是否正常; - 容器到宿主机:在容器内
ping 172.17.0.1(docker0网桥IP),确认容器网络栈工作; - 宿主机到容器:在宿主机
ping <CONTAINER_IP>(通过docker inspect <CONTAINER_ID> \| jq '.NetworkSettings.Networks.<NETWORK_NAME>.IPAddress'获取),确认宿主机路由; - 节点间overlay:在节点A执行
docker run --rm --network <OVERLAY_NETWORK> alpine ping -c 3 <NODE_B_CONTAINER_IP>,验证overlay隧道; - 物理网络:在节点A执行
ping <NODE_B_IP>,确认底层网络通畅; - 防火墙:在节点B执行
sudo iptables -L -n \| grep 4789,确认UDP 4789端口未被DROP; - Gossip健康:在节点A执行
docker node ls,查看节点B状态是否为Ready;若为Unknown,执行docker network inspect <OVERLAY_NETWORK> \| jq '.DriverOptions'确认gossip参数; - Ingress VIP:在任意节点执行
ipvsadm -Ln,查看是否有TCP *:80 -> ...的VIP规则; - 服务端口映射:执行
docker service inspect <SERVICE> \| jq '.[0].Endpoint.Ports',确认published端口正确; - 应用层防火墙:检查容器内应用是否监听0.0.0.0(而非127.0.0.1),如nginx需配置
listen 80 default_server;而非listen 127.0.0.1:80;。
提示:第4步(overlay隧道测试)是关键突破口。如果这一步失败,说明Swarm的底层网络层已损坏,必须重启docker服务或重置swarm。我们曾在一个KVM虚拟机集群中,因虚拟网卡驱动bug导致4789端口UDP包大量丢包,表现为服务间偶尔超时,但
docker node ls一切正常。最终通过tcpdump抓包发现4789端口的UDP包丢失率高达30%,更换网卡驱动后问题消失。
5.3 性能瓶颈定位:从CPU飙升到IO阻塞的全链路分析
当集群响应变慢,别急着加机器。先用Swarm原生命令定位瓶颈:
CPU瓶颈:执行docker stats --no-stream,查看各容器CPU%。如果某个服务持续>90%,执行docker exec <CONTAINER_ID> top,看是哪个进程占CPU