AI Infra 训练营
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
  • Day 0 · 环境与硬件

    • Day 0 新手现场接管 Runbook:先看懂,再动手
    • Day 0:5 节点裸 Ubuntu → K8s 装机基线
  • Week 1:K8s 内核 + 周边基础设施

    • Day 1:3 CP HA 集群 + CNI 选型 + DNS 调优
    • Day 2:控制面 deep dive + etcd 内核 + chaos drill
    • Day 3:CRD + Operator (kubebuilder 从 0 写)
    • Day 4:Longhorn 存储 + Cilium 二探(Hubble / NetworkPolicy / L7)
    • Day 5:PVC 在线扩容 + K8s 安全基线(RBAC / PSA / Secret 加密 / Kyverno)
    • Day 6:调度策略 + Prometheus / Loki 观测栈
    • Day 7:Harbor 私有镜像 + ArgoCD GitOps + Cilium WireGuard
  • Week 2:制品 + GitOps + AI Infra + 综合

    • Day 8:k3s 单节点 + NVIDIA Device Plugin + vLLM 跑 Qwen2.5-3B
    • Day 8(attempt 1):跨 WAN GPU 加入主集群(失败复盘)
    • Day 8:AlertManager 真接入 + PrometheusRule 实战
    • Day 8:集群内 CI 闭环 — Gitea + Jenkins + Kaniko
    • Day 9:Triton 多框架推理 + DCGM 跨集群可观测 + vLLM 实测
    • Day 10:MIG 硬切片 + AWQ 量化 + HPA Custom Metrics
    • Day 11:AI 业务端到端 —— chainlit + GitOps + 跨 WAN vLLM
    • Day 12:灾难恢复 + 生产事故注入
    • Day 13:LLM Operator + 多集群联邦 + Ambient Mesh + RAG
    • Day 14:CKA / CKS 真题演练 + 14 天技术栈横向汇总

Day 6:调度策略 + Prometheus / Loki 观测栈

Day 1-5 把控制面、CNI、存储、安全跑通之后,Day 6 是从「集群能跑 Pod」走向「集群能精准放 Pod + 看清楚自己」两件事:

  • 调度主线:把 kube-scheduler 5 阶段、4 种亲和、TopologySpread、Taint/Toleration、HPA 一锅过完,每个机制都配一个 demo 验证
  • 观测主线:kube-prometheus-stack(Prometheus + Grafana + AlertManager + node-exporter + kube-state-metrics)+ Loki + Promtail,跑出 28 个 target up
  • 顺带补一个 Day 2 遗留:当时 audit-policy 只装在 cp-1,cp-2 / cp-3 没装,2/3 的请求其实没审计 —— 必须先修,再做新东西

整篇按 A → G 7 个阶段走。


A. 修 Day 2 遗留:3 CP audit-policy 对称

A.1 发现盲点

Day 6 开头照例 survey 一下控制面状态,结果发现 audit-policy 配置不对称:

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
节点audit-policy.yamlapiserver argsaudit.log
cp-1有完整44MB(17 小时累积)
cp-2有(Day 2 复制过去)缺不存在
cp-3有(Day 2 复制过去)缺不存在

为什么这是大问题:kubectl 走 per-node HAProxy,每次请求随机命中 3 个 apiserver 之一。命中 cp-2 / cp-3 的请求没有 audit 记录,集群 2/3 的流量在审计上是黑的,但看 cp-1 的 audit.log 又一切正常 —— 这是最危险的「自以为安全」。

教训:HA 控制面的任何 manifest 变更必须所有 CP 同步,并且变更后跑一次对称性校验。Day 2 当时只验了 cp-1,没逐节点对照。

A.2 patch + 验证

3 cp 都已经有 --encryption-provider-config(Day 5 加的),用这一行做锚点,把 audit 相关 args / volumeMount / hostPath 注入到 cp-2 / cp-3 的 apiserver manifest:

# /tmp/patch-audit.py(cp-2 / cp-3 各跑一次,static pod 自动重启 ~45s)
arg_anchor = "    - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml"
arg_new = (
    "    - --audit-policy-file=/etc/kubernetes/audit-policy.yaml\n"
    + arg_anchor
    + "\n    - --audit-log-path=/var/log/kubernetes/audit.log"
    + "\n    - --audit-log-maxage=7    - --audit-log-maxbackup=3    - --audit-log-maxsize=100"
)
# volumeMount + hostPath 同样锚点法插入
# 先 os.makedirs('/var/log/kubernetes', exist_ok=True) 防 hostPath race
ssh m2 'python3 /tmp/patch-audit.py' && sleep 45
ssh m3 'python3 /tmp/patch-audit.py' && sleep 45

