☰
Docker Swarm集群实战:从单机到高可用Web集群的搭建与调优
2026/10/21 13:25:05 网站建设 项目流程

1. 从单机到集群:为什么我最终选了Docker Swarm而不是K8s

三年前我接手了一个内部工单系统,日活不到两千,单台4核8G的云主机跑着Nginx加两个Node服务,数据库单独一台。日子过得很安稳,直到有一次机房网络抖动,服务重启花了将近四分钟,业务方电话直接打到我手机上。那次之后我就开始琢磨:得搞一套能自动恢复、能滚动更新、能横向扩容的Web集群方案。

选型阶段我认真对比过几套主流方案。Kubernetes功能最全,生态最猛,但它的学习曲线和运维复杂度对一个小团队来说太重了——光是etcd、apiserver、controller-manager、scheduler这一堆组件的调优和证书轮换就够喝一壶。Nomad轻量,但服务发现和负载均衡的集成度不如Swarm原生。最后我选了Docker Swarm,理由很实在:它内置于Docker Engine,不需要额外安装控制平面,学习成本低,而且原生支持滚动更新、服务发现、密钥管理和覆盖网络。对于一个十几台机器的中小规模Web集群,Swarm的“够用且不折腾”恰恰是最优解。

这篇文章我会把整套方案的搭建过程、踩过的坑、调优参数、故障排查链路全部摊开讲。适合已经会用Docker基本命令、想进一步了解容器编排的开发者,也适合正在为小团队选型集群方案的技术负责人。我不会只给你一堆YAML让你复制,而是把每个决策背后的“为什么”讲清楚——比如为什么副本数要设成奇数、为什么滚动更新的parallelism不能设太大、为什么ingress网络和overlay网络要分开规划。

先给一个整体架构的轮廓,让你心里有张图:三台Manager节点组成Raft共识集群,负责调度和状态管理;五台Worker节点承载实际业务容器;所有节点通过overlay网络互联,外部流量经边缘节点的ingress网络进入,由Swarm内置的负载均衡器分发到各副本。数据层用独立的PostgreSQL主从,不放在Swarm里——这一点后面会详细解释原因。

提示:Swarm的Manager节点数量建议为奇数(1、3、5),因为Raft协议需要多数派存活才能维持集群可用。3个Manager可以容忍1个节点故障,5个可以容忍2个,但超过5个之后性能反而下降,因为节点间同步开销增大。

2. 集群初始化:Manager与Worker的节点规划与加入流程

2.1 节点角色分配与硬件基线

我用的机器配置不算豪华,但足够跑一个中等规模的Web集群。Manager节点我选了3台4核8G的机器,Worker节点5台8核16G。为什么Manager也要4核8G?因为Manager不仅负责调度决策,还要维护Raft日志、分发证书、处理API请求。如果Manager资源不足,整个集群的响应会变慢,甚至出现“脑裂”风险。Worker节点配置高一些是因为它们要跑实际的业务容器,CPU和内存直接决定能承载多少副本。

操作系统我统一用了Ubuntu 22.04 LTS,内核5.15。Docker版本锁定在24.0.x,这个版本对Swarm的overlay网络性能做了优化,特别是VXLAN的卸载处理。安装Docker的步骤这里不展开,但有一个关键点必须注意:所有节点的Docker版本必须一致,否则加入集群时可能因为API版本不匹配而失败。我吃过这个亏——有一台Worker不小心装了更新的Docker,结果docker node ls里一直显示down,排查了半天才发现是版本问题。

防火墙方面,Swarm需要开放以下端口:2377/tcp用于集群管理通信,7946/tcp和7946/udp用于节点间 gossip 协议,4789/udp用于VXLAN覆盖网络流量。如果你用了云厂商的安全组,记得这些端口要在内网互通。我一开始只开了2377,结果服务能创建但容器之间ping不通,折腾了一个小时才想起来4789没放行。

2.2 初始化第一个Manager与Token管理

在第一台Manager上执行初始化命令:

docker swarm init --advertise-addr 10.0.1.10 --default-addr-pool 10.20.0.0/16

--advertise-addr指定其他节点能访问到的本机IP,这个地址会写入集群的元数据,后续加入的节点都通过它通信。--default-addr-pool是给覆盖网络分配的地址池,默认是10.0.0.0/8,但如果你内网已经用了这个网段就会冲突。我内网用的是10.0.1.0/24,所以把覆盖网络改到10.20.0.0/16,避免路由混乱。

