0x00 前言
Kubernetes 的一切操作,最终都收敛为一句话:把一份 YAML(声明的期望状态)提交给集群,让控制平面不断把实际状态”收敛”到期望状态。理解 YAML 的组织格式、字段语义、资源关系网,以及它在控制平面内部的完整生效链路,是从”会copy YAML”进阶到”懂 Kubernetes”的分水岭
本文由浅入深介绍如下内容:
- YAML 的组织格式与核心 Key-Value 解剖:五大顶层字段、
metadata/spec/status的设计哲学、常见嵌套结构等 - 方法论与生产最佳实践:资源分类、关系网、QoS 与配额、高可用与安全、校验工具链、生产级完整模板
- 基于代码维度的生效原理:
kubectl apply -f config.yaml从终端到容器启动的完整生命周期源码剖析 kubectl apply的全链路总览
本文代码基于Kubernetes v1.21.1
相关文章阅读:
第一部分:YAML 的组织格式与核心 Key-Value 解析
0x01 核心骨架:五大顶层字段
几乎所有 Kubernetes 对象(无论原生资源还是 CRD 实例)都遵循同一套”五大核心顶层字段”骨架:
apiVersion: apps/v1 # 1. 该对象属于哪个 API 群组/版本
kind: Deployment # 2. 该对象是什么类型
metadata: # 3. 对象的元数据(身份、标签、归属)
name: nginx
namespace: default
spec: # 4. 期望状态(Desired State),由用户声明
replicas: 3
# ...
status: # 5. 实际状态(Observed State),由系统回填
availableReplicas: 3
# ...
这五个字段的职责边界非常清晰,可以用”谁来写、何时写、写什么”来区分:
| 字段 | 职责 | 由谁维护 | 底层对应 Go 类型 |
|---|---|---|---|
apiVersion |
声明 group/version,决定走哪条 REST 路径、用哪个 Scheme 解码 |
用户 | TypeMeta.APIVersion |
kind |
声明资源种类,与 apiVersion 共同构成 GVK |
用户 | TypeMeta.Kind |
metadata |
身份与归属元数据(名字、命名空间、标签、注解、属主) | 用户 + 系统 | ObjectMeta |
spec |
期望状态,用户声明”我要什么” | 用户(声明式) | 各资源的 XxxSpec |
status |
实际状态,系统回填”现在是什么” | 控制器(只读于用户) | 各资源的 XxxStatus |
apiVersion + kind 合称 GVK(GroupVersionKind),是 Kubernetes 类型系统的坐标。其 Go 定义位于 staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go:
// TypeMeta describes an individual object in an API response or request
type TypeMeta struct {
Kind string `json:"kind,omitempty" ...`
APIVersion string `json:"apiVersion,omitempty" ...`
}
为什么
apiVersion带群组(如apps/v1、networking.k8s.io/v1)而核心资源(Pod、Service、ConfigMap)是v1?因为核心资源属于”core / legacy group”,其 group 为空字符串,REST 路径是/api/v1/...;带群组的资源路径是/apis/<group>/<version>/...。GVK 到 REST 路径(GVR, GroupVersionResource)的映射由RESTMapper完成
0x02 metadata 解读:身份与关系的载体
metadata 对应 ObjectMeta(同样定义在 apimachinery/pkg/apis/meta/v1/types.go),它是对象的”身份证 + 关系网接口”。核心字段:
2.1 name 与 namespace:唯一定位
name:在”同一 namespace + 同一资源类型”下唯一。它是对象 key 的一部分(etcd 中 key 形如/registry/<resource>/<namespace>/<name>)namespace:命名空间,提供多租户隔离的逻辑边界。集群级资源(如Node、PersistentVolume、ClusterRole)没有 namespaceuid:系统分配的全局唯一 ID。注意name可复用(删了重建),但uid不会。属主引用正是靠uid精确锁定对象generateName:与name互斥的一种”前缀生成名”机制;但kubectl apply不允许使用generateName(见第三部分applyOneObject的校验逻辑)resourceVersion:乐观并发控制(Optimistic Concurrency)的版本号,本质是 etcd 的 revision。更新时若携带的resourceVersion与服务端不一致,会返回 409 Conflictgeneration与status.observedGeneration:generation在spec变更时自增;控制器把自己处理到的generation回填进status.observedGeneration,用于判断”控制器是否已看到最新的 spec”
2.2 labels:选择器的基石
labels 是 map[string]string,是 Kubernetes “松耦合关联”的核心机制。它不是给人看的备注,而是给选择器(Selector)用的索引:
- Service 通过
spec.selector匹配 Pod 的 labels,决定流量转发给哪些 Pod - Deployment/ReplicaSet 通过
spec.selector.matchLabels管理哪些 Pod 归自己 nodeSelector、亲和性、拓扑分布约束都基于 label
坑点:Deployment 的
spec.selector.matchLabels一旦创建不可变(immutable),且必须是spec.template.metadata.labels的子集,否则会报selector does not match template labels
2.3 annotations:非标识性元数据
annotations 同为 map[string]string,但与 labels 本质不同:annotations 不能用于选择,它承载的是”工具链/控制器消费的结构化信息”,典型用途:
kubectl.kubernetes.io/last-applied-configuration:kubectl apply客户端三方合并的”原始配置”快照(见后文)- Ingress controller、cert-manager、Prometheus 等通过 annotation 下发配置
- 值可以很大、可以是 JSON,而 label 的 value 有长度(63 字符)与字符集限制
2.4 ownerReferences:资源关系的”硬连接”
ownerReferences 是理解 Kubernetes 级联关系的关键。它声明”我这个对象的属主是谁”:
metadata:
name: nginx-66b6c48dd5-abcde # 一个 Pod
ownerReferences:
- apiVersion: apps/v1
kind: ReplicaSet
name: nginx-66b6c48dd5
uid: 1a2b3c4d-... # 靠 uid 精确锁定属主
controller: true # 标记这是"管理型"属主
blockOwnerDeletion: true # 删除属主时需先处理该依赖
它的底层意义如下:
- 级联删除(Garbage Collection):删除 Deployment 时,GC 控制器顺着
ownerReferences找到它的 ReplicaSet、再找到 Pod,依次回收。controller: true标记真正的管理者(一个对象只能有一个 controller owner) - 控制器的归属判定(Adoption / Orphaning):控制器通过
ownerReferences判断某个子对象是不是”自己的”。第三部分会看到DeploymentController.getReplicaSetsForDeployment和claimReplicaSets如何依据 selector + ownerRef 认领或释放 ReplicaSet
0x03 spec 与 status:声明式的灵魂
Kubernetes 最核心的设计哲学是 声明式(Declarative):用户只声明”期望状态”(spec),不关心”怎么做”;系统负责持续地把”实际状态”(status)收敛到期望
spec(Specification):期望态。用户写入,是”输入”status:实际态。控制器回填,是”输出”。用户即便在 YAML 里写了status,apply时也基本会被忽略(status 子资源有独立的更新路径/status)
这种”期望 vs 实际”的分离,使得整个系统可以用统一的 调谐循环(Reconciliation Loop) 来驱动:
flowchart LR
user["用户声明 spec<br/>(期望状态)"] --> store["etcd 存储对象"]
store --> ctrl["控制器 Watch 到变更"]
ctrl --> diff{"diff:<br/>spec vs status?"}
diff -->|"有差异"| act["执行动作<br/>创建/删除/更新子对象"]
act --> obs["观测实际结果"]
obs --> write["回填 status"]
write --> store
diff -->|"已一致"| idle["无操作, 等待下次事件"]
这就是著名的 Control Loop / Level-Triggered(水平触发) 模型:控制器不关心”发生了哪个事件”,只关心”当前期望与实际的差距”,因此天然具备幂等性和自愈能力。后文(part3)的 syncDeployment 会介绍循环的具体代码实现逻辑
0x04 常见嵌套结构:Pod 模板与书写规范
工作负载类资源(Deployment / StatefulSet / DaemonSet / Job)的 spec 里都嵌套了一个 Pod 模板(spec.template),其结构与一个独立 Pod 的 metadata + spec 完全一致。这是最常出错的部分:
spec:
replicas: 3
selector:
matchLabels:
app: nginx # (A) 必须与 (B) 匹配
template: # Pod 模板(PodTemplateSpec)
metadata:
labels:
app: nginx # (B) Pod 自身的 label, 供 (A) 选择
spec: # Pod 的 spec
containers:
- name: nginx
image: nginx:1.21.6
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
env:
- name: GREETING
value: "hello"
- name: POD_IP # Downward API: 注入字段值
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: DB_PASSWORD # 从 Secret 注入
valueFrom:
secretKeyRef:
name: db-secret
key: password
volumeMounts:
- name: config-vol
mountPath: /etc/app
readOnly: true
volumes:
- name: config-vol
configMap:
name: app-config
常见坑点清单:
- selector 与 template labels 不匹配:如上文 (A)/(B),Deployment 创建直接失败
- 缩进错误(YAML 致命伤):
env、volumeMounts是 container 级字段(在containers[]下),volumes是 Pod 级字段(在template.spec下) - list 与 map 混淆:
containers、env、ports、volumes都是 列表(list),每项以-开头;而matchLabels、labels是 map - 镜像 tag 用 latest:会导致
imagePullPolicy默认变为Always,且版本不可追溯。生产环境务必用确定 tag 或 digest - env 的 value 必须是字符串:
value: 123会在部分场景被解析为数字而报错,建议value: "123" - 数值与引号:端口、副本数是整数,不要加引号;而形如
"true"、"3.14"这种本应是字符串的要显式加引号,避免 YAML 把yes/no/on/off解析成bool
对象层级关系总览(以 Deployment 为例):
flowchart TB
dep["Deployment"] --> depMeta["metadata<br/>name/labels/annotations"]
dep --> depSpec["spec"]
dep --> depStatus["status (系统回填)"]
depSpec --> rep["replicas"]
depSpec --> sel["selector.matchLabels"]
depSpec --> tmpl["template (PodTemplateSpec)"]
tmpl --> tmplMeta["metadata.labels"]
tmpl --> tmplSpec["spec (PodSpec)"]
tmplSpec --> ctns["containers[]"]
tmplSpec --> vols["volumes[]"]
ctns --> img["image / ports"]
ctns --> envs["env[] / envFrom[]"]
ctns --> vm["volumeMounts[]"]
ctns --> res["resources.requests/limits"]
ctns --> probe["livenessProbe/readinessProbe/startupProbe"]
sel -. "必须匹配" .-> tmplMeta
vm -. "按 name 引用" .-> vols
第二部分:方法论与最佳实践
0x05 分类归纳法:如何驯服成百上千种资源
如何理解海量原生资源 + 海量 CRD?正确姿势是按 职责维度 归为五类,每类抓住”核心资源 + 关系锚点”即可:
- 工作负载(Workloads):定义”跑什么、跑几个、怎么升级”
Pod(最小调度单元)、ReplicaSet(维持副本数)、Deployment(声明式滚动升级,管理 RS)、StatefulSet(有状态、稳定网络标识与存储)、DaemonSet(每节点一个)、Job/CronJob(批处理 / 定时)
- 网络(Networking):定义”怎么被访问、流量怎么路由”
Service(稳定虚拟 IP + 负载均衡,类型 ClusterIP/NodePort/LoadBalancer)、Endpoints/EndpointSlice(后端 Pod 列表)、Ingress(七层路由)、NetworkPolicy(网络隔离策略)
- 存储(Storage):定义”数据放哪、怎么供给”
PersistentVolume(PV,实际存储资源,集群级)、PersistentVolumeClaim(PVC,用户的存储申领)、StorageClass(动态供给模板)、Volume(Pod 内的卷引用)
- 配置与安全(Config & Security):定义”配置注入、凭据、权限”
ConfigMap(非敏感配置)、Secret(敏感数据)、ServiceAccount(Pod 身份)、Role/ClusterRole/RoleBinding/ClusterRoleBinding(RBAC 授权)、ResourceQuota/LimitRange(资源治理)
- 集群级(Cluster-scoped):定义”集群骨架”
Node、Namespace、PersistentVolume、CustomResourceDefinition(CRD,扩展 API)
学习 CRD 的方法与原生资源完全一致:任何 CRD 实例也是
apiVersion/kind/metadata/spec/status骨架,其spec/status的字段由 CRD 的 OpenAPI v3 schema 定义,可用kubectl explain <crd>查询
0x06 核心关系网:资源如何交织成网
Kubernetes 的资源通过几类”关联字段”编织成一张网,如下图所示:
flowchart TB
dep["Deployment"] -->|"ownerRef"| rs["ReplicaSet"]
rs -->|"ownerRef"| pod["Pod"]
dep -. "spec.selector.matchLabels" .-> pod
svc["Service"] -. "spec.selector 匹配 labels" .-> pod
svc --> eps["EndpointSlice<br/>(回填后端 Pod IP)"]
ing["Ingress"] -->|"backend.service.name"| svc
pod -->|"volumes.persistentVolumeClaim"| pvc["PVC"]
pvc -->|"spec.storageClassName"| sc["StorageClass"]
pvc -. "绑定(bound)" .-> pv["PersistentVolume"]
sc -->|"动态供给"| pv
pod -->|"envFrom / volumes"| cm["ConfigMap"]
pod -->|"secretKeyRef"| sec["Secret"]
pod -->|"spec.serviceAccountName"| sa["ServiceAccount"]
sa -. "RoleBinding" .-> role["Role / ClusterRole"]
上图中,存在三根最关键的”线”:
selector.matchLabels(软关联):Deployment/Service 通过 label selector 松耦合地”选中”Pod。这是”标签 -> 选择器”的间接关联,Pod 并不知道自己被谁选中ownerReferences(硬关联):子对象通过 uid 指向属主,支撑级联删除与归属判定。这是”自下而上”的强引用storageClassName/claimRef(供需关联):PVC 声明需求(容量、storageClassName),StorageClass 动态供给 PV,PV 与 PVC 双向绑定(PVC 的volumeName指向 PV,PV 的claimRef指回 PVC)
0x07 生产实践一:资源配额、限制与 QoS
7.1 requests 与 limits 的科学设定
resources.requests:调度依据。调度器只看 requests 来判断节点是否”装得下”(见后文noderesources插件介绍)。requests 是对资源的”预订”resources.limits:运行时上限。由 kubelet/cgroup 强制执行。CPU 超限会被限流(throttle),内存超限会被 OOMKill
设定原则:
- requests 设为”稳态实际用量”(可观测 P50~P90),limits 设为”可容忍峰值”
- CPU 可”超售”(compressible,超限只限流不杀),内存不可超售(incompressible,超限即杀),因此内存的 requests 与 limits 建议设置得更接近
- 避免 limits 远大于 requests:会造成节点超售严重、争抢时被优先驱逐
7.2 QoS 三等级:源码级判定
Pod 的 QoS Class 不是用户直接声明的,而是 kubelet 依据 requests/limits 推导出来的,直接决定了节点资源紧张时的驱逐顺序。判定逻辑在 pkg/apis/core/v1/helper/qos/qos.go 的 GetPodQOS:
// pkg/apis/core/v1/helper/qos/qos.go
func GetPodQOS(pod *v1.Pod) v1.PodQOSClass {
// ... 遍历所有容器, 累加 requests / limits
// 规则 1: 所有容器都没设任何 requests/limits -> BestEffort
if len(requests) == 0 && len(limits) == 0 {
return v1.PodQOSBestEffort
}
// 规则 2: 每个容器的 cpu 和 memory 都设了 limits, 且 requests == limits -> Guaranteed
if isGuaranteed && len(requests) == len(limits) {
return v1.PodQOSGuaranteed
}
// 规则 3: 其余情况 -> Burstable
return v1.PodQOSBurstable
}
| QoS 等级 | 条件 | 驱逐优先级(紧张时先杀谁) |
|---|---|---|
Guaranteed |
每个容器 cpu/memory 的 requests == limits(且都已设置) | 最后被杀(最高保障) |
Burstable |
至少一个容器设了 requests/limits,但不满足 Guaranteed | 居中 |
BestEffort |
完全没设 requests/limits | 最先被杀 |
生产环境建议如下:核心服务用 Guaranteed(requests == limits),可弹性的用 Burstable,离线/可牺牲任务可用 BestEffort
7.3 ResourceQuota 与 LimitRange:命名空间级治理
ResourceQuota:对整个 namespace 的资源总量设限(CPU/内存总 requests、总 limits、对象数量如 Pod 数、PVC 数)。一旦开启,该 namespace 下的 Pod 必须显式声明 requests/limits,否则创建被拒LimitRange:对单个对象(Pod/Container/PVC)设定默认值与最小/最大值。它能为”没写 resources 的容器”自动注入默认 requests/limits(通过 Mutating Admission 实现),并校验是否越界
二者配合解决了:LimitRange 保证”每个容器都有合理的默认值”,ResourceQuota 保证”整个命名空间不超售”
0x08 生产实践二:高可用与安全相关
8.1 反亲和性与拓扑分布约束
为避免同一服务的多个副本挤在同一节点/机架/可用区,有两种机制:
spec:
template:
spec:
# 方式一: 反亲和性(硬性 required 或软性 preferred)
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 硬性: 不满足则不调度
- labelSelector:
matchLabels:
app: nginx
topologyKey: kubernetes.io/hostname # 同一节点不放两个
# 方式二: 拓扑分布约束(更现代, 可精确控制"倾斜度")
topologySpreadConstraints:
- maxSkew: 1 # 各拓扑域副本数差 <= 1
topologyKey: topology.kubernetes.io/zone # 按可用区打散
whenUnsatisfiable: DoNotSchedule # 或 ScheduleAnyway
labelSelector:
matchLabels:
app: nginx
podAntiAffinity:表达”不要和谁待在一起”,required为硬约束,preferred为软偏好topologySpreadConstraints(v1.19 GA):更精细,通过maxSkew控制各拓扑域(node/zone/region)之间副本数的最大倾斜度,是生产多可用区打散的首选
8.2 securityContext:最小权限
spec:
template:
spec:
securityContext: # Pod 级
runAsNonRoot: true # 禁止以 root 运行
runAsUser: 10001
fsGroup: 10001 # 卷的属组
containers:
- name: app
securityContext: # 容器级(优先级更高)
allowPrivilegeEscalation: false # 禁止提权
readOnlyRootFilesystem: true # 根文件系统只读
capabilities:
drop: ["ALL"] # 丢弃所有 Linux capabilities
安全基线:非 root 运行、根文件系统只读、禁止提权、丢弃不必要的 capabilities。这些也是 kube-linter、Pod Security Standards(Restricted 级别)重点检查的项
8.3 探针(Probes)配置策略
三种探针各司其职,勿混用:
startupProbe(启动探针):保护”慢启动”应用。成功前,另外两个探针不生效。适合 JVM 等冷启动慢的应用,避免启动期被 liveness 误杀readinessProbe(就绪探针):决定 Pod 是否接入 Service 流量。失败只摘流量,不重启livenessProbe(存活探针):决定是否重启容器。失败会触发 kubelet 重启
startupProbe:
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 10 # 最多容忍 30*10=300s 启动
readinessProbe:
httpGet: { path: /ready, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 15
periodSeconds: 20
注意点:liveness 探针不要检查下游依赖(数据库、外部 API),否则下游抖动会引发大面积无意义重启(级联故障)。建议liveness 只应检查”进程本身是否还活着”
0x09 自动化工具链说明
9.1 边学边查:kubectl explain 与 dry-run
# 查字段文档(直接读 API Server 的 OpenAPI schema, 最权威)
kubectl explain deployment.spec.template.spec.containers.resources
kubectl explain pod.spec --recursive # 递归展开所有字段
# 客户端校验(不连 API Server, 只做本地结构/schema 校验)
kubectl apply -f config.yaml --dry-run=client -o yaml
# 服务端校验(经过 API Server 的解码/默认值/准入, 但不落库)
kubectl apply -f config.yaml --dry-run=server
通常,--dry-run=client 只在本地校验,不经过准入控制;--dry-run=server 会真正走到 API Server 的默认值填充与(dry-run 感知的)准入 webhook,能发现更多问题,但不持久化到 etcd
0x0A 实战:生产级完整 YAML 模板
下面是一份包含 Deployment(探针 + 资源限制 + SecurityContext)、Service、ConfigMap、HPA 的配置文件,可直接作为生产模板基线参考:
# ============== 1. ConfigMap: 应用非敏感配置 ==============
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
namespace: prod
data:
APP_MODE: "production" # 普通键值
app.conf: | # 多行文件内容
log_level = info
max_conn = 1000
---
# ============== 2. Deployment: 核心工作负载 ==============
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: prod
labels:
app: web
spec:
replicas: 3 # 3 副本保证高可用
revisionHistoryLimit: 5 # 保留 5 个历史版本便于回滚
strategy:
type: RollingUpdate # 滚动升级, 不中断服务
rollingUpdate:
maxUnavailable: 0 # 升级过程中不允许少于期望副本
maxSurge: 1 # 最多多起 1 个新 Pod
selector:
matchLabels:
app: web # (A) 必须匹配下方 template.labels
template:
metadata:
labels:
app: web # (B) Pod 标签, 供 (A) 与 Service 选择
spec:
securityContext: # Pod 级安全上下文
runAsNonRoot: true # 禁止 root 运行
runAsUser: 10001
fsGroup: 10001
# 多副本按可用区打散, 保证高可用
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: registry.example.com/web:1.4.2 # 确定版本, 不用 latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: web-config # 批量注入 ConfigMap 为环境变量
resources: # Guaranteed QoS: requests == limits
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "500m"
memory: "512Mi"
securityContext: # 容器级安全上下文
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
startupProbe: # 慢启动保护
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 10
readinessProbe: # 就绪才接流量
httpGet: { path: /ready, port: 8080 }
periodSeconds: 10
livenessProbe: # 存活检测(只查进程本身)
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- name: tmp # 根文件系统只读, 需为可写目录挂 emptyDir
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
# ============== 3. Service: 稳定访问入口 ==============
apiVersion: v1
kind: Service
metadata:
name: web
namespace: prod
spec:
type: ClusterIP
selector:
app: web # 通过 label 选中上面的 Pod
ports:
- port: 80 # Service 暴露端口
targetPort: 8080 # 转发到容器端口
---
# ============== 4. HPA: 水平自动扩缩容 ==============
apiVersion: autoscaling/v2beta2 # v1.21 中 v2 的稳定形态为 v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: web
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web # 作用于上面的 Deployment
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU 平均利用率超 70% 则扩容
版本说明:在 v1.21 中,HPA 的
autoscaling/v2尚未 GA,稳定可用的多指标版本为autoscaling/v2beta2(autoscaling/v2在 v1.23 才 GA)。若只用单一 CPU 指标,也可用autoscaling/v1
第三部分:kubectl的原理(v1.21.1)
当在终端敲下 kubectl apply -f config.yaml,到容器真正在节点上跑起来,经历了一条贯穿客户端、API Server、控制器、调度器、Kubelet 的完整链路。本章节以 v1.21.1 源码为据,剖析 kubectl apply 从客户端合并、控制平面准入、控制器调谐、调度器打分,到 kubelet 经 CRI/CNI/CSI 起容器的全链路。整体链路时序如下:
sequenceDiagram
participant U as kubectl (客户端)
participant API as kube-apiserver
participant ETCD as etcd
participant DC as Deployment/RS Controller
participant SCH as kube-scheduler
participant KL as kubelet (节点)
participant CRI as CRI/CNI/CSI
U->>U: 解析 YAML -> Builder -> resource.Info
U->>API: apply(三方合并 patch 或 SSA Apply)
API->>API: 认证 -> 授权 -> Mutating准入 -> 校验 -> Validating准入
API->>ETCD: 持久化对象 (带 managedFields)
API-->>U: 返回结果
DC->>API: ListWatch Deployment
DC->>API: 创建 ReplicaSet
DC->>API: 由 RS 创建 Pod (spec.nodeName 为空)
SCH->>API: Watch 到未调度 Pod
SCH->>SCH: Filter(过滤) -> Score(打分) -> selectHost
SCH->>API: Bind: 写入 pod.spec.nodeName
KL->>API: Watch 到绑定到本节点的 Pod
KL->>CRI: SyncPod: 建 Sandbox(CNI) -> 挂卷(CSI) -> 起容器
KL->>API: 回填 pod.status
0x0B 环节一:客户端解析与 apply 合并
入口在 staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go。kubectl apply 先用 resource Builder 把 -f 的 YAML 解析为一组 resource.Info(每个对象一个),然后 Run() 遍历调用 applyOneObject():
// staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go
func (o *ApplyOptions) Run() error {
// ... GetObjects(): Builder 解析 YAML -> []*resource.Info
infos, err := o.GetObjects()
// ...
for _, info := range infos {
if err := o.applyOneObject(info); err != nil { // 逐对象 apply
errs = append(errs, err)
}
}
// ...
}
applyOneObject 里有一个关键分叉:Server-Side Apply(SSA) 还是传统 客户端三方合并(Client-Side Three-Way Merge)
11.1 传统客户端三方合并(Client-Side Apply)
这是 v1.21 默认行为(未加 --server-side)。它需要三份配置做 diff:
- original:对象 annotation
kubectl.kubernetes.io/last-applied-configuration里保存的”上次 apply 的配置” - modified:本次 YAML 的内容
- current:API Server 上该对象的当前状态
逻辑在 applyOneObject 的客户端分支:若对象不存在则直接 Create(并写入 last-applied annotation);若存在则交给 patcher.Patch:
// staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go (客户端分支摘录)
modified, err := util.GetModifiedConfiguration(info.Object, true, unstructured.UnstructuredJSONScheme)
if err := info.Get(); err != nil {
if errors.IsNotFound(err) {
// 对象不存在 -> 写 last-applied 注解 -> Create, 跳过三方合并
util.CreateApplyAnnotation(info.Object, ...)
helper.Create(info.Namespace, true, info.Object)
}
// ...
}
// 对象已存在 -> 三方合并 patch
patcher, _ := newPatcher(o, info, helper)
patchBytes, patchedObject, err := patcher.Patch(info.Object, modified, info.Source, info.Namespace, info.Name, o.ErrOut)
三方合并的核心计算在 patcher.go 的 patchSimple:根据对象类型选择 patch 策略,对注册在 Scheme 里的原生类型用 Strategic Merge Patch(能理解 list 的合并语义,如按 name 合并容器),否则回退到 JSON Merge Patch:
// staging/src/k8s.io/kubectl/pkg/cmd/apply/patcher.go
func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, ...) (...) {
current, _ := runtime.Encode(unstructured.UnstructuredJSONScheme, obj) // 当前态
original, _ := util.GetOriginalConfiguration(obj) // last-applied 注解
versionedObject, err := scheme.Scheme.New(p.Mapping.GroupVersionKind)
switch {
case runtime.IsNotRegisteredError(err):
// 非内置类型(如 CRD, 无 strategic 元数据): JSON Merge Patch 三方合并
patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...)
case err == nil:
// 内置类型: Strategic Merge Patch(优先用 OpenAPI schema, 否则用编译进二进制的结构体 tag)
patchType = types.StrategicMergePatchType
// ... CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite)
}
// 最终 PATCH 到 API Server
patchedObj, err := p.Helper.Patch(namespace, name, patchType, patch, nil)
return patch, patchedObj, err
}
Patch 外层还有冲突重试 + force 兜底:遇到 409 Conflict 会重新 Get 当前态再算 patch(最多 maxPatchRetry 次),若仍失败且带 --force,则 deleteAndCreate 删了重建:
// patcher.go
func (p *Patcher) Patch(current runtime.Object, modified []byte, ...) (...) {
patchBytes, patchObject, err := p.patchSimple(...)
for i := 1; i <= p.Retries && errors.IsConflict(err); i++ {
current, _ = p.Helper.Get(namespace, name) // 冲突则刷新当前态重试
patchBytes, patchObject, err = p.patchSimple(...)
}
if err != nil && (errors.IsConflict(err) || errors.IsInvalid(err)) && p.Force {
patchBytes, patchObject, err = p.deleteAndCreate(current, modified, namespace, name)
}
return patchBytes, patchObject, err
}
11.2 Server-Side Apply(SSA)与 ManagedFields
加上 --server-side 后,客户端不再本地算 diff,而是把整个对象用 Content-Type: application/apply-patch+yaml(types.ApplyPatchType)直接 PATCH 给 API Server,由服务端负责合并与冲突检测:
// staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go
if o.ServerSideApply {
data, _ := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object)
options := metav1.PatchOptions{ Force: &o.ForceConflicts }
obj, err := helper.Patch(info.Namespace, info.Name, types.ApplyPatchType, data, &options)
if errors.IsConflict(err) {
// 字段被其他 manager 占用 -> 提示用 --force-conflicts
}
}
SSA 的核心是 managedFields:对象的 metadata.managedFields 记录了”每个字段分别由哪个 field manager(如 kubectl、某控制器)管理”。服务端据此判断本次 apply 是否与他人管理的字段冲突。相关实现:
- 服务端合并与所有权追踪:
staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go(处理ApplyPatchType) - ManagedFields 的解析/提取工具:
staging/src/k8s.io/apimachinery/pkg/util/managedfields/(如extract.go),底层结构化合并由sigs.k8s.io/structured-merge-diff完成
客户端合并 vs SSA 对比:客户端合并依赖 last-applied 注解,多个客户端/控制器同时改同一对象时容易”互相覆盖”;SSA 把字段所有权下沉到服务端,用 managedFields 精确追踪、冲突显式化,是官方推荐的演进方向(v1.22 GA)
0x0C 环节二:控制平面处理流(认证 -> 授权 -> 准入 -> 持久化)
当请求到达 kube-apiserver 后,经过一条标准的处理链。核心顺序:认证(Authentication)-> 授权(Authorization)-> 准入控制(Admission)-> 校验(Validation)-> 写入 etcd
flowchart LR
req["HTTP 请求"] --> authn["认证<br/>Authentication"]
authn --> authz["授权<br/>Authorization (RBAC)"]
authz --> mutate["Mutating 准入<br/>(可改对象)"]
mutate --> schemaval["对象 Schema 校验<br/>+ 默认值填充"]
schemaval --> validate["Validating 准入<br/>(只读校验)"]
validate --> etcd["持久化到 etcd"]
12.1 认证与授权
- 认证:识别”你是谁”。支持客户端证书、Bearer Token、ServiceAccount Token、OIDC 等多种 authenticator,任一通过即可
- 授权:判断”你能否做这个操作”。生产集群主要用 RBAC,依据
(user, verb, resource, namespace)对照 Role/ClusterRole 的规则
12.2 准入控制:Mutating 与 Validating
准入控制是 YAML 生效前”最后一道关卡”,也是 LimitRange 注入默认值、各类策略治理(Gatekeeper/Kyverno)落地的地方。它分两阶段,顺序是先 Mutating(可修改对象)后 Validating(只读校验)。准入链的执行逻辑在 staging/src/k8s.io/apiserver/pkg/admission/chain.go:
// staging/src/k8s.io/apiserver/pkg/admission/chain.go
// 第一阶段: 遍历所有 Mutating 插件, 可修改对象
func (admissionHandler chainAdmissionHandler) Admit(ctx context.Context, a Attributes, o ObjectInterfaces) error {
for _, handler := range admissionHandler {
if !handler.Handles(a.GetOperation()) { continue }
if mutator, ok := handler.(MutationInterface); ok {
if err := mutator.Admit(ctx, a, o); err != nil {
return err // 任一失败立即返回
}
}
}
return nil
}
// 第二阶段: 遍历所有 Validating 插件, 只读校验
func (admissionHandler chainAdmissionHandler) Validate(ctx context.Context, a Attributes, o ObjectInterfaces) error {
for _, handler := range admissionHandler {
if !handler.Handles(a.GetOperation()) { continue }
if validator, ok := handler.(ValidationInterface); ok {
if err := validator.Validate(ctx, a, o); err != nil {
return err
}
}
}
return nil
}
准入插件分两类:
- 内置(编译进 apiserver):如
plugin/pkg/admission/limitranger(LimitRange 默认值注入)、plugin/pkg/admission/resourcequota(配额校验)、plugin/pkg/admission/antiaffinity等,源码都在plugin/pkg/admission/下 - 动态(Webhook):
MutatingAdmissionWebhook与ValidatingAdmissionWebhook两个特殊的内置插件,会回调用户注册的外部 HTTP 服务(这正是 Gatekeeper、Kyverno、cert-manager 的接入点)
一个 Mutating Webhook 的典型例子:给所有 Pod 自动注入 sidecar(如 Istio 的 Envoy),或给没写 resources 的容器补默认值
12.3 持久化到 etcd
通过所有准入与校验后,对象被序列化(通常是 protobuf)写入 etcd。存储层抽象在 staging/src/k8s.io/apiserver/pkg/storage/,etcd3 的具体实现在 storage/etcd3/(如 store.go 的 Create/GuaranteedUpdate)。etcd 的 key 形如 /registry/<resource>/<namespace>/<name>,resourceVersion 即 etcd 的 revision,是 Watch 与乐观锁的基础
对象一旦写入,API Server 会向所有 Watch 该资源的客户端(各控制器、scheduler、kubelet)推送变更事件。这就是整个”事件驱动”体系的源头
0x0D 环节三:控制器与调度器
13.1 控制循环:ListWatch 与 Informer
所有控制器都遵循同一套模式:通过 Informer 对资源做 ListWatch(先 List 全量建本地 cache,再 Watch 增量更新),事件触发时把对象 key 放入 workqueue,worker 取出 key 调用 syncHandler 做调谐
以 Deployment 控制器为例(pkg/controller/deployment/deployment_controller.go):
// pkg/controller/deployment/deployment_controller.go
func (dc *DeploymentController) Run(workers int, stopCh <-chan struct{}) {
// ... 等待 informer cache 同步完成
for i := 0; i < workers; i++ {
go wait.Until(dc.worker, time.Second, stopCh) // 启动 worker 协程
}
<-stopCh
}
func (dc *DeploymentController) worker() {
for dc.processNextWorkItem() {}
}
func (dc *DeploymentController) processNextWorkItem() bool {
key, quit := dc.queue.Get() // 从 workqueue 取一个 key
if quit { return false }
defer dc.queue.Done(key)
err := dc.syncHandler(key.(string)) // syncHandler = syncDeployment
dc.handleErr(err, key) // 失败则按退避重新入队
return true
}
13.2 Deployment -> ReplicaSet -> Pod 的调谐
syncHandler 实际指向 syncDeployment。它是一个典型的”水平触发”调谐函数:拿到期望(d.Spec)与实际(现有 RS、Pod),计算差异并驱动:
// pkg/controller/deployment/deployment_controller.go
func (dc *DeploymentController) syncDeployment(key string) error {
namespace, name, _ := cache.SplitMetaNamespaceKey(key)
deployment, err := dc.dLister.Deployments(namespace).Get(name) // 从本地 cache 读
// ...
d := deployment.DeepCopy()
// 1) 认领/释放 RS: 依据 selector + ownerReferences 找到归自己管的 ReplicaSet
rsList, err := dc.getReplicaSetsForDeployment(d)
// 2) 按 RS 分组收集 Pod
podMap, err := dc.getPodMapForDeployment(d, rsList)
// ...
// 3) 根据策略分发: 扩缩容 / 回滚 / 滚动 / 重建
if d.Spec.Paused { return dc.sync(d, rsList) }
if getRollbackTo(d) != nil { return dc.rollback(d, rsList) }
scalingEvent, _ := dc.isScalingEvent(d, rsList)
if scalingEvent { return dc.sync(d, rsList) }
switch d.Spec.Strategy.Type {
case apps.RecreateDeploymentStrategyType:
return dc.rolloutRecreate(d, rsList, podMap)
case apps.RollingUpdateDeploymentStrategyType:
return dc.rolloutRolling(d, rsList) // 滚动升级: 新建 RS, 逐步调整新旧 RS 副本
}
}
Deployment 控制器只负责”管 ReplicaSet”;真正”创建 Pod”的是 ReplicaSet 控制器(pkg/controller/replicaset/replica_set.go)。它的 syncReplicaSet 计算当前 Pod 数与期望 replicas 的差,调用 manageReplicas 补齐或删除:
// pkg/controller/replicaset/replica_set.go
func (rsc *ReplicaSetController) manageReplicas(filteredPods []*v1.Pod, rs *apps.ReplicaSet) error {
diff := len(filteredPods) - int(*(rs.Spec.Replicas))
if diff < 0 {
// 实际 < 期望: 批量创建 Pod(带 ownerReference 指向该 RS)
// slowStartBatch: 以 1,2,4,8... 指数批量创建, 避免雪崩
} else if diff > 0 {
// 实际 > 期望: 删除多余 Pod
}
// ...
}
此时创建出来的 Pod,其 spec.nodeName 为空——它还没有被调度到任何节点
flowchart LR
dep["Deployment<br/>syncDeployment"] -->|"创建/更新"| rs["ReplicaSet"]
rs -->|"manageReplicas<br/>slowStartBatch"| pod["Pod<br/>(nodeName 为空)"]
pod -.->|"等待调度"| sched["kube-scheduler"]
13.3 调度器:Filter 过滤 + Score 打分
调度器 Watch 到 spec.nodeName 为空的 Pod,为其选节点。v1.21 版本的调度器已全面基于 Scheduling Framework(扩展点插件化)。核心调度算法在 pkg/scheduler/core/generic_scheduler.go 的 Schedule:
// pkg/scheduler/core/generic_scheduler.go
func (g *genericScheduler) Schedule(ctx, fwk, state, pod) (result ScheduleResult, err error) {
// 1) 快照当前节点信息
if err := g.snapshot(); err != nil { return result, err }
// 2) 过滤(Predicates): 找出所有"能放得下"的节点
feasibleNodes, diagnosis, err := g.findNodesThatFitPod(ctx, fwk, state, pod)
if len(feasibleNodes) == 0 {
return result, &framework.FitError{Pod: pod, ...} // 无可用节点 -> Pending
}
if len(feasibleNodes) == 1 {
return ScheduleResult{SuggestedHost: feasibleNodes[0].Name, ...}, nil
}
// 3) 打分(Priorities): 对可行节点评分
priorityList, err := g.prioritizeNodes(ctx, fwk, state, pod, feasibleNodes)
// 4) 选出最高分节点(同分用蓄水池抽样随机)
host, err := g.selectHost(priorityList)
return ScheduleResult{SuggestedHost: host, ...}, err
}
调度框架定义了一系列扩展点(pkg/scheduler/framework/interface.go),插件按阶段挂载:PreFilter -> Filter -> PostFilter -> PreScore -> Score -> Reserve -> Permit -> PreBind -> Bind -> PostBind。常见插件在 pkg/scheduler/framework/plugins/ 下:
- Filter 阶段(过滤):
noderesources(节点资源是否够放下 Pod 的 requests)、nodeaffinity(节点亲和)、nodeports(端口冲突)、nodeunschedulable(节点是否可调度)、interpodaffinity(Pod 间亲和/反亲和)、podtopologyspread(拓扑分布约束的硬约束)、nodevolumelimits(节点可挂卷上限)等 - Score 阶段(打分):
noderesources(如 LeastAllocated:越空闲越高分)、imagelocality(镜像本地性)、interpodaffinity、podtopologyspread(软约束倾斜度)等,每个插件打0~100分后按权重加权
selectHost 用蓄水池抽样在最高分节点中随机挑一个,避免热点:
// pkg/scheduler/core/generic_scheduler.go
func (g *genericScheduler) selectHost(nodeScoreList framework.NodeScoreList) (string, error) {
maxScore := nodeScoreList[0].Score
selected := nodeScoreList[0].Name
cntOfMaxScore := 1
for _, ns := range nodeScoreList[1:] {
if ns.Score > maxScore {
maxScore = ns.Score; selected = ns.Name; cntOfMaxScore = 1
} else if ns.Score == maxScore {
cntOfMaxScore++
if rand.Intn(cntOfMaxScore) == 0 { selected = ns.Name } // 蓄水池抽样
}
}
return selected, nil
}
选定节点后,调度器执行 Bind:向 API Server 发一个 Binding,把 pod.spec.nodeName 写为目标节点名。至此 Pod 完成调度
flowchart LR
pod["未调度 Pod"] --> prefilter["PreFilter"]
prefilter --> filter["Filter<br/>(逐节点过滤出可行节点)"]
filter --> score["Score<br/>(对可行节点打分加权)"]
score --> select["selectHost<br/>(最高分 + 蓄水池抽样)"]
select --> bind["Bind<br/>(写 pod.spec.nodeName)"]
0x0E 环节四:节点端 Kubelet 与 CRI/CNI/CSI
Pod 的 spec.nodeName 被写入后,目标节点上的 kubelet 通过 ListWatch 发现”有一个绑定到我(本Node)的 Pod”,开始在本节点把它拉起来
kubelet 主循环在 pkg/kubelet/kubelet.go(syncLoop / syncLoopIteration),每个 Pod 由 pkg/kubelet/pod_workers.go 的 pod worker 串行处理,最终调用容器运行时管理器的 SyncPod。真正与 CRI 交互的在 pkg/kubelet/kuberuntime/kuberuntime_manager.go 的 SyncPod,它用如下几步把 Pod 拉起创建:
// pkg/kubelet/kuberuntime/kuberuntime_manager.go
func (m *kubeGenericRuntimeManager) SyncPod(pod *v1.Pod, podStatus *kubecontainer.PodStatus, ...) (result kubecontainer.PodSyncResult) {
// Step 1: 计算 sandbox 和容器的变化(哪些要建/要杀)
podContainerChanges := m.computePodActions(pod, podStatus)
// Step 2: 若 sandbox 变了, 杀掉整个 Pod
// Step 3: 杀掉不该保留的容器
// Step 4: 必要时创建 Pod sandbox(这一步内部会调用 CNI 配置网络)
if podContainerChanges.CreateSandbox {
podSandboxID, msg, err = m.createPodSandbox(pod, podContainerChanges.Attempt)
// 注释明确: createPodSandbox 可能返回来自 CNI / CSI / CRI 的错误
}
// Step 5: 启动 ephemeral containers
// Step 6: 启动 init 容器
// Step 7: 启动业务容器(startContainer: 拉镜像 -> 创建 -> 启动)
// ...
}
接口分工(都发生在 SyncPod 过程中)如下:
- CRI(Container Runtime Interface):kubelet 通过 gRPC 调用容器运行时(containerd / CRI-O / dockershim)。接口定义在
staging/src/k8s.io/cri-api/。createPodSandbox->RunPodSandbox、startContainer->CreateContainer+StartContainer、拉镜像 ->PullImage,都是 CRI 调用 - CNI(Container Network Interface):在创建 sandbox(即 Pod 的 “pause” 容器 / 网络命名空间)时配置网络:分配 Pod IP、建 veth、配路由。v1.21 中若用 dockershim,CNI 调用在
pkg/kubelet/dockershim/network/cni/;若用 containerd/CRI-O,则由运行时自身调用 CNI 插件。创建 sandbox 后,SyncPod通过determinePodSandboxIPs拿到分配好的 Pod IP - CSI(Container Storage Interface):若 Pod 引用了 PVC,卷的 attach/mount 由 CSI 完成。相关实现在
pkg/volume/csi/(如csi_attacher.go、csi_mounter.go)。注意卷的 attach 可能由外部的external-attacher+AttachDetachController先完成,节点上的 mount 由 kubelet 的 volume manager 驱动 CSI node 插件完成
容器启动后,kubelet 持续探测(probes)并把 Pod 的实际状态回填到 pod.status(阶段、容器状态、PodIP 等),API Server 更新 etcd。至此,一份 YAML 声明的”期望”终于变成了节点上运行的”实际”,闭环完成
flowchart TB
kl["kubelet syncLoop"] --> pw["pod_workers<br/>(每 Pod 串行)"]
pw --> sync["kuberuntime SyncPod"]
sync --> s4["Step4: createPodSandbox"]
s4 -->|"CRI RunPodSandbox"| rt["容器运行时"]
s4 -->|"CNI 配网络, 分配 PodIP"| cni["CNI 插件"]
sync --> vol["volume manager"]
vol -->|"CSI attach/mount"| csi["CSI 驱动"]
sync --> s7["Step7: startContainer"]
s7 -->|"CRI PullImage/Create/Start"| rt
sync --> st["回填 pod.status"]
st --> api["kube-apiserver -> etcd"]
第四部分:全链路总览
0x0F 一图总览:从 YAML 到容器
总结下,一条 kubectl apply 的完整生命周期:
flowchart TB
subgraph client [客户端]
y["config.yaml"] --> parse["kubectl 解析 -> resource.Info"]
parse --> merge["三方合并 / Server-Side Apply"]
end
subgraph cp [控制平面 kube-apiserver]
merge --> authn["认证"] --> authz["授权"] --> mut["Mutating 准入"]
mut --> val["校验 + Validating 准入"] --> store["写入 etcd"]
end
subgraph controllers [控制器 + 调度器]
store --> dc["Deployment Ctrl -> ReplicaSet"]
dc --> rsc["ReplicaSet Ctrl -> Pod(未调度)"]
rsc --> sch["Scheduler: Filter/Score/Bind"]
end
subgraph node [节点 kubelet]
sch --> kl["kubelet SyncPod"]
kl --> sb["CNI 建网络 + Sandbox"]
kl --> mnt["CSI 挂卷"]
kl --> run["CRI 起容器"]
run --> status["回填 status"]
end
status --> store
0x10 总结
一句话总结 Kubernetes 的本质:用户声明期望(spec),系统通过无数个调谐循环,持续地把实际状态(status)收敛到期望。理解了这一点,再看任何资源、任何 CRD,都是同一套心智模型