for h in m1 m2 m3; do ssh $h 'wc -l /var/log/kubernetes/audit.log'; done
# cp-1: 42717 行(17h 累积) / cp-2: 412 行 / cp-3: 226 行

3 CP 现在 audit + encryption + audit-log 完全对称,集群「零盲点审计」生效。


B. 调度速通 mini-book

写 demo 之前要先把概念过一遍,否则 yaml 字段全是死记。

B.1 kube-scheduler 的 5 阶段

Pod (Pending) ──► scheduler watch ──► priority queue
   ↓
1. Queue        Pod 入队(按 priorityClass 排)
2. Filter       硬过滤:NodeUnschedulable / NodeAffinity / NodeResourcesFit /
                PodTopologySpread / NodePorts / TaintToleration / VolumeBinding
3. Score        软评分:ImageLocality / NodeResourcesBalancedAllocation /
                InterPodAffinity / NodeAffinity(preferred) / TaintToleration(preferred)
4. Reserve      预留资源(cache 中标记,避免重复调度)
5. Bind         写 Pod.spec.nodeName

要点:

  • Filter 全 0 = 0/N nodes are available,常见原因是 affinity / taint 太严
  • Score 在 Filter 通过的节点之间排名,最高分胜出
  • preemption(优先级抢占):高优先级 Pod 可以驱逐低优先级 Pod 腾位置,在 Filter 失败后才触发

B.2 4 种亲和

机制字段拓扑域典型场景
nodeSelectorspec.nodeSelectornode 级(隐式)单 key=value 精确匹配
nodeAffinityspec.affinity.nodeAffinitynode 级(隐式)复杂表达式 In/NotIn/Exists/Gt/Lt
podAffinityspec.affinity.podAffinity任意 topologyKey「跟某 Pod 同节点 / 同 zone」(cache 协同)
podAntiAffinityspec.affinity.podAntiAffinity任意 topologyKey「分散」(HA、避免单点)

required vs preferred:

  • requiredDuringSchedulingIgnoredDuringExecution → Filter 阶段,不满足直接不调度
  • preferredDuringSchedulingIgnoredDuringExecution → Score 阶段,不满足也行,只是加权
  • 没有 DuringExecution 的真正生效形态 —— 运行中节点 label 变了不会重调度,只影响新调度

B.3 TopologySpreadConstraints

K8s 1.27+ 推荐用 TopologySpread 替代 podAntiAffinity 做「均匀分散」:

topologySpreadConstraints:
- maxSkew: 1                                # 任意两拓扑域 Pod 数差 ≤ 1
  topologyKey: topology.kubernetes.io/zone
  whenUnsatisfiable: DoNotSchedule          # required 语义
  labelSelector:
    matchLabels: {app: my-app}

跟 podAntiAffinity 的差异:

podAntiAffinity requiredTopologySpread
语义不能在一起(binary)尽量均匀(quantitative)
每域上限0 或 1不限,看 maxSkew
复杂度O(n²) pairs 比较O(n) 计数
6 副本 / 3 zone不可能(每域上限 1)2+2+2 完美
7 副本 / 3 zone不可能3+2+2(skew=1)

口诀:强排斥用 podAntiAffinity,均衡分布用 TopologySpread。

B.4 Taint / Toleration

kubectl taint node k8s-cp-1 dedicated=db:NoSchedule 给节点打污点。3 种 effect:NoSchedule(新 Pod 拒,老的不动)、PreferNoSchedule(软拒,Score 扣分)、NoExecute(老 Pod 也驱逐,kubectl drain 内部就是这个)。控制面默认就带 node-role.kubernetes.io/control-plane:NoSchedule。

B.5 HPA / VPA / CA

HPAVPACA
调整对象Pod 副本数Pod CPU/Memory request节点数
数据源metrics-server / Prometheusmetrics-server + 历史unscheduled Pod + 闲置节点
推荐必装用 recommendation 模式(不要 auto)公有云 + 容量波动大