执行完会输出加入集群的命令,包含一个Token。这里有个安全细节:Worker的加入Token和Manager的加入Token是不同的。默认输出的是Worker Token,如果要加入Manager节点,需要执行docker swarm join-token manager获取Manager Token。Token是敏感信息,不要直接贴在聊天记录或公开文档里。我习惯把它存到密码管理器,或者用docker swarm join-token --rotate定期轮换。

加入Worker节点:

docker swarm join --token SWMTKN-1-xxxxx 10.0.1.10:2377

加入Manager节点类似,只是Token换成Manager的。这里有个经验:不要一次性把所有Manager都加入。先加一个,确认docker node ls显示Ready且Leader状态正常,再加第二个,最后加第三个。每加一个都检查一下Raft集群的健康状态,用docker info看Managers数量是否与预期一致。

2.3 节点标签与调度约束

节点加入后,默认所有节点都可以被调度。但实际生产中,我希望Manager节点不跑业务容器,专门做管理。这可以通过节点标签和调度约束实现:

docker node update --label-add role=manager node-1 docker node update --label-add role=worker node-4

然后在服务定义里加约束:

deploy: placement: constraints: - node.labels.role == worker

这样业务容器只会调度到Worker节点。为什么不让Manager跑业务?因为业务容器的资源波动可能影响Manager的稳定性,而Manager一旦不稳定,整个集群的调度和状态同步都会出问题。另外,Manager节点上默认会跑ingress网络的负载均衡器,如果业务容器也挤在上面,端口冲突和资源争抢的概率会增大。

还有一个细节:给节点打上区域标签。比如zone=az1、zone=az2,然后在服务里用placement.preferences做分布偏好:

deploy: placement: preferences: - spread: node.labels.zone

这样副本会尽量分散到不同区域,避免一个机架断电导致所有副本同时挂掉。虽然Swarm没有K8s那种精细的拓扑感知调度,但spread偏好已经能满足基本的容灾需求。

3. 覆盖网络与Ingress:流量怎么从公网走到容器

3.1 Overlay网络的创建与加密选项

Swarm的覆盖网络是容器跨节点通信的基础。创建一个自定义的overlay网络:

docker network create --driver overlay --attachable --opt encrypted app-net

--attachable允许独立容器(非Swarm服务)也接入这个网络,方便调试。--opt encrypted开启网络层加密,基于IPSec。这里有个性能权衡:加密会带来大约10%-15%的吞吐下降,因为每个数据包都要加解密。如果你的服务间通信不涉及敏感数据,或者跑在已经加密的内网里,可以不加这个选项。我一开始全开了加密,后来发现数据库同步的延迟从2ms涨到了5ms,虽然不大,但对于高频写入的场景还是能省则省。

覆盖网络的网段规划也有讲究。Swarm默认会给每个overlay网络分配一个/24的子网,从--default-addr-pool里切。如果你创建了多个overlay网络,它们会依次分配10.20.0.0/24、10.20.1.0/24……这些网段不能和内网物理网段重叠,否则路由会出问题。我建议在初始化集群时就规划好地址池,比如10.20.0.0/16可以切出256个/24,足够用了。

3.2 Ingress网络的工作原理

当你创建一个服务并发布端口时,Swarm会自动使用ingress网络。这个网络是Swarm内置的,所有节点都默认加入。它的工作方式是:每个节点上都运行一个负载均衡器,监听你发布的端口。当外部请求到达任意节点时,该节点的负载均衡器会通过ingress网络把流量转发到实际运行容器的节点。

举个例子:你发布8080:80,集群有5个节点,服务有3个副本分别跑在node-2、node-3、node-4上。当你访问node-1的8080端口时,node-1的负载均衡器会收到请求,然后通过ingress网络转发到node-2、node-3或node-4的容器。这意味着你不需要在外部负载均衡器上配置后端节点列表,任意节点都能作为入口。

