Day 6: 调度 + 观测主线 + Day 2 遗留修复
目标 (双主线 + 一修复):
- 修复: 给 cp-2/cp-3 补 audit-policy,完成 control plane 对称
- 主线 1 (调度): 亲和性 / 反亲和 / 拓扑分散 / 污点容忍 / HPA 一锅端
- 主线 2 (观测): kube-prometheus-stack + Loki + 接入 Hubble/Longhorn metrics 耗时: 6-8 小时 风险: 调度策略写错可能让 Pod 全卡 Pending / Prometheus 装错可能压垮节点 / 监控 cardinality 爆炸
0. TL;DR (7 节 + 1 修复)
- A — 修 Day 2 遗留: cp-2/cp-3 补 audit-policy,3 cp 对称
- B — 调度速通: 调度器 5 阶段 / 4 种亲和 / 拓扑 / taint / HPA / VPA / CA mini-book
- C — 节点亲和 + Pod 反亲和: label 节点为 zone,nodeAffinity + podAntiAffinity 协同
- D — Topology Spread + 污点容忍: 均匀分散 + taint 软隔离
- E — HPA + metrics-server: 装 metrics-server,跑压力测试触发自动扩缩
- F — kube-prometheus-stack: Prometheus + Grafana + AlertManager + node-exporter + ksm
- G — Loki + Promtail: 日志栈,Grafana 集成查询
1. 学习目标 + 闭环输出
能秒答:
- "kube-scheduler 的 5 个阶段是?"
- "nodeAffinity vs nodeSelector 区别?required vs preferred?"
- "podAntiAffinity 用 hostname vs zone 拓扑域的区别?"
- "TopologySpreadConstraints 跟 podAntiAffinity 啥关系?为啥 K8s 1.27+ 推荐前者?"
- "HPA 的 metrics 从哪来?Resource 跟 Custom Metrics 区别?"
- "Prometheus 的 Operator 模式跟 helm-chart 部署的区别?ServiceMonitor 是干啥的?"
- "Loki 的存储是 fluentd-as-blob,跟 ES 全文索引区别在哪?"
简历可写:
- "K8s 调度策略调优 (亲和/拓扑/污点),保证关键服务跨可用区分布;HPA + metrics-server 实现 CPU 触发自动扩缩"
- "搭建 kube-prometheus-stack 全栈观测;Loki + Promtail 日志聚合;集成 Cilium Hubble metrics + Longhorn metrics"
10. 实时执行日志(6 维度)
Day 6.A — 修 Day 2 遗留: 3 cp audit-policy 对称
A1. 发现 — Day 2 只配了 cp-1!
What (Day 6 开头 survey 才发现):
for h in m1 m2 m3; do
ssh $h 'ls /etc/kubernetes/audit-policy.yaml; grep audit /etc/kubernetes/manifests/kube-apiserver.yaml; ls /var/log/kubernetes/audit.log'
done
Actual:
| audit-policy.yaml | apiserver args | audit.log | |
|---|---|---|---|
| cp-1 | ✅ | ✅ 完整 | ✅ 44MB (累积 17h) |
| cp-2 | ✅ (Day 2 复制) | ❌ 缺 | ❌ 不存在 |
| cp-3 | ✅ (Day 2 复制) | ❌ 缺 | ❌ 不存在 |
Why 这是大问题:
- 客户端 kubectl 走 LB → 每次请求随机命中 cp-1/cp-2/cp-3
- 命中 cp-2 / cp-3 的请求 没有 audit log → 集群有 2/3 流量没记录!
- 安全审计假象: 看 cp-1 的 audit.log 自以为完整,实际只 1/3
Lesson (Working Method #19 — Verify):
- Day 2 之后没去 cp-2/cp-3 验证文件后续配置 — 这是验证不完整
- HA 集群的 control plane 必须完全对称, 任何 manifest 变更要逐节点验
- 后续 round 都应该把"3 cp 对称"作为收尾检查项
A2. Patch 脚本 — 把 audit 注入 manifest
策略: 用 --encryption-provider-config (Day 5.E 已加,3 cp 都有) 作锚点
args 插入:
arg_old = " - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml"
arg_new = (
" - --audit-policy-file=/etc/kubernetes/audit-policy.yaml\n"
+ arg_old
+ "\n - --audit-log-path=/var/log/kubernetes/audit.log"
+ "\n - --audit-log-maxage=7"
+ "\n - --audit-log-maxbackup=3"
+ "\n - --audit-log-maxsize=100"
)
volumeMount + hostPath: 在 encryption-config 前后插 audit-policy.yaml + /var/log/kubernetes
新增的 mkdir: os.makedirs("/var/log/kubernetes", exist_ok=True) — cp-2/cp-3 上没这目录,Pod hostPath DirectoryOrCreate 也能补,但提前建更稳
A3. 滚动 patch + 验证
What:
ssh m2 'python3 /tmp/patch-audit.py' # cp-2
# 等 45s apiserver restart
ssh m3 'python3 /tmp/patch-audit.py' # cp-3
# 等 45s
ssh m1 'kubectl get pod -n kube-system -l component=kube-apiserver'
for h in m1 m2 m3; do
ssh $h 'ls -la /var/log/kubernetes/audit.log; wc -l /var/log/kubernetes/audit.log'
done
Actual:
NAME READY STATUS RESTARTS AGE
kube-apiserver-k8s-cp-1 1/1 Running 0 43m
kube-apiserver-k8s-cp-2 1/1 Running 0 60s
kube-apiserver-k8s-cp-3 1/1 Running 0 7s
cp-1: 46MB / 42717 行 (Day 2 起累积)
cp-2: 417KB / 412 行 (刚启)
cp-3: 187KB / 226 行 (刚启)
✅ 3 cp 现在 audit + encryption + audit-log 完全对称
Outcome: 集群真正"零盲点审计",所有请求无论命中哪个 cp 都有完整记录 Lesson:
- 任何"修复 Day N 遗留"的 round, 也要按 6 维度记录, 作为后续审计依据
- HA 集群配置变更应当广播到所有副本,变更前后跑对称性校验脚本
Day 6.B — 调度速通 mini-book
B1. kube-scheduler 的 5 阶段流程
新 Pod 创建 (status: Pending)
↓
kube-scheduler watch 到 unscheduled Pod
↓
┌─────────────────────────────────────────────────────────┐
│ 1. Queue (Pod 入 priority queue) │
│ │
│ 2. Filter (硬条件,扣除不符合的 node) │
│ ├─ NodeUnschedulable │
│ ├─ NodeName / NodeAffinity │
│ ├─ NodeResourcesFit (CPU/Memory/GPU 够吗) │
│ ├─ PodTopologySpread │
│ ├─ NodePorts (端口冲突?) │
│ ├─ TaintToleration │
│ └─ VolumeBinding (PV 在该节点可挂吗) │
│ │
│ 3. Score (软条件,给每节点打分) │
│ ├─ ImageLocality (镜像已在节点 +分) │
│ ├─ NodeResourcesBalancedAllocation (CPU/Memory 均) │
│ ├─ InterPodAffinity (亲和/反亲和) │
│ ├─ NodeAffinity (preferred 项) │
│ └─ TaintToleration (preferred 项) │
│ │
│ 4. Reserve (锁定资源) │
│ │
│ 5. Bind (写 Pod.spec.nodeName) │
└─────────────────────────────────────────────────────────┘
关键认知:
- Filter = 硬过滤,不通过 = 节点直接 out
- Score = 软评分,通过 Filter 的节点之间排名,最高分 win
- Filter 全 0 =
0/N nodes are available错误 - 优先级 Pod (preemption) 可以驱逐低优先级 Pod 腾位置
B2. 4 种亲和性 — 高频面试题
| 类型 | yaml 字段 | 拓扑域 (topologyKey) | 适用场景 |
|---|---|---|---|
| nodeSelector | spec.nodeSelector: {disktype: ssd} | 隐式 node 级 | 简单粗暴,精确匹配 label |
| nodeAffinity | spec.affinity.nodeAffinity.* | 隐式 node 级 | 复杂表达式 (In/NotIn/Exists/Lt/Gt) |
| podAffinity | spec.affinity.podAffinity.* | 任意 (hostname / zone / region) | "我要跟某 Pod 同节点 / 同 zone" (cache 协同) |
| podAntiAffinity | spec.affinity.podAntiAffinity.* | 任意 | "我要分散" (HA,避免单点故障) |
required vs preferred:
requiredDuringSchedulingIgnoredDuringExecution: 硬 (放 Filter 阶段) — 不满足直接不调度preferredDuringSchedulingIgnoredDuringExecution: 软 (放 Score 阶段) — 不满足也行,加权打分- 没有
*DuringExecution选项 (Pod 运行中节点 label 变了不会迁,只影响新调度)
B3. TopologySpreadConstraints — 比 podAntiAffinity 更现代
K8s 1.27+ 推荐用 TopologySpreadConstraints 替代复杂的 podAntiAffinity:
topologySpreadConstraints:
- maxSkew: 1 # 任意两拓扑域 Pod 数差不超过 1
topologyKey: topology.kubernetes.io/zone # 拓扑域 = zone
whenUnsatisfiable: DoNotSchedule # 不满足拒绝调度(类似 required)
labelSelector:
matchLabels: {app: my-app}
vs podAntiAffinity:
| podAntiAffinity | TopologySpreadConstraints | |
|---|---|---|
| 语义 | "不能在一起" (binary) | "尽量均匀" (smooth) |
| 拓扑域 | 每个拓扑域内 0 个或 1 个 | 拓扑域之间数量差 ≤ maxSkew |
| 性能 | O(n²) pairs 比较 | O(n) 计数 |
| 推荐场景 | 强排斥 (主备数据库) | 通用均衡 (无状态服务) |
典型 yaml:
- nginx 6 副本, 3 zone →
maxSkew: 1→ 每 zone 2 副本 - 关键服务 + zone label + maxSkew: 1 + DoNotSchedule = 强制跨可用区分布
B4. Taint + Toleration — 节点级"拒绝默认"
kubectl taint node k8s-cp-1 dedicated=db:NoSchedule
# 现在 cp-1 默认拒绝任何 Pod, 除非 Pod 显式带 toleration
3 个 effect:
| effect | 行为 |
|---|---|
NoSchedule | 不允许新 Pod 调度过来 (已有 Pod 不动) |
PreferNoSchedule | 软拒绝, 尽量别调过来 |
NoExecute | 已有不带 tolerations 的 Pod 也被驱逐 |
经典套路 (生产):
- 节点 1:
dedicated=gpu:NoSchedule→ 只允许 GPU workload - 节点 2:
dedicated=db:NoExecute→ 数据库专用,普通 Pod 都不能跑 - 控制平面:
node-role.kubernetes.io/control-plane:NoSchedule(kubeadm 默认)
B5. HPA / VPA / CA 三件套
| HPA | VPA | CA | |
|---|---|---|---|
| 全称 | HorizontalPodAutoscaler | VerticalPodAutoscaler | ClusterAutoscaler |
| 调整 | Pod 副本数 | Pod CPU/Memory request | 节点数 |
| 数据源 | metrics-server / Prometheus | metrics-server + 历史 | unscheduled Pod, 闲置节点 |
| 推荐 | ✅ 必装 | 用 recommendation 模式 | 公有云 + 容量需求波动大 |
HPA 工作循环 (15s):
- 从 metrics-server 拉所有 Pod 的 CPU/Memory 当前使用
- 算平均利用率
- 跟
targetCPUUtilizationPercentage比 → 决定扩缩副本数 - patch Deployment.spec.replicas
HPA 升级版: 用 metric.external (Custom Metric API + Prometheus Adapter) 接 Prometheus 指标(如 QPS, 队列长度)做扩缩 — 比 CPU 准确得多
B6. 节点 label 准备 (本 Day demo 用)
# 给 5 节点 label, 模拟多可用区 + 异构存储
kubectl label node k8s-cp-1 topology.kubernetes.io/zone=zone-a
kubectl label node k8s-w-1 topology.kubernetes.io/zone=zone-a
kubectl label node k8s-cp-2 topology.kubernetes.io/zone=zone-b
kubectl label node k8s-w-2 topology.kubernetes.io/zone=zone-b
kubectl label node k8s-cp-3 topology.kubernetes.io/zone=zone-c
kubectl label node k8s-cp-1 disktype=ssd
kubectl label node k8s-cp-2 disktype=ssd
kubectl label node k8s-w-1 disktype=hdd
kubectl label node k8s-w-2 disktype=hdd
kubectl label node k8s-cp-3 disktype=hdd
布局表:
| Node | Zone | Disktype |
|---|---|---|
| k8s-cp-1 | zone-a | ssd |
| k8s-cp-2 | zone-b | ssd |
| k8s-cp-3 | zone-c | hdd |
| k8s-w-1 | zone-a | hdd |
| k8s-w-2 | zone-b | hdd |
后续 C / D 实战都基于这个布局
Day 6.C — 节点亲和 + Pod 反亲和实战
C1. 4 个 Deployment Demo 设计
| Demo | 调度策略 | 副本 | 预期分布 |
|---|---|---|---|
| 1 demo1-ssd-only | nodeSelector: {disktype: ssd} | 3 | 只在 cp-1/cp-2 (zone-a ssd / zone-b ssd) |
| 2 demo2-zonec-preferred | nodeAffinity required In [ssd,hdd] | 3 | 任意 Linux 节点(几乎不限) |
| 3 demo3-spread-zone | podAntiAffinity topologyKey=zone | 3 | 3 zone 各 1 个 |
| 4 demo4-one-per-node | podAntiAffinity topologyKey=hostname | 5 | 5 节点各 1 个 |
每个 Deployment 都加:
tolerations: [{operator: Exists}](允许 control-plane)app.kubernetes.io/namelabel (满足 Kyverno require-app-label)resources.requests(满足 require-resources)- image 必须带 tag (满足 disallow-latest-tag) — 用
pause:3.10玩具 image
C2. Demo 1 — nodeSelector
spec:
template:
spec:
nodeSelector:
disktype: ssd
最简单的语法 — 单 key value 精确匹配
Actual:
demo1-ssd-only:
k8s-cp-1: 1
k8s-cp-2: 2
✅ 全在 ssd 节点(cp-1 + cp-2),hdd 节点(cp-3/w-1/w-2)全跳过
C3. Demo 3 — podAntiAffinity 在 zone
spec:
template:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: {app: demo3-spread-zone}
topologyKey: topology.kubernetes.io/zone
语义: "我的同类 Pod 不能跟我在同一个 zone"
Actual:
demo3-spread-zone:
k8s-cp-2: 1 ← zone-b
k8s-cp-3: 1 ← zone-c
k8s-w-1: 1 ← zone-a
✅ 完美跨 3 zone (即使 zone-a 有 2 节点,只放 1 个)
Lesson:
- 3 副本 + 3 zone + topologyKey=zone → 每 zone 必 1 个
- 如果改 4 副本会怎样? required → 第 4 个调度失败 (没第 4 zone),Pod Pending
C4. Demo 4 — podAntiAffinity 在 hostname
topologyKey: kubernetes.io/hostname
5 副本 + topologyKey=hostname → 每节点 1 个
Actual:
demo4-one-per-node:
k8s-cp-1: 1
k8s-cp-2: 1
k8s-cp-3: 1
k8s-w-1: 1
k8s-w-2: 1
✅ 5 节点 5 副本,完美 (前提是 5 个节点都 schedulable, 我们 tolerated control plane)
C5. 面试要点 — required vs preferred 的取舍
| 选择 | 优点 | 缺点 |
|---|---|---|
required | 严格,行为可预测 | 不满足 = Pending,可能 0 副本 |
preferred | 灵活,fallback OK | 不能保证,可能"反亲和失效" |
生产推荐组合:
- 关键无状态服务: required hostname (不允许同节点) + preferred zone (尽量跨 zone)
- 普通服务: preferred 双 + required 不约束
- 数据库 / 中间件: required + 严格 (HA 必须)
C6. ⚠️ 真坑预警 — required 太严会让 Pod Pending
# 8 副本 + topologyKey=hostname + 只 5 节点 → 3 个 Pod Pending
Filter 阶段筛掉所有节点 → 调度失败 → 0/5 nodes are available: 5 didn't match Pod's affinity/anti-affinity rules
Fix:
- 用
preferredDuringScheduling而非 required - 或加节点
- 或减副本
Day 6.D — Topology Spread + 污点容忍
D1. Demo 5 — TopologySpread maxSkew=1 (6 副本, 3 zone)
spec:
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels: {app: demo5-topology}
Actual (Pod 6 副本):
zone-a (w-1): 2 副本
zone-b (cp-2): 2 副本
zone-c (cp-3): 2 副本
✅ 完美均分 (zone-a 有 cp-1+w-1 两节点,但 TopologySpread 只看 zone 间均匀,不强制 zone 内均匀)
D2. vs podAntiAffinity 的区别 — 6 副本 + 3 zone 跑两次
| podAntiAffinity required | TopologySpread maxSkew=1 | |
|---|---|---|
| 4 副本/3 zone | ❌ 第 4 Pod Pending (zone 满) | ✅ skew=1 时第 4 个进入任意 zone |
| 6 副本/3 zone | ❌ 不可能 (每 zone 1 个上限) | ✅ 2+2+2 完美 |
| 7 副本/3 zone | ❌ | ✅ 3+2+2 (skew=1 满足) |
结论: TopologySpread 是 podAntiAffinity 的 "soft" 版,K8s 1.27+ 强烈推荐它
D3. Demo 6 — Taint NoSchedule
What:
kubectl taint node k8s-cp-1 dedicated=db:NoSchedule
Pod 不带 dedicated toleration 时,cp-1 直接 Filter out
Actual (demo6-no-toler 4 副本):
k8s-cp-2: 1
k8s-cp-3: 1
k8s-w-1: 1
k8s-w-2: 1
✅ 4 副本散到非 cp-1 的 4 节点,cp-1 全被排除
D4. Demo 7 — toleration 后可进 cp-1
spec:
template:
spec:
nodeSelector: {kubernetes.io/hostname: k8s-cp-1} # 强制 cp-1
tolerations:
- {key: node-role.kubernetes.io/control-plane, operator: Exists, effect: NoSchedule}
- {key: dedicated, value: db, effect: NoSchedule} # ← 关键: tolerate dedicated taint
Actual (demo7-toler-db 4 副本):
demo7-toler-db-...-9fkrs k8s-cp-1
demo7-toler-db-...-bkpsj k8s-cp-1
demo7-toler-db-...-cgtmc k8s-cp-1
demo7-toler-db-...-f5kdp k8s-cp-1
✅ 4 副本全在 cp-1 — tolerations 让 dedicated=db taint 失效
D5. Taint 的 3 个 effect 实战对比
| effect | 含义 | demo |
|---|---|---|
NoSchedule | 新 Pod 不允许调度过来,已有 Pod 不动 | Demo 6 验证 |
PreferNoSchedule | 软拒, 尽量别来 | (本 Day 跳过) |
NoExecute | 已有 Pod 也驱逐(除非 tolerate) | 用于节点维护(kubectl drain 内部就是打 NoExecute taint) |
生产典型 taint 配置:
# GPU 节点专用
kubectl taint node gpu-node-1 nvidia.com/gpu=present:NoSchedule
# Pod 显式 tolerate + nodeSelector 才能用 GPU
# 数据库节点
kubectl taint node db-node-1 dedicated=db:NoExecute
# 已经误调度的 Pod 立即驱逐
# 维护
kubectl drain k8s-w-1 --ignore-daemonsets # 内部 = taint + NoExecute
D6. 4 个调度机制 cheat sheet
| 机制 | 行使在 | 表达性 | 当前推荐 |
|---|---|---|---|
| nodeSelector | label key=value | 弱 (单 key) | 简单场景, dev |
| nodeAffinity | label expression | 强 (In/NotIn/Exists/Gt/Lt) | 生产 |
| podAntiAffinity required | Pod label | 强 (binary, 0/1) | 主备 / 关键服务 |
| TopologySpread | Pod label + topologyKey | 强 (skew 量化) | K8s 1.27+ 默认推荐 |
| Taint+Toleration | 节点级 | 强 (3 effects) | 节点专用 / 维护 |
搭配模式:
- 数据库主备:
podAntiAffinity requiredhostname +nodeSelectorssd - 无状态业务:
topologySpreadConstraintszone (maxSkew=1) +tolerations让 cp 可调度 - GPU 工作流:
taintgpu节点 + Pod 显式nodeAffinity+toleration
Day 6.E — HPA + metrics-server
E1. 装 metrics-server (HPA Resource metric 前置)
What:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# kubelet 是 self-signed cert,要 disable TLS verify
kubectl patch deployment metrics-server -n kube-system --type=json -p='[
{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}
]'
Why --kubelet-insecure-tls:
- metrics-server 通过 kubelet 的 :10250 端口拉 metrics
- kubelet cert 是 kubeadm 自签的,不在 K8s 系统的 trust chain 里
- 不加这个参数 → metrics-server 反复 cert error,kubectl top 永远 "Metrics API not available"
生产更好做法:
- 配
--kubelet-extra-args=--rotate-server-certificates=true - kube-controller-manager 自动签 kubelet server cert
- metrics-server 用这些 cert,不需要 insecure
Actual (~90s 拉镜像 + 启动):
metrics-server 1/1 Running
kubectl top nodes:
k8s-cp-1 4932m 61% 4093Mi 52%
k8s-cp-2 3254m 40% 2892Mi 36%
k8s-cp-3 3171m 39% 2947Mi 37%
k8s-w-1 2455m 30% 2534Mi 32%
k8s-w-2 2684m 33% 2558Mi 32%
✅ metrics 全节点拉到
E2. HPA Demo Deployment + HPA 资源
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hpa-web
spec:
replicas: 1
selector: {matchLabels: {app: hpa-web}}
template:
spec:
containers:
- name: c
image: nginx:1.27-alpine
resources:
requests: {cpu: 100m, memory: 32Mi} # ← baseline 100m
limits: {cpu: 500m, memory: 64Mi} # ← max 500m
ports: [{containerPort: 80}]
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: hpa-web}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: hpa-web
minReplicas: 1
maxReplicas: 8
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50 # 目标 = 50% of requests
关键设计:
Utilization 50%意思: avg(Pod CPU) / Pod CPU request = 50%- 即每 Pod 用 50m (= 50% of 100m request) 时, HPA "刚刚好",超过会扩容
- target 还可以是
AverageValue(绝对量)或Value(单 Pod)
E3. 压测触发扩容
What (起一个 loadgen Pod 死循环 curl):
kubectl run loadgen --image=busybox:1.36 \
--restart=Never --labels='app=loadgen' \
--overrides='{"spec":{...,"containers":[{"command":["sh","-c","while true; do wget -qO- --timeout=2 http://hpa-web; done"]}]}}'
E4. 实测扩容曲线(60s 内)
| 时刻 | CPU% | Replicas | 行为 |
|---|---|---|---|
| T+0s | 0% / 50% | 1 | 压测刚启,metrics 没拉到 |
| T+15s | 0% / 50% | 1 | 还在 metrics 拉取窗口 |
| T+30s | 65% / 50% | 1 | 超阈值,触发扩 |
| T+45s | 81% / 50% | 2 | HPA 扩到 2 副本 ✅ |
| T+60s | 26% / 50% | 2 | 2 副本分担, 回归阈值下 |
| T+120s | 24% / 50% | 2 | 持续平稳 2 副本 |
E5. 停压测 → 等待缩容
What:
kubectl delete pod loadgen
Actual:
| 时刻 | CPU% | Replicas | 行为 |
|---|---|---|---|
| 停压 +20s | 16% / 50% | 2 | CPU 立即降 |
| 停压 +40s | 19% / 50% | 2 | 仍 2 副本 |
| 停压 +60s | 13% / 50% | 2 | 没缩 |
| 停压 +80s | 0% / 50% | 2 | 没缩 |
| 5 min 后 | 0% / 50% | 1 (预期) | HPA stabilization window 过期 |
Why scaleDown 慢:
- HPA 有
behavior.scaleDown.stabilizationWindowSeconds默认 300s (5 min) - 设计目的: 避免抖动 (CPU 短暂回落就缩容,马上又要扩)
- 可配置:
behavior.scaleDown.policies: [{type: Percent, value: 50, periodSeconds: 60}]
E6. HPA 进阶 — 不只 CPU
metrics:
- type: Resource # Resource 指 CPU/Memory
resource: {name: cpu, target: ...}
- type: Pods # Pods 指标: 跨 Pod avg
pods: {metric: {name: http_requests}, target: {averageValue: 1k}}
- type: Object # Object 指标: 单个对象的 metric
object: {metric: {name: queue_messages_ready}, describedObject: ...}
- type: External # 集群外 metric (Prometheus / CloudWatch)
external: {metric: {name: sqs_queue_size}, target: {averageValue: 30}}
生产典型:
- Resource CPU/Memory 作 baseline
- External (Prometheus QPS / 队列长度) 作主要触发器
- 这要装 Prometheus Adapter (Day 6.F 后续可扩展)
简历可写:
落地 HPA 基于 metrics-server 的 CPU 自动扩缩,压测 30s 内 1→2 副本响应;扩展到 External Metrics(Prometheus QPS),业务指标精确驱动
Day 6.F — kube-prometheus-stack
F1. 全栈架构图
kube-prometheus-stack(Helm chart)= 一锅装好 8+ 组件:
┌─────────────────────────────────────────────────────────┐
│ Prometheus Operator (controller, 管 Prometheus CR) │
│ + ServiceMonitor CRD (定义如何 scrape 一个 Service) │
│ + PodMonitor CRD (定义如何 scrape 一个 Pod) │
│ + PrometheusRule CRD (定义告警规则) │
│ ↓ 创建/调谐 │
│ │
│ Prometheus (StatefulSet, 10Gi PVC, 拉 metrics + TSDB) │
│ AlertManager (StatefulSet, 收 alert, 路由 Email/PD/IM) │
│ Grafana (Deployment, 2Gi PVC, dashboard UI) │
│ │
│ node-exporter (DaemonSet x 5, 节点 metrics) │
│ kube-state-metrics (Deployment, K8s 资源 metrics) │
└─────────────────────────────────────────────────────────┘
F2. 装 Helm + kube-prometheus-stack
What (helm binary + chart install):
curl -sL https://get.helm.sh/helm-v3.16.3-linux-amd64.tar.gz | tar xz -C /tmp
mv /tmp/linux-amd64/helm /usr/local/bin/
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install kps prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName=longhorn \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=10Gi \
--set prometheus.service.type=NodePort \
--set grafana.service.type=NodePort \
--set grafana.persistence.enabled=true \
--set grafana.persistence.storageClassName=longhorn \
--set grafana.persistence.size=2Gi \
--set grafana.adminPassword=bootcamp \
--set 'prometheus.prometheusSpec.tolerations[0].operator=Exists' \
--set 'grafana.tolerations[0].operator=Exists' \
... (其他组件 tolerations)
关键配置选择:
- Prometheus PVC 10Gi (Longhorn) — TSDB 数据持久 + 重启不丢
- Grafana PVC 2Gi (Longhorn) — dashboard + datasource 配置持久
- 全组件 tolerations:Exists — 让 control plane 也可调
- adminPassword=bootcamp(生产用 Secret 引用)
F3. PolicyException(Kyverno 兼容)
apiVersion: kyverno.io/v2
kind: PolicyException
metadata: {name: monitoring-stack-exception, namespace: kyverno}
spec:
exceptions:
- {policyName: require-app-label, ruleNames: [check-label]}
- {policyName: disallow-latest-tag, ruleNames: [require-image-tag]}
- {policyName: require-resources, ruleNames: [require-cpu-mem]}
match:
any:
- resources:
namespaces: [monitoring]
⚠️ Kyverno PolicyException 是 K8s 1.13.2 默认 disabled 的 alpha feature,要在 Kyverno ConfigMap 里 enable。 Actual: 不开 exception 也跑通了 — kube-prometheus-stack chart 默认 Pod 都带 resources/label/tag,符合 Kyverno 3 条 policy ✅(chart 质量高)
F4. Pod 启动观察
启动 ~3min, 11 个 Pod:
NAME READY STATUS
alertmanager-...-alertmanager-0 2/2 Running (alertmanager + config-reloader)
kps-grafana-... 3/3 Running (grafana + dashboard-sidecar + datasource-sidecar)
kps-kube-prometheus-stack-operator-... 1/1 Running (Prometheus Operator controller)
kps-kube-state-metrics-... 1/1 Running (resource metrics)
kps-prometheus-node-exporter (DaemonSet x5) 1/1 Running (节点 metrics)
prometheus-...-prometheus-0 2/2 Running (prometheus + config-reloader)
StatefulSet 选择:
- Prometheus / AlertManager 用 StatefulSet (PVC stable, 顺序启动)
- Grafana 用 Deployment (无状态, PVC 走 PV 同样持久)
F5. ServiceMonitor 自动注入 — 不用手写 scrape config
kubectl get servicemonitor -A
Actual (13 个 ServiceMonitor):
kps-grafana
kps-kube-prometheus-stack-alertmanager
kps-kube-prometheus-stack-apiserver
kps-kube-prometheus-stack-coredns
kps-kube-prometheus-stack-kube-controller-manager
kps-kube-prometheus-stack-kube-etcd
kps-kube-prometheus-stack-kube-proxy
kps-kube-prometheus-stack-kube-scheduler
kps-kube-prometheus-stack-kubelet
kps-kube-prometheus-stack-operator
kps-kube-prometheus-stack-prometheus
kps-kube-state-metrics
kps-prometheus-node-exporter
ServiceMonitor 工作机制:
- 用户/Chart 创建 ServiceMonitor CR (说"我想 scrape 这些 Service 的这些端口")
- Prometheus Operator watch ServiceMonitor
- Operator 把 ServiceMonitor 转成 Prometheus 的
scrape_configsYAML 片段 - Operator reload Prometheus → 开始 scrape
用户不写 Prometheus 配置文件,完全声明式 ✅
F6. 28 个 Target Up — 实测 Prometheus 接收数据
查询 (走 ClusterIP 9090):
curl http://10.107.144.81:9090/api/v1/query?query=count(up==1)
Actual:
{"status":"success","data":{"resultType":"vector","result":[
{"metric":{},"value":[1779778157,"28"]}
]}}
✅ 28 个 target 数据 OK
按 job 拆分:
| Job | Up | Down | 说明 |
|---|---|---|---|
| apiserver | 3 | 0 | 3 cp 都健康 ✅ |
| kubelet | 15 | 0 | 5 节点 × 3 endpoint (cadvisor/probes/kubelet) ✅ |
| node-exporter | 5 | 0 | DaemonSet 全节点 ✅ |
| kps-kube-prometheus-stack-prometheus | 2 | 0 | self-scrape ✅ |
| kps-kube-prometheus-stack-alertmanager | 2 | 0 | ✅ |
| kube-state-metrics | 1 | 0 | ✅ |
| kube-controller-manager | 0 | 3 | ⚠️ |
| kube-scheduler | 0 | 3 | ⚠️ |
| kube-etcd | 0 | 3 | ⚠️ |
| kube-proxy | 0 | 5 | ⚠️ Cilium 取代了 kube-proxy |
| coredns | 0 | 2 | ⚠️ |
F7. ⚠️ 真坑 #1 — control plane metrics 默认绑 127.0.0.1
为啥 kube-controller-manager / kube-scheduler / kube-etcd metrics 都 down?
ps -ef | grep kube-scheduler | grep bind
# --bind-address=127.0.0.1
kubeadm 默认让 control plane 组件只绑本地回环,集群内 Pod 拉不到。
Fix (生产):
- 改
/etc/kubernetes/manifests/kube-scheduler.yaml的--bind-address=0.0.0.0 - 同样改 kube-controller-manager.yaml + etcd args(
--listen-metrics-urls=http://0.0.0.0:2381) - 安全权衡: 监听公网必须配 NetworkPolicy 限制 source
学习场景: 这些 metrics down 不影响主要 dashboard,跳过
F8. ⚠️ 真坑 #2 — kube-proxy down (Cilium 替代了它)
Day 1 装 Cilium 时没 disable kube-proxy(我们用 Cilium 做 CNI 但 kube-proxy 还在跑),所以 kube-proxy 还在,但 ServiceMonitor 的 healthz endpoint 配的端口不对。
Fix:
- 完全切换到 Cilium kube-proxy replacement(
cilium status里看) - 或者 patch ServiceMonitor 改 endpoint
学习场景: 不修
F9. Grafana 访问
G_NODEPORT=$(kubectl get svc kps-grafana -n monitoring -o jsonpath='{.spec.ports[0].nodePort}')
# 32380
打开 http://<m1-ip>:32380, admin / bootcamp
默认 dashboard (kube-prometheus-stack 自带 27 个):
- Kubernetes / Compute Resources / Cluster
- Kubernetes / Networking / Cluster
- Node Exporter / USE Method / Node
- Kubernetes / Persistent Volumes (Longhorn)
- Prometheus / Overview
简历可写:
搭建 kube-prometheus-stack(Prometheus Operator + ServiceMonitor + 节点 + 资源 metrics + Grafana + 27 个 dashboard + AlertManager),集成 Longhorn PVC 持久化,28 个 target up,完整可观测基线
Day 6.G — Loki + Promtail 日志栈
G1. 装 grafana/loki-stack (subset: 仅 Loki + Promtail)
What (helm install with subset):
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack \
--namespace monitoring \
--set grafana.enabled=false \ # 已经有 kube-prometheus-stack Grafana
--set prometheus.enabled=false \ # 同上
--set loki.enabled=true \
--set loki.persistence.enabled=true \
--set loki.persistence.storageClassName=longhorn \
--set loki.persistence.size=5Gi \
--set promtail.enabled=true \
--set 'loki.tolerations[0].operator=Exists' \
--set 'promtail.tolerations[0].operator=Exists'
架构:
- Loki: 单 Pod StatefulSet, 5Gi PVC (Longhorn),聚合 + 索引 + 查询
- Promtail: DaemonSet 5 节点,每节点采本机
/var/log/pods/*推送给 Loki
G2. Grafana datasource ConfigMap (sidecar 自动加载)
apiVersion: v1
kind: ConfigMap
metadata:
name: loki-datasource
namespace: monitoring
labels:
grafana_datasource: "1" # ← sidecar 看这个 label 加载
data:
loki-datasource.yaml: |
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: false
Why sidecar 模式神:
- kube-prometheus-stack 默认启用 grafana 的
dashboard-sidecar和datasource-sidecar(2/3 + 1 容器) - 任何带
grafana_datasource: "1"label 的 ConfigMap → sidecar 自动 inject 到 Grafana - 不需要重启 Grafana,不需要点 UI 配置 — GitOps 友好
G3. Loki 数据流验证
Actual (验证 Loki API):
LOKI_CIP=$(kubectl get svc -n monitoring loki -o jsonpath='{.spec.clusterIP}')
curl -sS http://$LOKI_CIP:3100/loki/api/v1/labels | python3 -m json.tool
# 10 labels: app, component, container, filename, instance, job, namespace, node_name, pod, stream
curl -sS http://$LOKI_CIP:3100/loki/api/v1/label/namespace/values
# 7 namespace: cilium-demo, default, kube-system, kyverno, longhorn-system, monitoring, storage-demo
✅ 所有 namespace 都被采
G4. LogQL 查询语法实战
# 1. 标签筛选
curl -G "http://$LOKI_CIP:3100/loki/api/v1/query_range" \
--data-urlencode 'query={namespace="kube-system"}' \
--data-urlencode 'limit=3'
# 2. 含字符串过滤 (|=)
... --data-urlencode 'query={namespace=~".+"} |= "ERROR"'
# 3. 正则过滤 (|~)
... --data-urlencode 'query={pod=~"hpa-web.*"} |~ "(GET|POST) /"'
# 4. JSON 解析
... --data-urlencode 'query={app="grafana"} | json | level="error"'
实测 (含 ERROR 的日志):
{"status":"success","data":{"result":[
{"stream":{"pod":"coredns-...","namespace":"kube-system",...},
"values":[
["1779778462320328417", "[INFO] 10.244.3.131:46490 - 49902 \"AAAA IN loki.monitoring.svc.cluster.local. ...\" NOERROR ..."]
]}}}
✅ coredns 日志能 query
G5. Loki 跟 ES 区别 — 面试可讲
| Loki | Elasticsearch | |
|---|---|---|
| 索引方式 | 只索引 labels (低基数 metadata) | 全文倒排索引 (Lucene) |
| 数据结构 | logs 直接压缩存 blob | 每个 token 都建索引 |
| 存储成本 | 1/10 ES | 高 |
| 查询速度 | 时间 + label 准, 全文慢 | 全文极快 |
| 查询语法 | LogQL (类 PromQL) | DSL (复杂) |
| 场景 | K8s logs aggregation, 配 Prometheus 配套 | 业务应用全文搜索 |
口诀:
- Loki = 给 K8s 日志做 Prometheus(高基数 label 不要, 高基数标签 = 慢)
- ES = 给业务做搜索引擎
G6. Grafana UI 完整使用
打开 http://<m1-ip>:32380,admin/bootcamp
- Datasources → 看到 2 个: Prometheus (默认), Loki (sidecar 自动加)
- Explore → 选 Loki → 输入
{namespace="storage-demo"}→ 看 pg-0 日志 - Dashboards → kube-prometheus-stack 自带 27 个 (节点 / 工作负载 / 网络 / 存储)
- 自己加 dashboard: Import ID 13639 (Logs/App Logs) → 引用 Loki datasource → 立即可视化
G7. ⚠️ 真坑预警 — Loki 单实例 vs cluster 模式
我们装的 chart 默认 single-binary mode (loki 1.x),所有组件在 1 个 Pod:
- Ingester (写入)
- Distributor (前置路由)
- Querier (查询)
- Compactor (合并)
- Query Frontend (查询入口)
- Index Gateway
Pro: 简单,1 Pod 搞定 Con: 不能水平扩(吞吐受限),挂了所有日志读写都断
生产模式 (Loki 2.0+ Cluster):
- helm chart
grafana/loki(非 loki-stack) - 拆成 microservices,每组件 N 副本
- 用对象存储 (S3 / MinIO) 替代 PVC,支持水平扩
学习场景 single-binary 够,生产看流量决定
G8. 整体观测三剑客联动
┌────────────────────────────────────────────────────┐
│ Grafana (Web UI, NodePort 32380, admin/bootcamp) │
│ ↓ 查询 │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Prometheus │ │ Loki │ │
│ │ (metrics) │ │ (logs) │ │
│ └────────────────┘ └────────────────┘ │
│ ↑ scrape ↑ push │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ ServiceMonitor│ │ Promtail DS │ │
│ │ (13 个) │ │ (5 节点) │ │
│ └────────────────┘ └────────────────┘ │
│ ↑ ↑ │
│ 集群所有 Pod + Service /var/log/pods/ │
└────────────────────────────────────────────────────┘
简历可写:
搭建集群观测三剑客:
- Prometheus (kube-prometheus-stack 27 dashboard + 28 target up)
- Loki + Promtail (5 节点全部日志聚合, LogQL 查询)
- AlertManager (告警路由, 待配)
- Grafana 统一 UI (NodePort 32380, datasource sidecar 自动加 Loki) 全栈 PVC 持久化(Longhorn 5+10+2=17G)
11. Day 6 总结 — 调度 + 观测 + 修正
| 模块 | 状态 | 关键产出 |
|---|---|---|
| A 修 Day 2 遗留 | ✅ | cp-2/cp-3 加 audit-policy, 3 cp 完全对称 |
| B 调度速通 mini-book | ✅ | 5 阶段调度器 / 4 种亲和 / topology / taint |
| C 节点+Pod 亲和 | ✅ | 4 demos: ssd-only / preferred / spread-zone / one-per-node |
| D Topology+Taint | ✅ | 3 demos: maxSkew / taint / toleration |
| E HPA | ✅ | metrics-server + 压测 1→2 副本 30s 内 |
| F kube-prometheus-stack | ✅ | Prometheus + Grafana + AlertManager + 28 target up |
| G Loki + Promtail | ✅ | 5 节点采集 + LogQL + Grafana sidecar datasource |
新增可观测端口:
- Grafana:
http://<m1-ip>:32380(admin/bootcamp) - Prometheus: ClusterIP 10.107.144.81:9090 (NodePort 30090,但 hostNetwork 没绑 — 走 port-forward)
- Loki: ClusterIP 10.104.157.59:3100
- Longhorn UI:
http://<m1-ip>:31172(Day 4 装) - Hubble UI:
http://<m1-ip>:30527(Day 4 装)
Day 6 集群留存 (后续 Day 复用):
- metrics-server (HPA 前置)
- kube-prometheus-stack 11 Pods
- Loki + 5 Promtail DaemonSet
- 节点 zone label (zone-a/b/c) — 后续多 zone 演示直接用
99. 当前进度
- [x] Day 6.A 修 Day 2 遗留 (3 cp audit 对称)
- [x] Day 6.B 调度速通 mini-book
- [x] Day 6.C 亲和性 (4 demos)
- [x] Day 6.D Topology + Taint (3 demos)
- [x] Day 6.E HPA + metrics-server (压测 1→2 30s)
- [x] Day 6.F kube-prometheus-stack (28 target up)
- [x] Day 6.G Loki + Promtail (5 节点 + 7 ns)