云原生-02-Kubernetes在测试环境中的部署扩容隔离和回收怎么做

Kubernetes 进入测试环境之后,最容易被低估的并不是部署动作本身,而是环境数量、资源生命周期和团队并发使用带来的治理压力。

刚开始接入 Kubernetes 时,关注点通常只有两件事:

  • 服务能不能部署起来
  • Pod 能不能扩起来

这两个问题当然重要,但它们通常只解决了“能不能跑”的第一层问题。

一旦测试环境开始真正承担日常提测、联调、回归、专项验证、性能摸底、灰度预演这些任务,问题会很快从“会不会部署”转成下面这些更实际的工程问题:

  • 同一个集群里并行跑十几个项目时,谁在抢资源
  • 一个版本开了三套测试环境后,哪些资源该保留,哪些该自动回收
  • 接口回归和性能摸底能不能共用一套命名和资源策略
  • 某个项目临时扩容之后,为什么第二天别的团队开始调度失败
  • 测试环境的数据、日志、域名和配置,怎么跟环境实例对应起来

所以这篇文章不讨论 Kubernetes 基础概念,也不写通用安装教程,而是专门收束到测试环境里最常见的四件事:

  • 部署怎么做
  • 扩容怎么做
  • 隔离怎么做
  • 回收怎么做

重点不是讲“有什么功能”,而是讲测试环境在 Kubernetes 里怎么治理,才能既支撑并发测试,又不把集群跑成长期脏环境。

一、测试环境里的 Kubernetes,治理对象首先不是 Pod,而是“环境实例”

很多测试团队一开始接 Kubernetes,会把注意力放在 Deployment、Service、Ingress、ConfigMap 这些资源对象上。

这没有错,但如果直接按资源对象治理,很快就会失控。

更合适的做法,是先把测试环境统一抽象成“环境实例”。

一个环境实例通常至少包含这些内容:

  • 一组服务部署
  • 一组配置
  • 一组域名或入口
  • 一组测试数据或依赖连接
  • 一组资源额度
  • 一组生命周期信息

换句话说,测试团队真正需要管理的,不是单独一个 Pod,而是:

某个项目、某个分支、某个版本、某次验证任务对应的一整套临时或阶段性运行环境。

只有先把治理对象定义成环境实例,后面下面这些动作才有统一入口:

  • 环境创建
  • 资源申请
  • 命名生成
  • 部署发布
  • 横向扩容
  • 访问接入
  • 证据留存
  • 自动回收

如果没有这个抽象,现场通常会变成下面这样:

  • 服务 A 的命名按项目走
  • 服务 B 的命名按分支走
  • Ingress 的域名按开发同学习惯写
  • ConfigMap 和 Secret 不知道归属于哪一套环境
  • 扩容时只改了副本数,没有记录是谁加的、为什么加、何时恢复
  • 任务结束后,Deployment 删了,但 PVC、Job、ConfigMap、Ingress 都还留着

最后不是 Kubernetes 不好用,而是测试环境没有统一治理模型。

二、测试环境的目标,不是“永远在线”,而是“按需创建、稳定可用、可控回收”

生产环境追求的是长期稳定和持续在线。

测试环境不一样。

测试环境更常见的特点是:

  • 数量多
  • 生命周期短
  • 使用高峰明显
  • 资源竞争强
  • 数据状态变化快
  • 对成本和集群容量更敏感

所以测试环境里更重要的目标,通常不是把单套环境做得无限稳,而是把环境体系做成下面这种状态:

  • 创建快:接到任务后能快速拉起可用环境
  • 边界清:谁用、干什么、占多少资源都清楚
  • 隔离稳:不同任务互不误伤
  • 扩缩有规则:不是谁先卡谁就手工扩
  • 回收干净:任务结束后资源能及时释放

围绕这个目标,测试环境里的 Kubernetes 更适合被拆成 4 条治理链路:

  1. 环境创建链路
  2. 容量扩缩链路
  3. 隔离与边界链路
  4. 生命周期回收链路

下面按这四条链路展开。

三、部署链路:测试环境不能只会 apply,必须先收入口、模板和参数

测试环境里的部署动作,如果只是把 Helm 或 YAML 交给每个人自由执行,很快就会出现结果不可控的问题。

更适合长期维护的方式,是把部署链路拆成下面四层。

1. 模板层:统一部署模板,而不是每次复制 YAML

至少要统一下面这些模板:

  • Deployment
  • Service
  • Ingress
  • ConfigMap
  • Secret 引用约定
  • HPA 或扩容参数模板
  • Job/CronJob 模板