但这个机制有个坑:ingress网络的负载均衡是基于IPVS的,默认使用轮询算法。如果你的服务需要会话保持,轮询会导致用户请求在不同副本间跳转,session丢失。解决方案有两种:一是用--endpoint-mode dnsrr,让DNS返回所有容器IP,由客户端自己做负载均衡;二是在应用层做session共享(比如Redis)。我选了后者,因为dnsrr模式下,外部负载均衡器需要支持DNS解析后端,配置起来更麻烦。

3.3 端口发布模式:ingress vs host

发布端口时有两种模式:

# ingress模式(默认) docker service create --publish published=8080,target=80 --name web nginx # host模式 docker service create --publish published=8080,target=80,mode=host --name web nginx

ingress模式就是上面说的,任意节点都能访问。host模式则是只在运行容器的节点上监听端口,不经过ingress负载均衡。host模式的性能更好,因为没有额外的转发跳数,但缺点是访问时必须知道容器在哪个节点上,而且如果副本迁移了,端口监听也跟着迁移。

我实际用下来,对于Web服务,ingress模式更方便,因为前面可以挂一个外部负载均衡器(比如云厂商的LB),后端直接填所有节点的IP,LB会把流量分发到任意节点,再由ingress转发到容器。对于数据库这类需要固定端点的服务,host模式更合适,因为客户端需要直连特定节点。

注意:如果你用了host模式,并且副本数大于1,那么同一个端口不能在同一个节点上被两个副本同时监听。Swarm会确保每个节点上最多只有一个该服务的副本。所以host模式下,副本数不能超过节点数。

4. 服务定义与滚动更新:让发布不再心惊胆战

4.1 一个完整的服务定义长什么样

我用docker-compose.yml来定义服务,因为比命令行直观得多。下面是一个Web服务的完整定义:

version: "3.8" services: web: image: registry.example.com/web:1.2.3 ports: - target: 80 published: 8080 protocol: tcp mode: ingress networks: - app-net environment: - NODE_ENV=production - DB_HOST=db.internal secrets: - db_password deploy: mode: replicated replicas: 6 update_config: parallelism: 2 delay: 10s failure_action: rollback monitor: 30s order: start-first rollback_config: parallelism: 1 delay: 5s restart_policy: condition: on-failure delay: 5s max_attempts: 3 window: 120s resources: limits: cpus: "0.50" memory: 512M reservations: cpus: "0.25" memory: 256M placement: constraints: - node.labels.role == worker preferences: - spread: node.labels.zone healthcheck: test: ["CMD", "curl", "-f", "http://localhost/health"] interval: 15s timeout: 5s retries: 3 start_period: 20s logging: driver: json-file options: max-size: "10m" max-file: "3" secrets: db_password: external: true networks: app-net: external: true

这个定义里有几个关键点值得展开。replicas: 6是偶数,为什么不是奇数?因为Web服务是无状态的,副本数只影响并发能力,不涉及投票决策。如果是数据库这类有状态服务,才需要考虑奇数副本做Quorum。update_config里的order: start-first表示先启动新容器再停止旧容器,这样更新过程中服务不会中断。但代价是需要额外的资源来同时运行新旧副本,所以parallelism不能设太大。

4.2 滚动更新的参数调优与实测数据

parallelism: 2表示每次更新2个副本。为什么是2而不是6?因为如果一次性更新所有副本,服务会短暂不可用。但如果每次只更新1个,6个副本要6轮,每轮delay: 10s,总共60秒,太慢。2个一批,3轮,30秒完成,而且任意时刻至少有4个副本在运行,容量足够。

monitor: 30s是更新后观察新容器是否稳定的时间。如果30秒内新容器崩溃,Swarm会自动回滚。这个值我设过10秒,结果有一次新版本启动慢,健康检查还没通过就被判定失败,触发了不必要的回滚。后来改成30秒,配合start_period: 20s的健康检查,就稳了。

failure_action: rollback是更新失败时的动作。默认是pause,服务会停在中间状态,需要人工介入。我改成rollback,让它自动回滚到上一个版本。但要注意:回滚也需要时间,如果新版本已经更新了一半,回滚会再花一半的时间。所以parallelism越小,回滚越快。

实测数据:6副本、parallelism=2、delay=10s、monitor=30s,一次完整更新大约需要45-60秒。期间服务容量最低降到4副本,QPS从1200降到800左右,但没有任何请求失败。这个表现对于内部系统完全够用。

4.3 健康检查与就绪探针的配合