HPA 循环 15s:拉 Pod CPU/Memory → 跟 target 比 → patch Deployment.spec.replicas。升级版用 metric.external + Prometheus Adapter 接 Prometheus(QPS / 队列长度),比 CPU 准。

B.6 节点 label 准备

5 节点模拟 3 可用区 + 异构存储,后续 C / D demo 都基于这个布局:

kubectl label node k8s-cp-1 topology.kubernetes.io/zone=zone-a disktype=ssd
kubectl label node k8s-cp-2 topology.kubernetes.io/zone=zone-b disktype=ssd
kubectl label node k8s-cp-3 topology.kubernetes.io/zone=zone-c disktype=hdd
kubectl label node k8s-w-1  topology.kubernetes.io/zone=zone-a disktype=hdd
kubectl label node k8s-w-2  topology.kubernetes.io/zone=zone-b disktype=hdd

C. 节点亲和 + Pod 反亲和实战

4 个 Deployment demo 覆盖典型场景。所有 demo 都加:tolerations: [{operator: Exists}](允许跑在 CP)、app.kubernetes.io/name label(满足 Kyverno)、resources.requests、明确的 image tag(用 pause:3.10 玩具镜像)。

C.1 Demo 1:nodeSelector

spec:
  template:
    spec:
      nodeSelector: {disktype: ssd}

3 副本只落在 cp-1 / cp-2(仅这两台 ssd):k8s-cp-1: 1 k8s-cp-2: 2,hdd 节点全跳过。最简单的姿势。

C.2 Demo 3:podAntiAffinity 在 zone 维度

spec:
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels: {app: demo3-spread-zone}
            topologyKey: topology.kubernetes.io/zone

语义:「同类 Pod 不能跟我同 zone」。3 副本散到 3 zone:cp-2 (zone-b) / cp-3 (zone-c) / w-1 (zone-a)。zone-a 有 cp-1 + w-1 两个节点,但 antiAffinity 一个 zone 只放一个。

C.3 Demo 4:podAntiAffinity 在 hostname 维度

把 topologyKey 换成 kubernetes.io/hostname,5 副本散到 5 节点各 1 个(前提是 tolerate 了 control-plane taint)。

C.4 真坑:required 太严会让 Pod Pending

如果改 8 副本 + topologyKey=hostname + 只有 5 节点:

0/5 nodes are available: 5 didn't match Pod's affinity/anti-affinity rules

3 个 Pod 永远 Pending。required 是硬过滤,Filter 全 fail = 调度失败。应对:改 preferredDuringScheduling、加节点、减副本,或把 hostname 改成更粗的 zone 拓扑域。

生产推荐组合:

  • 关键无状态服务:required hostname + preferred zone(不允许同节点,尽量跨 zone)
  • 普通服务:双 preferred
  • 数据库 / 中间件:required hostname 严格保 HA

D. TopologySpread + 污点容忍

D.1 Demo 5:TopologySpread maxSkew=1

topologySpreadConstraints:
- maxSkew: 1
  topologyKey: topology.kubernetes.io/zone
  whenUnsatisfiable: DoNotSchedule
  labelSelector:
    matchLabels: {app: demo5-topology}

6 副本 / 3 zone:

zone-a (w-1):  2 副本
zone-b (cp-2): 2 副本
zone-c (cp-3): 2 副本

完美 2+2+2。注意 zone-a 还有 cp-1,但 TopologySpread 只看 zone 间均匀,不强制 zone 内均匀(不指定 hostname topologyKey 时)。

D.2 Demo 6:Taint NoSchedule 隔离 cp-1

kubectl taint node k8s-cp-1 dedicated=db:NoSchedule

不带 toleration 的 Pod,Filter 阶段直接把 cp-1 排除。4 副本 demo6-no-toler:

k8s-cp-2: 1   k8s-cp-3: 1   k8s-w-1: 1   k8s-w-2: 1

cp-1 全被排除。

D.3 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 db taint

4 副本全在 cp-1 —— toleration 让 dedicated=db 这个 taint 对该 Pod 失效。

D.4 4 个调度机制 cheat sheet

机制表达性推荐场景
nodeSelector弱(单 key)简单 dev 场景
nodeAffinity强(In/NotIn/Exists/Gt/Lt)生产复杂表达式
podAntiAffinity required强(binary 0/1)主备 / 强排斥
TopologySpread强(skew 量化)K8s 1.27+ 通用均衡首选
Taint + Toleration强(3 effects)节点专用 / 维护 drain