模板统一的价值不在“写得整齐”,而在于可以保证下面这些东西长期一致:

  • 标签规范
  • 资源请求与限制写法
  • 探针策略
  • 日志和监控注解
  • 环境变量注入方式
  • 命名格式

如果模板不统一,测试环境里最容易出现的现场就是:

  • 有的服务有 readinessProbe,有的没有
  • 有的环境限制了 CPU 和内存,有的完全没限制
  • 有的服务带了统一标签,能查到环境归属,有的查不到
  • 有的环境改一处 values 就能起,有的要改 5 份 YAML

2. 参数层:把环境差异收进 values,而不是改模板正文

测试环境的差异通常主要来自:

  • 命名空间
  • 镜像版本
  • 副本数
  • 域名
  • 数据库连接
  • Redis、MQ、ES 等依赖地址
  • 功能开关
  • 资源额度

这些差异更适合放进参数层统一管理,而不是让每次部署都去改模板正文。

常见可执行做法是:

  • 每个项目保留一套基础模板
  • 每种环境类型保留一套 values 基线
  • 每次创建环境实例时,只生成本次实例的差异参数文件

例如可以按下面的目录组织:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
deploy/
charts/
app/
Chart.yaml
templates/
deployment.yaml
service.yaml
ingress.yaml
values/
base.yaml
test-regression.yaml
test-feature.yaml
test-performance.yaml
instances/
order-api/
feature-login-20230826.yaml
release-1-9-3.yaml

这样做的好处是:

  • 模板变更和实例差异分开
  • 环境回放更容易
  • 哪次环境用了什么参数能追溯
  • 后续清理时可以直接按实例文件定位资源

3. 入口层:部署动作必须有统一入口

测试环境里最危险的做法之一,就是让每个人直接在集群里手敲 kubectl apply -f

更合适的做法,是至少收成一个统一部署入口,入口可以是:

  • 平台上的环境创建按钮
  • Jenkins 或 Pipeline 参数化任务
  • GitOps 仓库合并后自动部署
  • 内部 CLI 工具

不管入口长什么样,至少要做到这几件事:

  • 环境实例名统一生成
  • 命名空间和资源标签自动带上
  • 默认资源额度自动注入
  • 基础探针和日志注解自动带上
  • 创建人、用途、过期时间自动记录

4. 校验层:部署成功不等于环境可用

很多测试环境“部署成功”只是资源对象创建成功。

真正的可用校验至少还应该包括:

  • Pod 全部 Ready
  • Service 可访问
  • Ingress 路由生效
  • 配置是否正确注入
  • 关键依赖连接可用
  • 核心健康检查通过

更适合实际落地的部署执行骨架如下:

  1. 创建环境实例记录,写入项目、分支、用途、申请人、过期时间。
  2. 生成命名空间、实例名、域名、镜像标签和参数文件。
  3. 执行模板渲染或 Helm 部署。
  4. 轮询 Deployment、StatefulSet、Job 等资源状态。
  5. 做健康检查、依赖探活和入口连通性校验。
  6. 生成环境访问地址、版本信息和资源占用摘要。
  7. 把环境实例写入生命周期队列,进入回收和巡检体系。

如果这一步只做到了第 3 步,后面的大量环境问题其实都是“半成品环境”带出来的。

四、资源与命名策略:测试环境最怕的不是资源少,而是资源没有边界

测试环境多了之后,最先失控的通常不是 YAML,而是命名和资源归属。

1. 命名策略要先回答“这是谁的哪一套环境”

测试环境的资源命名至少要能回答四个问题:

  • 属于哪个项目
  • 对应什么用途
  • 对应哪次实例
  • 生命周期何时结束

比较实用的命名结构通常类似这样:

1
<project>-<envType>-<branchOrVersion>-<serial>

例如:

1
2
3
orderapi-regression-r1-9-3-01
orderapi-feature-login-20230826-01
gateway-perf-20230826-01

命名不是为了好看,而是为了服务下面这些动作:

  • 快速识别资源归属
  • 做批量清理
  • 做资源统计
  • 做问题排查
  • 做访问地址映射

2. 标签策略比名字更重要

命名只能解决人眼识别问题,治理真正依赖的是标签。

测试环境里的 Kubernetes 资源,至少建议统一下面这些标签:

  • app
  • project
  • env.instance
  • env.type
  • owner
  • branch
  • version
  • expire_at
  • managed_by

这些标签的价值主要体现在:

  • 批量查询资源
  • 统计环境占用
  • 自动回收筛选
  • 故障定位时快速拉齐上下文

例如在排查时,如果统一有 env.instance 标签,就能非常快地把某套环境的 Deployment、Pod、Service、Ingress、ConfigMap、PVC 一次性查出来。

