Skip to content

第二部分 Kubernetes 进阶实战

第一部分我们打好了地基:集群搭建、核心对象、基本排错。本部分进入"进阶实战"阶段——你将真正理解 kube-scheduler 如何做决策、requests/limits 背后发生了什么、Service 流量如何穿过 iptables/IPVS、如何用 RBAC 与 PSS 把集群管起来、如何用 Prometheus 把集群"看"清楚、以及如何用 Helm 与 Kustomize 把应用"发"出去。这些内容既是生产运维的日常,也是 Kubernetes 面试的高频考点。

适用版本:Kubernetes v1.28+,RKE2 v1.28+,Helm 3.x

本部分目录

章节主题
第 1 章调度进阶
第 2 章资源管理进阶
第 3 章存储进阶
第 4 章网络进阶
第 5 章安全进阶
第 6 章可观测性入门到实战
第 7 章应用打包与发布
第 8 章常见面试题与进阶排错套路

第 1 章 调度进阶

1.1 kube-scheduler 工作原理

调度器的职责:为 Pending 状态的 Pod 选择一个最合适的节点。整个过程分两个阶段:

┌─────────────────────────────────────────────────────────────┐
│                      kube-scheduler                          │
│                                                              │
│  ┌───────────┐     ┌──────────────────┐     ┌────────────┐  │
│  │ 调度队列   │ ──> │ Filtering(过滤)  │ ──> │ Scoring(打分)│  │
│  │(activeQ)  │     │  不满足条件的出局 │     │  按策略排序  │  │
│  └───────────┘     └──────────────────┘     └─────┬──────┘  │
│                                                    │         │
│                                            ┌───────▼──────┐  │
│                                            │ Bind: 写 API │  │
│                                            │ (异步,失败回队列)│ │
│                                            └──────────────┘  │
└─────────────────────────────────────────────────────────────┘
  • Filtering(过滤):检查节点资源是否够用、taints 是否被容忍、端口是否冲突、亲和性是否满足等。插件如 NodeResourcesFitTaintTolerationNodeAffinityVolumeBinding
  • Scoring(打分):对剩余节点打分,插件如 NodeResourcesFit(least/most allocated)、ImageLocality(优先有镜像的节点)、InterPodAffinity
  • Bind:调度器更新 Pod 的 spec.nodeName(通过 Binding 子资源),之后由该节点上的 kubelet 接手。

注意:调度器只在 Pod 创建或节点标签变化触发"重新调度事件"时工作。它不会主动迁移已经运行的 Pod——这是 descheduler(外部项目)做的事。

查看调度决策过程:

bash
kubectl get events --field-selector reason=FailedScheduling
kubectl describe pod <pending-pod>   # 看 Events 里的 FailedScheduling 信息
# 调度器日志(RKE2 中调度器是静态 Pod,位于 control-plane 节点)
kubectl -n kube-system logs kube-scheduler-rke2-server1 | grep -i "unable to schedule"

1.2 nodeSelector:最简单的定向调度

bash
kubectl label node rke2-agent1 disktype=ssd
yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-ssd
spec:
  nodeSelector:
    disktype: ssd
  containers:
  - name: nginx
    image: nginx:1.25

特点:硬性要求,没有匹配节点则 Pod 一直 Pending。适合简单场景。

1.3 nodeAffinity:节点亲和性

比 nodeSelector 更强大,支持 In / NotIn / Exists / DoesNotExist / Gt / Lt 运算符,支持软/硬两种模式:

字段含义类比
requiredDuringSchedulingIgnoredDuringExecution硬亲和,必须满足nodeSelector 增强版
preferredDuringSchedulingIgnoredDuringExecution软亲和,尽量满足(权重 1~100)偏好排序

"IgnoredDuringExecution" 是考点:Pod 运行期间节点标签被移除,Pod 不会被驱逐(K8s 1.26+ 引入了 ...RequiredDuringExecution 的 Alpha 特性,默认不开启)。

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cache
spec:
  replicas: 3
  selector:
    matchLabels: { app: cache }
  template:
    metadata:
      labels: { app: cache }
    spec:
      affinity:
        nodeAffinity:
          # 硬性:必须调度到生产环境节点
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: environment
                operator: In
                values: ["prod"]
          # 软性:优先 ssd 节点(权重 80)和高带宽节点(权重 20)
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 80
            preference:
              matchExpressions:
              - key: disktype
                operator: In
                values: ["ssd"]
          - weight: 20
            preference:
              matchExpressions:
              - key: bandwidth
                operator: Gt
                values: ["10"]
      containers:
      - name: redis
        image: redis:7

1.4 podAffinity / podAntiAffinity:Pod 间亲和与反亲和

已经运行在节点上的 Pod 的标签为参照做调度,必须搭配 topologyKey(拓扑域,通常是 kubernetes.io/hostnametopology.kubernetes.io/zone)。

yaml
spec:
  affinity:
    # 反亲和:同一副本不要落在同一节点(高可用常用)
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels: { app: web }
        topologyKey: kubernetes.io/hostname
    # 亲和:尽量和 cache 同节点(减少网络延迟)
    podAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels: { app: cache }
          topologyKey: kubernetes.io/hostname
场景推荐用法
同一应用多副本分散到不同节点podAntiAffinity + hostname
同一应用分散到不同可用区podAntiAffinity + zone(软)
前端紧贴缓存部署podAffinity
多副本 > 节点数时必须用软反亲和,否则多出的副本 Pending

最佳实践:生产环境的硬反亲和要谨慎——当副本数超过拓扑域数量时,超出的 Pod 永远 Pending。多数场景用 preferred 或直接用 topologySpreadConstraints(见 8.5)。

1.5 topologySpreadConstraints:拓扑分布约束

K8s 1.19 稳定,比 podAntiAffinity 更灵活:可以控制"最大倾斜度",而不是简单的"绝不在一起"。

yaml
spec:
  topologySpreadConstraints:
  - maxSkew: 1                        # 任意两个拓扑域的 Pod 数差值不超过 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: DoNotSchedule  # 硬约束;ScheduleAnyway 为软约束
    labelSelector:
      matchLabels: { app: web }
  - maxSkew: 2
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels: { app: web }

验证分布效果:

bash
kubectl get pods -l app=web -o wide --sort-by=.spec.nodeName

1.6 Taints 与 Tolerations:污点与容忍

亲和性是 Pod 的"主动吸引",污点是节点的"主动排斥"。两者配合使用。

bash
# 给节点打污点(三个 effect 见下表)
kubectl taint nodes rke2-gpu1 dedicated=gpu:NoSchedule
# 删除污点(注意末尾的 -)
kubectl taint nodes rke2-gpu1 dedicated=gpu:NoSchedule-
# 查看节点污点
kubectl describe node rke2-gpu1 | grep Taints
effect行为
NoSchedule不容忍的新 Pod 不调度上来(已有的不动)
PreferNoSchedule尽量不调度(软约束)
NoExecute不容忍的 Pod 立即驱逐(新 Pod 也不调度)

容忍污点的 Pod:

yaml
spec:
  tolerations:
  - key: dedicated
    operator: Equal
    value: gpu
    effect: NoSchedule
  # 容忍所有 key 为 dedicated 的污点(value 不限)
  - key: dedicated
    operator: Exists
    effect: NoExecute
    tolerationSeconds: 300   # NoExecute 特有:被驱逐前宽限 300 秒

