2.7 扩展与伸缩


2.7 扩展与伸缩

本节摘要:扩展是给服务增加容器实例以扛住更多流量,伸缩是反过来减少实例省资源。Compose 用 up 命令的 --scale 参数完成这件事,但前提是服务无状态。本节先讲 --scale 的两种命令写法与 deploy.replicas 的适用边界,再拆解会话、本地文件两类有状态陷阱,解释数据库为什么不能简单复制,最后说明 Compose 不提供内置负载均衡的边界,以及走向自动伸缩的方向。

学习目标

  1. 能用 --scale 参数把服务扩展到指定实例数,并说出新旧命令写法。
  2. 能判断一个服务是否具备水平扩展的前提,识别会话与文件系统的坑。
  3. 能解释有状态服务扩展需要额外机制的深层原因。
  4. 能说清 Compose 在负载均衡与自动伸缩上的能力边界。
  5. 能搭建基于指标的简单自动伸缩原型并评估其局限。

一、先分清扩展与伸缩

扩展(Scaling Out)是增加实例数换取处理能力与吞吐量;伸缩(Scaling In)是减少实例数降低资源消耗与成本。两者目标一致:让实例数量跟着负载走,既不浪费也不被压垮。

方向不同,前提相同:只有无状态服务才谈得上"加实例就行"。无状态的含义是"任何实例都能独立完成请求,不需要记住上一个请求的任何信息"。判断方法很简单:把实例从三个减到一个,行为是否完全一致?会话、缓存、上传文件只要存在任一实例的本地,答案就是"不一致",扩展前先解决这些状态。

扩展还有一条铁律:永远不要在实例数变化时依赖容器名。容器名带序号,扩缩后序号可能变化,代码里引用容器名的做法在扩展场景会直接失效。

二、--scale 的两种写法

扩展实例数不需要改配置文件,up 命令带上 --scale 即可:

docker-compose up --scale web=3 -d

新版本 Docker 推荐不带连字符的命令:

docker compose up --scale web=3 -d

两种写法等价,web=3 表示把 web 服务扩展到三个实例。要点是 --scale 要和 up 一起用,单独执行没有意义;实例数变化后 Compose 只重建需要增减的部分。想缩回两个实例:docker compose up --scale web=2 -d。全部停掉仍然是 docker compose down,扩展出来的实例一并清理。

配置文件里还有一个相关字段 deploy.replicas:

services: web: image: nginx:1.21 ports: - "80:80" deploy: replicas: 3

注意这里的坑:deploy.replicas 只在 Swarm 模式(docker stack deploy)下生效,普通 docker compose up 会直接忽略它。不少新手写完 replicas: 3 发现只起了一个实例,就是这个原因。单机 Compose 场景,请认准 --scale。

三、为什么扩展要求无状态

把 Nginx 从 1 个扩到 3 个,流量会被分发到三个实例上。如果应用把状态留在实例本地,问题立刻出现。

会话的坑。 用户登录后的会话存在实例一的本地内存,下一次请求被分到实例二,实例二不认识这个会话,用户被强制登出。解法是把会话外置到 Redis 这类共享存储,所有实例读写同一个会话源。

文件系统的坑。 用户上传的图片写在实例一的磁盘上,实例二收到图片请求时文件不存在。上传目录必须落到共享存储——网络文件系统或者数据库,绝不能是容器本地目录。

内存缓存的坑。 实例各自维护一份热点数据缓存,命中率下降不说,缓存数据还可能互相矛盾。缓存放 Redis,问题消失。

判断扩展安全性的试金石就一句话:杀掉任意一个实例,用户无感知吗? 有感知就有状态,先把状态搬出去再扩展。

四、有状态服务为什么不能简单复制

数据库是最典型的有状态服务。直接 --scale db=3 会发生什么?三个 PostgreSQL 实例同时挂同一个数据卷,互相不知道对方的存在,各自写各自的数据,很快文件锁与数据损坏就来了。共享存储解决不了"谁该接受写入"的问题——这是分布式系统的一致性难题,不是挂个卷能解决的。

数据库要扩展,必须引入编排机制:主从复制让从库分担读流量;分片把数据按规则分散到多节点;Redis 这类内存库走集群模式。以 Redis 为例,官方集群模式需要显式开启:

services: redis: image: redis:7.2 command: redis-server --cluster-enabled yes --appendonly yes ports: - "6379:6379" volumes: - redis_data:/data volumes: redis_data:

--cluster-enabled yes 开启集群,--appendonly yes 开启 AOF 持久化。即便如此,真正的 Redis 集群还需要节点发现、槽位分配、故障转移一套完整流程,单机 compose 顶多起个原型。结论:数据库的扩展是架构问题,不是参数问题,生产环境交给专业的集群方案或托管服务,Compose 管好单实例加持久化就够了。

五、负载均衡:Compose 的能力边界

实例扩到三个,流量怎么分?这里必须把话说清楚:Compose 不提供内置负载均衡组件。容器通过宿主机端口映射暴露服务时,Docker 的端口转发规则会在同一端口的多个容器间做轮询分发,但它只是连接级的简单转发——没有健康检查、没有会话保持、没有加权与熔断,后端实例挂了一个,流量照样往它身上打,直到连接失败。

所以生产环境的正确姿势是前置一个真正的负载均衡层:Nginx、HAProxy、Traefik 都行,放在 Compose 项目里作为一个服务,由它做健康检查与流量分发,后端挂三个 web 实例。这一层不属于"扩展"的附属品,而是扩展能成立的前提。

