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.yaml | apiserver args | audit.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 种亲和
| 机制 | 字段 | 拓扑域 | 典型场景 |
|---|---|---|---|
| nodeSelector | spec.nodeSelector | node 级(隐式) | 单 key=value 精确匹配 |
| nodeAffinity | spec.affinity.nodeAffinity | node 级(隐式) | 复杂表达式 In/NotIn/Exists/Gt/Lt |
| podAffinity | spec.affinity.podAffinity | 任意 topologyKey | 「跟某 Pod 同节点 / 同 zone」(cache 协同) |
| podAntiAffinity | spec.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 required | TopologySpread | |
|---|---|---|
| 语义 | 不能在一起(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
| HPA | VPA | CA | |
|---|---|---|---|
| 调整对象 | Pod 副本数 | Pod CPU/Memory request | 节点数 |
| 数据源 | metrics-server / Prometheus | metrics-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 requiredhostname +nodeSelectorssd - 无状态业务:
topologySpreadConstraintszone (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'
实测扩容曲线:
| T | CPU% / target | Replicas | 行为 |
|---|---|---|---|
| 0s | 0% / 50% | 1 | 压测刚启,metrics 还没拉 |
| 15s | 0% / 50% | 1 | 仍在拉取窗口 |
| 30s | 65% / 50% | 1 | 超阈值,准备扩 |
| 45s | 81% / 50% | 2 | HPA 扩到 2 副本 |
| 60s | 26% / 50% | 2 | 2 副本分担,回归阈值下 |
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 拆分:
| Job | Up | 说明 |
|---|---|---|
| apiserver | 3 / 3 | 3 CP 健康 |
| kubelet | 15 / 15 | 5 节点 × 3 endpoint(cadvisor / probes / kubelet) |
| node-exporter | 5 / 5 | DaemonSet 全节点 |
| prometheus / alertmanager / kube-state-metrics | 2+2+1 | self-scrape |
| kube-controller-manager | 0 / 3 | 见 F.5 真坑 |
| kube-scheduler | 0 / 3 | 同上 |
| kube-etcd | 0 / 3 | 同上 |
| kube-proxy | 0 / 5 | Cilium 替代了它,endpoint 不对 |
| coredns | 0 / 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 + Taint | 3 demos:maxSkew=1 / NoSchedule / toleration |
| E HPA | metrics-server + 压测 30 秒内 1 → 2 副本 |
| F kube-prometheus-stack | 11 Pods,28 target up,27 dashboard |
| G Loki + Promtail | 5 节点采集,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 阶段:
- Queue —— Pod 按 priorityClass 入优先队列
- Filter —— 硬过滤,NodeAffinity / NodeResourcesFit / Taint / TopologySpread / VolumeBinding 等不通过的节点直接 out
- Score —— 软评分,ImageLocality / NodeResourcesBalancedAllocation / InterPodAffinity / preferred 项给每个节点打分
- Reserve —— 在 scheduler cache 预留资源,防止下一个 Pod 重复调度到同位置
- 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.ioAPI 来(metrics-server 实现),最常用 - Pods —— Pod 自定义指标 avg(如
http_requests),从custom.metrics.k8s.ioAPI 来 - Object —— 单对象指标(如某 Ingress 的 QPS)
- External —— 集群外(Prometheus / SQS / CloudWatch),从
external.metrics.k8s.ioAPI 来
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 低基数,内容查询临时解析。