RKE2 控制平面默认带污点:

bash
kubectl describe node rke2-server1 | grep -i taint
# Taints: node-role.kubernetes.io/control-plane=true:NoSchedule

常见面试题:节点 NotReady 后 Pod 多久被驱逐?答:节点控制器给节点打 node.kubernetes.io/not-ready:NoExecute 污点,Pod 默认通过自动注入的 toleration 容忍 300 秒tolerationSeconds,受 default-not-ready-toleration-seconds 控制),之后被驱逐并在其他节点重建。K8s 1.27+ 还可以结合 Pod Disruption Budget 控制。

1.7 优先级与抢占:PriorityClass

资源不足时,高优先级 Pod 可以**抢占(Preemption)**低优先级 Pod:低优 Pod 被驱逐,高优 Pod 上位。

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000                # 越大越优先;>10亿 保留给系统组件
globalDefault: false
description: "核心在线业务"
preemptionPolicy: PreemptLowerPriority   # Never 可禁用抢占
---
# Pod 中使用
spec:
  priorityClassName: high-priority
资源不足时的决策链:
新 Pod Pending

   ├─ priorityClass 比现有 Pod 高? ──否──> 继续 Pending




逐节点尝试"驱逐低优 Pod 后能否放下"

   ├─ 能 ──> 驱逐低优 Pod(尊重 PDB 与 terminationGracePeriod)──> 绑定节点
   └─ 不能 ──> Pending

注意:抢占遵守 PodDisruptionBudget;被抢占的 Pod 有优雅退出时间;system-cluster-critical(2000000000)和 system-node-critical(2000001000)是内置最高优先级,kubelet 的静态 Pod 也受其保护。

1.8 本章小结与面试题

Q1:nodeSelector 和 nodeAffinity 的区别? nodeSelector 只支持等值匹配的硬约束;nodeAffinity 支持多运算符、多表达式、软/硬两种模式。

Q2:taint 和 affinity 有什么关系? 互补。affinity 是 Pod 说"我想去哪",taint 是节点说"谁能来"。要独占一组节点(如 GPU 节点),通常"污点 + 亲和"双管齐下:节点打 NoSchedule 污点挡住普通 Pod,专用 Pod 加 toleration + nodeAffinity 确保只去这些节点。

Q3:如何实现"每个节点最多跑 1 个副本"? 硬 podAntiAffinity(topologyKey=hostname)或 topologySpreadConstraints(maxSkew=1 + DoNotSchedule)。


第 2 章 资源管理进阶

2.1 requests / limits 深入

维度CPU内存
requests 的作用调度依据;分配 cpu.shares(相对权重)调度依据
limits 超限后果throttle(限流),不杀容器OOMKill,杀容器(OOM Score 提高)
单位500m = 0.5 核128Mi1Gi
yaml
resources:
  requests:
    cpu: 250m        # 保证:调度时按此值找节点
    memory: 512Mi
  limits:
    cpu: "1"         # 封顶:最多用 1 核
    memory: 1Gi

要点:

  • CPU 是可压缩资源,超 limit 只是被 CFS 限流(表现:CPU Throttled 指标升高、延迟抖动)。
  • 内存是不可压缩资源,超 limit 直接触发 cgroup OOM Kill(退出码 137,reason: OOMKilled)。
  • 调度只认 requests。节点可分配量 = 节点容量 - system-reserved - kube-reserved - eviction-reserved。
bash
kubectl describe node rke2-agent1 | grep -A 8 "Allocated resources"
kubectl top pod -A --sort-by=memory

2.2 QoS 等级与 OOM 策略

K8s 根据 requests/limits 的组合给 Pod 分 QoS:

QoS条件驱逐/OOM 优先级
Guaranteed每个容器 requests == limits(CPU 和内存都设置且相等)最后被杀
Burstable设置了 requests,但不满足 Guaranteed中间
BestEffort什么都没设置最先被杀

节点内存压力时的两级处理:

节点内存不足
  ├─ kubelet eviction(软/硬驱逐阈值,如 memory.available<100Mi)
  │    按 QoS + 超出 requests 的量排序,驱逐 BestEffort → Burstable → Guaranteed
  └─ 内核 OOM Killer(kubelet 反应不过来时)
       按 oom_score_adj 排序:BestEffort(1000) > Burstable > Guaranteed(-997~-998)
bash
kubectl get pod xxx -o jsonpath='{.status.qosClass}'

最佳实践:生产关键服务设 Guaranteed(req == limit);Java 应用务必显式设置 -Xmx 并小于容器 limit 的 75%~80%,否则 JVM 按宿主机内存堆栈化导致 OOMKill(JDK 8u191+/JDK 10+ 已感知 cgroup);CPU limit 对延迟敏感服务可只设 requests 不设 limit(K8s 社区争议点,视业务取舍)。

2.3 HPA:水平自动伸缩(autoscaling/v2)

bash
# 前置条件:metrics-server 必须可用
kubectl top node
yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 10
  metrics:
  # 资源指标:CPU 利用率达 60% 触发扩容(利用率 = 实际使用 / requests)
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  # 资源指标:内存绝对值
  - type: Resource
    resource:
      name: memory
      target:
        type: AverageValue
        averageValue: 800Mi
  # 自定义指标:每个 Pod 每秒 1000 请求(需 prometheus-adapter)
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"
  # 扩缩容行为控制(v2 重要特性)
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300   # 缩容冷却 5 分钟,防止抖动
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60               # 每分钟最多缩掉 50%
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Pods
        value: 4
        periodSeconds: 60               # 每分钟最多加 4 个 Pod
      selectPolicy: Max

期望副本数计算公式

desiredReplicas = ceil(currentReplicas × currentMetricValue / desiredMetricValue)

例如当前 4 副本、CPU 利用率 90%、目标 60%:ceil(4 × 90/60) = 6 副本。

常用命令:

bash
kubectl get hpa
kubectl describe hpa web-hpa    # 看 Events:指标获取失败、扩容记录
kubectl get --raw /apis/metrics.k8s.io/v1beta1/pods | jq .

注意:HPA 的 Resource 指标要求目标 Pod 必须设置 requests,否则无法计算利用率;多指标时取各指标计算结果的最大值;HPA 与 VPA 不要同时作用于同一指标。

2.4 VPA:垂直自动伸缩

VPA(Vertical Pod Autoscaler,社区组件,需单独安装)自动推荐/调整 Pod 的 requests/limits。

bash
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler && ./hack/vpa-up.sh
yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: web-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  updatePolicy:
    updateMode: "Auto"    # Off=只给建议; Initial=创建时注入; Recreate/Auto=重建生效
  resourcePolicy:
    containerPolicies:
    - containerName: "*"
      minAllowed: { cpu: 100m, memory: 128Mi }
      maxAllowed: { cpu: 2, memory: 4Gi }
bash
kubectl get vpa web-vpa -o jsonpath='{.status.recommendation}' | jq .

最佳实践:生产先用 Off 模式观察推荐值(至少积累 1~2 周数据),确认合理后再切 Auto。VPA 调整资源会重建 Pod,注意 PDB 与滚动节奏。

2.5 KEDA 简介:事件驱动自动伸缩

