阶段二 · 进阶与实践

K8s 进阶概览

本篇定位

前七篇覆盖了 K8s 日常操作的核心知识:架构,Pod,工作负载,Service,Ingress,配置,存储.
本篇是一个进阶索引: 快速介绍Scheduler调度策略,弹性伸缩,RBAC,Helm,Kustomize,CRD/Operator等进阶主题.
目标不是精讲每个领域,而是建立"知道有什么,什么时候需要深入"的全景地图.

Scheduler的调度策略

默认情况下,Scheduler 根据资源剩余量自动选择节点.但生产中常需要更精细的控制:

  • "GPU Pod 只跑在 GPU 节点"
  • "两个 Pod 不要放同一节点"
  • "维护中的节点不接受新 Pod"
  • ......

nodeSelector - 最简单的节点选择

spec:
nodeSelector:
gpu: "true" # 只调度到有 gpu=true 标签的节点

# 管理员给节点打标签:
# kubectl label nodes node-3 gpu=true

简单但不灵活: 只支持精确匹配,不能表达"尽量放到 zone-a,放不下再去 zone-b".

Node Affinity - 灵活的节点亲和

spec:
affinity:
nodeAffinity:
# 硬性要求:必须满足(不满足则 Pod 不调度)
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: ["us-east-1a", "us-east-1b"]

# 软性偏好:尽量满足(不满足也可以调度)
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80 # 权重 1-100
preference:
matchExpressions:
- key: node-type
operator: In
values: ["high-memory"]
类型 含义 效果
required...(硬亲和) 必须满足 不满足 → Pod Pending
preferred...(软亲和) 尽量满足 不满足 → 还是会调度,只是优先级降低

Pod Affinity / Anti-Affinity

不是"Pod 要去哪个节点",而是"Pod 之间的关系":

spec:
affinity:
# Pod 亲和:和某些 Pod 放在一起
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: cache # 我要和 cache Pod 在同一节点
topologyKey: kubernetes.io/hostname

# Pod 反亲和:和某些 Pod 分开
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: web # 同一个 Deployment 的 Pod 尽量分散
topologyKey: kubernetes.io/hostname

topologyKey 是什么

topologyKey 定义"同一/不同"的粒度.

  • kubernetes.io/hostname 表示节点级别(同/不同节点)
  • topology.kubernetes.io/zone 表示可用区级别(同/不同 AZ)

反亲和常用于高可用:同一个服务的 Pod 分散到不同节点/AZ,避免单点故障.

Taints 与 Tolerations

Affinity 是"Pod 主动选节点",Taint 是"节点主动排斥 Pod".

# 给节点打 Taint("我有毒,普通 Pod 别来")
$ kubectl taint nodes node-gpu dedicated=gpu:NoSchedule

# 效果:没有对应 Toleration 的 Pod 不会被调度到 node-gpu
# Pod 要去 node-gpu,必须声明"我能容忍这个 Taint"
spec:
tolerations:
- key: dedicated
operator: Equal
value: gpu
effect: NoSchedule
Taint Effect 含义
NoSchedule 新 Pod 不调度到该节点(已有 Pod 不受影响)
PreferNoSchedule 尽量不调度(软性)
NoExecute 新 Pod 不调度 + 驱逐已有不容忍的 Pod
典型组合:
Taint: dedicated=gpu:NoSchedule + Toleration + nodeSelector: gpu=true
→ 只有 GPU 工作负载能跑在 GPU 节点,GPU 节点不会被普通 Pod 占用

Taint: node.kubernetes.io/not-ready:NoExecute(K8s 自动添加)
→ 节点 NotReady 时自动驱逐上面的 Pod(容错机制)

弹性伸缩 - HPA / VPA / CA

HPA - 水平 Pod 自动伸缩

HPA(Horizontal Pod Autoscaler)根据指标自动调整 Deployment/StatefulSet 的 replicas 数量.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
minReplicas: 3 # 最少 3 个
maxReplicas: 50 # 最多 50 个
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 平均 CPU 利用率超过 70% → 扩容
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
HPA 控制循环(默认每 15 秒一次):
1. 从 Metrics Server 获取当前所有 Pod 的 CPU/内存使用量
2. 计算平均利用率
3. 对比目标值(如 70%)
4. 期望副本数 = 当前副本数 × (当前指标 / 目标指标)
例:当前 3 Pod,平均 CPU 90%,目标 70%
期望 = 3 × (90/70) = 3.86 → 向上取整 = 4
5. 更新 Deployment 的 replicas
6. 冷却期(默认扩容 0s,缩容 5min)避免频繁伸缩