这一层还可以顺带解决另一个问题:TLS 证书。多个实例各自配证书是浪费,证书放在负载均衡一层统一终结 TLS,后端实例走内网 HTTP,证书管理集中在一处,到期续签也只需改一处。

三种做法放在一起对比,各自的定位很清楚:

方案 健康检查 会话保持 证书终结 适用场景
Docker 端口转发轮询 临时验证、小流量单机
Nginx / HAProxy 反代 单机生产、规模可控
Traefik 动态发现 有,可自动续签 服务频繁增删、多域名场景

无状态服务水平扩展架构图

架构图如下:负载均衡前端承接所有请求,三个无状态实例各自处理,会话与数据全部落在共享后端,实例本身不留任何状态。

无状态服务水平扩展架构图

六、伸缩策略与自动化的方向

手动伸缩适合负载可预测的场景:活动开始前扩、结束后缩。负载不可预测时,需要基于指标的自动伸缩——用 Prometheus 采集指标,脚本按阈值调用 scale 命令。cAdvisor 负责暴露容器指标,Prometheus 每 15 秒抓一次:

services: cadvisor: image: google/cadvisor:latest ports: - "8080:8080" volumes: - /:/rootfs:ro - /var/run:/var/run:rw - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro

脚本侧的核心循环不复杂:查 CPU 利用率,超过 70% 就扩一个实例,低于阈值一半就缩,上下限各自封顶,比如最小 1 个、最大 5 个,每分钟检查一次。但要说清楚,这种自研方案只适合原型验证:没有冷却期会导致抖动,没有健康感知会把故障实例也计入数量。生产环境的自动伸缩,请交给 Kubernetes 这类原生支持 HPA 的编排平台,Compose 的定位是单机编排,别硬扛集群的活。

自动伸缩脚本最容易踩的坑是抖动:指标短暂越过阈值就扩容,回落后马上缩容,实例数量像心电图一样上下跳。解法是加冷却期——触发扩容后至少等五分钟再评估下一次,且缩容条件比扩容更保守。指标本身也要平滑,取五分钟均值而不是瞬时值,决策才稳。脚本里获取当前实例数可以复用 docker-compose ps 的输出,按服务名过滤运行中的容器数量;注意 ps 的输出格式在不同版本有差异,解析前先手工看一遍。

七、扩展前后的检查清单

扩展前:确认服务无状态(会话外置、文件落共享存储、缓存进 Redis);确认端口策略——多个实例暴露同一宿主机端口时,依赖的是 Docker 端口转发,前置负载均衡更可靠;给服务配好健康检查,实例异常能自动重启(见 2.6 节)。

扩展后:用 docker stats 观察各实例资源是否均衡;在测试环境完整演练扩容与缩容;确认 down 不会误删数据卷——扩展只增删实例,卷与网络不受影响。

⚠️ 别对数据库执行 --scale db=N。多数数据库没有多写者能力,直接复制实例只会换来数据损坏。有状态服务的扩展方案是主从复制、分片或集群模式,属于架构设计范畴。

💡 观察"该不该扩"先看指标再看感觉:请求延迟、连接数、CPU 利用率三条曲线比任何直觉都准。给 compose 项目挂上监控再谈伸缩,没有数据支撑的伸缩决策都是拍脑袋。

八、实例命名与资源配额

扩展出的实例按"项目名加服务名加序号"命名,比如 demo_web_1、demo_web_2、demo_web_3,日志与监控里靠序号区分。每个实例都该有资源上限,防止单个实例的突发流量吃光整机:

services: web: image: nginx:1.21 deploy: resources: limits: cpus: "0.5" memory: 256M

上限的意义在扩展场景尤其明显:三个实例各限 0.5 核,总量可控;不限的话,一个实例的内存泄漏就能拖垮宿主机上的所有服务。deploy.resources 在普通 compose up 下同样生效,与 replicas 不同,可以放心使用。资源上限只约束单实例,总量控制要靠实例数上限,两者配合才完整。多实例的端口策略要提前想:所有实例共享一个宿主端口时,依赖 Docker 端口转发的简单轮询;需要独立端口时用随机端口映射,或让负载均衡直接访问容器网络。

九、扩展行不通时的替代方案

扩展不是唯一出路。纵向扩展——给宿主机加 CPU 加内存——是很多小项目的务实选择,配置零改动,只是成本有上限。更值得考虑的是拆分:把重计算拆成独立服务单独伸缩,把突发流量交给异步队列削峰,消息中间件让生产与消费两端各自伸缩、互不拖累。第三章模板里会出现不少"主服务加队列加 Worker"的组合,思路都是同一个:让每个组件只扛自己该扛的负载。

本节速览

  • 扩展加实例 伸缩减实例:--scale 与 up 配合使用,新旧命令写法等价。
  • deploy.replicas 是 Swarm 专属:普通 docker compose up 会忽略它,别被配置骗了。
  • 无状态是扩展的前提:会话、本地文件、内存缓存三类状态必须先外置。
  • 有状态服务不能简单复制:数据库扩展是主从、分片、集群的架构问题。
  • Compose 没有内置负载均衡:同端口多实例只是简单轮询,生产前置 Nginx、HAProxy、Traefik。
  • 自动伸缩要指标支撑:Prometheus 加 cAdvisor 可以搭原型,生产交给 Kubernetes。
  • 扩展前后都要检查:无状态确认、健康检查、测试演练,一步不能少。

七种模式到此全部讲完:从单服务到多服务,从卷到网络,从配置到健康检查再到扩展。下一章开始,我们会把这套积木组合进几十个真实应用模板里,照抄即可部署。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U