KEDA(Kubernetes Event-driven Autoscaling)以外部事件源(Kafka 积压、Redis 队列长度、Prometheus 指标、Cron 等)驱动伸缩,支持缩到 0。

bash
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda -n keda --create-namespace
yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-consumer-scaler
spec:
  scaleTargetRef:
    name: kafka-consumer        # 目标 Deployment
  minReplicaCount: 0            # 无消息时缩到 0,省资源
  maxReplicaCount: 30
  triggers:
  - type: kafka
    metadata:
      bootstrapServers: kafka-svc:9092
      consumerGroup: order-group
      topic: orders
      lagThreshold: "10"        # 每个副本承担 10 条积压

HPA vs VPA vs KEDA 对比

维度HPAVPAKEDA
伸缩方向水平(改副本数)垂直(改 requests/limits)水平(改副本数,可缩到 0)
触发依据CPU/内存/自定义指标历史资源用量外部事件源(50+ Scaler)
是否重建 Pod是(Auto 模式)
典型场景Web 流量波动资源规格调优消息队列消费者、定时任务

2.6 LimitRange 与 ResourceQuota 实战

LimitRange:命名空间级,给 Pod/容器设置资源默认值与上下限(防止有人不设 requests)。

yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: dev
spec:
  limits:
  - type: Container
    default:            # 相当于 limits 默认值
      cpu: 500m
      memory: 512Mi
    defaultRequest:     # 相当于 requests 默认值
      cpu: 100m
      memory: 128Mi
    max: { cpu: "2", memory: 2Gi }
    min: { cpu: 50m, memory: 64Mi }
    maxLimitRequestRatio:    # limit/request 比值上限,防超卖失衡
      cpu: "4"

ResourceQuota:命名空间级总量管控。

yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    requests.cpu: "10"
    requests.memory: 20Gi
    limits.cpu: "20"
    limits.memory: 40Gi
    pods: "50"
    services.loadbalancers: "2"
    persistentvolumeclaims: "10"
bash
kubectl describe quota -n dev       # 查看已用/剩余

注意:命名空间设置了 requests.cpu 配额后,该空间所有 Pod 必须显式声明 requests(否则创建被拒),所以 ResourceQuota 通常和 LimitRange 搭配使用。面试高频:"如何保证每个命名空间资源不被打满?"——答案就是这两个对象。

2.7 本章小结与面试题

Q1:容器被 OOMKilled,exit code 137,如何排查?kubectl describe podLast State: OOMKilled;确认 limit 设置与 JVM/应用参数;用 kubectl top 与 Prometheus 看增长曲线(泄漏 or 突发);调整 limit 或修复泄漏。

Q2:HPA 为什么不扩容? 依次检查:metrics-server 是否正常(kubectl top);Pod 是否设了 requests;指标是否真达阈值;kubectl describe hpa 看 Events(FailedGetResourceMetric 等);是否到 maxReplicas。

Q3:CPU limit 该不该设? 设了会 throttle 影响延迟敏感业务;不设可能挤爆节点。折中:延迟敏感服务不设 CPU limit 但设 requests(Guaranteed 内存 + Burstable CPU),配合节点 monitoring。


第 3 章 存储进阶

3.1 PV / PVC 生命周期与回收策略

        ┌──────────────────────────────────────────────────┐
        │                  生命周期                          │
        │                                                   │
  Provision ──> Bound ──> Released ──> (Retain / Delete / 回收)
  (静态创建或     绑定    PVC 被删除      │
   动态供给)                            ├─ Retain: 保留数据, 人工清理后可复用
        ▲                               ├─ Delete: 自动删除底层卷(动态供给默认)
        │                               └─ Recycle: 已废弃(deprecated)
        └────── Retain 的 PV 清理 claimRef 后重新 Available

状态流转:Available → Bound → Released →(Retain 处理)→ Available,或 → Failed

回收策略对比:

reclaimPolicy行为适用
DeletePVC 删除时连底层存储一起删云盘动态供给默认;无状态数据
Retain保留 PV 与数据,需人工处理数据库等宝贵数据(生产推荐)
Recyclerm -rf 后复用已废弃,用动态供给替代

Retain 复用手动操作:

bash
kubectl delete pvc old-pvc
kubectl get pv          # 状态 Released
kubectl patch pv old-pv -p '{"spec":{"claimRef": null}}'   # 解绑
kubectl get pv          # 状态 Available,可被新 PVC 绑定

注意:PVC 被使用中的 Pod 引用时删除会卡在 Terminating(由 kubernetes.io/pvc-protection Finalizer 保护),Pod 删除后才真正释放。

3.2 StorageClass 详解

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"   # 默认 SC
provisioner: rancher.io/local-path       # 供给器(见下表)
parameters:
  type: gp3                              # provisioner 自定义参数
reclaimPolicy: Retain                    # 动态 PV 的回收策略(默认 Delete)
volumeBindingMode: WaitForFirstConsumer  # 延迟绑定(默认 Immediate)
allowVolumeExpansion: true               # 允许扩容
mountOptions:
- noatime
字段说明关键点
provisioner谁来创建卷内置插件已移除(1.26 完成 CSI 迁移),全部用 CSI 驱动名
reclaimPolicy动态 PV 回收策略生产改 Retain 防误删数据
volumeBindingMode绑定时机WaitForFirstConsumer:等 Pod 调度后再在所在节点/可用区创建卷,避免"卷在 A 区、Pod 调度到 B 区"
allowVolumeExpansion是否允许在线扩容CSI 驱动需支持

常见 provisioner:

provisioner存储
rancher.io/local-pathRancher local-path-provisioner(RKE2 常用)
driver.longhorn.ioLonghorn 分布式块存储
ebs.csi.aws.comAWS EBS
cephfs.csi.ceph.com / rbd.csi.ceph.comCeph CSI
kubernetes.io/no-provisioner静态 Local PV

3.3 CSI 原理

CSI(Container Storage Interface)把存储驱动从 K8s 核心中剥离。一个 CSI 驱动通常部署为两部分:

┌──────────────────────────┐     ┌──────────────────────────────┐
│ Controller Plugin (STS)  │     │ Node Plugin (DaemonSet)       │
│ ┌──────────────────────┐ │     │ ┌──────────────────────────┐ │
│ │ csi-provisioner      │ │     │ │ node-driver-registrar    │ │
│ │ csi-attacher         │ │     │ │ csi-driver (业务逻辑)     │ │
│ │ csi-resizer          │ │     │ └──────────────────────────┘ │
│ │ csi-snapshotter      │ │     │ 负责: NodeStage/Publish       │
│ │ csi-driver           │ │     │ (在 kubelet 要求时挂载到节点)  │
│ └──────────────────────┘ │     └──────────────────────────────┘
│ 负责: Create/Delete 卷,   │                  ▲
│       Attach/Detach      │     gRPC over unix socket
└──────────────────────────┘
   Sidecar 监听 K8s 对象(PVC/VolumeAttachment/Snapshot)并调 CSI 接口

一次动态供给的完整链路:

用户创建 PVC
  → external-provisioner 发现 PVC 的 SC 属于自己 → 调 CSI CreateVolume → 创建 PV 对象
  → PV Controller 绑定 PVC ↔ PV
  → Pod 调度到节点 → external-attacher 创建 VolumeAttachment → 驱动 Attach(云盘挂到 VM)
  → kubelet 调 Node Plugin NodeStageVolume + NodePublishVolume(格式化+挂载到 Pod 目录)