搭配模式:

  • 数据库主备:podAntiAffinity required hostname + nodeSelector ssd
  • 无状态业务:topologySpreadConstraints zone (maxSkew=1) + tolerations 让 CP 可调度
  • GPU 工作流:节点打 taint + Pod 显式 nodeAffinity + toleration

E. HPA + metrics-server

E.1 装 metrics-server

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# kubelet 是 kubeadm 自签 cert,metrics-server 默认会 cert error
kubectl patch deployment metrics-server -n kube-system --type=json -p='[
  {"op": "add", "path": "/spec/template/spec/containers/0/args/-",
   "value": "--kubelet-insecure-tls"}
]'

为什么必须加 --kubelet-insecure-tls:metrics-server 通过 kubelet :10250 拉 metrics,kubelet 的 server cert 是 kubeadm 自签的,不在 K8s trust chain 里。不加这个参数,kubectl top 永远是 Metrics API not available。

生产更好做法:kubelet 加 --rotate-server-certificates=true,kube-controller-manager 自动签 kubelet server cert,metrics-server 不再需要 insecure。

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%

E.2 HPA Deployment + HPA 资源

apiVersion: apps/v1
kind: Deployment
metadata: {name: hpa-web}
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: c
        image: nginx:1.27-alpine
        resources:
          requests: {cpu: 100m, memory: 32Mi}   # baseline
          limits:   {cpu: 500m, memory: 64Mi}
---
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     # avg(Pod CPU) / request 达到 50% 就扩

Utilization 50% 意思是 avg(Pod CPU) / Pod CPU request 达到 50%。即每 Pod 用到 50m(100m request 的一半)时刚好不扩,超过就扩。target 还可以是 AverageValue(绝对量)或 Value(单 Pod 值)。

E.3 压测扩容曲线

起一个 loadgen Pod 死循环 curl:

kubectl run loadgen --image=busybox:1.36 --restart=Never \
  --command -- sh -c 'while true; do wget -qO- --timeout=2 http://hpa-web; done'

实测扩容曲线:

TCPU% / targetReplicas行为
0s0% / 50%1压测刚启,metrics 还没拉
15s0% / 50%1仍在拉取窗口
30s65% / 50%1超阈值,准备扩
45s81% / 50%2HPA 扩到 2 副本
60s26% / 50%22 副本分担,回归阈值下

30 秒内完成 1 → 2 扩容。

E.4 缩容窗口 5 分钟

kubectl delete pod loadgen 后 CPU 立即降到 0%,但 Pod 数维持 2 副本直到约 5 分钟才缩回 1。scaleDown 默认 stabilizationWindowSeconds: 300s,设计目的是防抖(短暂回落马上又涨,反复扩缩抖动)。要调快加 behavior.scaleDown.stabilizationWindowSeconds: 60 + policies 限速。

E.5 HPA 进阶 metric 类型

4 种 metric:

  • Resource —— CPU / Memory(最常用),走 metrics.k8s.io
  • Pods —— Pod 自定义指标 avg(如 http_requests),走 custom.metrics.k8s.io
  • Object —— 单对象指标(如某 Ingress 的 QPS)
  • External —— 集群外(Prometheus / CloudWatch / SQS),走 external.metrics.k8s.io

生产典型:Resource CPU 作 baseline 兜底,External(Prometheus QPS / 队列长度)作主触发器 —— CPU 跟业务负载不一定相关。要装 Prometheus Adapter 把 PromQL 暴露成 K8s metric API。


F. kube-prometheus-stack

F.1 全栈架构

kube-prometheus-stack(helm chart)一锅装 8+ 组件:

Prometheus Operator (controller)
  ├ ServiceMonitor CRD   定义如何 scrape Service
  ├ PodMonitor CRD       定义如何 scrape Pod
  └ PrometheusRule CRD   定义告警规则
        ↓ 调谐
Prometheus (StatefulSet, PVC) ──► TSDB
AlertManager (StatefulSet, PVC) ──► 告警路由
Grafana (Deployment, PVC) ──► dashboard UI
node-exporter (DaemonSet × 5) ──► 节点 metrics
kube-state-metrics (Deployment) ──► K8s 资源 metrics