HPA 前提:必须设置 resources.requests

HPA 按"实际使用 / requests"计算利用率.
如果 Pod 没有设置 resources.requests.cpu,HPA 无法计算利用率,不会触发伸缩.同时需要集群安装 Metrics Server(大多数托管集群已自带).

VPA - 垂直 Pod 自动伸缩

VPA 不改副本数,而是自动调整单个 Pod 的 CPU/内存 requests 和 limits.

与 HPA 的区别:

  • HPA:流量大了 → 加更多 Pod(水平扩展)
  • VPA:单个 Pod 需要更多资源 → 加大 Pod 的 CPU/内存配额(垂直扩展)

适用场景:

  • 不确定该给 Pod 分配多少资源(VPA 自动推荐)
  • 无法水平扩展的应用(如单实例数据库)
  • 优化资源利用率(避免过度分配)

VPA 的限制

  • VPA 调整 requests 时需要重建 Pod(当前不支持热更新资源配额)
  • HPA 和 VPA 不应该同时基于 CPU 指标(会冲突)

Cluster Autoscaler - 节点级伸缩

HPA 管 Pod 数量,Cluster Autoscaler(CA)管节点数量--两者配合实现全自动弹性:

  1. HPA 增加 Pod 副本数 → 节点资源不足 → Pod 卡在 Pending
  2. CA 检测到 Pending Pod → 向云平台申请新节点 → Pod 调度成功
  3. 流量回落 → HPA 缩减 Pod → 节点上 Pod 过少 → CA 回收空闲节点降成本

RBAC - 权限控制

RBAC(Role-Based Access Control)回答"谁能对什么资源做什么操作".

四个核心对象

┌────────────────────────────────────────────────────────────┐
│ Role / ClusterRole 定义"能做什么" │
│ - resources: [pods, services, deployments] │
│ - verbs: [get, list, create, update, delete] │
├────────────────────────────────────────────────────────────┤
│ RoleBinding / ClusterRoleBinding 把"能做什么"绑给"谁" │
│ - subjects: [User / Group / ServiceAccount] │
│ - roleRef: 引用哪个 Role/ClusterRole │
└────────────────────────────────────────────────────────────┘

作用域:
Role + RoleBinding → namespace 级别(只管这个 ns 内的资源)
ClusterRole + ClusterRoleBinding → 集群级别(所有 ns + 集群资源)
# 定义角色:允许读取 default namespace 的 Pod 和 Service
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: default
rules:
- apiGroups: [""] # "" = core API group(Pod,Service 等)
resources: ["pods", "services"]
verbs: ["get", "list", "watch"]

---
# 把角色绑定给 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: ServiceAccount
name: monitoring-sa # 这个 ServiceAccount
namespace: default
roleRef:
kind: Role
name: pod-reader # 获得 pod-reader 的权限
apiGroup: rbac.authorization.k8s.io

最小权限原则

生产环境应为每个应用创建独立的 ServiceAccount,只授予它需要的最小权限.
默认的 default ServiceAccount 权限太少(好事)或被管理员额外赋权(危险).
永远不要给应用 cluster-admin 权限.

Helm - K8s 的包管理器

核心概念

概念 类比 含义
Chart npm package / apt .deb 一组模板化的 K8s YAML 文件 + 默认值
Release npm install 的实例 Chart 安装到集群后的一个运行实例
Repository npm registry 存放 Chart 的远程仓库
values.yaml 配置文件 用户传入的变量值,覆盖 Chart 默认值

常用命令

# 添加仓库
$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
$ helm repo update

# 搜索 Chart
$ helm search repo ingress-nginx

# 安装(创建 Release)
$ helm install my-nginx ingress-nginx/ingress-nginx \
--namespace ingress \
--create-namespace \
--set controller.replicaCount=2

# 查看已安装的 Release
$ helm list -A

# 升级(修改配置或升级 Chart 版本)
$ helm upgrade my-nginx ingress-nginx/ingress-nginx \
--set controller.replicaCount=3

# 回滚
$ helm rollback my-nginx 1 # 回滚到 revision 1

# 卸载
$ helm uninstall my-nginx -n ingress

Chart 目录结构

my-chart/
├── Chart.yaml # Chart 元数据(名称,版本,描述)
├── values.yaml # 默认配置值
├── templates/ # 模板文件目录
│ ├── deployment.yaml # {{ .Values.replicas }} 等模板变量
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── _helpers.tpl # 模板辅助函数
│ └── NOTES.txt # 安装后的提示信息
└── charts/ # 依赖的子 Chart

Kustomize - 无模板的配置定制