bash
kubectl get csidrivers
kubectl get volumeattachment
kubectl get pods -n longhorn-system   # 以 Longhorn 为例看 CSI 组件

3.4 VolumeSnapshot:卷快照

快照涉及三个 CRD + 快照控制器:

bash
kubectl api-resources | grep snapshot
# volumesnapshotclasses / volumesnapshotcontents / volumesnapshots
yaml
# 1. 快照类(类比 StorageClass)
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: csi-snapclass
driver: driver.longhorn.io
deletionPolicy: Delete       # 删除快照对象时是否删底层快照
---
# 2. 创建快照
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: mysql-snap-20260719
spec:
  volumeSnapshotClassName: csi-snapclass
  source:
    persistentVolumeClaimName: mysql-data
---
# 3. 从快照恢复(创建新 PVC)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data-restore
spec:
  storageClassName: longhorn
  dataSource:
    name: mysql-snap-20260719
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 10Gi

注意:RKE2 自带 snapshot-controller 与 CRD(可通过 disable: [rke2-snapshot-controller] 关闭),但快照能力取决于 CSI 驱动(local-path 不支持真快照,Longhorn/EBS 支持);deletionPolicy: Retain 可在删除对象时保住底层快照。

3.5 Local PV:本地持久卷

场景:高性能本地 SSD、有状态中间件(如自建 ES/Kafka)。特点:性能高,但数据绑定节点,节点挂则卷不可用

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer   # 必须! 等 Pod 调度再绑定
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv-agent1
spec:
  capacity:
    storage: 100Gi
  accessModes: ["ReadWriteOnce"]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /data/local-pv
  nodeAffinity:                       # 必须声明卷所在节点
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values: ["rke2-agent1"]

对比 local-path-provisioner(RKE2 默认插件 rke2-local-storage 可装):自动创建目录、自动带 nodeAffinity,但不做容量隔离,本质是简化版 Local PV。

3.6 PVC 扩容

前提:StorageClass allowVolumeExpansion: true + CSI 驱动支持。

bash
# 直接编辑 PVC 的 requests.storage(只能改大不能改小)
kubectl patch pvc mysql-data -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
kubectl describe pvc mysql-data
# 事件: Resizing → FileSystemResizePending(等Pod重启挂载扩容文件系统) → 完成
kubectl get pvc mysql-data    # CAPACITY 变为 20Gi

注意:块设备扩容快,但文件系统扩容通常需要 Pod 重建(部分驱动支持在线扩容 ExpandInUsePersistentVolumes,1.24+ 默认可用)。

3.7 本章小结与面试题

Q1:PV 和 PVC 的关系?为什么要两层? PVC 是"申请单"(应用视角:我要多大、什么模式),PV 是"实际卷"(运维视角:具体存储细节)。解耦后应用不用关心后端是 NFS、云盘还是 Ceph。

Q2:PVC 一直 Pending? 检查:StorageClass 是否存在且 provisioner Pod 正常;kubectl describe pvc 看 provisioner 报错;是否有匹配 PV(静态供给时 accessModes/capacity/selector 是否匹配);volumeBindingMode 为 WaitForFirstConsumer 时 Pod 未创建前 Pending 是正常的

Q3:数据安全的最佳实践? 生产 SC 设 reclaimPolicy: Retain;重要数据定期 VolumeSnapshot;删除 PVC 前确认 kubectl get pv 的 CLAIM 与回收策略。


第 4 章 网络进阶

4.1 Service 实现原理:iptables vs IPVS

kube-proxy 负责把 Service 的虚拟 IP 规则落到每个节点。

iptables 模式(默认)

bash
iptables -t nat -L KUBE-SERVICES -n | head -30
iptables -t nat -L KUBE-SVC-XXXX -n

规则链:PREROUTING/OUTPUT → KUBE-SERVICES → KUBE-SVC-xxx(按概率 statistic 模块分流)→ KUBE-SEP-xxx(DNAT 到具体 Pod IP)

IPVS 模式

bash
ipvsadm -Ln | head -30

对比表(面试必考):

维度iptablesIPVS
数据结构线性链式匹配 O(n)哈希表 O(1)
大规模表现数千 Service 时规则膨胀、更新慢、延迟抖动万级 Service 无压力
负载均衡算法仅随机(probability)rr/wrr/lc/wlc/sh/dh 等多种
会话保持不支持真亲和(仅 Service 层 timeout)支持
内核要求通用需加载 ip_vs 模块

RKE2 启用 IPVS:在 server/agent 的 config 中配置 kube-proxy 参数(或安装时通过 HelmChartConfig 改 rke2-kube-proxymode: ipvs)。

流量路径(ClusterIP 为例):

Pod A ──> 10.43.0.10:80 (ClusterIP)
   │  本机 netfilter: DNAT → 选中后端 Pod IP 10.42.1.15
   │  (记录 conntrack,回程自动做 SNAT 还原)

Pod B (可能在另一节点, 经 CNI VXLAN 隧道)

externalTrafficPolicy(NodePort/LoadBalancer):Cluster(默认,SNAT 后任意转发,丢源 IP)vs Local(只发本节点后端,保留源 IP,可能不均)。

4.2 CoreDNS 与 NodeLocal DNSCache

CoreDNS 配置(RKE2 中 ConfigMap 为 kube-system/coredns,通过 HelmChartConfig rke2-coredns 定制):

.:53 {
    errors
    health
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {   # K8s 域名解析
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
    }
    prometheus :9153
    forward . /etc/resolv.conf    # 非集群域名转发到上游 DNS
    cache 30
    loop
    reload
    loadbalance
}

常用定制:

# stubDomain 效果:corp.example.com 走公司内网 DNS
corp.example.com {
    forward . 10.10.0.2 10.10.0.3
    cache 30
}

# rewrite:把 external-svc.default.svc.cluster.local 重写到真实服务
rewrite name exact old-svc.default.svc.cluster.local new-svc.other-ns.svc.cluster.local

验证解析:

bash
kubectl run dnsutils --rm -it --image=registry.k8s.io/e2e-test-images/jessie-dnsutils:1.3 -- nslookup kubernetes.default
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50

NodeLocal DNSCache:在每个节点跑一个本地缓存(DaemonSet),Pod 查询走节点本地 169.254.20.10,避免大量 DNS 查询打到 CoreDNS 造成的 conntrack 表满与延迟。RKE2 支持通过 HelmChartConfig 启用 nodelocal 配置。

无 NodeLocal:  Pod ──UDP──> CoreDNS ClusterIP ──DNAT──> 某 CoreDNS Pod
                     (每个查询占 conntrack 条目, UDP 丢包无重传感知)
有 NodeLocal:  Pod ──> 本节点 nodelocal-dns (缓存命中直接返回)
                     未命中 ──TCP──> CoreDNS (TCP 不占 conntrack 槽位问题小)

4.3 Ingress 深入:Ingress-NGINX

RKE2 默认安装 rke2-ingress-nginx(DaemonSet + hostNetwork 可选)。以 Helm 部署为例:

bash
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace \
  --set controller.service.type=LoadBalancer

TLS 配置

