跳到正文

容器平台弹性、任务与迁移 ​

一句话:弹性、定时任务、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 或机房迁移,常见错误是先切流量再验证微观差异。正确顺序是:

  1. 依赖盘点:镜像是否有目标架构的 manifest;有没有 JNI、本地库、特定 CPU 指令;Sidecar 和 Init Container 是否也有多架构镜像。
  2. 调度条件:目标集群的节点标签、污点、资源配额、存储类是否满足;nodeSelector 写死架构会让 Pod 永远 Pending。
  3. 数据与状态:有状态服务的 PVC、备份、复制延迟;缓存和会话是否依赖源机房。
  4. 网络与身份:负载均衡 IP、DNS TTL、证书、外部白名单(对方认的是源 IP)、注册中心地址。
  5. 目标侧验证:先小流量或影子流量,验证功能、性能、日志、监控,再灰度放大。
  6. 回滚窗口:源集群保持可运行,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: Max
text
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 高峰来时再花几分钟扩回来,期间接口超时。

排查:两个控制器都在维护“自己认为的期望状态”,会互相覆盖。

定位层取证据判定
谁在写 replicaskubectl 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 池大小,任一项不满足,自动化补不回来。

把一门技术讲透到既懂也会用