3. 资源请求和限制必须成基线

测试环境的资源边界如果不预先定好,扩容永远会变成“谁先喊卡谁先加”。

更适合的方式是按环境类型给出基线。

例如:

  • 功能联调环境:低副本、低配额、强调快速创建
  • 回归环境:固定副本、强调稳定性和观测
  • 性能摸底环境:独立额度、独立调度、限制并发数量
  • 临时问题复现环境:短生命周期、强制过期时间

资源基线至少要明确:

  • CPU request / limit
  • Memory request / limit
  • 副本基线
  • 存储额度
  • 是否允许 HPA
  • 是否允许占用独占节点

如果这层没有事先定义,扩容和隔离都无从谈起。

五、扩容链路:测试环境的扩容重点不是“把副本加上去”,而是先判断扩什么、为什么扩、扩多久

测试环境里说“要扩容”,通常至少有三种完全不同的背景:

  • 服务并发压力真的上来了
  • 当前副本不够导致测试阻塞
  • 其实不是容量问题,而是配置、依赖或数据问题

如果这三种情况不先区分,现场很容易出现“遇到慢就扩容、遇到失败就加副本”的误操作。

1. 扩容前要先做一次最小判断

更合适的扩容前检查通常包括:

  • 当前问题是 CPU、内存、连接数还是依赖瓶颈
  • Pod 是真的打满,还是 readiness 反复失败
  • 当前环境是单套问题,还是整个命名空间资源紧张
  • 扩容会不会把别的环境挤爆
  • 这次扩容是临时动作还是应该固化成基线

只有这些问题明确了,再决定扩什么。

2. 测试环境常见的扩容对象,不只有 Deployment

测试环境里的扩容对象常见有四类:

  • 应用副本数扩容
  • 节点池或节点资源扩容
  • 存储资源扩容
  • 周边依赖扩容,例如 Redis、MQ、ES 或数据库连接池

很多“扩了还是慢”的现场,本质上是只扩了应用层,没有扩真实瓶颈层。

3. 更适合测试环境的扩容策略,是“默认基线 + 临时升级 + 自动回落”

生产环境常常强调弹性自动化。

测试环境更实用的策略通常是:

  • 默认基线:每类环境有标准资源配置
  • 临时升级:专项测试期间提升副本或配额
  • 自动回落:专项结束后恢复到基线

这三步缺一不可。

如果只有升级,没有回落,测试集群最终一定会被历史临时扩容拖垮。

4. 一个可执行的扩容骨架

  1. 先确认当前问题是否真由容量引起。
  2. 确认扩容对象是应用、节点、存储还是依赖层。
  3. 判断扩容范围是单服务、单环境还是独立节点池。
  4. 记录本次扩容原因、起止时间、责任人和恢复时间。
  5. 执行扩容并观察关键指标:Ready 数、CPU、内存、错误率、时延、队列积压。
  6. 测试结束后自动回落到基线。
  7. 如果该问题高频出现,再回写基线模板和容量规划。

如果第 4 步和第 6 步缺失,这次扩容通常会在一个月后变成新的脏资源来源。

六、隔离链路:测试环境的隔离目标,是避免误伤,不是追求绝对独占

测试环境的隔离经常被理解成“每个项目一套独立集群”。

这当然最干净,但通常不是成本最合适的方案。

更常见也更现实的做法,是在共享集群里做分层隔离。

1. 命名空间隔离是第一层,但绝对不够

做了 Namespace,就以为隔离已经完成。

实际通常还差下面这些层:

  • ResourceQuota
  • LimitRange
  • NetworkPolicy
  • RBAC
  • 节点亲和和污点容忍
  • 存储目录或 PVC 隔离
  • 域名与入口隔离

Namespace 解决的是分组问题,不是资源抢占、网络互访和权限越界问题。

2. 测试环境更适合做三层隔离

比较实用的分层方式通常是:

第一层:环境类型隔离

例如把下面几类环境分开:

  • 日常联调
  • 版本回归
  • 性能专项
  • 问题复现

这样做的原因是,不同环境类型的稳定性要求和资源波动完全不同。

第二层:资源隔离

针对每个命名空间或环境类型,定义:

  • CPU 和内存总额度
  • 默认请求和上限
  • PVC 配额
  • 并发环境实例上限

这样可以避免单个团队把共享集群吃空。

第三层:调度隔离

对性能专项、压测、需要高 IO 的任务,建议单独打节点标签或单独节点池,避免和普通回归环境混跑。