bash
kubectl create secret tls web-tls --cert=tls.crt --key=tls.key -n prod
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  namespace: prod
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2        # 路径重写
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: 50m
spec:
  ingressClassName: nginx
  tls:
  - hosts: ["app.example.com"]
    secretName: web-tls
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /api(/|$)(.*)
        pathType: ImplementationSpecific    # rewrite-target $2 捕获组配合
        backend:
          service:
            name: api-svc
            port:
              number: 8080

常用 annotation 速查

annotation作用
rewrite-target重写后端路径
ssl-redirect / force-ssl-redirectHTTP 强制跳 HTTPS
canary: "true" + canary-weight: "10"灰度:10% 流量到新版本
canary-by-header: "X-Canary"按请求头灰度
limit-rps: "20"限流
auth-type: basic + auth-secretBasic 认证
proxy-body-size上传大小限制
configuration-snippet自定义 NGINX 片段

金丝雀发布示例

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"   # 10% 流量到 v2
spec:
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-v2
            port:
              number: 80

排查 Ingress:

bash
kubectl -n ingress-nginx logs -l app.kubernetes.io/component=controller --tail=100
kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf | less
kubectl describe ingress web

4.4 Gateway API 简介

Ingress 的继任者(GA since v1.0,2023)。角色分离 + 更丰富的流量管理能力:

┌─────────────┐   ┌──────────┐   ┌───────────┐   ┌─────────┐
│ GatewayClass│──>│ Gateway  │──>│ HTTPRoute │──>│ Service │
│ (基础设施商  │   │(运维:监听 │   │(开发者:    │   │(后端)   │
│  定义实现)   │   │ 端口/TLS) │   │ 路由规则)  │   └─────────┘
└─────────────┘   └──────────┘   └───────────┘
yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: prod-gateway
spec:
  gatewayClassName: nginx        # 或 cilium / istio / traefik
  listeners:
  - name: https
    protocol: HTTPS
    port: 443
    tls:
      certificateRefs:
      - name: web-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: web-route
spec:
  parentRefs:
  - name: prod-gateway
  hostnames: ["app.example.com"]
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /v2
    backendRefs:
    - name: web-v2
      port: 80
      weight: 10          # 原生支持流量拆分(灰度), 无需 annotation
    - name: web-v1
      port: 80
      weight: 90

Ingress vs Gateway API:Ingress 单一资源、能力靠各实现 annotation 扩展;Gateway API 角色分离(GatewayClass/Gateway/Route)、标准化流量拆分与 header 匹配。新项目可关注,存量 Ingress 无需急于迁移。

4.5 NetworkPolicy(Calico 示例)

RKE2 默认 CNI 是 Canal(Calico + Flannel),支持 NetworkPolicy。策略是白名单模型:一旦某 Pod 被 policy 选中,未声明的流量一律拒绝。

yaml
# 1. 默认拒绝某命名空间所有入站
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: prod
spec:
  podSelector: {}            # 空 = 选中所有 Pod
  policyTypes: ["Ingress"]
---
# 2. 只允许 frontend 访问 backend 的 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: prod
spec:
  podSelector:
    matchLabels: { app: backend }
  policyTypes: ["Ingress"]
  ingress:
  - from:
    - podSelector:
        matchLabels: { app: frontend }
    ports:
    - protocol: TCP
      port: 8080
---
# 3. 放行 DNS(出方向几乎必备,否则 default-deny 后无法解析)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: prod
spec:
  podSelector: {}
  policyTypes: ["Egress"]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53

排错思路:kubectl get networkpolicy -A;临时用 kubectl run test --rm -it --image=busybox:1.36 -- wget -qO- --timeout=3 http://svc:port 验证连通性;Calico 环境可用 calicoctl 查看下发规则。

注意:NetworkPolicy 依赖 CNI 支持(纯 Flannel 不支持);命名空间打 default-deny记得放行 DNS 出口,这是最常见的踩坑点。

4.6 本章小结与面试题

Q1:Service ClusterIP 不通,排查顺序? Service selector 是否匹配 Pod 标签 → Endpoints/EndpointSlice 是否有地址 → Pod 是否 Ready(readinessProbe)→ kube-proxy 规则(iptables -t nat / ipvsadm)→ CNI 网络是否通 → NetworkPolicy 是否拦截。

Q2:Ingress 502 的常见原因? 后端 Service 无 Endpoints(Pod NotReady);端口不匹配(Service targetPort vs 容器监听);proxy-body-size/超时限制;后端 OOM 重启中。

Q3:外部流量经过 NodePort 后源 IP 丢失怎么办?externalTrafficPolicy: Local,或 LB 层用 PROXY protocol / Ingress 直接 hostNetwork。


第 5 章 安全进阶

5.1 RBAC 深入

RBAC 四要素:Role/ClusterRole(权限集合)+ RoleBinding/ClusterRoleBinding(绑定到主体)

Subject (User / Group / ServiceAccount)

        │  RoleBinding (限某命名空间) / ClusterRoleBinding (集群范围)

Role (命名空间级权限)  /  ClusterRole (集群级权限)


rules: apiGroups + resources + verbs
yaml
# Role:只允许在 dev 命名空间读写 Pod 和 Deployment
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-manager
  namespace: dev
rules:
- apiGroups: ["", "apps"]
  resources: ["pods", "deployments"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-manager-binding
  namespace: dev
subjects:
- kind: ServiceAccount
  name: app-sa
  namespace: dev
roleRef:
  kind: Role
  name: app-manager
  apiGroup: rbac.authorization.k8s.io

ClusterRole 聚合规则(给内置 admin/edit/view 追加权限的经典手法):

yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: aggregate-crd-view
  labels:
    rbac.authorization.k8s.io/aggregate-to-view: "true"   # 自动并入内置 view 角色
rules:
- apiGroups: ["monitoring.coreos.com"]
  resources: ["servicemonitors"]
  verbs: ["get", "list", "watch"]

ServiceAccount 使用

yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: dev
automountServiceAccountToken: true
---
# Pod 中引用
spec:
  serviceAccountName: app-sa

常用排查命令:

bash
kubectl auth can-i create deployments -n dev --as=system:serviceaccount:dev:app-sa
kubectl auth can-i --list -n dev --as=system:serviceaccount:dev:app-sa
kubectl get rolebindings,clusterrolebindings -A | grep app-sa

最佳实践:最小权限原则;不用 default SA 跑业务;避免给 * verbs / * resources;给 CI/CD 的 SA 用 RoleBinding 限定命名空间而非 cluster-admin。

5.2 Pod Security Standards / Admission

K8s 1.23+ 内置 Pod Security Admission(替代已删除的 PodSecurityPolicy),三个等级:

级别说明
privileged不限制
baseline禁止特权容器、hostNetwork、hostPath 等明显危险项
restricted最严格:必须 runAsNonRoot、禁特权提升、seccomp、限制 capabilities

对命名空间打标签启用:

bash
kubectl label namespace prod \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/warn=restricted
# enforce=拒绝; audit=记录审计; warn=客户端警告

restricted 合规的 Pod 示例:

yaml
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: myapp:1.0
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]

测试违规效果:

bash
kubectl run test --image=nginx -n prod --overrides='{"spec":{"containers":[{"name":"test","image":"nginx","securityContext":{"privileged":true}}]}}'
# Error: pods "test" is forbidden: violates PodSecurity "baseline:latest": privileged

5.3 Secret 加密

