主题
容器平台弹性、任务与迁移
一句话:弹性、定时任务、Serverless 和迁移看似四件事,本质都是「工作负载的生命周期由谁控制」——指标、时间、请求或事件、人的计划;选错控制器,系统就会在错误的时间扩缩或停启。
适用版本:Kubernetes 1.27+(CronJob 时区字段稳定),Knative Serving 1.12+,KEDA 2.12+。默认值以官方文档为准。
为什么需要
Deployment 假设副本常驻。真实业务里至少四类负载不满足:流量有峰谷,需要按指标伸缩;任务按时间跑,跑完就该退出;请求稀疏且突发,空闲时不该占资源;集群要换架构或机房,工作负载要整体搬家。每类负载都需要一个「什么时候创建和销毁 Pod」的控制器,而不同控制器的语义差异很大。
平台如果把四类都塞进 Deployment,会得到一堆常驻的空转副本、跑不完的定时任务和无法回滚的迁移。
核心机制
HPA:三个时间尺度叠加
HPA 的公式很简单:期望副本 = ceil(当前副本 × 当前指标 / 目标指标),并带 10% 容差避免在目标值附近来回抖。难的是它处在三个时间尺度里:
- 采集:metrics-server 默认约 15 秒抓一次,HPA 控制器约 15 秒同步一次,从负载上升到副本变化通常有几十秒延迟。
- 决策:
behavior.scaleUp默认没有稳定窗口,可以快速扩容;scaleDown默认有 300 秒稳定窗口,取窗口内最大推荐值,所以缩容天然慢。 - 容量:HPA 只改
replicas,Pod 能不能调度取决于节点余量和 requests。没有节点自动伸缩或预留余量时,HPA 会把 Deployment 扩到 Pending。
防抖动的正确姿势不是把阈值调得很敏感,而是:目标值留出余量(如 CPU 70%,经验值)、缩容窗口足够长、用多指标时理解「取最大值」的语义、核心服务设 minReplicas 覆盖故障域。
指标来源决定适用性:CPU/内存是资源指标,适合计算密集型;QPS、队列长度是自定义指标,更接近真实压力,但需要 Prometheus Adapter 之类的管道。内存做 HPA 尤其要谨慎——内存涨上去不一定降下来,GC 后指标滞后。
三种伸缩模式的分工:
| 模式 | 依据 | 提前量 | 适用 | 失效方式 |
|---|---|---|---|---|
| 指标伸缩(HPA) | 实时 CPU/QPS/队列 | 无,滞后几十秒 | 日常波动 | 突发流量来不及 |
| 定时伸缩 | Cron 表达式 | 分钟级 | 可预测的峰谷(早晚高峰) | 节假日或异常不跟手 |
| 预测伸缩 | 历史序列预测未来 | 小时级 | 周期稳定的业务 | 历史不准就过度扩容 |
定时和预测伸缩最终也是改副本数,必须和 HPA 约定唯一控制者:要么定时任务只改 minReplicas,让 HPA 在区间内工作;要么在定时窗口内暂停 HPA。两个控制器同时改同一个 Deployment,会互相覆盖。
这张图有两个反馈环:指标 → HPA → 副本 → 指标是控制环;副本 → 调度 → 节点容量是约束环。只盯第一个环会以为 HPA 没生效,实际是第二个环卡住了——副本数已经变了,Pod 却一直 Pending。
CronJob:时间语义比任务本身更容易错
CronJob 的坑集中在四个字段:
concurrencyPolicy:Allow允许重叠,Forbid跳过本次(不是排队),Replace杀掉正在跑的再起新的。数据一致性要求高的用Forbid;状态同步只要最新结果的用Replace。startingDeadlineSeconds:错过计划时间后还允许启动多久;不设时,控制器如果长时间不可用,可能累积大量待调度任务。控制器错过超过 100 次调度会停止继续调度并报事件。timeZone:不显式设置就用控制器的本地时区,跨机房或跨云时会出现「明明配的凌晨 2 点,实际下午跑」。- 重试与超时:
backoffLimit(默认 6)、activeDeadlineSeconds、restartPolicy: OnFailure/Never。超时和重试要一起设,否则一个卡住的任务会一直占着并发名额。
CronJob 不保证「错过补跑」。集群故障期间的计划点会直接丢失;需要补跑的业务,要么在任务里按业务日期扫描缺口,要么用外部调度系统。任务本身必须幂等:重试、Replace、人工重跑都会造成同一业务键执行多次,靠唯一约束或状态机兜底,而不是假设「只会跑一次」。
清理也要设计:successfulJobsHistoryLimit(默认 3)和 failedJobsHistoryLimit(默认 1)只保留少量 Job,Pod 和日志会一起消失;要留痕就得把日志送到外部,而不是回头 kubectl logs。
Serverless:Knative 与 KEDA 的分工
两者都能缩到零,但控制点不同:
- Knative Serving 管 HTTP 请求路径:Service → Configuration → Revision,Route 按百分比切流量;没请求时缩到零,来请求时 Activator 先接住再拉起 Pod。它替换了 Deployment/Service/Ingress 这一套,应用要按 Knative 的方式部署。
- KEDA 管事件源:ScaledObject 读 Kafka lag、队列长度、Prometheus 指标,通过 HPA 调整现有 Deployment/Job 的副本,也能缩到零。它不改变应用,也不做流量路由。
所以选择很直接:请求驱动的 API、需要按 Revision 做灰度和快速回滚,用 Knative;消息队列消费、积压驱动、已有 Deployment 不想改造,用 KEDA。两者可以共存:入口用 Knative,异步消费用 KEDA。
边界同样清楚:Knative 的请求有超时(默认 300 秒),缩到零会终止空闲实例,长任务、WebSocket、有状态连接都不适合;冷启动取决于镜像大小、运行时初始化和并发目标,延迟敏感的服务要设 minScale=1,那等于放弃了缩到零的成本收益。
迁移:切换流量是最后一步
x86 → ARM 或机房迁移,常见错误是先切流量再验证微观差异。正确顺序是:
- 依赖盘点:镜像是否有目标架构的 manifest;有没有 JNI、本地库、特定 CPU 指令;Sidecar 和 Init Container 是否也有多架构镜像。
- 调度条件:目标集群的节点标签、污点、资源配额、存储类是否满足;
nodeSelector写死架构会让 Pod 永远 Pending。 - 数据与状态:有状态服务的 PVC、备份、复制延迟;缓存和会话是否依赖源机房。
- 网络与身份:负载均衡 IP、DNS TTL、证书、外部白名单(对方认的是源 IP)、注册中心地址。
- 目标侧验证:先小流量或影子流量,验证功能、性能、日志、监控,再灰度放大。
- 回滚窗口:源集群保持可运行,DNS TTL 提前调小;回滚时间由 TTL、数据同步方向和连接 draining 决定,不是固定的「5 分钟」。
只切流量的问题在于:镜像架构不匹配会在切换后变成 ImagePullBackOff;外部白名单没更新会让请求全部被拒;DNS TTL 太长会让回滚变成几十分钟;有状态数据没同步会让新集群读到旧数据。这些都不是流量层能发现的。
迁移在平台上应该是一条有状态的记录:准备、部署、验证、切换、下线,每一步有检查项和回滚动作;下线源集群要延迟到观察期结束。
固定 IP 与 IPv6:能力边界先于实现
固定 IP 的需求通常来自外部白名单或审计。它和弹性天然冲突:IP 池按副本数申请,HPA 扩容会超出池子,所以平台一般直接禁止「固定 IP + HPA」。同样,固定 IP 也不适合频繁滚动重建——每次重建都要保证拿回同一地址,这要求 CNI 的 IPAM 支持保留,或者用 StatefulSet 的稳定身份。
选型顺序:能用稳定 DNS 就不要固定 IP(StatefulSet + Headless Service 给的是稳定域名,不是 IP);必须固定地址时,用 CNI 的 IPPool/Whereabouts 或云网卡方案;固定 IP 的数量上限要大于最大副本数,并接受「不能弹性」的代价。详见 05-企业级K8s核心特性。
IPv6 是集群能力问题:CNI 要支持双栈,Service 要声明地址族策略,节点和负载均衡也要支持。平台能做的是在创建时检查集群能力并拒绝不支持的组合,而不是在发布后才发现解析不了。固定 IPv6 在不少 CNI 上仍不成熟,遇到这种需求应先确认基础设施,而不是先改应用。
探针与 Pod 生命周期的细节见 06-Pod生命周期与探针,本文不重复。
实战场景
一、HPA 把副本从 6 扩到 24,24 个 Pod 全在 Pending
现象:晚高峰 QPS 上升,HPA 判定 CPU 超过目标,把 Deployment 扩到 24 副本。但集群没有节点余量、也没开节点自动伸缩:新 Pod 全部 Pending,指标继续恶化,HPA 又继续加副本,最后 kubectl get events 里全是 Insufficient cpu,服务反而因为大量未就绪实例的调度活动变慢。
排查:HPA 只改 replicas,能不能调度取决于节点余量与 requests。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 期望 vs 实际 | kubectl get hpa 的 DESIRED/CURRENT 与 Pod Pending 数 | 差距持续即容量不足 |
| 调度失败原因 | Pending Pod 的 Events:0/N nodes are available: Insufficient cpu | 见 K8s 核心概念与调度 |
| 节点池余量 | 各节点 requests 分配率、可调度未就绪节点数 | 分配率长期 >85% 就是没有弹性缓冲 |
| 伸缩器状态 | Cluster Autoscaler 日志:是否达到 max node、是否被 pod 优先级/污点挡住 | CA 未生效时 HPA 等于空转 |
| 反应滞后 | metrics-server 约 15s 采集 + HPA 约 15s 同步 | 突发流量来不及(指标伸缩无提前量) |
| 容差与抖动 | 10% 容差之外的反复扩缩 | 目标值设得过敏感 |
对症做法:
yaml
# 1. 预留缓冲:给弹性留空节点,节点池按峰值的 1.3~1.5 倍配置(经验值,需校准)
# 2. minReplicas 覆盖故障域:单节点故障不能导致无实例可服务
spec:
minReplicas: 8 # = 支撑峰值所需副本 × 容忍故障比例
maxReplicas: 40
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 扩容快
policies: [{ type: Percent, value: 100, periodSeconds: 30 }]
scaleDown:
stabilizationWindowSeconds: 300 # 缩容慢(默认即 300s 稳定窗口)
selectPolicy: Maxtext
3. 弹性必须有“容量兜底”:
- 开 Cluster Autoscaler,并设 max nodes 与实例规格匹配业务;
- 或预留固定空闲节点(priority 低的批处理任务可被抢占腾位);
- 或给核心服务设 PriorityClass + 预留 quota,保证扩容优先落位。
4. 扩容速度受启动时间限制:
应用冷启动 90 秒时,HPA 的“反应”至少是采集滞后 + 决策滞后 + 启动时间三段之和;
对突发尖峰要靠限流/熔断/预热,而不是指望 HPA 兜住。
5. 可预测峰谷用定时伸缩提前抬 minReplicas,让 HPA 在区间内工作。复盘要点:判据是“扩容请求到 Pod Ready 的成功率”与“Pending 持续时间”——这两个数证明弹性真的存在。把“HPA 必须有节点伸缩或预留配套”作为准入规则:设了 maxReplicas 而没有容量兜底的服务不允许上线。
二、定时伸缩与 HPA 同时改副本数,早晚高峰被缩回去
现象:为了应付每天 09:30 的流量爬坡,加了 CronHPA 在 09:00 把副本数设为 20;HPA 同时也在运行。结果 09:05 左右 HPA 判断当前 CPU 利用率低(因为副本多),把副本缩回 8,09:30 高峰来时再花几分钟扩回来,期间接口超时。
排查:两个控制器都在维护“自己认为的期望状态”,会互相覆盖。
| 定位层 | 取证据 | 判定 |
|---|---|---|
谁在写 replicas | kubectl get deploy -o jsonpath='{.metadata.managedFields}' 的 manager 列表 | 出现两个 manager 即冲突 |
| HPA 行为 | kubectl describe hpa 的事件时间线(何时 scale down) | 缩容时间与高峰前时间重合即命中 |
| 稳定窗口 | scaleDown.stabilizationWindowSeconds 与取值策略 | 窗口内取最大推荐值,但仍会跟随低负载 |
| 定时任务的作用对象 | CronHPA 改的是 replicas 还是 minReplicas | 改 replicas 就是与 HPA 抢方向盘 |
| 指标滞后 | 扩容后 CPU 立即下降 → HPA 判定可缩 | 典型“扩了又缩”的振荡 |
| GitOps 同步 | 仓库里的固定副本数是否把两者都覆盖 | 第三方写入让冲突更隐蔽 |
对症做法:
yaml
# 正确分工:定时只抬地板,HPA 在区间内自由工作
# 09:00 的定时任务改的是 minReplicas,而不是 replicas
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: order-service }
spec:
minReplicas: 20 # 由定时任务在高峰窗口设为 20,窗口结束改回 8
maxReplicas: 40
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
behavior:
scaleDown:
stabilizationWindowSeconds: 600 # 高峰前禁止快速回落(经验值,需校准)text
1. 唯一控制者原则:同一 Deployment 的副本数只能有一个权威写入方。
需要两套机制时,用“地板/天花板”分层:定时改 minReplicas,HPA 在其上决策。
2. 或者在定时窗口内暂停 HPA(reconcile 冻结),窗口结束恢复,并在事后回写仓库;
这条路要求变更流程可控,一般只用于发布/大促这种短时场景。
3. 指标选型避免“扩容后指标立刻下降”的自毁反馈:
CPU 适合计算密集;请求排队类用 QPS/队列长度更接近真实压力。
4. 缩容要有“不再缩”的下限:minReplicas 覆盖故障域,
防止夜间低负载把副本压到 2,一次节点故障就没人服务。复盘要点:判据是“同一 Deployment 的 managedFields 里 manager 唯一”+“高峰窗口前 15 分钟不出现 scale down 事件”。这类问题的通用形状与 GitOps 覆盖、HPA 与手工 edit 打架是同一件事:声明式调谐不关心中间发生过什么,只看当前与期望之差,所以多写入方必然互相覆盖。
三、用内存做 HPA 指标,扩上去就缩不下来
现象:一个 JVM 服务用 memory 利用率做 HPA 目标(60%)。运行两周后副本数从 8 一路爬到 26 且不回落;重启 Pod 后立刻降回正常。内存曲线在 GC 后明显下降,但 HPA 指标依旧高位。
排查:内存指标的工作集语义与 JVM 堆驻留特性不匹配。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 指标定义 | HPA 用的是 working_set_bytes 还是堆使用量 | working_set 不随 GC 立即下降 |
| JVM 行为 | GC 日志中 Full GC 后堆是否归还操作系统(默认不主动归还) | 不归还即指标长期高位 |
| requests 设定 | requests.memory 与真实峰值 | requests 偏小会让利用率虚高 |
| 缩容条件 | HPA 缩容需连续低于目标且过稳定窗口 | 高位持续即永不满足 |
| 副作用 | 26 副本 × requests 造成的节点成本与调度压力 | 成本失控是最终症状 |
| 备选指标 | QPS、线程池队列、P99、队列积压 | 这些能反映压力且会回落 |
对症做法:
yaml
# 1. 资源指标用 CPU,内存交给 requests=limits 与驱逐策略保护,不做伸缩依据
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
# 2. 需要按压力伸缩时用自定义指标(Prometheus Adapter)
metrics:
- type: Pods
pods:
metric: { name: http_server_requests_per_second }
target: { type: AverageValue, averageValue: "120" }text
3. JVM 侧配合:
- 显式设 -Xmx 与容器 limits.memory 对齐,避免堆外与容器驱逐互相误判;
- 用 ZGC/G1 并观察是否启用堆内存自动调整(-XX:+UseContainerSupport);
- 明确“内存高不等于压力大”,把伸缩依据放回请求侧。
4. 内存类指标用于告警与容量评估是合适的,用于伸缩要谨慎:
涨上去不一定降下来,GC 后指标存在滞后。
5. 给伸缩设“最大副本”和成本上限,避免异常指标导致副本无限增长。复盘要点:判据是“伸缩指标能在负载回落后 15 分钟内把副本降回基线”,做不到就说明指标选错。三种伸缩模式的分工要写进平台文档:指标伸缩无提前量、定时伸缩分钟级、预测伸缩小时级,且它们最终都改副本数,必须约定唯一控制者。
最小上手
给一个带伸缩行为的 HPA,观察缩容为什么慢:
yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: order}
spec:
scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: order}
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource: {name: cpu, target: {type: Utilization, averageUtilization: 70}}
behavior:
scaleDown: {stabilizationWindowSeconds: 300}bash
kubectl apply -f hpa.yaml
kubectl get hpa order -w
kubectl describe hpa order | sed -n '/Events:/,$p'把负载打上去再撤掉,会看到扩容很快、缩容要等稳定窗口;Events 里能看到每次推荐的副本数和最终决策。
生产动作
- HPA:所有容器设 requests;核心服务
minReplicas覆盖故障域;节点侧配自动伸缩或预留余量;同一 Deployment 只允许一个控制器改副本,定时或预测伸缩与 HPA 用minReplicas或暂停窗口协调。 - CronJob:显式
timeZone、startingDeadlineSeconds、activeDeadlineSeconds;一致性任务用Forbid;日志外送;对失败和未执行告警。 - Serverless:延迟敏感设
minScale;明确请求超时;长任务改用 KEDA + Job;监控冷启动比例和成本。 - 迁移:多架构 manifest 作为准入门槛;切换前调小 DNS TTL;源集群保留到观察期结束;每一步可回滚。
- 固定 IP:池容量不小于最大副本;禁止与 HPA 同开;重建后验证地址真的没变。
边界
- HPA 既看不见业务语义,也不解决容量:CPU 低但队列积压很常见,要用自定义指标;副本数增加不等于能调度,节点容量是另一层问题。
- CronJob 不补跑:错过的计划点会丢失;「至少一次」要靠任务幂等和外部补偿。
- Serverless 不适合长任务:请求超时、缩到零、连接生命周期都会打断;有状态服务不要硬上。
- 迁移不能只切流量:镜像架构、依赖、数据、白名单、TTL 任一没准备好,切换都会放大故障。
- 固定 IP 与 IPv6 都受基础设施约束:固定 IP 与弹性互斥,StatefulSet 给的是稳定 DNS;IPv6 需要 CNI、节点、负载均衡同时支持,平台只能检查,不能补齐。
讲清楚
四类能力是同一个问题:谁来决定 Pod 的创建和销毁。HPA 按指标,受采集、稳定窗口和节点容量约束;CronJob 按时间,坑在并发、时区、错过不补跑和幂等;Serverless 按请求或事件,Knative 管 HTTP 流量,KEDA 管队列积压;迁移按人的计划,切流量只是最后一步。
弹性最容易误判的,是把「副本数变了」当成「扩容成功」:没有节点余量时 Pod 会 Pending,指标环和调度环要一起看。
迁移和固定 IP 的共性是能力边界:多架构镜像、数据同步、DNS TTL、IP 池大小,任一项不满足,自动化补不回来。