如果这一层不做,最常见的现象就是:

  • 回归环境突然抖动
  • 巡检任务大面积超时
  • 各个团队都会觉得自己服务变慢了

本质上是某个高资源任务把共享节点打满。

3. 访问和权限也必须隔离

测试环境里还有一类很容易被忽略的隔离问题:

  • A 团队误操作了 B 团队环境
  • 公共域名覆盖了别的环境路由
  • Secret 在多个环境被混用
  • 日志和监控看不到实例边界

更适合的做法包括:

  • 不同环境实例使用独立入口前缀或子域名
  • RBAC 按项目或环境类型收权限
  • Secret 由模板引用,不直接跨环境复制
  • 日志、指标、链路追踪带环境实例标签

七、回收链路:测试环境的长期稳定,靠的不是人工自觉,而是自动过期和强制清理

测试环境最常见、也最容易被拖成历史包袱的问题,就是回收做不起来。

表面上看,测试环境里的资源很多都是临时创建的。

实际上,最容易长期残留的资源恰恰不是 Deployment,而是:

  • PVC
  • ConfigMap
  • Secret
  • Ingress
  • Job 和 CronJob
  • 临时域名配置
  • 环境实例元数据

1. 回收策略必须在创建时就写进去

最危险的方式,是环境先创建,等后面有人有空再清理。

更适合的做法是,环境创建时就强制写入:

  • 创建时间
  • 过期时间
  • 申请人
  • 用途
  • 是否允许续期
  • 默认回收动作

如果没有过期时间,所谓“临时环境”通常都会长期留下来。

2. 回收要分级,不是只做 delete namespace

测试环境的回收更适合分成三层:

第一层:软提醒

环境接近过期时:

  • 给申请人发提醒
  • 给项目群发提醒
  • 提供续期入口

第二层:冻结

超过过期时间但未续期时:

  • 停止对外入口
  • 缩容到最小副本
  • 标记为冻结环境

冻结的目的,是给业务方最后一次确认窗口,同时立刻释放大部分运行资源。

第三层:彻底清理

超过冻结窗口后:

  • 删除 Deployment、StatefulSet、Service、Ingress
  • 删除 PVC 或执行数据归档后删除
  • 删除 ConfigMap、Secret、Job
  • 删除环境实例记录和域名映射

3. 回收动作必须有白名单和保护对象

测试环境不能无脑批量删。

至少要预留下面几类保护规则:

  • 正在执行回归或压测的环境不回收
  • 标记为 protected=true 的环境不自动删
  • 挂着排障任务单的环境可以延长生命周期
  • 包含数据留证需求的环境先做快照或归档

4. 一个更实用的回收执行骨架

  1. 每次创建环境实例时,写入过期时间和默认回收策略。
  2. 定时任务扫描即将过期和已过期实例。
  3. 对即将过期环境发提醒并允许续期。
  4. 对已过期未续期环境先做冻结和缩容。
  5. 冻结窗口结束后执行彻底清理。
  6. 清理后做资源核对,确认 Deployment、PVC、Ingress、ConfigMap、Secret 没有残留。
  7. 输出回收日报,统计释放的 CPU、内存、存储和环境数量。

只有做到第 6 步,回收才算真的完成。

的问题不是没有回收脚本,而是删完核心资源之后,没有核对残留对象,结果集群里长期堆着一批“看不见但还在占资源”的历史垃圾。

八、建议直接落地的一套最小执行骨架

如果测试团队准备把 Kubernetes 测试环境治理真正落地,可以先按下面这套最小骨架推进,而不是一开始就追求特别重的平台。

1. 环境模型先统一

先明确下面这些字段:

  • 项目名
  • 环境类型
  • 环境实例名
  • 分支或版本
  • 申请人
  • 创建时间
  • 过期时间
  • 当前状态

2. 模板和参数分离

至少把:

  • 模板
  • 基线 values
  • 实例差异参数

这三层分开。

3. 强制资源标签和配额基线

无论是不是平台入口创建,都统一要求:

  • 必带环境实例标签
  • 必带过期时间标签
  • 必带 owner 标签
  • 必带资源 request 和 limit

4. 建立三类环境基线

建议最先定义这三类:

  • 联调环境
  • 回归环境
  • 专项环境

不同类型给不同的:

  • 默认副本
  • CPU/内存额度
  • 生命周期
  • 是否允许扩容
  • 是否允许独占节点

5. 先做自动提醒和自动冻结,再做彻底清理

直接一步删掉,风险太高。

更稳妥的推进顺序是:

  • 第一步:先有到期提醒
  • 第二步:再做自动冻结
  • 第三步:最后做彻底删除和残留核对

