本文聚焦「弹性伸缩与 Serverless 混部」(原书第 3 章)与「资源成本与 FinOps」(原书第 9 章)。 写法是「白话建直觉 → 原理拆解 → 可运行配置 → 面试追问」,所有 YAML/bash/JSON 均为可落地模板,代码围栏成对出现。
一、KEDA 基于 Prometheus 指标伸缩
1.1 白话直觉:消息"清空"不等于"活干完了"
KEDA(Kubernetes Event-driven Autoscaler)像一个按外部事件计件的车间主任:它盯着消息队列的 Lag(积压量)决定开几条产线。问题出在"计件逻辑"本身——
类比:快递分拣中心按"待分拣包裹数"决定开几扇窗口。某时刻包裹数突然归零,主任立刻关窗。但真正的情形是:包裹已经被搬到窗口里的台面上正在拆,此时关窗会把正在拆的件直接扔掉。
Kafka Lag 突降为 0,只代表"还没拉取的新消息没了",已经 poll() 进消费者内存、正在被 CPU 处理的那批消息并不计入 Lag。于是 CPU 仍高、Lag 却为 0,KEDA 只会看到"没活了",从而过早缩容。
1.2 防过早缩容:Lag+CPU 组合判定
KEDA 的 ScaledObject 支持多个 trigger,默认用 max 策略合并(取各触发器算出的副本数最大值)。所以把"Kafka Lag"和"CPU 利用率"并列放进同一个 ScaledObject,天然得到:
最终副本数 = max( 按Lag算的副本 , 按CPU算的副本 )
只要 CPU 还高,缩容就不会发生。再配合 cooldownPeriod(缩容冷却)与 minReplicaCount(保底副本),双保险。
# scaledobject-combined.yaml
# 关键点:一个 ScaledObject 里同时挂 kafka 与 cpu 两个 trigger
# multipleScalersCalculation 默认就是 max,无需显式写
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-consumer
namespace: shop
spec:
scaleTargetRef:
name: order-consumer # 目标 Deployment
minReplicaCount: 2 # 保底 2 个,避免完全归零后冷启动
maxReplicaCount: 30
pollingInterval: 15 # 每 15s 采集一次指标
cooldownPeriod: 120 # 缩容前冷却 120s,给在途消息兜底
triggers:
# —— 触发器 1:Kafka Lag(事件驱动)——
- type: kafka
metadata:
bootstrapServers: kafka.svc:9092
consumerGroup: order-cg
topic: order-topic
lagThreshold: "50" # 每多 50 条积压就多加 1 个副本
activationLagThreshold: "5" # Lag>5 才激活(否则停在 minReplicaCount)
# —— 触发器 2:CPU 利用率(资源驱动)——
# 即使 Lag=0 但 CPU 仍高,这一项会"顶住"副本数不下降
- type: cpu
metricType: Utilization
metadata:
value: "60" # 目标 CPU 利用率 60%
# 部署并观察:人为让 Lag 归零,看副本是否仍被 CPU 顶住
kubectl apply -f scaledobject-combined.yaml
# 实时看 KEDA 推荐的副本数(HPA 对象由 KEDA 自动生成)
kubectl get hpa -n shop keda-hpa-order-consumer -w
# 制造"Lag=0 但 CPU 高"的现场:停止生产消息,同时让消费者跑满 CPU
# 预期:副本数不回落到 minReplicaCount 以下,因为 cpu trigger 生效
面试追问:为什么不直接把
cooldownPeriod调很大?因为冷却只延缓缩容,不解决"缩容决策本身错误"——CPU 高时本就不该缩。组合 trigger 才是治本。
1.3 KEDA ScaledJob 读 Kafka,控制最大并发度 500
ScaledJob 与 ScaledObject 的区别:它跑的是 Job 而不是长期 Deployment,适合"来一批消息就起一批短任务"的场景(如离线对账)。用 maxReplicaCount 卡死并发 Job 数上限。
# scaledjob-kafka.yaml
# 需求:从 Kafka 拉消息,最多同时跑 500 个 Job,每个 Job 处理一批
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: kafka-batch-job
namespace: batch
spec:
jobTargetRef:
template:
spec:
containers:
- name: worker
image: registry.example.com/batch-worker:v1
args: ["--batch-size", "200"]
resources:
requests:
cpu: "250m"
memory: "256Mi"
restartPolicy: Never
# —— 并发度控制核心 ——
maxReplicaCount: 500 # 同时最多 500 个 Job(Pod)
scalingStrategy:
strategy: "accurate" # accurate=尽量贴近目标
maxConcurrentJobs: 500 # 与 maxReplicaCount 对齐,硬上限
multipleScalersCalculation: "max"
# —— 触发源:Kafka Lag ——
triggers:
- type: kafka
metadata:
bootstrapServers: kafka.svc:9092
consumerGroup: batch-cg
topic: batch-topic
lagThreshold: "200" # 每 200 条积压起 1 个 Job
# 看正在跑的 Job 数,验证不会超过 500
kubectl get jobs -n batch | wc -l
# 若想临时压低并发做压测,改 maxReplicaCount 即可
kubectl patch scaledjob kafka-batch-job -n batch \
--type merge -p '{"spec":{"maxReplicaCount":100}}'
1.4 对 KEDA Operator 本身做 HPAScaleToZero
KEDA 控制面(operator + metrics-adapter)平时也要占资源。大集群里它可以缩到 0 省成本,但缩到 0 后谁来把它唤醒?答案是依赖 HPAScaleToZero 特性门控 + 一个常驻的"哨兵"——通常是 metrics-adapter 不能缩(它要对外提供指标),而 operator 可以缩。
# keda-operator-hpa.yaml
# 开启 HPAScaleToZero 后,HPA 允许 minReplicas=0
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: keda-operator
namespace: keda
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: keda-operator
minReplicas: 0 # 关键:允许缩到 0(需 API Server 开启 HPAScaleToZero)
maxReplicas: 2
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 40
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 安静 5 分钟才缩到 0
# 1) 确认 API Server 已开启特性门控(否则 minReplicas=0 会被拒绝)
kubectl get apiservice v1.autoscaling | grep -i hpascaletozero
# 或检查 kube-apiserver 启动参数含 --feature-gates=HPAScaleToZero=true
# 2) 部署 HPA
kubectl apply -f keda-operator-hpa.yaml
# 3) 验证:长时间无伸缩事件后 operator 副本归零
kubectl get deploy -n keda keda-operator
代价说明:operator 归零期间,ScaledObject 不会再被 reconcile,新消息不会触发扩容。所以只建议在流量极低、且 metrics-adapter 仍常驻的次要集群使用;核心生产环境不建议 operator 缩零。
flowchart TD
A[Kafka 新消息入队] --> B{Lag > activationLagThreshold?}
B -- 否 --> C[停在 minReplicaCount]
B -- 是 --> D[KEDA operator 计算副本]
D --> E[max by Lag]
D --> F[max by CPU]
E --> G[取较大值作为目标副本]
F --> G
G --> H[更新 HPA / 拉起 Job]
H --> I{CPU 仍高?}
I -- 是 --> G
I -- 否且 Lag=0 --> J[冷却后缩容]二、虚拟节点 ECI/Spot 混部成本优化
2.1 白话直觉:Spot 实例是"随时可能被收回的廉价厂房"
Spot/抢占式实例价格可能只有按量付费的 1~2 折,但云厂商有权随时回收。当回收率(单位时间内被收回的比例)>30%,意味着三成机器会"说没就没"。对"订单支付"这种不能失败的关键链路,必须设计优雅迁移窗:
类比:租用了一批"随时可能被房东收回"的临时仓库。合同里写明了"收回前 30 秒会给书面通知"。聪明做法是——收到通知立刻把里面值钱货搬去正规仓库,而不是等房东上门才慌。
2.2 Spot 回收率>30% 时设计优雅迁移窗
云厂商在回收前会通过元数据服务/事件发送终止信号(如阿里云 ECI 的 Preempt 事件、AWS Spot 的 instance-action 元数据,通常提前约 30s~2min)。我们要做三件事:
- 监听到终止信号 → 立刻
cordon该节点,新 Pod 不再调度上来; - 给在途 Pod 一个 迁移窗(如 60s)把状态落库/完成支付幂等回执;
- 关键 Pod 用
nodeAffinity/toleration强制只跑按量/包年的真实节点,绝不进 Spot。
# spot-tolerant-pod.yaml
# 允许跑在 Spot 虚拟节点上的"可中断"任务(如日志归档)
apiVersion: v1
kind: Pod
metadata:
name: log-archive
spec:
containers:
- name: archive
image: registry.example.com/archive:v1
tolerations:
- key: "eks.amazonaws.com/spot" # 容忍 Spot 污点(阿里云为 virtual-kubelet.io/provider)
operator: "Exists"
effect: "NoSchedule"
terminationGracePeriodSeconds: 60 # 收到 SIGTERM 后有 60s 优雅退出窗
# payment-pod-ondemand.yaml
# 支付类关键 Pod:用 nodeAffinity 反亲和 Spot,强制落在真实按量节点
apiVersion: v1
kind: Pod
metadata:
name: payment-core
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "eks.amazonaws.com/capacityType" # 阿里云为 node.kubernetes.io/instance-type 组合判断
operator: "NotIn"
values: ["SPOT"] # 绝不调度到 Spot
containers:
- name: pay
image: registry.example.com/pay:v1
# 监听 Spot 回收事件并自动 cordon(伪代码式脚本,可放进 DaemonSet)
# 阿里云 ECI:kubectl get events --field-selector reason=Preempt
# AWS:curl http://169.254.169.254/latest/meta-data/spot/instance-action
NODE=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
if [ -n "$NODE" ] && kubectl get node "$NODE" >/dev/null 2>&1; then
echo "收到 Spot 回收信号,cordon $NODE"
kubectl cordon "$NODE"
fi
2.3 基于 VK 将 GPU 工作负载调度到 ECI 的 NodeAffinity
Virtual-Kubelet(VK)把"云上弹性容器实例(如阿里云 ECI)“伪装成一个虚拟节点注册进 K8s。把 GPU 推理任务调度到 ECI 虚拟节点,既能免运维 GPU 机器,又能按需计费。
# gpu-to-eci.yaml
# 通过 nodeAffinity 选中 VK 虚拟节点,并声明 GPU 资源
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu-infer
spec:
replicas: 1
selector:
matchLabels: { app: gpu-infer }
template:
metadata:
labels: { app: gpu-infer }
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
# VK 虚拟节点通常会被打上 provider 标签
- key: "type"
operator: "In"
values: ["virtual-kubelet"] # 只去虚拟节点
- key: "kubernetes.io/role"
operator: "In"
values: ["eci"] # 且是 ECI
containers:
- name: infer
image: registry.example.com/infer:gpu
resources:
limits:
nvidia.com/gpu: 1 # 向虚拟节点申请 1 张 GPU
2.4 虚拟节点 Pod 设不同 QoS Class 触发不同计费
真实节点上 K8s 按 requests/limits 自动判定 QoS(Guaranteed/Burstable/BestEffort)。ECI 虚拟节点会映射 QoS 到不同计费档位:Guaranteed(requests=limits)通常按"预留规格"计费更贵但稳定;BestEffort(都不设)最便宜但有被驱逐风险。
# qos-guaranteed.yaml —— 稳定型,计费较高
apiVersion: v1
kind: Pod
metadata:
name: qos-guaranteed
annotations:
k8s.aliyun.com/eci-qos-class: "Guaranteed" # 显式声明(不同厂商注解键不同)
spec:
containers:
- name: c
image: nginx
resources:
requests: { cpu: "1", memory: "1Gi" }
limits: { cpu: "1", memory: "1Gi" } # requests==limits → Guaranteed
---
# qos-besteffort.yaml —— 弹性型,计费最低
apiVersion: v1
kind: Pod
metadata:
name: qos-besteffort
annotations:
k8s.aliyun.com/eci-qos-class: "BestEffort"
spec:
containers:
- name: c
image: nginx
# 不写 requests/limits → BestEffort,单价最低但优先被回收
flowchart LR
S[Spot/ECI 虚拟节点] -->|接收终止信号| W[优雅迁移窗 60s]
W -->|落库/回执| O[真实按量节点]
P[支付关键 Pod] -.->|nodeAffinity 反亲和| S
P --> O
G[GPU 任务] -->|nodeAffinity=virtual-kubelet| E[ECI 虚拟节点]
E -->|按 QoS Class 计费| B[BestEffort/Guaranteed 档位]三、基于 QPS 的预测式伸缩
3.1 白话直觉:别等水漫金山才掏沙袋
传统 HPA 是事后响应——CPU 已经高了才加机器,中间有一段"水位上涨期"用户已感知到慢。预测式伸缩像看天气预报提前加固堤坝:用历史 QPS 训练模型,提前 5 分钟把副本加上去。难点在于历史数据里的"毛刺”(秒杀、爬虫、故障重试)会污染模型,必须先清洗。
3.2 LSTM 清洗历史 QPS 毛刺
用滚动中位数 + MAD(中位数绝对偏差)识别异常点,再用线性插值替换,避免极端值误导 LSTM。
# clean_qps.py —— 清洗 QPS 毛刺,输出平滑训练集
import pandas as pd
import numpy as np
# 1) 读取历史 QPS(每分钟一个点)
df = pd.read_csv("qps_history.csv", parse_dates=["ts"])
qps = df["qps"].astype(float)
# 2) 滚动中位数 + MAD 检测毛刺
window = 15 # 15 分钟滚动窗口
median = qps.rolling(window, center=True, min_periods=3).median()
mad = (qps - median).abs().rolling(window, center=True, min_periods=3).median()
# 阈值:偏离中位数超过 3 倍 MAD 视为毛刺
mask = (qps - median).abs() > 3 * 1.4826 * mad
# 3) 毛刺点用前后线性插值替换
clean = qps.copy()
clean[mask] = np.nan
clean = clean.interpolate(method="linear")
print("被清洗的毛刺点数:", mask.sum())
clean.to_csv("qps_clean.csv", index=False)
# 4) 之后用 clean 序列训练 LSTM(此处省略模型代码,
# 重点:输入 [t-60..t] 预测 t+5min 的 QPS)
3.3 将预测结果写入 HPA v2 的 CRD
Kubeflow Pipelines 跑完预测后,把"未来 5 分钟建议副本数"写进 HorizontalPodAutoscaler(autoscaling/v2)的 spec.minReplicas,作为实时 HPA 的兜底下限,让实时伸缩只能在这个下限之上、上限之下活动。
# predict_to_hpa.sh
# 假设预测服务返回建议最小副本数
PRED_MIN=$(curl -s http://predictor.svc/predict?metric=qps | jq '.minReplicas')
# 用 kubectl patch 更新 HPA v2 的 minReplicas(CRD 字段)
kubectl patch hpa shop-frontend -n shop --type merge \
-p "{\"spec\":{\"minReplicas\": $PRED_MIN, \"maxReplicas\": 50}}"
echo "已写入预测下限 minReplicas=$PRED_MIN"
# 对应 HPA v2 模板(autoscaling/v2)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: shop-frontend
namespace: shop
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: shop-frontend
minReplicas: 5 # 由预测脚本动态写入
maxReplicas: 50 # 由预测脚本动态写入
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 55
3.4 预测伸缩与实时伸缩冲突时设优先级避免震荡
冲突场景:预测说"大促要来了,minReplicas 设 20",但此刻真实 CPU 很低,实时 HPA 想缩到 5。两套逻辑打架会反复横跳(抖动)。解法——预测定下限、实时管区间内、并加冷却:
最终副本 = clamp( 实时HPA算出的副本 , 预测minReplicas , maxReplicas )
且:实时缩容必须满足 scaleDown.stabilizationWindowSeconds
# 用 HPA behavior 给实时缩容加稳定窗,预测只碰 minReplicas
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: shop-frontend
namespace: shop
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: shop-frontend
minReplicas: 5
maxReplicas: 50
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 55 } }
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 实时缩容 5 分钟稳定窗,消除抖动
policies:
- type: Percent
value: 10
periodSeconds: 60 # 每次最多缩 10%
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
sequenceDiagram
participant P as 预测控制器
participant H as HPA v2
participant R as 实时指标(CPU)
P->>H: 写入 minReplicas=20(预测大促)
R->>H: 当前 CPU 低,建议 5
H->>H: final = clamp(5, 20, 50) = 20
Note over H: 实时只能缩到预测下限,不会跌破 20
R->>H: 大促真开始,CPU 高,建议 40
H->>H: final = clamp(40, 20, 50) = 40四、Serverless 工作流
4.1 白话直觉:Serverless 工作流像"乐高式流水线"
阿里云 FunctionFlow(FnF)把多个函数/步骤串成有状态的长流程,自带重试、补偿、状态持久化。它最怕两件事:①重试导致重复扣款(非幂等);②流程状态无限增长把存储和日志成本撑爆。
4.2 FunctionFlow 设重试策略避免幂等订单重复扣款
重试必须建立在幂等之上:每次扣款携带 idempotencyKey(订单号),服务端先查"这笔是否已扣",已扣则直接返回成功而不重复扣。FnF 重试配置里要排除"业务已成功但网络超时"这类不该再试的情况,并用指数退避+抖动。
# fnf-retry.yaml(概念配置,对应 FnF 的 Definition)
# 关键:重试仅对 transient 错误,且扣款步骤用 idempotencyKey 保证幂等
flow:
steps:
- name: deductPayment
type: task
resource: acs:fc:::services/pay/functions/deduct
input:
orderId: $.orderId
idempotencyKey: $.orderId # 同一订单号 → 幂等,重复调用不重复扣
retry:
maxAttempts: 3 # 最多重试 3 次
interval: 2 # 首次间隔 2s
backoff: 2.0 # 指数退避:2s,4s,8s
jitter: 1.0 # 加入随机抖动避免重试风暴
errors: # 仅对 transient 错误重试
- "ServiceUnavailable"
- "Timeout"
noRetryErrors: # 业务明确失败(如余额不足)绝不重试
- "InsufficientBalance"
- "DuplicatedOrder"
4.3 Serverless 工作流 + TableStore 实现库存扣减 Saga 事务
Saga 用"正向步骤 + 补偿步骤“替代分布式事务:扣库存失败就反向补偿已创建的订单。下面是基于 FnF 定义语言(JSON)的库存扣减 Saga。
{
"Name": "InventoryDeductSaga",
"StartAt": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "Task",
"Resource": "acs:fc:::services/order/functions/create",
"Next": "DeductInventory",
"Catch": [
{ "ErrorEquals": ["*"], "Next": "FailEnd" }
]
},
"DeductInventory": {
"Type": "Task",
"Resource": "acs:tablestore:::ots/Inventory/deduct",
"Next": "Pay",
"Catch": [
{ "ErrorEquals": ["InventoryNotEnough"], "Next": "CompensateCreateOrder" }
]
},
"Pay": {
"Type": "Task",
"Resource": "acs:fc:::services/pay/functions/deduct",
"End": true,
"Catch": [
{ "ErrorEquals": ["*"], "Next": "CompensateInventory" }
]
},
"CompensateInventory": {
"Type": "Task",
"Resource": "acs:tablestore:::ots/Inventory/rollback",
"Next": "CompensateCreateOrder"
},
"CompensateCreateOrder": {
"Type": "Task",
"Resource": "acs:fc:::services/order/functions/cancel",
"Next": "FailEnd"
},
"FailEnd": { "Type": "Fail", "Error": "SagaFailed" }
}
}
4.4 工作流执行历史 >90 天自动归档 OSS 并清理日志降本
FnF 的执行历史(每一步输入/输出)默认长期保留,90 天后价值低却占存储、还产生日志费用。用生命周期式归档脚本定期搬运到 OSS(冷存储)并清空控制台历史。
# archive_fnf_history.sh
# 把 90 天前的执行实例导出到 OSS,再删除本地历史以降本
OSS_BUCKET="oss://fnf-archive"
CUTOFF=$(date -d '90 days ago' +%Y-%m-%d)
# 1) 列出早于截止日的执行
for exec in $(fnf list-executions --flow InventoryDeductSaga \
--started-before "$CUTOFF" --query 'executions[].executionName' -o tsv); do
# 2) 导出执行详情到 OSS
fnf get-execution-history --execution "$exec" > /tmp/$exec.json
ossutil cp /tmp/$exec.json "$OSS_BUCKET/$exec.json"
# 3) 删除控制台历史(释放存储/日志成本)
fnf stop-execution --execution "$exec" >/dev/null 2>&1
done
echo "归档完成:早于 $CUTOFF 的执行已迁入 OSS"
flowchart TD
A[开始: CreateOrder] --> B[DeductInventory 扣库存]
B -->|成功| C[Pay 支付]
B -->|库存不足| R1[补偿: 取消订单]
C -->|成功| D[结束]
C -->|支付失败| R2[补偿: 回滚库存]
R2 --> R1
R1 --> E[Fail 终态]五、冷启动优化与镜像加速
5.1 白话直觉:镜像别"整箱搬”,要"按需取"
传统容器启动要把整个镜像层从仓库拉下来解压,几百 MB 甚至 GB,冷启动自然慢。镜像加速技术做两件事:①懒加载(Nydus)——先挂上"目录",用到哪块数据再拉哪块;②P2P 分发(Dragonfly)——节点间互相传,不让仓库被拉爆。
5.2 Nydus 懒加载:block 大小对 P99 启动延迟的影响
Nydus(RAFS 格式)把镜像切成若干 block,运行时按需从远端拉 block。block 太小 → 拉取请求数爆炸、元数据多;block 太大 → 单次拉取带宽浪费、首屏等待长。需要在 P99 启动延迟上做权衡实验。
# bench_nydus.sh —— 在不同 block 大小下测量 P99 启动延迟
for BS in 4K 64K 512K 1M; do
# 转换镜像为指定 block 大小的 Nydus 格式
nydus-image create --bootstrap bootstrap.json \
--blob blob.bin --block-size "$BS" --fs-version 5
# 启动容器并记录冷启动耗时(从 kubectl create 到 Ready)
START=$(date +%s%N)
kubectl run bench-$BS --image=registry.example.com/app:nydus-$BS
kubectl wait pod bench-$BS --for=condition=Ready --timeout=120s
END=$(date +%s%N)
echo "block=$BS 启动延迟=$(( (END-START)/1000000 ))ms"
done
# 结论:通常 512K~1M 在"拉取请求数"与"首屏带宽"间较平衡,P99 最优
5.3 Dragonfly 做 P2P 镜像分发并限制单节点带宽 50MB/s
Dragonfly 的 dfdaemon 作为镜像拉取代理,节点间 P2P 互传;用 rateLimit 卡住单节点出口带宽,防止拖垮业务网。
# dfdaemon-config.yaml
# Dragonfly dfdaemon 配置:开启 P2P 并限制单节点带宽 50MB/s
apiVersion: v1
kind: ConfigMap
metadata:
name: dfdaemon-config
namespace: dragonfly
data:
dfdaemon.yaml: |
verbose: false
p2p:
enable: true # 开启 P2P 分发
clusterID: 1
proxy:
registryMirrors:
- "https://registry.example.com" # 回源镜像仓库
defaultFilter: "overlayfs" # 仅 P2P 加速镜像层
# —— 关键:单节点带宽上限 50MB/s ——
# 单位 MB(不是 Mb),50MB/s ≈ 400Mbps
scheduler:
rateLimit: 50 # 单节点 P2P 上行限速 50MB/s
download:
perPeerRateLimit: 50M # 单 peer 下载限速 50MB/s
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: dfdaemon
namespace: dragonfly
spec:
selector:
matchLabels: { app: dfdaemon }
template:
metadata:
labels: { app: dfdaemon }
spec:
containers:
- name: dfdaemon
image: dragonflyoss/dfdaemon:v2.0.0
args: ["--config", "/etc/dragonfly/dfdaemon.yaml"]
volumeMounts:
- { name: cfg, mountPath: /etc/dragonfly }
volumes:
- name: cfg
configMap: { name: dfdaemon-config }
5.4 containerd 切到 crun:验证是否真正启用 runc 替代
crun(C 实现)比 runc(Go 实现)更轻更快。切换不是改个名字就完事——必须验证运行时真的走了 crun,否则只是"配置写了但没生效"。
# 1) 修改 containerd 配置,新增 crun runtime
# /etc/containerd/config.toml 增加:
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun]
# runtime_type = "io.containerd.runc.v2"
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun.options]
# BinaryName = "/usr/bin/crun"
sudo systemctl restart containerd
# 2) 部署一个测试 Pod,指定 runtimeClass=crun(或集群默认已切)
kubectl run crun-test --image=busybox --command -- sleep 3600
# 3) 验证:找到容器进程,看它用的是 crun 还是 runc
PID=$(kubectl get pod crun-test -o jsonpath='{.status.containerStatuses[0].containerID}' \
| sed 's#containerd://##' | xargs -I{} ctr -n k8s.io t asks {} 2>/dev/null; \
ps -eo pid,comm,args | grep -i crun | grep -v grep | head -1 | awk '{print $1}')
# 更直接:看进程命令行
ps -ef | grep -E "crun|runc" | grep -v grep
# 期望只看到 crun,看不到 runc —— 说明真正替代成功
# 4) 确认版本
crun --version
flowchart LR
P[Pod 创建请求] --> C{containerd 配置}
C -->|runtime=crun| R1[crun 启动容器]
C -->|未配置/写错| R2[runc 启动容器]
R1 --> V[ps 验证见 crun 进程]
R2 --> X[验证见 runc → 切换失败]六、实时成本展示与分摊(FinOps)
6.1 白话直觉:看不见的成本,就是失控的成本
FinOps 的核心是"谁花了多少钱,摊到哪个团队"。OpenCost / KubeCost 把 K8s 资源账单按 namespace、label、团队拆开,像给每个部门发"云资源水费单"。
6.2 对 GPU 节点按型号做差异化定价
不同 GPU(A100 贵、T4 便宜)单位成本天差地别。OpenCost 允许通过自定义定价配置覆盖默认价。
# opencost-gpu-pricing.yaml
# 在 OpenCost 的 pricing 配置里按节点标签区分 GPU 型号单价(美元/小时)
apiVersion: v1
kind: ConfigMap
metadata:
name: opencost-config
namespace: opencost
data:
# 自定义定价:以节点 label gpu-model 区分
"pricing.gpu.A100": "3.67" # A100 每卡小时价
"pricing.gpu.T4": "0.95" # T4 每卡小时价
# 或在 default.json 中按 instance type 映射
# 给 GPU 节点打型号标签,供 OpenCost 识别
kubectl label nodes gpu-node-01 gpu-model=A100
kubectl label nodes gpu-node-02 gpu-model=T4
# 查询某团队 GPU 成本
kubectl exec -n opencost deploy/opencost -- \
curl -s "localhost:9003/allocation?window=1d&filter=team=ml" | jq '.'
6.3 基于 KubeCost + Prometheus 的业务团队成本看板
KubeCost 以 Prometheus 为数据源,Grafana 出图。下面给出 dashboard 配置关键片段。
{
"title": "团队每日云成本",
"type": "timeseries",
"datasource": "Prometheus",
"targets": [
{
"expr": "sum by (team) (kubecost_allocation_cost_total{window=\"1d\"})",
"legendFormat": "{{team}}"
}
]
}
6.4 Spot 实例价格飙升自动飞书告警
当 Spot 单价超过阈值,立刻通知对应团队换规格,避免"省钱变烧钱"。
# spot-price-alert.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: spot-price-surge
namespace: monitoring
spec:
groups:
- name: cost
rules:
- alert: SpotPriceSurge
expr: aws_spot_price > 0.5 # 单价超 0.5$/h 触发
for: 5m
labels: { severity: warning }
annotations:
summary: "Spot 价格飙升"
feishu: "https://open.feishu.cn/open-apis/bot/v2/hook/xxxx"
# 告警路由到飞书(Alertmanager webhook 侧脚本节选)
curl -s -X POST "$FEISHU_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"⚠️ Spot 单价超阈值,请评估切换规格\"}}"
七、弹性伸缩成本优化(FinOps)
7.1 Karpenter NodePool 权重优先选择 AMD 实例
同规格下 AMD(如 m6a)常比 Intel 便宜 10%~15%。Karpenter 用 NodePool 的 weight 让调度器偏好便宜机型。
# karpenter-nodepool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: cheap-amd
spec:
template:
spec:
requirements:
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["m6a", "m5a", "c6a"] # AMD 家族优先
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m"]
nodeClassRef:
name: default
# 权重:数值越大越优先被选中
weight: 100
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: fallback-intel
spec:
template:
spec:
requirements:
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["m6i", "m5"]
weight: 10 # 仅当 AMD 不可用才 fallback
7.2 CAST AI 自动将低利用率 Pod 迁移到更小规格节点
CAST AI 持续分析利用率,把"大马拉小车"的 Pod 迁到更省的小节点。策略片段:
{
"rightsizing": {
"enabled": true,
"cpuHeadroom": "10%",
"memoryHeadroom": "15%",
"minSavingPercent": 15,
"excludeLabels": { "critical": "true" }
},
"automation": { "enabled": true, "action": "resize" }
}
7.3 HPA 与 Cluster Autoscaler 竞态:设扩容优先级避免浪费
经典竞态:HPA 先扩容 Pod,但集群没节点 → CA 加节点;CA 加得太慢或加太多 → Pod 又缩。解法:①给节点池设扩容优先级,便宜池先扩;②HPA 的 scaleUp 与 CA 的 scale-down-delay 错峰;③用 Pod priorityClassName 保证关键 Pod 先抢到新节点。
# 关键 Pod 高优先级,确保新节点先给它
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-cost-priority
value: 1000
globalDefault: false
description: "关键服务,扩容时优先获得节点"
---
# CA 节点池优先级(示例 annotation)
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-priority-expander
namespace: kube-system
data:
priorities: |
10:.*-spot.*$ # Spot 池优先级 10(便宜优先扩)
50:.*-ondemand.*$ # 按量池优先级 50(兜底)
flowchart TD
H[HPA 想扩容 Pod] --> C{有空闲节点?}
C -- 否 --> A[Cluster Autoscaler 加节点]
A -->|按优先级扩 Spot 池| S[Spot 节点先上]
A -->|Spot 不足| O[按量节点兜底]
S --> P[Pod 调度]
O --> P
C -- 是 --> P
P --> D{CA 检测节点空闲>10min}
D -- 是 --> R[缩容空闲节点省成本]八、资源限额与配额治理(FinOps)
8.1 OPA + Kyverno 双重准入防配额冲突
两个策略引擎同时挂为 Mutating/Validating Webhook 时,可能都去设默认值或都拒绝,导致 Pod 创建失败。解法:用 matchConditions 划分职责——OPA 管"安全合规类拒绝",Kyverno 管"默认值注入",互不重叠;failurePolicy: Fail 时尤其要错开。
# kyverno-mutate-quota.yaml —— Kyverno 只负责注入默认 resources
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: default-resources
spec:
rules:
- name: add-default
match:
any:
- resources:
kinds: ["Pod"]
mutate:
patchStrategicMerge:
spec:
containers:
- (name): "*"
resources:
requests:
cpu: "100m"
memory: "128Mi"
# opa-deny-no-team.yaml —— OPA 只负责"无 team 标签就拒绝"
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: opa-validating-webhook
webhooks:
- name: validation.openpolicyagent.org
failurePolicy: Fail
matchPolicy: Equivalent
# 与 Kyverno 错开:OPA 只校验 label,不注入
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
8.2 ResourceQuota + PriorityClass 保证核心服务资源
ResourceQuota 给 namespace 封顶;PriorityClass 让核心服务在资源争抢时优先被调度,不被挤压。
# resourcequota-core.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: core-quota
namespace: core
spec:
hard:
requests.cpu: "40"
requests.memory: 80Gi
limits.cpu: "80"
limits.memory: 160Gi
pods: "50"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: core-priority
value: 2000 # 高于其他业务,调度与抢占时优先
globalDefault: false
description: "核心服务,保障资源"
8.3 Namespace 配额耗尽自动扩容并通知财务
配额用尽时,自动化脚本临时提升 Quota 并触发飞书/邮件审批流,避免业务卡住。
# quota_auto_expand.sh
NS="team-a"
USED=$(kubectl get resourcequota -n $NS -o jsonpath='{.items[0].status.used.pods}' 2>/dev/null)
HARD=$(kubectl get resourcequota -n $NS -o jsonpath='{.items[0].spec.hard.pods}' 2>/dev/null)
if [ "$USED" = "$HARD" ]; then
# 临时 +20% 配额,保证业务不中断
kubectl patch resourcequota core-quota -n $NS --type merge \
-p "{\"spec\":{\"hard\":{\"pods\":\"$(($HARD*12/10))\"}}}"
# 通知财务走正式扩容审批
curl -s -X POST "$FEISHU_WEBHOOK" \
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"team-a 配额耗尽,已临时+20%,请审批\"}}"
fi
九、多云成本对比与竞价实例(FinOps)
9.1 基于 Spot Instance Advisor 预测回收概率
AWS Spot Instance Advisor 提供各机型"中断频率",可预测未来回收概率,挑低中断机型跑批处理。
# spot_advisor.sh
# 查询某机型中断频率(百分比越低越稳)
aws ec2 describe-spot-price-history \
--instance-types m5.large \
--product-descriptions "Linux/UNIX" \
--query 'SpotPriceHistory[0]'
# 中断频率来自 Advisor API(简化):频率 <10% 才用于可中断批任务
curl -s "https://spot-instance-advisor.com/api/v1/frequency" \
| jq '.[] | select(.instanceType=="m5.large" and .frequency<10)'
9.2 Terraform + Cloud Broker 跨云比价模块
用 Terraform 模块统一查询多云同规格单价,自动选最便宜。
# main.tf —— 跨云比价模块(示意)
module "price_compare" {
source = "github.com/example/cloud-broker"
instance_type = "c6.large"
clouds = ["aws", "aliyun", "azure"]
}
output "cheapest" {
value = module.price_compare.lowest_price_cloud # 输出最便宜的云
}
9.3 阿里云抢占式实例释放,自动在 AWS 申请同等规格 Spot 迁移
监听阿里云抢占释放事件,自动跨云申请等价 Spot 并把 Pod 重新调度。
# cross_cloud_migrate.sh
# 阿里云事件触发:抢占式实例即将释放
EVENT=$(aliyun ecs DescribeEvents | jq -r '.Events[].Reason' | grep -i preempt)
if [ -n "$EVENT" ]; then
SPEC=$(kubectl get node ali-spot-01 -o jsonpath='{.metadata.labels.instance-type}')
# 在 AWS 申请同等规格 Spot
aws ec2 request-spot-instances \
--instance-type "$SPEC" \
--spot-price "0.05" \
--target-capacity-specification '{ "TotalTargetCapacity": 1, "SpotTargetCapacity": 1 }'
# 将 Pod 从阿里云 Spot 节点驱逐,K8s 重新调度到新节点
kubectl cordon ali-spot-01
kubectl drain ali-spot-01 --ignore-daemonsets --delete-emptydir-data
fi
flowchart TD
A[阿里云抢占实例释放事件] --> B[获取规格 SPEC]
B --> C[在 AWS 申请同等 Spot]
C --> D[cordon 阿里节点并 drain]
D --> E[Pod 重调度到 AWS Spot]
E --> F[业务不中断,成本最低]十、碳排放与绿色计算(FinOps)
10.1 白话直觉:算力也有"碳账单"
每度电都对应碳排放。FinOps 的尽头是"用更少的能,办同样的事"——Kepler 用 eBPF 测每个 Pod 实际耗能,像给容器装了"电表"。
10.2 Kepler 对 Pod 级能耗实时估算并导出 Prometheus 指标
# kepler-deploy.yaml(节选)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kepler-exporter
namespace: kepler
spec:
template:
spec:
containers:
- name: kepler
image: quay.io/sustainable-computing-io/kepler:latest
securityContext:
privileged: true # 需 eBPF 权限采集能耗
args: ["--exporter.metric.labels=container,pod,namespace"]
ports:
- { containerPort: 9102 }
# 查询某 Pod 能耗(焦耳)与对应碳排
kubectl exec -n kepler ds/kepler-exporter -- \
curl -s localhost:9102/metrics | grep 'kepler_pod_joules_total'
# 碳排 = 能耗(kWh) × 区域电网碳强度(gCO2/kWh)
10.3 Scheduler Plugin 优先调度到 PUE<1.2 数据中心
PUE(电源使用效率)越低越绿。自定义调度插件给低 PUE 节点更高打分。
// lowpue_plugin.go(调度插件打分函数节选)
func (pl *LowPUE) Score(ctx context.Context, state *framework.CycleState,
pod *v1.Pod, nodeName string) (int64, *framework.Status) {
node := getNode(nodeName)
pue, _ := strconv.ParseFloat(node.Labels["datacenter.pue"], 64)
// PUE 越低分数越高:1.1 → 100 分,1.5 → 0 分
score := int64((1.5 - pue) / 0.4 * 100)
if score < 0 { score = 0 }
return score, nil
}
10.4 碳排超标自动降低非核心服务副本数并记录审计
# carbon_throttle.sh
CARBON=$(curl -s http://carbon-api/intensity | jq '.gco2_per_kwh')
if awk "BEGIN{exit !($CARBON > 500)}"; then # 碳强度超 500g 阈值
# 降非核心服务副本,核心服务不动
kubectl get deploy -n batch -o name | while read d; do
kubectl scale "$d" --replicas=1
done
echo "$(date) 碳排超标,已降非核心副本" >> /var/log/carbon-audit.log
fi
flowchart TD
K[Kepler eBPF 采集 Pod 能耗] --> P[Prometheus 指标]
P --> C[Carbon API 计算碳强度]
C -->|超阈值| T[降非核心副本+审计]
C -->|正常| N[维持]
S[Scheduler 低 PUE 插件] --> D[新 Pod 优先落低 PUE 节点]自测题与动手练习
概念理解题
- KEDA 的 Lag 突降为 0 但 CPU 仍高,为什么不能立刻缩容?用本文的"快递窗口"类比复述一遍。
ScaledObject与ScaledJob分别适合什么场景?最大并发度 500 是通过哪两个字段共同约束的?- Spot 回收率 >30% 时,“优雅迁移窗"要解决的核心风险是什么?支付类 Pod 如何从根本上避开 Spot?
配置实战题
4. 给出一个 ScaledObject YAML,使"Kafka Lag"和"CPU"共同决定副本数(取最大值),并说明 cooldownPeriod 的作用。
5. 写一段 kubectl patch 命令,把名为 api 的 HPA 的 minReplicas 设置为预测脚本算出的 12。
6. 用 nodeAffinity 写一段 Pod 配置,使其只调度到 VK 虚拟节点(label type=virtual-kubelet)且申请 1 张 GPU。
FinOps 综合题 7. OpenCost 如何对 A100/T4 做差异化定价?为什么要按型号而不是统一单价? 8. HPA 与 Cluster Autoscaler 竞态时,文中给出的三条化解手段是什么? 9. 当 Namespace 配额耗尽,自动化脚本做了哪两件事?为什么要"先临时扩容再走审批"而不是"直接拒绝”? 10. Kepler 估算 Pod 能耗依赖什么技术?碳排超阈值时为什么只降"非核心"服务副本?
动手练习
- 练习 A:在本机
kind集群装 KEDA,跑 3.3 节的ScaledObject组合触发器,用kubectl get hpa -w观察 Lag=0 但 CPU 高时副本是否被"顶住"。 - 练习 B:用文中 Dragonfly 配置起一个
dfdaemonDaemonSet,故意拉一个 1GB 镜像,用iftop验证单节点带宽是否真的被限制在 50MB/s。 - 练习 C:把 10.3 节的 Go 调度插件编译进自定义 scheduler,部署后
kubectl get events验证新 Pod 是否优先落到datacenter.pue=1.1的节点。
本章小结
- 弹性伸缩(第 3 章):KEDA 用"多触发器取 max"根治 Lag 归零但 CPU 仍高的过早缩容;
ScaledJob用maxReplicaCount+maxConcurrentJobs卡死并发;虚拟节点/Spot 混部靠"终止信号→cordon→迁移窗"保住关键链路,GPU 走 VK、QoS 触发差异化计费;预测式伸缩让"预测定下限、实时管区间内"消除抖动;Serverless 工作流靠幂等键 + Saga 补偿 + 历史归档降本;Nydus/Dragonfly/crun 把冷启动与分发成本压到最低。 - 成本治理(第 9 章):FinOps 的抓手是"看得见、摊得开、控得住"——OpenCost/KubeCost 做分摊,Karpenter/CAST AI 做弹性降本,ResourceQuota+PriorityClass 做配额护栏,跨云比价与 Spot 迁移做极致压价,Kepler+低 PUE 调度+碳阈节流把可持续性也纳入成本账本。
- 一条主线:弹性是把"资源供给"贴合"真实负载",FinOps 是把"资源开销"对齐"业务价值"——两者共同目标是用最少的钱,扛最稳的峰。
- KEDA 多触发器取 max:当 Kafka Lag=0 但 CPU 仍高时,仅看 Lag 会导致过早缩容;配置多个触发器取 max 才能同时兼顾消息堆积和计算压力。
- Spot 混部核心风险:云厂商提前 30 秒通知回收,关键链路必须在这 30 秒内完成 cordon + drain + 迁移到新节点,否则 Pod 直接消失。支付类服务绝对不能用 Spot。
- FinOps 三件套:OpenCost/KubeCost(看得见)+ Karpenter/CAST AI(弹性降本)+ ResourceQuota(护栏),三步缺一不可。
- 下一篇讲边缘计算——它把云原生的弹性能力延伸到了离用户最近的设备端。
设太小(如 5 秒):Lag 在阈值附近波动时,HPA 会反复缩扩容,产生"缩 - 扩-缩"抖动,增加调度开销和冷启动延迟。
设太大(如 10 分钟):流量骤降后不敢缩容,浪费资源成本;或者刚扩完的 Pod 还没来得及承载流量就被缩掉。
经验值:消费型场景(Kafka Lag 触发)通常 5