Swarm的健康检查是容器级别的,它只能判断容器是否存活,不能判断应用是否准备好接收流量。比如Node服务启动后需要连接数据库、加载缓存,这期间端口已经监听了,但请求会失败。Swarm的健康检查如果只检查端口,会误判为健康。

我的做法是在应用里加一个/health端点,它会检查数据库连接和缓存状态。健康检查命令用curl -f http://localhost/health,只有返回200才算健康。start_period: 20s给应用足够的启动时间,这期间健康检查失败不会导致容器重启。interval: 15s是检查间隔,timeout: 5s是单次检查超时,retries: 3是连续失败3次才标记为不健康。

这里有个坑:Swarm在滚动更新时,会等待新容器的健康检查通过后才继续更新下一批。如果健康检查配置不当,比如start_period太短,新容器还没启动完就被判定失败,更新会卡住甚至回滚。我建议start_period至少是应用冷启动时间的1.5倍。你可以先手动跑一次容器,用time docker run测一下从启动到/health返回200的时间,然后据此设置。

5. 密钥管理与配置分离:别把密码写在YAML里

5.1 Docker Secret的创建与使用

把数据库密码写在环境变量里是常见做法,但环境变量会出现在docker inspect的输出里,任何能访问Docker API的人都能看到。Swarm提供了Secret机制,密钥会被加密存储在Raft日志里,只有被授权的服务才能挂载。

创建Secret:

echo "mysecretpassword" | docker secret create db_password -

在服务里引用:

secrets: - db_password

容器内会在/run/secrets/db_password路径下看到这个文件。应用读取文件内容即可。注意:Secret在容器内是以tmpfs挂载的,不会写入磁盘,所以即使容器被导出,密钥也不会泄露。

但Secret有个限制:它只能以文件形式挂载,不能直接注入环境变量。有些应用只支持从环境变量读密码,这时候可以用一个启动脚本,在容器启动时读取Secret文件并导出为环境变量:

export DB_PASSWORD=$(cat /run/secrets/db_password) exec "$@"

这个脚本要放在镜像的entrypoint里。虽然多了一步,但安全性提升明显。

5.2 Config与Secret的区别

Config和Secret类似,也是挂载文件,但Config不加密,适合存放非敏感的配置文件,比如Nginx的nginx.conf、应用的config.yaml。创建方式:

docker config create nginx_conf ./nginx.conf

在服务里引用:

configs: - source: nginx_conf target: /etc/nginx/nginx.conf mode: 0444

mode: 0444设置文件权限为只读。Config的好处是配置和镜像分离,改配置不需要重新构建镜像,只需要更新Config并重启服务。但要注意:更新Config不会自动触发服务重启,需要手动执行docker service update --config-rm和--config-add,或者直接docker service update --force。

我一般把Config用于Nginx配置、日志配置、特性开关这类不常变的内容。Secret用于数据库密码、API密钥、TLS证书私钥。两者配合,基本能把敏感信息和非敏感配置都管起来。

5.3 密钥轮换的实操流程

密钥不是创建完就一劳永逸的。定期轮换是安全最佳实践。Swarm的Secret轮换流程如下:

  1. 创建新版本的Secret,用不同的名字,比如db_password_v2。
  2. 更新服务,把db_password替换为db_password_v2。
  3. 确认服务正常运行后,删除旧的Secret。
echo "newpassword" | docker secret create db_password_v2 - docker service update --secret-rm db_password --secret-add db_password_v2 web docker secret rm db_password

这个过程会导致服务重启(因为Secret变了),所以要在低峰期做。如果不想重启,可以用--secret-add先挂载新Secret,应用同时读取两个文件,平滑切换后再移除旧的。但这需要应用支持双密钥读取,实现起来复杂一些。对于内部系统,我一般选择直接重启,因为滚动更新本身就不会中断服务。

6. 故障排查实录:从节点失联到服务恢复的完整链路

6.1 节点状态异常的排查顺序

某天早上,监控告警显示一个Worker节点失联。我按以下顺序排查:

第一步,docker node ls查看节点状态。发现该节点显示Down,状态是Unknown。这说明Manager已经超过一定时间没收到该节点的心跳了。默认心跳间隔是5秒,超过20秒没收到就标记为Unknown,超过node-down-after(默认也是20秒左右)就标记为Down。