默认 Secret 在 etcd 中只是 base64(不是加密!)。静态加密需配置 EncryptionConfiguration:

yaml
# /etc/rancher/rke2/encryption-provider-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
  providers:
  - aescbc:
      keys:
      - name: key1
        secret: <32字节随机串的base64>
  - identity: {}      # 兜底:明文(迁移期用), 完成后移除

RKE2 更简单——直接用内置命令:

bash
rke2 secrets-encrypt enable     # 自动生成并应用加密配置
rke2 secrets-encrypt status
rke2 secrets-encrypt rotate     # 轮换密钥

验证(在 control-plane 节点直接查 etcd):

bash
crictl exec $(crictl ps --name etcd -q) \
  etcdctl --cert=/var/lib/rancher/rke2/server/tls/etcd/server-client.crt \
  --key=/var/lib/rancher/rke2/server/tls/etcd/server-client.key \
  --cacert=/var/lib/rancher/rke2/server/tls/etcd/ca.crt \
  get /registry/secrets/default/my-secret | head -c 100
# 密文以 k8s:enc:aescbc:v1: 开头即加密生效

KMS 方案:云上或高安全场景用 KMS provider(AWS KMS / Vault),密钥不落盘到节点,apiserver 通过 envelope 加密(DEK 加密数据、KEK 在 KMS 保管)。RKE2 支持 KMS v2 配置。

其他最佳实践:限制 Secret 的 RBAC 读取;用外部 Secret 管理(ESO/Vault);避免 env 注入大 Secret(进程环境可被 kubectl exec env 看到),优先 volume 挂载。

5.4 镜像安全扫描概念

防线分层:

构建期:  Dockerfile 检查(hadolint) + 基础镜像最小化(distroless/alpine)
推送期:  Registry 扫描(Harbor 内置 Trivy 自动扫描 + 阻止高危镜像拉取)
部署期:  Admission 校验(只允许受信仓库签名镜像, cosign/Kyverno)
运行期:  持续扫描(Trivy Operator 定期扫描集群内镜像生成 VulnerabilityReport)
bash
# 手动扫描示例
trivy image nginx:1.25
trivy image --severity CRITICAL,HIGH myapp:1.0

配合策略:Harbor 项目开启"阻止漏洞级别 ≥ High 的镜像被拉取";Kyverno/OPA Gatekeeper 策略拒绝 :latest 标签与非受信 registry。

5.5 审计日志

审计策略定义"哪些 API 请求要记录、记到什么级别":

yaml
# /etc/rancher/rke2/audit-policy.yaml(节选)
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
  resources:
  - group: ""
    resources: ["secrets", "configmaps"]   # Secret 访问必须记录
- level: RequestResponse
  verbs: ["delete", "deletecollection"]    # 删除操作记录完整请求响应
- level: Metadata                          # 其余默认级别

RKE2 中通过 server 配置启用:

yaml
# /etc/rancher/rke2/config.yaml
kube-apiserver-arg:
  - "audit-policy-file=/etc/rancher/rke2/audit-policy.yaml"
  - "audit-log-path=/var/lib/rancher/rke2/server/logs/audit.log"
  - "audit-log-maxage=30"
  - "audit-log-maxbackup=10"
  - "audit-log-maxsize=100"

审计级别:None < Metadata(元数据)< Request(+请求体)< RequestResponse(+响应体)。级别越高磁盘压力越大,生产常用 Metadata + 对敏感资源提级。

5.6 本章小结与面试题

Q1:Secret 是安全的吗? 默认不是:base64 可逆、etcd 明文存储、任何有 get 权限的人可读。加固:EncryptionConfiguration 静态加密 + RBAC 最小化 + 审计 + 外部密管。

Q2:PodSecurityPolicy 没了用什么? 内置 Pod Security Admission(命名空间打 label)做基础防护;复杂策略用 Kyverno / OPA Gatekeeper。

Q3:如何给开发人员只读权限? 绑定内置 ClusterRole view(RoleBinding 限定命名空间),需要看 CRD 再用聚合 ClusterRole 扩展。


第 6 章 可观测性入门到实战

6.1 Metrics Server

资源指标(CPU/内存)采集器,HPA 与 kubectl top 的数据源。RKE2 默认安装。

bash
kubectl top node
kubectl top pod -A --sort-by=cpu
kubectl get apiservice v1beta1.metrics.k8s.io   # AVAILABLE=True 为正常

自建集群安装:

bash
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# 自签证书集群需加启动参数 --kubelet-insecure-tls

排错:kubectl top 报错 Metrics API not available → 检查 metrics-server Pod 日志(多为证书 SAN 或 10250 端口不通)。

6.2 kube-prometheus-stack 部署

一条 Helm 命令装齐 Prometheus + Alertmanager + Grafana + node-exporter + kube-state-metrics + 一套默认告警规则:

bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm install monitoring prometheus-community/kube-prometheus-stack \
  -n monitoring --create-namespace \
  --set prometheus.prometheusSpec.retention=15d \
  --set prometheus.prometheusSpec.storageClassName=longhorn \
  --set prometheus.prometheusSpec.volumeClaimTemplate.spec.resources.requests.storage=50Gi \
  --set grafana.adminPassword='xxx' \
  --set grafana.service.type=NodePort
bash
kubectl -n monitoring get pods
kubectl -n monitoring port-forward svc/monitoring-grafana 3000:80
# Grafana: admin / xxx,内置 K8s 全套看板

架构:

┌────────────────────────────────────────────────────────────┐
│ Prometheus (时序库 + 抓取 + 规则评估)                        │
│   ▲ scrape                                                 │
│   │ /metrics                                               │
│   ├── node-exporter (DaemonSet, 节点 CPU/内存/磁盘/网络)     │
│   ├── kube-state-metrics (K8s 对象状态: Pod 重启数/副本数)  │
│   ├── kubelet/cadvisor (容器指标)                           │
│   └── 业务 Pod (ServiceMonitor/PodMonitor 自动发现)          │
│        │                                                   │
│        ├──> Alertmanager (告警分组/静默/路由 → 邮件/钉钉/飞书) │
│        └──> Grafana (可视化) ◄── Loki (日志, 另一数据源)      │
└────────────────────────────────────────────────────────────┘

让 Prometheus 抓取业务指标:

yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: myapp
  namespace: monitoring          # 注意 release 的 serviceMonitorSelector
  labels:
    release: monitoring          # kube-prometheus-stack 默认按此标签选择
spec:
  selector:
    matchLabels: { app: myapp }
  endpoints:
  - port: http-metrics
    interval: 15s

6.3 Grafana 实战

  • 常用社区看板 ID:15759(K8s 集群总览)、1860(Node Exporter Full)、13639(Loki 日志)。
  • 导入:Grafana → Dashboards → Import → 输入 ID。
  • 配置多数据源:Prometheus + Loki + Alertmanager。

6.4 告警规则与 Alertmanager

自定义 PrometheusRule(会被自动加载):

yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: app-alerts
  namespace: monitoring
  labels:
    release: monitoring
spec:
  groups:
  - name: app.rules
    rules:
    - alert: PodRestartTooOften
      expr: increase(kube_pod_container_status_restarts_total[1h]) > 5
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} 1小时内重启超过5次"
    - alert: NodeMemoryPressure
      expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 85
      for: 10m
      labels:
        severity: critical
      annotations:
        summary: "节点 {{ $labels.instance }} 内存使用率超过 85%"
    - alert: PvcAlmostFull
      expr: kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes > 0.85
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "PVC {{ $labels.persistentvolumeclaim }} 使用率超 85%"

