ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Docker Swarm负载均衡与自动扩缩容实战:原理、实践与踩坑

2026/10/1 4:02:22 拓冰建站 浏览量
Docker Swarm负载均衡与自动扩缩容实战:原理、实践与踩坑 如果你在一台服务器上用docker service create起了个服务想当然地认为 Swarm 的负载均衡是开箱即用、自动扩缩容无非是docker service scale敲两下就完事那后面踩坑的肯定是你。我在生产环境维护 Docker Swarm 集群这几年最大的体会就是Swarm 的部署门槛确实比 K8s 低一大截但负载均衡和自动扩缩容这两个词后面藏的东西一点都不比 K8s 少。这篇文章就专门聊 Swarm 集群里的 autoscaling 与 load balancing。我会先讲清楚流量在集群内部的真实路径再讲没有原生 HPA 的 Swarm 是怎么做自动扩缩容的然后把我踩过的那些坑——包括一次扩容后流量倾斜到差点把线上打挂的事故——原原本本复盘一遍。适合正在维护 Swarm 集群、或者正准备从单机 Docker 迁到 Swarm 的运维和开发同学参考。1. Swarm集群中负载均衡和自动扩缩容到底管什么1.1 先分清Swarm里的三个负载均衡很多人一听到 Swarm 负载均衡就以为只是外部请求能均匀打到多个副本。其实 Swarm 的负载均衡至少分三个层面三层搞混了排查问题的时候就会像无头苍蝇。第一层是服务发现层面的负载均衡。集群内部任何服务通过服务名访问另一个服务时比如前端服务请求后端服务web这个 DNS 名会解析到一组后端容器流量在幕后被切分到不同副本。这一层默认开着用的是 Linux 内核的 IPVS性能很好但问题在于均匀是会话级别的不是请求级别的——你压测的时候经常看到个别副本 CPU 高、其他副本闲着原因多半就在这里。第二层是 Ingress 网络的路由网格Routing Mesh。当你给一个服务执行--publish published8080,target80Swarm 会在所有节点上监听 8080 端口无论请求打到哪个节点都会被 Ingress 网络转发到真正跑着容器的节点上。这一层的好处是外部负载均衡器随便挂到哪个节点都行坏处是每个发布端口会在每个节点占住一个监听端口端口号就成了集群级稀缺资源。第三层才是你真正要关心的对外入口。请求从用户的浏览器或者客户端进来经过 DNS、云负载均衡、Swarm 集群节点、Ingress 网络、IPVS最后才到容器。任何一个环节分布不均最后表现出来的都是负载不均衡。1.2 自动扩缩容在Swarm里的真实位置Docker Swarm 没有 Kubernetes 那样的 HPAHorizontal Pod Autoscaler原生层面只有docker service scale和docker service update --replicas这种手动操作。这不是 Swarm 团队偷懒而是他们的设计哲学编排器只负责把副本数调度到位至于什么时候该加副本这种业务决策留给外部系统和运维自己判断。所以你在 Swarm 里谈 autoscaling实际上要解决的是两件事决策来源CPU、内存、请求量、队列长度到底看哪个指标执行通道怎么把需要从 5 个副本变成 12 个副本这个动作安全地交给 Swarm 执行。我见过不少团队搞自动扩缩容脚本逻辑也就十分钟写完了可一上生产就出事。原因基本都出在没搞清楚上面三个负载均衡层面和自动扩缩容之间的联动关系。比如扩容时只看 CPU没考虑新副本需要多久才能注册进 VIP 的负载池结果流量进来了容器还没就绪直接给用户返回 503。1.3 VIP与DNSRR两种endpoint-mode的取舍Swarm 创建服务时有个--endpoint-mode参数默认是vip可选dnsrr。这个参数直接决定了负载均衡的工作方式很多人从来不看。在vip模式下Swarm 给服务分配一个虚拟 IP集群内所有节点上的转发规则会把发往这个 VIP 的流量均匀分发到服务的各个副本。优点是对客户端完全透明客户端只管连接服务名即可负载分配由集群内部完成。缺点是会话粘性不好控制虽然 IPVS 支持源地址哈希但 Swarm 默认策略下同一客户端的多次请求不保证打到同一个副本。在dnsrr模式下Swarm 不做 VIP 转发DNS 查询直接返回所有任务容器的 IP 列表让客户端自己选择一个去连。好处是省掉了 VIP 这一层适合那些客户端数量不多、而且自己会做连接重试的场景比如某些内部批处理任务。坏处也很明显不是所有客户端都会对多个 A 记录做加权或合理重试有的客户端拿到第一个 IP 就死磕结果就是一台副本被打满其他副本闲着。我的建议很简单除非你明确知道自己在做什么否则永远用默认的vip模式。我在生产环境碰到过两次服务之间调用慢最后查出来都是有人图省事开了dnsrr而客户端库的负载均衡策略又恰好不适合。2. 扩容前必须想清楚的三个问题健康检查、资源余量与节点亲和性2.1 健康检查就是自动扩缩容的生命线Swarm 判断一个容器是否健康、能不能进入负载池靠的是容器健康检查healthcheck。如果你的镜像没有定义健康检查Swarm 就会把所有处于运行状态running的容器都当成健康的哪怕你的应用其实还在启动过程中、端口都还没监听。这在自动扩缩容场景下是致命的。试想一下流量高峰期触发扩容Swarm 在 3 秒内把新副本拉起来容器进程启动了但应用还没完成初始化此时 VIP 转发已经把这个新副本加入了负载池——前几个请求直接打到还没就绪的应用上超时、报错、重试然后又触发新一轮扩容。我见过有个团队因此搞出了扩容雪崩30 分钟内副本数从 6 个一路涨到 80 个。正确的姿势是给服务加上完善的健康检查并配合start_period参数给应用留出初始化时间。下面是我常用的配置services: web: image: nginx:1.27-alpine healthcheck: test: [CMD, curl, -f, http://localhost/healthz] interval: 10s timeout: 3s retries: 3 start_period: 30s deploy: replicas: 3 update_config: order: start-first这个配置里有几个细节值得注意。interval是两次健康检查的间隔timeout是单次检查超时retries是连续失败几次后才判定不健康start_period则是启动期内不计入失败次数。我把start_period设成 30s就是给 Spring Boot 这类启动慢的应用一个缓冲期避免它还没起来就被打上不健康标签踢出负载池。2.2 资源限制与预留别让任务卡在pending自动扩缩容的本质是按需加副本那加出来的副本跑在哪跑在集群节点的空闲资源上。如果你的服务在创建时压根没定义 CPU 和内存的 limits 与 reservationsSwarm 调度器就不知道新副本需要多少资源结果是扩容指令下了新任务可能全部卡在 pending 状态因为节点资源早就被其他任务吃光了。我强烈建议每个服务都写清楚资源预留reservations和资源上限limitsservices: web: image: nginx:1.27-alpine deploy: resources: limits: cpus: 0.5 memory: 512M reservations: cpus: 0.2 memory: 128M这里有个容易忽略的点limits只限制容器能用的资源上限reservations才是调度器用来决策的关键值。你的副本能不能被调度到某台节点上看的是该节点是否满足所有任务的reservations总和。如果你把reservations设得虚高明明实际只吃 100M 内存却预留 1G那集群利用率会变得非常难看设得太低扩容时又容易导致多个高负载副本挤在同一台节点上互相抢 CPU。2.3 有状态服务别随便参与自动扩缩容写自动扩缩容策略之前先回答一个问题你的服务是有状态还是无状态如果它持有会话状态、写本地磁盘、或者依赖固定的网络标识那它就不适合参与自动扩缩容。我见过一个团队把 Redis 集群当成无状态服务在 CPU 飙升时执行docker service scale redis6结果新副本是起来了但数据全部是空的客户端请求被路由到新副本后疯狂报错。Swarm 不负责数据同步也不负责把已有数据迁移到新副本。Redis 哨兵、Kafka、MySQL 这类有状态中间件在 Swarm 里更应该用固定副本数加外部存储的方案而不是用无脑扩容来解决。自动扩缩容最合适的场景就是无状态 Web 服务和计算型任务请求打到哪个副本都行副本消失了也不影响数据一致性。如果你的服务不符合这个前提先不要急着上 autoscaling否则它扩的越快事故越大。3. 没有内置HPA的Swarm自动扩缩容落地实战3.1 选型思路第三方工具、商业面板还是自研脚本Swarm 自动扩缩容没有统一标准答案我大体上把方案分成三类。第一类是商业面板。像 Portainer 的企业版自带服务伸缩策略可以在界面上配置基于 CPU 或内存的自动扩容。好处是快、图形化、团队容易接受坏处是收费而且策略的灵活度有限想按请求量、队列深度这类自定义指标来做就很吃力。第二类是开源第三方组件。GitHub 上能找到一些 Swarm autoscaler 项目原理基本是监听监控系统的告警然后调用 Docker API 调整副本数。但多数项目维护不活跃我建议谨慎评估后再引入毕竟自动扩缩容是要在线上自动改副本数的代码没人维护的风险太大。第三类是自研小脚本。这也是我最推荐、且实际生产环境用得最稳的方案。原理不复杂定时采集服务指标判断是否越过阈值越过就调 Docker API 改变副本数同时引入冷却时间避免抖振。整套逻辑两百行 Python 就能写完出了问题也容易排查。3.2 指标采集的两条路Docker API与Prometheus自研自动扩缩容第一步是指标采集。这里我走过两条路。一条是直接用 Docker API 的 stats 接口。好处是零额外组件原生就有坏处是拿到的数据是单容器维度的 CPU 和内存如果你想按服务整体的请求量或队列长度来伸缩它就无能为力了。另一条是接 Prometheus。用 cAdvisor 采集容器指标配上节点指标node-exporter再用 PromQL 把服务下所有副本的指标聚合成一个值。这条路前期配置工作多一些但后续想加任何自定义指标都很顺手。比如你想按HTTP 502 响应数每秒超过多少就扩容只要应用把指标暴露成 Prometheus 格式自动扩缩容脚本稍微改一行查询语句就行。如果让我给建议我会说生产环境直接接 Prometheus。用 Docker stats 做 POC概念验证没问题但线上自动扩缩容的决策必须基于可靠、可追溯的指标源Prometheus 的查询结果可以随时拉出来核对出问题时不至于连它到底看到了什么数据都答不上来。3.3 一份可以直接跑起来的Python POC脚本这里我给一份我早期做 POC 时用的脚本基于 Docker SDK for Python。逻辑很简单每隔 30 秒查一次服务下所有副本的平均 CPU 使用率超过高水位就扩容两个副本低于低水位且当前副本数多于最小值就缩容一个副本每次操作后进入冷却期。import time import docker import logging logging.basicConfig(levellogging.INFO, format%(asctime)s %(message)s) CPU_HIGH 70.0 # 平均 CPU 超过该值触发扩容 CPU_LOW 30.0 # 平均 CPU 低于该值触发缩容 STEP_UP 2 # 每次扩容增加的副本数 STEP_DOWN 1 # 每次缩容减少的副本数 MIN_REPLICAS 3 # 服务的最小副本数 MAX_REPLICAS 20 # 服务允许的最大副本数 COOLDOWN_SECONDS 120 # 冷却时间 client docker.from_env() service client.services.get(web) def get_avg_cpu(service): tasks [t for t in service.tasks() if t[Status][State] running] if not tasks: return 0.0 cpu_percents [] for task in tasks: container_id task[Status][ContainerStatus][ContainerID] container client.containers.get(container_id) # 第一次采样通常拿不到可靠的 precpu_stats间隔 2 秒再采一次 container.stats(streamFalse) time.sleep(2) stats container.stats(streamFalse) cpu_delta stats[cpu_stats][cpu_usage][total_usage] - \ stats[precpu_stats][cpu_usage][total_usage] sys_delta stats[cpu_stats][system_cpu_usage] - \ stats[precpu_stats][system_cpu_usage] if sys_delta 0: cpu_percents.append(0.0) else: cpu_percents.append(round(cpu_delta / sys_delta * 100.0, 2)) return sum(cpu_percents) / len(cpu_percents) def current_replicas(service): return service.attrs[Spec][Mode][Replicated][Replicas] last_op_time 0.0 while True: avg_cpu get_avg_cpu(service) replicas current_replicas(service) logging.info(freplicas{replicas} avg_cpu{avg_cpu}%) now time.time() if now - last_op_time COOLDOWN_SECONDS: time.sleep(30) continue if avg_cpu CPU_HIGH and replicas MAX_REPLICAS: new_replicas min(replicas STEP_UP, MAX_REPLICAS) service.scale(new_replicas) logging.info(fscale up: {replicas} - {new_replicas}) last_op_time now elif avg_cpu CPU_LOW and replicas MIN_REPLICAS: new_replicas max(replicas - STEP_DOWN, MIN_REPLICAS) service.scale(new_replicas) logging.info(fscale down: {replicas} - {new_replicas}) last_op_time now time.sleep(30)这份脚本的本质逻辑可以沿用但直接照搬到生产环境之前有几个点必须改。第一个是容器的状态会变。扩容后新任务还在启动中时Status[State]可能不是running脚本里过滤掉即可但任务在节点间漂移时容器 ID 会变甚至服务在缩容时会把正在查的容器删掉这时候client.containers.get()会抛异常生产版本需要加异常处理。第二个是别用单个服务的 stats 算整个集群的伸缩。真实场景要接 Prometheus用一句avg(rate(container_cpu_usage_seconds_total{service_nameweb}[5m]))代替上面的采样逻辑稳定性会好很多。第三个是必须加操作审计。每次自动扩缩容的触发原因、当时的指标值、操作结果都要写日志甚至推到告警平台不然线上副本数变了你想查是谁、什么时候、为什么变的一点线索都没有。4. Ingress网络与VIP一次请求在Swarm内部到底怎么走4.1 Ingress网络的建立与发布端口的隐蔽代价Swarm 集群初始化的时候会自动创建一个名为ingress的 overlay 网络所有声明了发布端口的服务都会挂到这个网络上。这个网络是集群级别的意味着每一台加入 Swarm 的节点无论它是 manager 还是 worker都会在这个网络上有一个接口。当你给服务设置--publish published8080,target80所有节点的宿主机都会监听 8080 端口。也就是说请求打到你集群里任何一台机器上只要端口对上Swarm 都有办法把它转发到正确的容器。这个设计初衷是好的外部负载均衡器不用感知每一台具体跑容器的节点随便指一台能通的就行。但代价是端口号在集群中是全域占用的。你在 A 服务上发布了 8080就不能在 B 服务再发布宿主机 8080。集群规模一大服务一多端口冲突是早晚的事。我在一个跑着 40 多个服务的集群里花了很大力气做端口规划最后还是决定把大部分服务收敛到少数几个统一的对外端口上靠外部负载均衡按域名路径转发端口压力这才缓解。4.2 从外部请求到容器响应的完整链路把一次请求的完整链路拆开看你会发现它比想象中长。假设外部负载均衡把请求送到了集群的 node-3 这台机器目标端口是 8080。第一步node-3 上的 Swarm 代理进程接收这个连接因为 8080 是web服务发布的端口而web服务的副本实际可能运行在 node-1 和 node-2 上所以 node-3 不能直接把请求交给本地进程。第二步这个连接被送入ingress这个 overlay 网络通过 VXLAN 隧道到运行着副本的节点。在那一端Swarm 内置的 IPVS 规则会根据目标服务的 VIP 做负载均衡决策把连接交给某个具体容器。第三步才是容器内应用处理请求、返回响应响应再原路返回。这里面有一个很容易被忽视的性能特征跨节点转发是常态不是意外。只要发布端口流量就可能从任意节点进来再绕到其他节点多一跳网络开销。所以在 Swarm 里贴标签做调度约束比如让前端服务的副本固定跑在与入口节点相同的机器上对性能改善非常明显尤其是对延迟敏感的服务。4.3 为什么很多团队还是愿意在Swarm前面放一层负载均衡有人可能会问Swarm 自己已经有 Ingress 负载均衡了为什么还要在前面单独放一层负载均衡呢我的答案很直接因为 Ingress 的负载均衡是集群内部的它解决不了对外的高可用问题。用户访问你的服务DNS 解析到的是你的 VIP 或者域名这一层入口挂了集群内部的负载均衡再完美也没有用。而且外部负载均衡能干的很多事Swarm 的 ingress 干不了按 URL 路径路由到不同服务、按域名做分流、TLS 证书统一终结、基于请求内容的灰度发布。我生产环境的典型架构是云负载均衡作为最外层入口后面挂着 2-3 台 Swarm 集群的节点作为后端健康检查打到这些节点的某个统一端口。外部负载均衡只负责哪台节点活着就派活给哪台节点内部的服务路由和副本分发全部交给 Swarm 自己完成。这样既利用了 Swarm 内置负载均衡的能力又把入口的高可用和灵活的流量治理留给了更专业的组件。5. 对外流量入口PublishPort、外部负载均衡和动态发现的配合5.1 三种对外暴露方式的对比我把 Swarm 对外暴露服务的常用方式整理成了一张对比表这里直接放出来方式适用场景优点缺点直接 PublishPort小型集群、内部工具配置最简单一条命令搞定端口占用严重节点故障要自己处理无会话粘性外部负载均衡 PublishPort生产集群、中大规模入口高可用健康检查自由控制需要额外维护负载均衡层反向代理Nginx/HAProxy入 Swarm多服务、按域名路由路由灵活TLS 终结集中便于灰度代理本身成为新的运维对象如果你只有三五台机器的 Swarm 集群、服务就几个内部组件那直接 PublishPort 也不是不行省事。但一旦服务数量超过十个或者有对外提供服务的要求我强烈建议至少走到第二种方案。我自己是从第二种起步的后来服务从十几个涨到四十多个才被迫加了第三种方案的反向代理层。5.2 外部负载均衡如何感知Swarm的扩缩容这里有个特别容易被忽略的联动问题外部负载均衡的健康检查对象是节点不是容器。Swarm 自动扩缩容是在服务副本层面操作的副本数从 3 变成 10外部负载均衡完全不知道也不关心——因为它只需要把请求送到节点的发布端口剩下的事 Swarm 内部自己分发。但有一个陷阱如果你的服务发布端口在集群里的某个服务上而该服务的副本数为 0或者服务的任务全部处于 unhealthy 状态Swarm 依然会在节点上保留端口监听只是转发时没有可用后端请求会被丢弃或返回连接拒绝。外部负载均衡的健康检查这时候是假绿的——它能 TCP 连上端口但业务已经挂了。我在生产环境里特意给外部负载均衡配了业务级健康检查每个服务对外暴露一个/healthz接口返回 200 才健康。外部负载均衡的探测路径不是只探节点端口而是走完整的 HTTP 请求链路这样配置在自动扩缩容场景下尤其重要。因为缩容可能会把某个服务的副本缩到最小如果此时探活还是按 TCP 端口来做很可能出现探活通过、请求全挂的诡异事故。5.3 自动扩缩容时代的端口规划与域名收敛做了自动扩缩容之后服务的副本数不再是一个固定值但对外暴露的维度最好是稳定的。这听起来像废话但我见过不止一个团队在扩容时遇到底层网络配置跟不上服务加了副本安全组没放行新节点的端口外部负载均衡探测失败流量 502 了好几分钟运维才发现。所以我的经验是端口规划要站在集群维度统一管理域名收敛要站在用户维度统一收敛。每个服务尽量只暴露少量端口能走 80/443 就走TCP 短连接能复用就在代理层复用。域名也好路径也好最终都收敛到同一组入口端口上。这样自动扩缩容的脚本只需要关注副本数不需要关心底层网络放行和负载均衡配置是否跟得上。6. 一次线上事故复盘扩容时引发的流量倾斜与连接中断6.1 事故现象新副本CPU很低错误率却在上升那次事故发生在一个流量高峰期。我监控着服务的平均 CPU发现指标已经压到 75% 以上自动扩缩容脚本按预案扩容副本数从 8 个加到 14 个。扩容指令执行得很顺利任务全部进入 running 状态但诡异的事情发生了新副本的 CPU 使用率极低几乎空转可服务的整体错误率反而在往上走。一开始我以为是监控数据延迟或者新副本还没被加入到 VIP 负载池。可等了五分钟新副本还是凉凉的错误率也没有回落。我用docker service ps web看了任务分布又手动登录到新副本所在的节点从容器里直接 curl 本机的健康检查接口发现应用本身是好的能正常响应。那就奇怪了应用没问题为什么流量就是到不了新副本6.2 排查链路任务状态、健康检查生命周期与ingress重平衡我先查了任务在 VIP 转发规则里的登记情况。Swarm 的任务只要进入 running 状态就会往 VIP 的负载池里注册新副本确实已经在池子里了。但为什么没有流量打过来这里涉及一个我之前没太在意的机制Ingress 网络不会因为新副本加入而立刻把已有连接打散重分配。那些在扩容前就建立的、还在存活期内的长连接依然被 IPVS 钉在旧的副本上。新副本能分到的只有扩容后新建的连接。而那个场景下客户端大量复用长连接新建连接占比不高于是新副本一直空转老副本继续承压整体错误率还在上升——因为老副本真的扛不住了。另外一个隐蔽问题是健康检查的阶段交替。部分新副本的start_period设得太短只有 10 秒而应用真正可服务需要 20 秒左右导致新副本起来后短暂被标记为 unhealthy被踢出负载池。等它恢复健康客户端那波重试请求已经打完了流量早就撤走。所以那次排查的最终结论其实非常朴素新副本不是没有被路由而是连接迁移机制加上健康检查窗口共同导致它在最需要接流量的时候没有接上流量。Ingress 的 IPVS 转发是连接级的不是请求级的这个特性决定了 Swarm 的负载均衡对长连接服务天然不友好。6.3 修复方案与后续防复发措施那次事故之后我做了一系列调整这里分享给你。第一给服务开启update-config的order: start-first并且严格要求镜像健康检查。保证新副本在完全就绪之后才会被加入负载池旧副本在替换时先摘除流量再停止。配置示例deploy: update_config: order: start-first failure_action: rollback rollback_config: parallelism: 0 delay: 30s第二健康检查的start_period按应用实际启动耗时放宽到 30-40 秒宁可探活慢一点也不要造成假健康进 LB然后半分钟后又踢出来的抖动。第三对外服务的连接策略改为短连接优先或者至少客户端要做连接建立失败时的重试。长连接全部堆在一批固定副本上任何扩容都没有意义只有连接能够分散建立扩容的新副本才能接到流量。第四自动扩缩容脚本增加了一个观察期——扩容后必须等 90 秒再判断效果避免因为在启动期内 CPU 指标还没升上来就误判扩容无效又继续扩把集群副本数顶到上限。老实说这次事故让我对 Swarm 的负载均衡有了一个更清醒的认识它的转发机制是可靠的但可靠不等于让你感知不到存在。连接级的负载均衡意味着你扩容后不可能立刻看到新副本 CPU 飙升这是机制决定的不是配置错了。理解了这一点再去设计自动扩缩容策略整个思路都会不一样。7. 自动扩缩容策略的调参建议抖动抑制、冷却时间与容量规划7.1 上下阈值与冷却时间用迟滞区间避免抖振自动扩缩容最容易犯的错是照着CPU 超过 70% 扩容、低于 70% 缩容这种单阈值逻辑写。后果就是抖动指标在阈值附近晃动缩容指令刚执行完CPU 又因为流量反弹超过 70%于是又扩容一小时内副本数上上下下前端监控图锯齿状后端集群被无效调度折腾得半死。正确做法是设置迟滞区间hysteresis也就是高水位和低水位之间留一个空白带。比如扩容触发条件是平均 CPU 高于 75%缩容触发条件是低于 35%。这样指标从 35% 涨到 75% 需要跨越一大段不会因为毛刺触发扩容而扩容之后如果 CPU 回落到 70%也不会立刻触发缩容要跌破 35% 才缩给扩容后的副本留足生效时间。我常用的调参表供你参考指标值说明扩容阈值75% CPU持续 5 分钟避免瞬时毛刺缩容阈值35% CPU持续 15 分钟缩容更保守扩容步长当前副本数的 50%向上取整快速应对尖峰缩容步长每次缩 20%最小不小于配置下限平滑释放资源冷却时间扩容后 10 分钟缩容后 20 分钟防止抖振这里的持续 5 分钟不是指测量间隔而是指指标越过阈值后需要持续一段时间才真正确认要扩容。实现上也可以在脚本里记录连续 N 个采样周期都超过阈值再触发至少能过滤掉绝大多数因为 GC垃圾回收、偶发慢请求带来的假信号。7.2 扩缩容的不同心态扩容要快缩容要慢我把这个原则写进过团队的操作手册扩容要快缩容要慢。原因很现实。扩容慢半拍流量的尖峰可能直接把你仅剩的几个副本打垮这是可用性事故缩容快一拍副本数快速回落到低水位结果流量又涨回来应用还在冷启动阶段就被迫再扩容这是稳定性事故。两者比起来可用性事故的代价要高得多。所以我在设置脚本参数时扩容的阈值可以相对激进一些比如 70% 就扩冷却时间也可以短一些5 分钟缩容的阈值则要保守35% 以下才缩冷却时间要拉长到 15 分钟以上甚至要求 CPU 低水位持续 20 分钟再缩。缩容时也建议一次只缩少量副本而不是一次从 20 个缩回 5 个——万一流量反弹至少还有 10 个副本在顶着。另外凡是有状态依赖的服务或者启动时需要加载大数据缓存的服务缩容更得格外小心。新副本冷启动代价很高每缩一个副本就是在把流量和压力往剩下的副本上摊摊得过猛就是连锁故障。7.3 容量规划自动扩缩容是保险丝不是主引擎做自动扩缩容最成功的一种认知是把它定位成保险丝而不是容量主引擎。什么意思就是你的集群日常容量规划应该按预估峰值的 1.5 到 2 倍做自动扩缩容只是用来应对突发流量和预测外的事情。我见过团队把自动扩缩容当成万能药平时集群只有 2 台节点扩容全靠脚本临时加副本。结果高峰一来脚本倒是触发了但集群是所有节点总资源都不够新副本全在 pending 状态再多的副本数规划也没用。自动扩缩容能解决的是有资源但副本数不够的问题它解决不了集群总容量本身就撑不住的问题。所以我的容量规划思路是三层日常运行层副本数固定在基线水平满足大部分请求自动扩缩容层处理 30%-100% 的流量波动在集群预留资源范围内自动调整紧急预案层一旦逼近集群总容量上限人工介入、加节点或者降级非核心服务。自动扩缩容脚本的MAX_REPLICAS一定要根据集群总资源和现有任务占用量做计算不要在脚本里拍脑袋填一个很大的数。最后再分享一个小技巧扩容之后别急着看那几个新副本的 CPU 数值先把docker service ps service和docker service logs --since 5m service打开观察两分钟。如果新副本的日志是干净的、健康检查是通过的那大概率扩容是成功的如果新副本还在不断拉取镜像、初始化连接池你的健康检查参数得再松一档。我在实际操作中类似的观察期挽回过至少三次以为要凉的局面这套流程比任何监控告警都来得直观。