第二步,SSH到该节点,检查Docker服务是否运行。systemctl status docker显示服务正常,但docker info报错说无法连接到Swarm Manager。这说明节点上的Docker daemon是活的,但和集群的通信断了。

第三步,检查网络连通性。ping 10.0.1.10(Manager IP)通,但telnet 10.0.1.10 2377不通。说明2377端口被阻断了。查了一下,原来是安全组规则被误改,2377端口的内网访问被限制了。恢复规则后,节点自动重新加入集群,状态变回Ready。

这里有个经验:节点失联后,Swarm不会立即重新调度该节点上的容器。默认要等node-down-after时间过后,才会把该节点标记为Down,然后开始在其他节点上重建副本。这个时间可以通过docker swarm update --node-down-after 30s调整。我设成了30秒,比默认的稍长,避免网络抖动导致误判。

6.2 服务副本无法启动的常见原因

有时候docker service ls显示副本数是3/6,说明有3个副本起不来。用docker service ps <service>查看具体原因。常见的错误和解决方案:

错误信息原因解决方案
no suitable node调度约束不满足检查节点标签和placement.constraints
port already in use端口冲突检查是否有其他服务占用了同一端口
image not found镜像拉取失败检查镜像仓库地址和认证信息
health check failed健康检查不通过查看容器日志,调整健康检查参数
insufficient resources资源不足检查节点剩余CPU和内存,调整resources.limits

我遇到最多的是no suitable node。有一次我给服务加了node.labels.role == worker的约束,但新加入的节点忘了打标签,导致副本无法调度。解决方法是给节点补上标签:docker node update --label-add role=worker node-6。

还有一个隐蔽的坑:资源预留(reservations)和资源限制(limits)的区别。reservations是调度时保证分配的资源,limits是运行时允许使用的上限。如果reservations设得太高,节点剩余资源不足,副本就无法调度。我一般把reservations设为limits的一半,给调度留出余量。

6.3 覆盖网络不通的排查方法

容器之间ping不通是另一个常见问题。排查步骤:

  1. 进入源容器,ping <目标容器IP>。如果目标容器IP都ping不通,说明overlay网络有问题。
  2. 检查两个容器是否在同一个overlay网络里。docker inspect <container>看Networks字段。
  3. 检查节点的4789/udp端口是否开放。VXLAN流量走这个端口。
  4. 检查--default-addr-pool是否和物理网络冲突。如果冲突,路由会走错接口。

我遇到过一次,两个容器在不同节点上,ping不通但同节点上的容器能通。最后发现是其中一台节点的4789/udp被防火墙拦了。VXLAN的流量是UDP,很多防火墙默认只放行TCP,UDP需要手动加规则。

提示:如果排查困难,可以临时创建一个--attachable的overlay网络,用docker run启动两个测试容器接入该网络,手动ping测试。这样能快速定位是网络配置问题还是应用问题。

7. 数据持久化与有状态服务的处理策略

7.1 为什么数据库不放在Swarm里

Swarm对无状态服务支持得很好,但对有状态服务(比如数据库)就比较吃力。主要问题是存储卷的本地性。Swarm的卷默认是本地卷,容器调度到哪个节点,卷就在哪个节点上。如果容器迁移了,卷不会跟着走,数据就丢了。虽然可以用NFS或云盘做共享存储,但性能和可靠性都不如本地SSD。

我的做法是:数据库、Redis、消息队列这类有状态服务,用独立的主机部署,不纳入Swarm集群。Swarm里的Web服务通过内网DNS或固定IP访问这些外部服务。这样虽然少了自动扩缩容的便利,但换来了数据的安全和性能的稳定。对于中小规模系统,这个取舍是值得的。

如果你确实需要在Swarm里跑有状态服务,可以用placement.constraints把容器固定到特定节点:

deploy: placement: constraints: - node.hostname == db-node-1

然后在该节点上挂载本地卷。但这样就没有高可用了,节点挂了服务就挂了。要真正高可用,还是得用数据库原生的主从复制或集群方案。

7.2 共享存储的选型与挂载

对于需要在多个节点间共享的文件(比如用户上传的图片),我用了NFS。在Swarm里创建一个NFS卷:

docker volume create --driver local \ --opt type=nfs \ --opt o=addr=10.0.1.100,rw,nfsvers=4 \ --opt device=:/data/uploads \ uploads