Alertmanager 通知(以 Webhook/钉钉为例)通过 Helm values:

yaml
alertmanager:
  config:
    route:
      receiver: dingtalk
      group_by: ["namespace", "alertname"]
      group_wait: 30s
      repeat_interval: 4h
      routes:
      - match:
          severity: critical
        receiver: dingtalk-critical
    receivers:
    - name: dingtalk
      webhook_configs:
      - url: http://dingtalk-webhook:8060/dingtalk/ops/send

6.5 Loki 日志

轻量日志系统,只索引标签不索引全文,与 Grafana 无缝集成:

bash
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack -n monitoring \
  --set promtail.enabled=true \
  --set loki.persistence.enabled=true \
  --set loki.persistence.storageClassName=longhorn \
  --set loki.persistence.size=20Gi

Grafana 添加 Loki 数据源后,在 Explore 中用 LogQL 查询:

logql
{namespace="prod", app="web"} |= "ERROR"
{namespace="prod"} |~ "timeout|refused" | json | line_format "{{.msg}}"
rate({namespace="prod"} |= "ERROR" [5m])

Promtail(采集端)以 DaemonSet 运行,自动读取 /var/log/pods 并附加 K8s 元数据标签。

6.6 常用 PromQL 速查

需求PromQL
节点 CPU 使用率100 - avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m]))*100
节点内存使用率(1 - node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes)*100
Pod CPU(核)sum by(namespace,pod)(rate(container_cpu_usage_seconds_total{container!="",container!="POD"}[5m]))
Pod 内存sum by(namespace,pod)(container_memory_working_set_bytes{container!="",container!="POD"})
Pod 重启次数increase(kube_pod_container_status_restarts_total[1h])
Deployment 不可用副本kube_deployment_spec_replicas - kube_deployment_status_replicas_available
PVC 使用率kubelet_volume_stats_used_bytes/kubelet_volume_stats_capacity_bytes*100
容器限流率rate(container_cpu_cfs_throttled_periods_total[5m])/rate(container_cpu_cfs_periods_total[5m])
HTTP QPSsum(rate(http_requests_total[5m]))
P99 延迟histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by(le))

6.7 本章小结与面试题

Q1:metrics-server 和 Prometheus 的区别? metrics-server 只存最近的 CPU/内存(内存态,给 HPA 和 kubectl top 用);Prometheus 是完整时序库,长期存储 + 告警 + 丰富指标。HPA v2 自定义指标需要 prometheus-adapter 把 Prometheus 指标转成 custom.metrics.k8s.io API。

Q2:如何监控自己应用的指标? 应用暴露 /metrics(client_golang/prom-client 等)→ 创建 ServiceMonitor(标签匹配 release)→ Grafana 出图 + PrometheusRule 告警。


第 7 章 应用打包与发布

7.1 Helm 深入

Chart 结构

mychart/
├── Chart.yaml          # 元数据: name/version/appVersion/dependencies
├── values.yaml         # 默认配置(可覆盖)
├── templates/          # 模板目录
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── _helpers.tpl    # 命名模板(define/template/include)
│   ├── NOTES.txt       # 安装后提示
│   └── tests/
├── charts/             # 依赖子 chart
└── crds/               # 先于模板安装的 CRD

Chart.yaml 示例

yaml
apiVersion: v2
name: myapp
description: My Application
type: application
version: 1.2.0          # chart 版本(每次改动递增)
appVersion: "2.3.1"     # 应用版本
dependencies:
- name: redis
  version: "18.x.x"
  repository: https://charts.bitnami.com/bitnami
  condition: redis.enabled

模板与函数

yaml
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "mychart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "mychart.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - containerPort: {{ .Values.service.targetPort }}
        resources:
          {{- toYaml .Values.resources | nindent 10 }}
        {{- if .Values.env }}
        env:
          {{- range $key, $value := .Values.env }}
          - name: {{ $key }}
            value: {{ $value | quote }}
          {{- end }}
        {{- end }}

常用内置对象与函数:

类别示例
内置对象.Values.Chart.Release(Name/Namespace/Revision)、.Capabilities
字符串quoteuppertrunc 63(名称超长截断)、trimSuffix "-"
默认值default "nginx" .Values.image
控制流if/elserangewith
管道.Values.labels | toYaml | nindent 4
调试helm templatehelm install --dry-run --debug

Hooks(在 release 生命周期特定点执行):

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "mychart.fullname" . }}-db-migrate
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade     # 升级前跑迁移
    "helm.sh/hook-weight": "5"                   # 多 hook 排序
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: migrate
        image: myapp-migrator:{{ .Chart.AppVersion }}

钩子类型:pre-install / post-install / pre-upgrade / post-upgrade / pre-delete / post-delete / pre-rollback / post-rollback / test

常用命令

bash
helm repo add bitnami https://charts.bitnami.com/bitnami
helm search repo nginx --versions
helm show values bitnami/nginx              # 查看可配置项

helm install myapp ./mychart -n prod --create-namespace \
  --set image.tag=2.3.1 -f values-prod.yaml

helm list -A
helm status myapp -n prod
helm history myapp -n prod
helm upgrade myapp ./mychart -n prod --set image.tag=2.3.2
helm rollback myapp 3 -n prod               # 回滚到 revision 3
helm diff upgrade myapp ./mychart -n prod   # 需 helm-diff 插件
helm template ./mychart --debug | less      # 纯渲染调试
helm uninstall myapp -n prod

helm lint ./mychart                         # 静态检查
helm package ./mychart                      # 打包成 tgz

私有仓库(以 Harbor Chart Museum / OCI 为例):

bash
# OCI 方式(Helm 3.8+ 推荐,Harbor 2.x 支持)
helm registry login harbor.example.com -u admin
helm package ./mychart
helm push mychart-1.2.0.tgz oci://harbor.example.com/charts
helm install myapp oci://harbor.example.com/charts/mychart --version 1.2.0

最佳实践:values 分层(values.yaml 默认 + values-prod.yaml 环境差异);敏感信息不入 values(用 ExternalSecret 或 --set + CI 密钥);用 --atomic --timeout 做 CI 部署(失败自动回滚);镜像 tag 不用 latest

7.2 Kustomize 深入

Kustomize 无模板、基于"YAML 叠加":kubectl 内置(kubectl apply -k)。

myapp/
├── base/                    # 通用配置
│   ├── kustomization.yaml
│   ├── deployment.yaml
│   └── service.yaml
└── overlays/
    ├── dev/
    │   ├── kustomization.yaml
    │   └── patch-replicas.yaml
    └── prod/
        ├── kustomization.yaml
        ├── patch-resources.yaml
        └── ingress.yaml

base/kustomization.yaml

yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
  app: myapp
images:
- name: myapp
  newTag: 2.3.1

overlays/prod/kustomization.yaml

yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
namePrefix: prod-
resources:
- ../../base
- ingress.yaml
images:
- name: myapp
  newTag: 2.3.1
replicas:
- name: myapp
  count: 6
configMapGenerator:
- name: app-config
  literals:
  - LOG_LEVEL=warn
  behavior: merge