Helm 是模板引擎(Go template),Kustomize 是另一种思路:不修改原始 YAML,而是在上面叠加补丁.

Helm:模板化
deployment.yaml.tpl → 填入 values → 生成最终 YAML
优点:灵活,一套模板适配多种场景
缺点:模板语法复杂,调试困难

Kustomize:叠加补丁
base/deployment.yaml(原始 YAML,可直接 kubectl apply)
overlays/prod/patch.yaml(只写差异部分)
→ kustomize build overlays/prod → 合并输出最终 YAML
优点:原始 YAML 保持可读,补丁直观
缺点:复杂定制时补丁会很多
目录结构:
base/
├── kustomization.yaml
├── deployment.yaml # 原始 YAML(replicas: 1)
└── service.yaml

overlays/
├── dev/
│ ├── kustomization.yaml # resources: [../../base] patches: [...]
│ └── replicas-patch.yaml # replicas: 1
└── prod/
├── kustomization.yaml
└── replicas-patch.yaml # replicas: 5
# Kustomize 已内置在 kubectl 中
$ kubectl apply -k overlays/prod/

# 预览合并结果
$ kubectl kustomize overlays/prod/

Helm vs Kustomize:怎么选

用 Helm:安装第三方应用(ingress-nginx,cert-manager,Prometheus)--它们只提供 Helm Chart;或者你的应用需要高度参数化的模板
用 Kustomize:管理自己团队的应用部署--原始 YAML 保持简单,通过 overlays 区分环境.

两者也可以结合:Helm 生成基础 YAML,Kustomize 在上面打补丁.

CRD + Operator - 扩展 K8s 的能力

CRD - 自定义资源

K8s 内置了 Pod,Service,Deployment 等资源类型.CRD(Custom Resource Definition)允许你定义自己的资源类型--扩展 K8s API.

# 定义一个新的资源类型:MySQLCluster
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: mysqlclusters.database.example.com
spec:
group: database.example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
replicas:
type: integer
version:
type: string
storage:
type: string
scope: Namespaced
names:
plural: mysqlclusters
singular: mysqlcluster
kind: MySQLCluster
# CRD 注册后,可以像内置资源一样操作
$ kubectl apply -f mysqlcluster.yaml
$ kubectl get mysqlclusters
NAME REPLICAS VERSION STORAGE
prod-mysql 3 8.0 500Gi
# 创建自定义资源实例
apiVersion: database.example.com/v1
kind: MySQLCluster
metadata:
name: prod-mysql
spec:
replicas: 3
version: "8.0"
storage: "500Gi"

Operator - CRD 的 Controller

CRD 只定义了数据结构--"K8s API 能存储 MySQLCluster 对象了".
但光存储不够,还需要有人执行:看到 replicas: 3 就真的创建 3 个 MySQL Pod,配置主从复制,处理故障切换.这个"执行者"就是 Operator.

Operator = CRD + 自定义 Controller

CRD 定义了"期望状态长什么样"(如 MySQLCluster spec)
Operator(Controller)持续将实际状态调整为期望状态

MySQL Operator 的控制循环:
watch MySQLCluster 对象变化
→ replicas 从 1 变为 3?
→ 创建新的 StatefulSet Pod
→ 配置 MySQL 复制拓扑(主从)
→ 等 Pod Ready → 初始化数据同步
→ 检测到 mysql-0 不健康?
→ 触发 failover:提升 mysql-1 为新主
→ 更新 Service 端点
→ 发送告警

本质:把 DBA 的运维经验编码成程序

CRD = 在 K8s 菜单上添加了一道新菜.
Operator = 知道怎么做这道菜的厨师.你点了一份"三副本 MySQL 集群",厨师(Operator)负责采购食材(创建 Pod/PVC),烹饪(配置复制),处理突发(节点挂了就切换主从).

常见 Operator 生态

Operator 管理对象 核心能力
Prometheus Operator Prometheus / AlertManager 自动发现监控目标,管理告警规则
cert-manager Certificate / Issuer 自动签发和续签 TLS 证书
Strimzi Kafka Cluster Kafka 集群的声明式管理
CloudNativePG PostgreSQL Cluster HA 切换,备份恢复,滚动升级
Redis Operator Redis Cluster/Sentinel Redis 集群创建,扩缩,故障切换
External Secrets Operator ExternalSecret 从 Vault/AWS SM 同步密钥到 K8s Secret

Operator 开发框架

如果你想开发自己的 Operator:Go 语言用 kubebuilderoperator-sdk;它们自动生成脚手架代码,你只需要实现 Reconcile 函数(控制循环的核心逻辑).这是 Go 后端开发者深入 K8s 生态的一个自然方向.