九、测试环境里最常见的 8 个坑

1. 环境名字能看懂,但查不全资源

原因通常是只有名字统一,没有标签统一。

2. 扩容生效了,但别的环境开始调度失败

原因通常是共享集群没有 ResourceQuota 或节点隔离,某个环境临时扩容挤占了公共额度。

3. Deployment 删了,存储和入口还留着

原因通常是回收只删工作负载,没有做残留资源核对。

4. 环境看起来起好了,但实际不可测

原因通常是只看资源创建成功,没有做健康检查、依赖探活和入口校验。

5. 一套环境被多个任务共用,最后谁都说不清状态

原因通常是环境实例和任务实例没有绑定,导致环境边界长期模糊。

6. 性能专项和普通回归放在一起,整个集群抖动

原因通常是高资源任务没有调度隔离。

7. 为了省事直接复制线上配置,结果测试环境互相污染

原因通常是 Secret、域名、数据库连接和回调地址没有做环境实例化。

8. 回收规则写了,但没有续期和保护机制

原因通常是回收策略只考虑“删不删”,没有考虑排障、留证和任务进行中状态。

十、真实案例:一场“扩容后更乱”的测试环境事故是怎么收敛的

场景

某次版本回归前,支付链路相关服务需要在测试集群中同时支撑下面三类任务:

  • 常规接口回归
  • UI 回归
  • 一轮支付峰值专项摸底

这三类任务原本都在同一个 Kubernetes 测试集群中运行。

为了赶进度,专项测试开始前,相关服务被临时从 2 个副本扩到了 6 个副本,同时消息消费服务也做了扩容,但没有调整命名空间配额,也没有把专项任务调度到独立节点。

执行

现场执行动作大致是:

  • 运维侧直接修改了 Deployment 副本数
  • 测试侧继续按原计划触发回归
  • 回归环境和专项环境共用同一套 Ingress 和公共依赖
  • 扩容动作没有登记恢复时间,也没有写回环境实例记录

现象

扩容后问题并没有缓解,反而出现了几类新现象:

  • 部分回归任务开始频繁超时
  • 多个服务 Pod 一直 Pending
  • 某些环境里的 Ingress 响应变慢
  • 集群节点 CPU 抬高,普通联调环境开始不稳定

表面上看像是“副本加了还不够”。

排查

沿着环境和资源链路排查后,问题逐步收敛到下面几层:

  1. 相关命名空间没有 ResourceQuota 保护,临时扩容把共享节点资源吃掉了。
  2. 性能专项和普通回归任务没有做节点隔离,高负载压测把普通任务一起拖慢。
  3. 扩容只改了应用副本,没有同步检查消息中间件和数据库连接池,依赖层先到瓶颈。
  4. 环境实例记录里没有扩容历史,值班同学最开始甚至不知道哪些服务被改过。
  5. 专项结束后没有自动回落策略,临时扩容本来还准备继续保留到第二天。

修复

最后的修复动作不是继续加副本,而是按治理链路做了四件事:

  1. 把专项测试迁到带独立标签的节点池,和回归环境彻底分开。
  2. 给命名空间补上 ResourceQuota 和 LimitRange,明确共享额度。
  3. 把扩容动作接入环境实例记录,要求写入原因、起止时间和回落时间。
  4. 给专项环境增加自动回落和到期回收规则,结束后恢复默认副本数。

修复后,同一套集群里的回归任务和专项任务不再互相误伤,后续再做临时扩容时,也能很快看清本次扩的是哪套环境、占用了多少额度、何时恢复。

这个案例说明一个很现实的问题:

测试环境里的扩容如果没有和隔离、配额、生命周期管理绑在一起,扩容本身就可能变成新的故障来源。

十一、结语

Kubernetes 放进测试环境之后,真正难的通常不是把服务部署起来,而是把环境体系治理起来。

测试环境里的部署、扩容、隔离和回收,本质上是一条完整的生命周期管理链路。

如果只做其中一段,现场通常还是会乱:

  • 只会部署,不会回收,资源会越来越脏
  • 只会扩容,不做隔离,集群会越来越抖
  • 只会分 Namespace,不做配额和权限,边界依然不清
  • 只会写 YAML,不建环境实例模型,后续根本管不住

更稳妥的推进方式通常是:

  1. 先统一环境实例模型。
  2. 再统一模板、参数和部署入口。
  3. 再补资源配额、节点隔离和扩缩规则。
  4. 最后把过期提醒、冻结和自动回收真正接起来。

做到这一步,Kubernetes 在测试环境里才不只是一个运行平台,而会真正变成一套可治理、可扩展、可回收的环境基础设施。