secretGenerator:
- name: db-secret
  envs:
  - db.env
patches:
- path: patch-resources.yaml

strategic merge patch(patch-resources.yaml):

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
      - name: myapp
        resources:
          requests: { cpu: 500m, memory: 1Gi }
          limits: { cpu: "2", memory: 4Gi }

JSON6902 patch(精确到路径):

yaml
# kustomization.yaml 中
patches:
- target:
    kind: Deployment
    name: myapp
  patch: |-
    - op: replace
      path: /spec/template/spec/containers/0/imagePullPolicy
      value: Always

components(1.24+,可复用的横切配置,如"加监控注解"):

yaml
# components/monitoring/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
patches:
- target:
    kind: Deployment
  patch: |-
    - op: add
      path: /spec/template/metadata/annotations
      value:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9090"
yaml
# overlay 中引用
components:
- ../../components/monitoring

使用:

bash
kubectl apply -k overlays/prod
kubectl kustomize overlays/prod | less       # 只渲染不应用
kubectl diff -k overlays/prod                # 与线上对比

7.3 Helm vs Kustomize 对比

维度HelmKustomize
范式模板引擎(Go template)声明式叠加(无模板)
学习曲线较陡(模板语法+函数)平缓(纯 YAML)
发布管理有(release 历史/回滚/hook)无(纯 apply,回滚靠 Git)
参数化values 文件覆盖overlay + patch
生态海量现成 chart需自己写
kubectl 内置是(apply -k)
复杂逻辑强(if/range/hook)弱(靠 patch 组合)

常见组合用法:第三方组件用 Helm(先 helm template 渲染成 YAML),再用 Kustomize 做环境差异化 patch——这正是 RKE2 管理其内置组件(HelmChart/HelmChartConfig)的思路。GitOps(ArgoCD/Flux)对两者都有原生支持。

7.4 本章小结与面试题

Q1:Helm 回滚的原理? 每次 release 操作存一份完整 manifest 到 Secret(sh.helm.release.v1.xxx),helm rollback 把指定 revision 的 manifest 重新 apply。注意:它只管 K8s 资源,不回滚数据(如 PVC/DB 变更)。

Q2:hook 执行失败会怎样? release 标记为失败;配合 --atomic 可自动回滚。pre-upgrade hook 失败则升级中止。


第 8 章 常见面试题与进阶排错套路

8.1 进阶面试题精选

Q1:Pod 一直 Pending 的排查步骤?

kubectl describe pod → Events
├─ FailedScheduling: insufficient cpu/memory → 资源不足, 扩容或降 requests
├─ node(s) had untolerated taint → 缺 toleration
├─ didn't match Pod's node affinity → 标签/亲和性不满足
├─ persistentvolumeclaim not found / pvc pending → 存储未就绪
└─ 无 Events → 检查 scheduler 是否运行, Pod 是否卡在卷绑定

Q2:Pod CrashLoopBackOff 排查?kubectl logs --previous 看崩溃前日志(关键!);describe pod 看退出码(1=应用错误,137=OOMKill,143=被 SIGTERM);探针配置是否过严;依赖服务/配置是否就绪;镜像入口命令是否正确。

Q3:Service 四层负载均衡是怎么做到的?长连接怎么办? kube-proxy 的 iptables/IPVS 是连接级 DNAT(首次连接选后端,conntrack 记住后续包),对长连接(gRPC/WebSocket)会造成负载不均。方案:用七层 LB(Ingress/gRPC 感知 LB 如 Envoy/Linkerd)或 headless Service + 客户端 LB。

Q4:如何不停机更新一个 Deployment? RollingUpdate 策略(默认 maxSurge=25%、maxUnavailable=25%)+ readinessProbe(就绪才接流量)+ preStop hook 或优雅退出(先停接流量再处理完在途请求)+ PDB(保证驱逐/升级期间最少副本)。

Q5:etcd 在 K8s 中的角色?如何备份? 唯一持久化存储,存所有集群状态。RKE2 默认每 12 小时自动快照(etcd-snapshot-schedule-cron),手动:

bash
rke2 etcd-snapshot save --name manual-backup
ls /var/lib/rancher/rke2/server/db/snapshots/
# 恢复见第一部分《RKE2 控制面仲裁丢失灾难恢复》文档

Q6:一个命名空间删不掉(Terminating)? 有资源残留或 Finalizer 卡住:

bash
kubectl get all -n stuck-ns                    # 找残留资源
kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl get -n stuck-ns 2>/dev/null
kubectl get ns stuck-ns -o json | jq '.spec.finalizers'
# 万不得已: 清空 finalizers(有风险, 会跳过资源清理)
kubectl replace --raw "/api/v1/namespaces/stuck-ns/finalize" -f <(kubectl get ns stuck-ns -o json | jq '.spec.finalizers=[]')

Q7:生产集群升级策略? 先升 control-plane 再升 worker(版本偏差 ≤1 个小版本);kubectl drain 腾空节点(配合 PDB);逐台升级、观察、再下一台;RKE2 用 system-upgrade-controller 自动化;升级前 etcd 快照 + 备份证书。

8.2 进阶排错套路总表

症状第一反应深入工具
Pod Pendingdescribe pod 看 Events查 quota / taint / pvc
CrashLoopBackOfflogs --previous + 退出码探针、依赖、OOM
ImagePullBackOffdescribe 看错误registry 认证(imagePullSecret)、网络、tag 不存在
Service 不通endpoints 是否有 IPkube-proxy 规则、CNI、NetworkPolicy
DNS 解析失败进 Pod nslookupCoreDNS 日志、resolv.conf、ndots:5 导致的多次查询
节点 NotReadydescribe node 看 Conditionskubelet 日志(journalctl -u rke2-agent)、磁盘压力、网络插件
性能抖动top node/pod + PrometheusCPU throttle(limit 过低)、磁盘 IO、conntrack 表
证书错误openssl s_client 验证RKE2 证书 1 年到期:rke2 certificate rotate
etcd 慢/告警etcdctl endpoint status磁盘延迟(fdatasync >10ms 告警)、碎片整理

8.3 排错通用方法论

1. 定界: 是单个 Pod / 单个节点 / 整个命名空间 / 全集群?
2. 看面: kubectl get -o wide → describe → logs (--previous)
3. 看底: 节点 journalctl -u rke2-server/-u rke2-agent
         crictl ps / crictl logs(容器运行时层)
4. 看网: 容器内 ping/curl/nslookup → Service → Endpoints → kube-proxy → CNI
5. 看数: Prometheus/Grafana 历史曲线, 找时间相关性(变更? 流量突增? 证书到期?)
6. 复现: 最小化复现(临时 busybox Pod) → 二分法定位

黄金命令合集:

bash
kubectl get events -A --sort-by=.lastTimestamp | tail -20
kubectl get pods -A -o wide | grep -vE 'Running|Completed'
kubectl describe node <node> | sed -n '/Conditions/,/Addresses/p'
journalctl -u rke2-agent --since "1 hour ago" | grep -iE 'error|fail'
crictl ps -a | grep -v Running

本部分完。下一部分将围绕 K8s 辅助工具生态(Rancher、K9s、ArgoCD、cert-manager、备份工具 Velero 等)展开,把本部分的进阶能力落到日常运维效率上。