NetworkPolicy - 网络层访问控制

默认情况下,集群内所有 Pod 可以互相通信(扁平网络).NetworkPolicy 允许你限制 Pod 的入站/出站流量.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-policy
namespace: production
spec:
podSelector:
matchLabels:
app: api # 对哪些 Pod 生效
policyTypes:
- Ingress
- Egress
ingress: # 入站:谁能访问我
- from:
- podSelector:
matchLabels:
app: frontend # 只有 frontend Pod 能访问 api
- namespaceSelector:
matchLabels:
env: production # 且必须在 production namespace
ports:
- port: 8080
protocol: TCP
egress: # 出站:我能访问谁
- to:
- podSelector:
matchLabels:
app: database # api 只能访问 database Pod
ports:
- port: 5432

NetworkPolicy 需要 CNI 支持

NetworkPolicy 对象只是声明: 实际执行依赖 CNI 插件.
Calico,Cilium,Weave 支持 NetworkPolicy;Flannel 默认不支持.安装 Flannel 的集群创建 NetworkPolicy 不会报错,但也不会生效.

资源管理进阶

LimitRange - namespace 级别默认值

apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: dev
spec:
limits:
- default: # 不写 limits 时的默认值
cpu: 500m
memory: 512Mi
defaultRequest: # 不写 requests 时的默认值
cpu: 100m
memory: 128Mi
type: Container

ResourceQuota - namespace 资源配额

apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "10" # namespace 内所有 Pod 的 CPU requests 总和不超过 10 核
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
pods: "50" # 最多 50 个 Pod
persistentvolumeclaims: "20" # 最多 20 个 PVC

配额的意义:多团队共享集群时,防止某个团队/namespace 把资源吃光.

可观测性快览

维度 工具 用途
指标(Metrics) Prometheus + Grafana CPU/内存/请求量/延迟 → 仪表盘 + 告警
日志(Logs) Fluentd/Filebeat + ES + Kibana 集中式日志收集和查询
追踪(Traces) Jaeger / Tempo + OpenTelemetry 分布式调用链追踪
事件(Events) kubectl get events / kube-state-metrics 集群内资源状态变化

可观测性是一个独立的大主题,详见本仓库的"可观测性"分类.这里只需知道:K8s 原生提供 Events 和基础的 kubectl top(需要 Metrics Server),完整的监控体系需要额外部署 Prometheus 栈.

小结

快速回顾

  • 调度策略:nodeSelector(简单)→ Node Affinity(灵活硬/软约束)→ Pod Anti-Affinity(分散部署)→ Taints/Tolerations(节点排斥)
  • 弹性伸缩三层:HPA(Pod 数量)→ VPA(Pod 资源)→ Cluster Autoscaler(节点数量)
  • RBAC:Role/ClusterRole 定义"能做什么",RoleBinding/ClusterRoleBinding 绑给"谁";最小权限原则
  • Helm:K8s 的包管理器,Chart = 模板化 YAML 包;适合安装第三方应用和高度参数化场景
  • Kustomize:无模板方式定制 YAML,base + overlays 分环境管理;适合团队自己的应用
  • CRD + Operator:扩展 K8s API + 自定义 Controller;把运维逻辑编码成程序(如数据库自动主从切换)
  • NetworkPolicy:Pod 级别的网络访问控制(谁能访问我,我能访问谁);需要 CNI 支持
  • 资源管理:LimitRange(namespace 默认值)+ ResourceQuota(namespace 配额上限)

动手练习

  1. 给一个节点打 Taint(env=staging:NoSchedule),创建一个不带 Toleration 的 Pod,验证它不会调度到该节点.然后加上 Toleration,验证调度成功.
  2. 创建一个 Deployment(replicas=2)+ Pod Anti-Affinity(hostname 级别),验证两个 Pod 是否分布在不同节点.
  3. 部署 Metrics Server(k3d 通常自带),创建一个 HPA(目标 CPU 50%).用 stress 工具模拟 CPU 压力,观察 Pod 是否自动扩容.
  4. 用 Helm 安装一个应用(如 ingress-nginx),熟悉 helm install / upgrade / rollback / uninstall 流程.
  5. 创建一个简单的 CRD(如 Greeting 对象,spec 只有 message 字段),用 kubectl apply 创建实例,用 kubectl get 查看.
  6. 创建一个 NetworkPolicy:只允许带 role=frontend 标签的 Pod 访问带 role=api 标签的 Pod 的 8080 端口.用 curl 验证策略是否生效.