F.2 helm 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.deploymentStrategy.type=Recreate \
  --set grafana.adminPassword=bootcamp \
  --set 'prometheus.prometheusSpec.tolerations[0].operator=Exists' \
  --set 'grafana.tolerations[0].operator=Exists'

关键选择:

  • Prometheus PVC 10Gi(Longhorn),TSDB 持久 + 重启不丢
  • Grafana PVC 2Gi,dashboard + datasource 持久
  • Grafana 只有 1 个副本且挂 RWO PVC,所以发布策略用 Recreate:先停旧 Pod、释放卷,再起新 Pod。用默认 RollingUpdate 时,新旧 Pod 会争同一个 Longhorn 卷,容易卡在 Multi-Attach。
  • 全组件 tolerations: Exists 让 CP 也可调度(节点少)
  • adminPassword=bootcamp 学习用,生产必须从 Secret 引用

启动 ~3 分钟,11 个 Pod 全 Running:alertmanager(StatefulSet)、grafana(Deployment,含 dashboard + datasource sidecar)、prometheus(StatefulSet)、operator、kube-state-metrics、node-exporter DaemonSet × 5。Prometheus / AlertManager 用 StatefulSet(PVC 稳定 + 顺序启动)。

F.3 ServiceMonitor 自动注入

ServiceMonitor CR ──► Operator watch ──► gen scrape_configs ──► Prometheus reload

kubectl get servicemonitor -A 列出 13 个,覆盖 apiserver / coredns / kube-* / kubelet / node-exporter / kube-state-metrics / grafana / alertmanager / prometheus / operator。完全声明式,用户从不写 prometheus.yml。

F.4 28 target up

curl http://10.107.144.81:9090/api/v1/query?query=count(up==1)
# {"value":[..., "28"]}

按 job 拆分:

JobUp说明
apiserver3 / 33 CP 健康
kubelet15 / 155 节点 × 3 endpoint(cadvisor / probes / kubelet)
node-exporter5 / 5DaemonSet 全节点
prometheus / alertmanager / kube-state-metrics2+2+1self-scrape
kube-controller-manager0 / 3见 F.5 真坑
kube-scheduler0 / 3同上
kube-etcd0 / 3同上
kube-proxy0 / 5Cilium 替代了它,endpoint 不对
coredns0 / 2类似

F.5 真坑:control plane metrics 默认绑 127.0.0.1

ps -ef | grep kube-scheduler | grep bind   # --bind-address=127.0.0.1