然后在服务里挂载:

volumes: - uploads:/app/uploads

NFS的优点是配置简单,所有节点都能挂载同一个目录。缺点是性能受网络影响,小文件读写延迟比本地盘高。如果对性能要求高,可以考虑云厂商的对象存储,或者用GlusterFS这类分布式文件系统。但对于日上传量不大的内部系统,NFS完全够用。

这里有个坑:NFS的nfsvers要和服务端匹配。我用的是NFSv4,如果服务端只支持v3,挂载会失败。另外,NFS的权限映射也要注意,容器里的用户ID和NFS服务端的用户ID要对应,否则会出现写入权限拒绝。我一般把NFS目录设为777(测试环境)或配置all_squash和anonuid(生产环境)。

7.3 备份策略与恢复演练

不管存储多可靠,备份都是必须的。我的备份策略是:

  • 数据库:每天凌晨全量备份,每小时增量备份,备份文件传到对象存储。
  • 上传文件:NFS目录每天rsync到另一台机器,保留7天。
  • Swarm配置:docker service inspect的输出定期导出,存在Git仓库里。

恢复演练我每季度做一次。流程是:在一台测试机上重新初始化Swarm,用备份的配置重建服务,恢复数据库,验证功能。这个过程能发现很多平时注意不到的问题,比如某个Secret忘了备份、某个Config的版本不对。没有经过恢复演练的备份,等于没有备份。

8. 监控与日志:集群跑起来之后怎么知道它好不好

8.1 节点和服务的核心监控指标

Swarm本身没有内置的监控面板,但Docker Engine暴露了Prometheus格式的指标。我在每个节点上跑了cadvisor和node-exporter,采集容器和主机的资源使用情况。关键指标包括:

  • 节点层面:CPU使用率、内存使用率、磁盘IO、网络吞吐。
  • 容器层面:每个容器的CPU、内存、网络、磁盘读写。
  • 服务层面:副本数是否达到期望、更新状态、健康检查通过率。

我用Grafana做了几个面板:集群概览(节点数、服务数、总副本数)、资源热力图(哪个节点负载高)、服务状态表(哪些服务副本不足)。这样一眼就能看出集群的健康状况。

有一个指标特别重要:服务的副本数与期望值的差异。如果docker service ls显示5/6,说明有一个副本起不来。这个指标要设告警,因为副本不足意味着容量下降,如果此时流量上涨,可能导致服务过载。

8.2 日志聚合的轻量方案

Swarm的日志默认存在各节点的/var/lib/docker/containers下,查日志要SSH到具体节点,很不方便。我用了轻量级的日志聚合方案:在每个节点上跑一个fluentd容器,采集所有容器的日志,转发到中心的Elasticsearch。然后在Kibana里按服务名、容器ID、时间范围搜索。

配置方式是在服务的logging字段里指定fluentd驱动:

logging: driver: fluentd options: fluentd-address: "10.0.1.200:24224" tag: "docker.{{.Name}}"

这样日志会直接发到fluentd,不落本地磁盘。但要注意:如果fluentd挂了,日志会丢失。所以我在fluentd前面加了一个缓冲层,用Redis做队列,避免日志丢失。对于内部系统,这个方案足够用了,比ELK全家桶轻量得多。

8.3 告警规则的设置经验

告警不在多,在于准。我设了三条核心告警:

  1. 节点Down超过1分钟:说明节点可能挂了,需要立即处理。
  2. 服务副本不足超过5分钟:说明调度出了问题,容量在下降。
  3. 容器重启次数超过3次/小时:说明应用不稳定,可能在崩溃循环。

其他指标比如CPU高、内存高,我设的是警告级别,不直接打电话,而是发到群里。因为CPU高不一定有问题,可能是正常业务高峰。但节点Down和副本不足是硬故障,必须立即响应。

告警的阈值要根据实际情况调。我一开始把CPU告警设在80%,结果每天收到几十条告警,都是正常的业务波动。后来改成持续5分钟超过90%才告警,噪音就少了很多。告警疲劳比没有告警更危险,因为你会开始忽略告警。

9. 扩容与缩容:流量来了怎么加机器,流量走了怎么省资源

9.1 横向扩容的操作步骤

当流量上涨,现有副本扛不住时,扩容很简单:

docker service scale web=12

Swarm会自动在可用节点上创建6个新副本。但要注意:新副本的调度需要时间,从创建容器到健康检查通过,大概需要20-30秒。所以扩容要提前做,不能等QPS已经打满了才扩。我一般设一个告警:当平均CPU使用率持续5分钟超过70%时,自动触发扩容脚本。

自动扩容可以用Docker的API实现。写一个脚本,定期查询服务的CPU使用率,如果超过阈值就调用docker service scale。但Swarm本身没有内置的自动扩缩容(HPA),需要自己实现。对于中小规模系统,手动扩容+告警提醒就够了,没必要上复杂的自动扩缩容。

9.2 缩容时的连接排空

缩容比扩容麻烦,因为要确保正在处理的请求不被中断。Swarm在缩容时会发送SIGTERM信号给容器,然后等待stop_grace_period(默认10秒),如果容器还没退出就发SIGKILL强制杀死。

所以应用需要处理SIGTERM信号:收到信号后,停止接受新请求,等待正在处理的请求完成,然后退出。Node.js里可以这样写:

process.on('SIGTERM', () => { server.close(() => { console.log('Server closed'); process.exit(0); }); });

stop_grace_period要设得比最长请求处理时间稍长。我设的是30秒,因为有些报表请求可能要20秒。如果设得太短,请求会被强制中断,用户看到502错误。

9.3 节点扩容与集群再平衡

增加Worker节点后,新节点默认是空的,已有服务的副本不会自动迁移过去。需要手动触发再平衡:

docker service update --force web

--force会强制重启所有副本,Swarm会重新调度,把部分副本分配到新节点上。但这个操作会导致服务短暂中断(滚动更新),所以要在低峰期做。另一个方法是等自然的重启周期,比如下次更新时新节点就会被纳入调度。

Swarm没有K8s的descheduler那种自动再平衡机制,所以节点扩容后需要手动干预。我一般是在扩容节点后,挑几个资源使用率高的服务,逐个--force更新,把负载匀过去。

10. 我踩过的三个印象最深的坑

10.1 时间不同步导致证书失效

Swarm节点间通信用TLS证书,证书的有效性依赖系统时间。有一次一台Worker的时间慢了5分钟,导致它无法和Manager建立TLS连接,节点一直显示Down。排查了半天才发现是NTP服务没配好。所有节点必须配置NTP时间同步,这是集群稳定的基础。我后来在初始化脚本里加了timedatectl set-ntp true,确保每台机器时间一致。

10.2 镜像标签用latest导致更新混乱

早期我图省事,镜像标签都用latest。结果有一次更新,Swarm在部分节点上拉到了新镜像,部分节点因为缓存还是旧镜像,导致集群里同时跑着两个版本的应用,行为不一致。生产环境绝对不要用latest标签,要用明确的版本号,比如web:1.2.3。这样每个节点拉到的镜像是一致的,更新也可控。

10.3 资源限制设得太紧导致OOM

有一次我给一个Java服务设了memory: 512M的限制,结果服务频繁被OOM Killer杀掉。Java应用的堆内存加上元空间、线程栈,实际占用远不止512M。后来改成memory: 1G,并调整了JVM的-Xmx参数,才稳定下来。资源限制要比应用实际峰值占用高20%-30%,给GC和突发流量留余量。你可以先用docker stats观察一段时间,了解应用的真实资源消耗,再设置限制。

11. 后续可以继续打磨的几个方向

这套Swarm集群跑了两年多,整体很稳,但有几个地方我还在持续优化。一是日志的采样和过滤,现在全量采集,存储成本有点高,打算在fluentd层加采样规则,只保留ERROR和WARN级别的日志,INFO级别按1%采样。二是服务的依赖管理,Swarm没有depends_on的启动顺序保证,我打算在应用层加重试逻辑,让服务在依赖未就绪时自动等待。三是成本优化,把一些低峰期用不到的服务在夜间缩容到1副本,早上再扩回来,能省不少资源。

如果你也在用Swarm,或者正在选型集群方案,我的建议是:先跑起来,再优化。不要一开始就追求完美的架构,Swarm的简单性让你能快速上线,遇到问题再逐步调整。很多所谓的“最佳实践”,都是在实际踩坑中总结出来的,不一定适合你的场景。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询