kubeadm 默认让 kube-scheduler / kube-controller-manager / etcd 只绑本地回环,集群内 Pod 拉不到 metrics。修法:改 manifest 里的 --bind-address=0.0.0.0(etcd 加 --listen-metrics-urls=http://0.0.0.0:2381),同时必须配 NetworkPolicy 限制 source。学习场景这些 down 不影响主 dashboard,先跳过。生产必须修。

kube-proxy down 是另一个故事:Day 1 装 Cilium 时没显式 disable kube-proxy,ServiceMonitor 配的 healthz 端口对不上。彻底解法是切到 Cilium kube-proxy replacement,或 patch ServiceMonitor。

F.6 Grafana 访问

NodePort 32380,http://<m1-ip>:32380,admin / bootcamp。chart 自带 27 个 dashboard:Cluster Compute / Networking / node-exporter USE / Persistent Volumes / Prometheus Overview 等。


G. Loki + Promtail

G.1 装 grafana/loki-stack(仅 Loki + Promtail)

helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack \
  --namespace monitoring \
  --set grafana.enabled=false \
  --set grafana.sidecar.datasources.enabled=false \
  --set prometheus.enabled=false \
  --set loki.enabled=true \
  --set loki.isDefault=false \
  --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

不要把注释放在反斜杠后面。Shell 里 \ 必须是行尾最后一个字符,否则下一行不会继续接上,命令会被拆坏。

grafana.sidecar.datasources.enabled=false 是为了关闭 loki-stack chart 自动生成的 loki-loki-stack datasource ConfigMap。这里已经有 kube-prometheus-stack 的 Grafana,再由我们自己创建 loki-datasource,避免出现两个默认 datasource 导致 Grafana CrashLoop。

G.2 Grafana datasource via sidecar ConfigMap

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

kube-prometheus-stack 的 Grafana 默认启用 datasource-sidecar 容器,任何带 grafana_datasource: "1" label 的 ConfigMap 都自动 inject 进 Grafana。不需要重启、不需要 UI 点击 —— GitOps 友好。

如果 Grafana 日志出现下面的错误:

Datasource provisioning error: datasource.yaml config is invalid. Only one datasource per organization can be marked as default

检查 datasource ConfigMap:

kubectl get configmap -n monitoring -l grafana_datasource=1 -o yaml | grep -n "name:\|isDefault"

同一个 Grafana org 只能有一个 isDefault: true。本教程里 Prometheus 是默认 datasource,Loki 必须保持 isDefault: false。

G.3 数据流验证 + LogQL

LOKI=$(kubectl get svc -n monitoring loki -o jsonpath='{.spec.clusterIP}')

curl -sS http://$LOKI:3100/loki/api/v1/labels
# 10 labels: app, component, container, filename, instance, job, namespace, node_name, pod, stream

curl -sS http://$LOKI:3100/loki/api/v1/label/namespace/values
# 7 ns: cilium-demo, default, kube-system, kyverno, longhorn-system, monitoring, storage-demo

LogQL 跟 PromQL 同源,会 Prometheus 几乎零学习成本:

{namespace="kube-system"}                          # 标签筛选
{namespace=~".+"} |= "ERROR"                       # 含字符串过滤
{pod=~"hpa-web.*"} |~ "(GET|POST) /"               # 正则
{app="grafana"} | json | level="error"             # JSON 解析

G.5 Loki vs ES

Loki 跟 ES 设计哲学不同:Loki 只索引 labels(低基数 metadata),logs 直接压缩存 blob,存储成本约 ES 的 1/10,时间 + label 查询快但全文慢;ES 是全文倒排索引(Lucene),每个 token 都建索引,全文极快但贵。口诀:Loki = 给 K8s 日志做 Prometheus;ES = 给业务做搜索引擎。

G.6 真坑:label cardinality 爆炸

Loki 不索引内容,只索引 label。如果给日志加了高基数 label(request_id / user_id / trace_id),每个 unique 值都会产生独立的 chunk + index entry:100 万 request_id × 1 KB index = 1 GB 索引,查询慢且贵;Ingester 维护 stream 状态在内存里,stream 爆炸直接 OOM。

防御:label 数量 ≤ 10,label 值只用低基数维度(namespace / app / pod / level),高基数 id 放进 log line 用 | json 临时解析;Promtail pipeline_stages 主动 drop / rename 高基数 label;Loki limits_config: max_label_names_per_series / max_streams_per_user 兜底。

G.7 single-binary vs cluster 模式

loki-stack chart 默认是 single-binary:Ingester / Distributor / Querier / Compactor / Query Frontend / Index Gateway 都在 1 个 Pod。简单,但不能水平扩,挂了所有日志读写都断。生产用 grafana/loki chart 拆成 microservices,用对象存储(S3 / MinIO)替代 PVC,支持水平扩。

G.8 三剑客联动

Grafana (NodePort 32380)
   │
   ├─► Prometheus (metrics) ◄── ServiceMonitor (13 个) ◄── Pod + Service
   │
   └─► Loki (logs) ◄── Promtail DS (5 节点) ◄── /var/log/pods/

全栈 PVC 持久化:Loki 5Gi + Prometheus 10Gi + Grafana 2Gi = 17 GiB(Longhorn)。


Day 6 集群形态

模块关键产出
A 修 Day 2 遗留3 CP audit-policy 对称,零盲点审计
B 调度速通scheduler 5 阶段 + 4 种亲和 + TopologySpread + Taint 概念到位
C 节点 + Pod 亲和4 demos:ssd-only / preferred / spread-zone / one-per-node
D Topology + Taint3 demos:maxSkew=1 / NoSchedule / toleration
E HPAmetrics-server + 压测 30 秒内 1 → 2 副本
F kube-prometheus-stack11 Pods,28 target up,27 dashboard
G Loki + Promtail5 节点采集,LogQL 查询,sidecar 自动注入 datasource

集群入口:Grafana http://<m1-ip>:32380(admin / bootcamp)、Prometheus ClusterIP 9090(port-forward)、Loki ClusterIP 3100、Longhorn UI 31172、Hubble UI 30527。留存给后续 Day 复用:metrics-server、kube-prometheus-stack、Loki + Promtail、5 节点 zone label。


面试常见题

Q1:kube-scheduler 调度一个 Pod 经过哪些阶段?

5 阶段:

  1. Queue —— Pod 按 priorityClass 入优先队列
  2. Filter —— 硬过滤,NodeAffinity / NodeResourcesFit / Taint / TopologySpread / VolumeBinding 等不通过的节点直接 out
  3. Score —— 软评分,ImageLocality / NodeResourcesBalancedAllocation / InterPodAffinity / preferred 项给每个节点打分
  4. Reserve —— 在 scheduler cache 预留资源,防止下一个 Pod 重复调度到同位置
  5. Bind —— 写 Pod.spec.nodeName,kubelet watch 到后启动

Filter 全 0 = 0/N nodes are available。preemption 在 Filter 失败后才触发,高优先级 Pod 可以驱逐低优先级 Pod 腾位置。

Q2:TopologySpreadConstraints 跟 podAntiAffinity 怎么选?

差异点:

  • podAntiAffinity required 是 binary(每域 0 或 1),TopologySpread 是 quantitative(域之间差 ≤ maxSkew)
  • 6 副本 / 3 zone:podAntiAffinity required 不可能(每域上限 1,超过 3 副本就 Pending);TopologySpread 完美 2+2+2
  • TopologySpread 是 O(n) 计数,podAntiAffinity 是 O(n²) pairs 比较,大集群差异明显

选择:

  • 强排斥(数据库主备必须不同节点)→ podAntiAffinity required
  • 均匀分散(无状态服务跨 zone 均衡)→ TopologySpread maxSkew=1

K8s 1.27+ 推荐 TopologySpread 作为默认均衡方案,podAntiAffinity 留给强排斥场景。

Q3:HPA 的 metrics 从哪里来?Resource 跟 Custom Metrics 有什么区别?

4 种 metric type:

  • Resource —— CPU / Memory,从 metrics.k8s.io API 来(metrics-server 实现),最常用
  • Pods —— Pod 自定义指标 avg(如 http_requests),从 custom.metrics.k8s.io API 来
  • Object —— 单对象指标(如某 Ingress 的 QPS)
  • External —— 集群外(Prometheus / SQS / CloudWatch),从 external.metrics.k8s.io API 来

Resource 走 metrics-server,Custom / External 走 Prometheus Adapter(把 PromQL 暴露成 K8s metric API)。

生产典型:Resource CPU 作 baseline 兜底,External(Prometheus QPS / 队列长度)作主触发器 —— CPU 跟业务负载不一定相关,QPS 更准。

Q4:Prometheus Operator 模式跟直接 helm 部署 Prometheus 有什么区别?

直接 helm 装:用户写 prometheus.yml 配 scrape_configs,改目标要改 ConfigMap + reload,多实例配置无法复用。

Operator 模式(kube-prometheus-stack):Prometheus / ServiceMonitor / PodMonitor / PrometheusRule 都是 CR;Operator watch CR 编译成 prometheus.yml 并 reload;多个 Prometheus 实例共享 ServiceMonitor,按 prometheusSelector 隔离。

收益:完全声明式 + GitOps 友好 + 多实例配置复用 + 上游 chart 默认 27 个 dashboard。生产几乎都用 Operator 模式。

Q5:Loki 的 label cardinality 爆炸是怎么回事?怎么解?

Loki 不索引内容,只索引 labels。把高基数维度(request_id / user_id / trace_id)放进 label:每个 unique 值产生独立 chunk + index entry,100 万 id × 1 KB = 1 GB 索引;Ingester 维护 stream 状态在内存里,stream 爆炸直接 OOM。

解决:label 数量 ≤ 10,label 值只用低基数维度(namespace / app / pod / level / job),高基数 id 放进 log line 用 | json | request_id="xxx" 临时解析;Promtail pipeline_stages 主动 drop / rename 高基数 label;Loki limits_config: max_label_names_per_series / max_streams_per_user 兜底。口诀:label 低基数,内容查询临时解析。

在 GitHub 上编辑此页
Prev
Day 5:PVC 在线扩容 + K8s 安全基线(RBAC / PSA / Secret 加密 / Kyverno)
Next
Day 7:Harbor 私有镜像 + ArgoCD GitOps + Cilium WireGuard