AI时代的Kubernetes教程36招-第2招-Kubernetes有哪些专有名词、核心架构和使用场景?
K8s 完整术语与架构图:从 API Server 到 kubelet 的职责边界
组件职责说不清,故障定位就只能靠猜。
[读者] 正在给业务团队交付 K8s 集群的平台工程师
[痛点] 术语背得出,故障一上来就分不清该查谁
[现在读] 集群开始出现 Pending 和 Service 不通,定位成本在飙升
[读完] 拿到一张职责边界表和一条可复用的故障定位路径
[旧方案] kubectl 能部署能跑,就认为集群没问题
|
v
[新需求] 多团队多命名空间,Pod、Service、存储、网络全上
|
v
[冲突] 所有组件都通过 API Server 读写 etcd,故障现象却指向完全不同的组件
|
v
[后果] 排查凭直觉,改错一层,问题从 Pending 变成雪崩
我是老李,做内部平台工程。手里这个集群是给七个业务团队共用的,控制面三台、工作节点六台,从命名空间配额到 Ingress 网关都在我这边落地。
集群本身没出过什么大事故,真正让我头疼的是「出问题时该找谁」。团队里每个人都能背出 kube-apiserver、kube-scheduler、kubelet 这些名字,但没有一个人能在故障发生的第一分钟说清边界在哪。
我的任务是把这个边界钉死:一份术语表、一张架构图、一条从现象回溯到组件的路径,任何人拿到告警都能按图索骥。
同一个 Pending 的 Pod,为什么三个人能给出三种互相矛盾的根因?
遇到「服务不通」,是先翻 kube-proxy 的网络规则,还是先看 Service 背后的端点列表?
如果分不清 Desired State 和 Actual State 那条边界,一次误判会把什么问题从局部拖成全局?
01、故事
集群最早是三个团队在用,五个命名空间,三十来个 Deployment,日子过得很舒服。当前方案也很简单:业务方写 YAML,我扫一眼,kubectl apply 上去,能跑就算交付。
变化从今年一季度开始。团队从三个扩到七个,命名空间涨到四十多个,工作负载从无状态 Deployment 变成了 Deployment 加 StatefulSet 加 Job 加 CronJob,存储挂上了 CSI,入口从 NodePort 换成了 Ingress,网络策略开始按命名空间隔离。
冲突在这一刻暴露出来:整个集群里,写状态的组件只有一个(etcd),读写入口只有一个(kube-apiserver),其余十几个组件都是围着它转的客户端;可故障现象却五花八门——Pending、CrashLoopBackOff、ClusterIP 超时、PVC 卡住、节点 NotReady——每一条看起来都指向不同的组件。
现场最典型的一幕发生在周二下午。一个业务的 Pod 卡在 Pending,值班同学说是节点资源不够,另一个同学说是调度器挂了,我扫了一眼存储指标,说可能跟磁盘有关。三个人,三种判断,没有一个能立刻被证伪。
后来我们花了四十分钟才弄清楚:kube-scheduler 根本没报错,它工作得很正常——它只是找不到满足 requests 的节点,于是诚实地什么都不做,把 Pod 留在未绑定状态。这个「什么都不做」,恰恰是最难被看见的行为。
一图分主次
一表定职责
一问见边界
一查有依据
02、问题
旧方案失效得很彻底:靠「能跑就行」和口口相传的组件印象,支撑不了多团队、多工作负载下的故障定位。
业务影响是直接的——告警一来,值班同学的第一反应变成了「重启试试」。重启能不能解决问题,完全取决于运气,而运气不构成运维能力。
技术表现可以归成四类:
- 现象与组件对不上号。看到 Pending 就怀疑调度器,看到超时就怀疑网络插件,实际上产生现象的位置和产生原因的位置经常不在一层。
- 状态没有归属。分不清哪个字段是「期望」、哪个字段是「观测」,于是不知道该看谁写的值。
- 组件之间的调用关系被想象成网状。以为 Scheduler 会通知 kubelet、以为 Controller 会直接操作容器,实际上它们之间只有一条通路。
- 缺少可执行证据。所有判断停留在「我觉得」,没有一条命令能立刻推翻或坐实。
可验证的完成标准我定成三条。给定任意一条集群告警,必须能在五分钟内回答:这个现象由哪个组件产生;该组件读写的状态存在哪里;至少一条可以直接执行的验证命令是什么。
旧法靠经验
新局要边界
现象乱如麻
归因须有据
03、原理
这一篇只需要四条原理,多一条都是噪音。
第一条,单入口。kube-apiserver 是整个控制面唯一的读写入口,它对外暴露 REST API,负责认证、鉴权、准入、校验,然后把对象持久化。任何组件想读或想写集群状态,都必须经过它。
第二条,单事实源。etcd 是集群状态的唯一持久化存储,其他组件内存里的东西都只是缓存。etcd 一旦不可写,整个控制面就变成了只读系统——已经跑着的容器不受影响,但没有任何新的期望状态能被记录下来。
第三条,控制循环。控制器不断做同一件事:读取期望状态,读取观测到的实际状态,计算差异,执行动作让实际状态向期望状态收敛。ReplicaSet 控制器、Deployment 控制器、Node 控制器、EndpointSlice 控制器,形态各异,骨架相同。
第四条,接口分层。kubelet 要拉起容器,需要通过 CRI 和容器运行时对话;节点要接入网络,需要 CNI 插件实现 Pod 网络;持久卷要挂载,需要 CSI 驱动完成挂载与挂接。这三个都是接口规范,不是具体实现,所以同一个集群可以换不同的运行时、网络插件和存储驱动。
这里有一个反直觉判断:kube-scheduler 和 kube-controller-manager 从来不下发指令给 kubelet。它们只往 API Server 写对象字段,kubelet 自己 watch 出变化,然后干活。
还有第二个反直觉判断:etcd 里从来没有「Pod 正在运行」这个事实。etcd 里只有「Pod 应该运行」以及「kubelet 上报说它现在处于某个状态」。真正的运行事实在节点的容器运行时里,控制面看到的永远是二手信息。
理解这两条,后面所有的排查都有了方向:现象在节点上,原因可能在 etcd 的某个字段里;现象在字段里,原因可能在控制器的收敛逻辑里。
单口通万象
单源定真伪
循环自收敛
谁写谁负责
04、架构
[输入] kubectl apply / 控制器写入 / kubelet 上报
|
v
[模块] kube-apiserver(认证 → 鉴权 → 准入 → 校验)
|
v
[数据/状态] etcd(期望状态 + 被观测到的实际状态)
|
v
[处理] Scheduler 绑定节点 + Controller 调谐副本 + kubelet 执行
|
v
[输出] 节点上的容器 / Service 端点 / 状态回写
这张图的边界很清楚:apisverver 之上是用户和客户端,之下是唯一的持久化层,两侧是并行工作的控制器和节点代理。所有跨组件的交互都被压扁成「读写 API 对象」这一种形式。
下面把术语按职责层分组,逐条钉死。
控制面与集群骨架:
| 术语 | 负责什么 | 不负责什么 |
|---|---|---|
| Cluster | 一组工作节点加上一套控制面,共享同一份 etcd 状态 | 不负责跨集群调度 |
| Control Plane | 做出全局决策的组件集合,通常跑在独立节点上 | 不运行业务容器 |
| Node | 真正承载 Pod 的机器,可以是物理机也可以是虚拟机 | 不决定 Pod 该不该来 |
| Namespace | 给对象名提供作用域,做逻辑隔离与配额边界 | 不是网络隔离,也不是安全边界 |
控制面组件:
| 术语 | 负责什么 |
|---|---|
| kube-apiserver | 控制面入口,暴露 REST API,做认证鉴权准入校验,读写 etcd |
| etcd | 一致且高可用的键值存储,集群全部持久状态的唯一来源 |
| kube-scheduler | 为尚未绑定的 Pod 选择节点,把结果写进 spec.nodeName |
| kube-controller-manager | 运行内置控制器,持续把实际状态推向期望状态 |
| cloud-controller-manager | 承载云厂商相关的控制逻辑,如节点、路由、负载均衡器 |
节点组件:
| 术语 | 负责什么 |
|---|---|
| kubelet | 节点上的代理,watch 绑定到本节点的 Pod,驱动容器达到期望状态并上报状态 |
| container runtime | 真正创建和运行容器的软件,通过 CRI 被 kubelet 调用 |
| kube-proxy | 维护节点上的转发规则,让 Service 的虚拟地址能被访问到 |
接口与插件:
| 术语 | 负责什么 |
|---|---|
| CRI | kubelet 与容器运行时之间的接口规范 |
| CNI | 容器网络插件的接口规范,负责给 Pod 分配地址并打通网络 |
| CSI | 容器存储接口规范,负责卷的创建、挂载与挂接 |
工作负载与流量:
| 术语 | 负责什么 |
|---|---|
| Pod | 最小可部署单元,一组共享网络与存储的容器 |
| ReplicaSet | 维持指定数量的 Pod 副本存在 |
| Deployment | 管理 ReplicaSet,提供声明式的滚动更新与回滚 |
| Service | 为一组 Pod 提供稳定的虚拟地址和 DNS 名称 |
| Ingress | 把集群外的 HTTP 与 HTTPS 流量按规则路由到 Service |
| Gateway | Gateway API 中的资源,把路由能力按角色拆开,是 Ingress 的演进方向,具体状态需按当前官方文档核验 |
扩展与治理:
| 术语 | 负责什么 |
|---|---|
| Admission | 对象写入 etcd 之前的最后一道关卡,可变更也可拒绝 |
| CRD | 向 API Server 注册自定义资源类型,让集群认识新的对象 |
收益是显而易见的:十几个名字被压缩成四层,任何一条告警都能先定位到层,再定位到组件。
代价是这张表只描述稳定职责,不描述版本差异。kube-proxy 在部分 CNI 实现下可以被完全替代,Gateway API 的成熟度在不同版本里差别很大,cloud-controller-manager 在自建集群里可能根本不存在。这些都需要按当前官方文档核验。
适用条件是:集群组件没有被大量替换。一旦换掉 kube-proxy、换掉运行时、接入自研调度器,职责表需要同步重画。
入口唯一处
状态藏一处
调度只管放
执行在节点
05、实战一次
先把最小可用版本跑起来,目的是让术语和真实进程一一对上号。
环境要求:一个可用的 Docker 环境、kubectl、kind。版本请按当前官方文档核验,本文不假设具体版本号。
第一步,写集群配置。
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: arch-lab
nodes:
- role: control-plane
- role: worker
- role: worker
第二步,创建集群。
kind create cluster --config kind-3node.yaml
第三步,把控制面组件的真实进程列出来。
kubectl get pods -n kube-system -o wide
示例输出:
NAME READY STATUS AGE
coredns-5d78c9869d-2kqxp 1/1 Running 2m
etcd-arch-lab-control-plane 1/1 Running 2m
kube-apiserver-arch-lab-control-plane 1/1 Running 2m
kube-controller-manager-arch-lab-control-plane 1/1 Running 2m
kube-proxy-8xqzn 1/1 Running 2m
kube-scheduler-arch-lab-control-plane 1/1 Running 2m
kindnet-4kt9p 1/1 Running 2m
这一步的价值在于对号入座:术语表里的 kube-apiserver、etcd、kube-scheduler、kube-controller-manager 在这里是控制面节点上的静态 Pod;kube-proxy 和 CNI 插件(这里是 kindnet)是每个节点都有的守护进程;cloud-controller-manager 没有出现,因为本地集群不在任何云上。
第四步,部署一个最小工作负载。
kubectl create deployment web --image=nginx:1.27 --replicas=3
kubectl expose deployment web --port=80 --target-port=80
kubectl rollout status deployment/web
第五步,验证 Scheduler 到底做了什么。
kubectl get pods -l app=web -o wide
kubectl get pod -l app=web -o jsonpath='{.items[0].spec.nodeName}'
第一个命令的输出里,NODE 列就是 kube-scheduler 写入的结果;第二个命令直接把这个字段取出来,证明所谓「调度」,本质上就是给 spec.nodeName 赋了一个值。
第六步,验证 Service 背后是谁在维护端点。
kubectl get endpointslices -l kubernetes.io/service-name=web
示例输出:
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
web IPv4 80 10.244.1.5,10.244.2.7,10.244.2.8 40s
这些地址由 kube-controller-manager 里的端点控制器写入,kube-proxy 只是把它们变成节点上的转发规则。
第七步,验证 kubelet 在写状态。
kubectl get pod -l app=web -o jsonpath='{.items[0].status.phase}'
kubectl get pod -l app=web -o jsonpath='{.items[0].status.podIP}'
status 里的内容全部来自 kubelet 的上报,用户和控制器都改不了它。
V1 到这里就通了。它解决了「术语看得见」的问题,但留下了一个明显的缺口:这套流程全靠人手写规范,没有任何机制保证下一次提交的 YAML 是合格的。
先起三节点
再对号入座
术语落进程
图上有真名
06、排查
诊断链一:Pod 长期 Pending
现象:新创建的 Pod 一直停在 Pending,不报错也不消失。
怀疑:kube-scheduler 出问题了。
检查:
kubectl describe pod web-7d9f8c5b4-abcde
证据(示例输出):
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 12s default-scheduler 0/2 nodes are available: 2 Insufficient cpu.
进一步确认绑定结果:
kubectl get pod web-7d9f8c5b4-abcde -o jsonpath='{.spec.nodeName}'
返回为空。
根因:kube-scheduler 运行正常,它只是执行了过滤逻辑,发现两个工作节点都没有满足 requests 的剩余资源,于是不做绑定。Pod 的 spec.nodeName 保持为空,etcd 里记录的就是「还没被安排」。
修复:把容器的 resources.requests 降到节点可满足的范围,或者给集群加节点。
错误尝试:直接删掉 Pod 让它重建。为什么错——调度是一个纯函数式的决策过程,输入是节点可分配资源和 Pod 的 requests。两个输入都没变,重建只是把同样的输入重新算了一遍,结果必然相同。删 Pod 解决不了这一类问题,只会浪费时间。
诊断链二:ClusterIP 完全不通
现象:集群内访问 Service 的地址超时,Pod 本身看起来是 Running。
怀疑:kube-proxy 的转发规则没有下发。
检查:
kubectl get pods -l app=web
kubectl get endpointslices -l kubernetes.io/service-name=web
证据(示例输出):
NAME READY STATUS RESTARTS AGE
web-7d9f8c5b4-abcde 0/1 Running 0 3m
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
web IPv4 80 <unset> 3m
Pod 存在,但 READY 是 0/1;EndpointSlice 的端点列表是空的。
根因:端点控制器只把「就绪」的 Pod 地址写进 EndpointSlice。readinessProbe 一直失败,Pod 就不被认定为就绪,端点列表为空,kube-proxy 面对一个空集合,自然没有任何目标可以转发。
修复:修正 readinessProbe 的路径与端口,让探针通过;端点列表随即会被填充。
错误尝试:重启 kube-proxy。为什么错——kube-proxy 只是 EndpointSlice 的消费者,它做的是把已知端点翻译成转发规则。源数据是空的,重启一个消费者不会凭空产生数据。这个尝试即使侥幸让问题短暂消失,也只是掩盖。
两条链放在一起看,规律很清楚:现象出现在最下游,原因却常常在最上游的一个字段。
现象非根因
证据才开口
错解要记下
下次不绕路
07、优化
从第 06 章的两条链里能抽出同一个根因:非法输入进入了 etcd。
链一里,缺少合理 requests 的 Pod 被写进了 etcd;链二里,缺少可用就绪探针的 Deployment 被写进了 etcd。两者都是在对象已经持久化之后,才在执行层暴露问题。
于是 V2 的修改方向很明确:把校验前移到准入阶段。
修改一,用准入策略拒绝不合格的工作负载。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-requests-and-probe
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: >-
object.spec.template.spec.containers.all(c,
has(c.resources) && has(c.resources.requests) &&
has(c.resources.requests.cpu) && has(c.resources.requests.memory))
message: "每个容器必须声明 cpu 与 memory 的 requests"
- expression: >-
object.spec.template.spec.containers.all(c, has(c.readinessProbe))
message: "每个容器必须声明 readinessProbe"
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-requests-and-probe-binding
spec:
policyName: require-requests-and-probe
validationActions: ["Deny"]
修改二,用 CRD 把服务契约变成集群里的一等对象。
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: servicecontracts.platform.example.com
spec:
group: platform.example.com
scope: Namespaced
names:
kind: ServiceContract
plural: servicecontracts
singular: servicecontract
shortNames: ["sc"]
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: ["owner", "alertChannel"]
properties:
owner:
type: string
alertChannel:
type: string
components:
type: array
items:
type: string
这么做的原因有两层。第一层是技术上的:准入发生在对象写入 etcd 之前,不合格的输入根本不会产生后续状态,Pending 和空端点这两类现象会在源头消失。第二层是协作上的:过去「这个服务归谁管」散落在文档和聊天记录里,现在它变成了一个 CRD 对象,可以被 kubectl 查询、被 RBAC 控制、被控制器消费。
新行为是什么样?提交一个缺少 requests 的 Deployment,预期结果如下。未在当前环境实测,以下为预期结果:
Error from server: error when creating "web.yaml":
The deployments "web" is invalid: [每个容器必须声明 cpu 与 memory 的 requests]
验证方法也很直接:先提交一个合规的 Deployment 确认能创建成功,再提交一个删掉 readinessProbe 的版本,确认被拒绝。两次结果不一致,说明策略确实生效了。
需要注意,ValidatingAdmissionPolicy 的可用版本与字段细节需按当前官方文档核验;如果集群版本不支持,等价的兜底方案是 ValidatingWebhookConfiguration 加一个自建 webhook,代价是要自己维护证书和高可用。
根因进准入
错误挡门外
新规要可验
旧债慢慢还
08、演进
[同一个 kubectl apply 请求]
|
+--[V1] 直接写入 etcd
| 代价:非法对象进入集群,问题推迟到 kubelet 阶段暴露
|
+--[V2] 在 apiserver 准入阶段拦截
| 代价:策略表达式的正确性成为新的故障面
|
[Trade-off]
得到什么 / 非法输入零成本拒绝,故障点前移
失去什么 / 策略写错会拦下合法对象,需要版本化管理
适用边界 / 只能覆盖已知的失败模式,未知模式仍需人工排查
逐项比较。
正确性上,V2 更优。V1 依赖每个提交者自觉,V2 把规则变成了集群的强制约束。
稳定性上,V2 引入了一个新的失败点:准入策略本身。表达式写错、failurePolicy 设为 Fail 而策略服务不可用,都会导致合法对象被拒绝。这是必须接受的代价,缓解方式是先在 audit 模式下观察一段时间再切到 Deny,用 warn 模式收集误伤。
复杂度上,V1 简单到几乎为零,V2 多了两个对象类型和一个 CRD。这些对象本身也需要版本管理、需要评审、需要测试。
成本上,V2 的增量主要是认知成本和维护成本,不涉及额外的基础设施。
适用范围上,V1 适合单人维护的小集群,V2 适合多团队共用、需要契约的集群。V2 的收益随提交者数量线性增长,随集群规模增长则趋于平缓。
遗留问题有三个。第一,准入策略只能覆盖已经被我们见过的失败模式,没见过的问题照样会漏过去。第二,CRD 对象需要有人真正使用,否则它只是另一种形式的文档。第三,云相关的路径没有覆盖,cloud-controller-manager 在本地集群根本不出现,负载均衡器、路由表这些行为在这套实验里验证不到。
旧路凭人治
新路靠机制
得失去取舍
边界要写清
09、洞见
9.1 现象和组件之间隔着一条状态边界
第 06 章的两条链里,问题现象分别是 Pending 和超时,但根因都在一个字段上:一个是被调度器判定为不可绑定的 spec.nodeName,一个是被端点控制器判定为未就绪的 Pod 状态。
反直觉判断:Kubernetes 里绝大多数被当成「网络问题」的现象,第一现场其实在 etcd 的某个字段里,而不是在网络链路上。
这条判断的实用价值是:排查时不要先去看协议栈,先去看对象状态。对象状态是一手信息,网络只是它的下游表现。
9.2 控制面组件之间不互相调用
kube-scheduler 不通知 kubelet,kube-controller-manager 也不指挥 kube-scheduler。它们唯一的共同语言是 API 对象。
反直觉判断:kube-scheduler 并不会「告诉」kubelet 把 Pod 放到哪台机器,它只是往 API Server 写了一个 spec.nodeName 字段,剩下的靠 kubelet 自己 watch 出来。
理解了这一点,就能理解为什么控制面可以水平扩展——组件之间没有直接依赖,交互全部通过一个可扩展的 API 层完成,新增一个组件不需要改动其他任何组件。也能理解为什么 API Server 一挂,整个控制面就哑了,但已有容器照样跑:因为它们之间本来就不靠心跳维持关系。
9.3 实际状态永远追不上期望状态
第 03 章讲过控制循环。收敛不是一次性动作,是持续动作。
反直觉判断:集群「稳定」不是因为实际状态等于期望状态,而是因为控制器一直在纠偏。你看到的平静,是动态平衡的结果,不是静止。
推论是:如果想判断某个控制器是否正常工作,看的不是状态快照,而是它有没有在产生变更。一个停止工作的控制器和一个无事可做的控制器,在状态面上长得一模一样。
9.4 准入是成本最低的失败点
第 07 章的改动之所以值得,是因为它把失败从执行层挪到了准入层。
反直觉判断:降低故障成本最有效的办法不是让故障修得更快,而是让它根本不在系统里落地。一个被拒绝的 Deployment 不占用 etcd、不产生事件、不惊动任何人。
这条判断的边界也很清楚:准入只能拦住「可被形式化描述」的错误。资源够不够、探针写得对不对,这些都可以拦;业务逻辑对不对、流量洪峰扛不扛得住,拦不了。
直觉常有误
边界最见真
一图通全局
排查自成章
10、系统落地
原来有什么:一台能跑起来的集群,一份存在脑子里的术语印象,一套「出问题就重启」的处理习惯。
本篇新增了什么:一张覆盖控制面、节点、接口、负载、扩展五层的职责表;一次可复现的三节点实验,把术语和真实进程对应起来;两条带证据的排查链;两个准入策略对象和一个服务契约 CRD。
现在能做什么:看到任意一条集群告警,先定位到层,再定位到组件,再用一条命令拿到证据。提交一个不合格的工作负载,会在写入 etcd 之前被拒绝。服务归属可以通过 kubectl 查到,而不是去翻聊天记录。
还缺什么:第一,可观测性缺失。当前只能靠人工执行命令,没有把「现象 → 组件 → 验证命令」的映射变成自动化的检查项。第二,CRD 只是定义了类型,还没有控制器去消费它,服务契约目前只能被人读,不能被系统用。第三,云相关组件完全没覆盖,cloud-controller-manager 的职责在本地集群里无从验证。第四,准入策略的表达式没有测试,误伤风险靠人工评审兜底。
下一步如何演进:先把两条排查链固化成检查清单,挂到告警规则上,让值班同学拿到告警的同时就拿到验证命令;然后给 ServiceContract 写一个小控制器,自动校验被它管的工作负载是否满足契约要求,把两套机制接起来。至于云相关的部分,需要在一个真实的托管集群上重新做一遍第 05 章的对照实验。
旧有徒手查
新增准入关
能查能拦能追
仍缺自动修复
11、小结
Q1 → 三种根因之所以互相矛盾,是因为判断停在了现象层;Pending 的直接证据是 FailedScheduling 事件和空的 spec.nodeName,它指向 kube-scheduler 的过滤结果,而不是节点或 etcd
Q2 → 先看 EndpointSlice。它是源数据,kube-proxy 的规则是它的下游派生结果;源数据为空时,改下游没有任何意义
Q3 → 分不清这条边界,会把「执行层的一次失败」误判成「控制面的一次故障」,进而去重启本不该重启的组件,把小范围的输入错误扩散成整个集群的抖动
状态 → 一张五层职责表、一次可复现的三节点实验、两条带证据的排查链,以及两个把非法输入挡在 etcd 之外的准入策略对象
带回一句话:现象在下游,原因在上游的字段,证据在 API Server。
问题有答案
状态可交付
一图一表一链
边界即是能力
12、作业
12.1 理解题:kube-apiserver 挂了,为什么已经在运行的 Pod 不会立刻停止?
参考答案:因为已经运行的容器由节点上的 container runtime 管理,kubelet 是节点上的独立进程,不依赖到 API Server 的长连接来维持容器存活。但影响是分阶段的:节点状态不再上报,超过节点监控宽限期后 Node 会被标记为不可用,进而触发驱逐逻辑,届时 Pod 才会被清理。所以「立刻不停」不等于「永远不停」。
12.2 实战题:构造一个必定处于 Pending 的 Deployment,并给出证明它处于 Pending 而不是其他状态的命令。
参考答案:把 requests 设成超过任何一个节点可分配资源的值,例如 cpu 填一个远超单节点容量的数字,副本数设为 2。触发后执行 kubectl describe pod 查看 Events 中的 FailedScheduling 记录,再用 kubectl get pod 加 jsonpath 取 spec.nodeName 确认该字段为空。两者同时成立,才能证明是调度阶段未绑定,而不是启动阶段失败。
12.3 排障题:Service 的 ClusterIP 不通,但直接用 Pod IP 能通,应该先查什么?
参考答案:两条路径完全不同。ClusterIP 的流量要经过节点上的转发规则,由 kube-proxy 或替代它的 CNI 组件实现;Pod IP 的流量走的是 CNI 建立的网络平面。Pod IP 通说明网络平面和 Pod 本身正常,问题被锁定在转发规则这一层。此时先确认 EndpointSlice 里是否有就绪端点,再检查转发规则是否存在,顺序不能颠倒,因为规则是端点的派生结果。
12.4 架构判断题:为什么不建议绕过 kube-apiserver 直接读改 etcd 来修状态?
参考答案:有三层理由。第一,绕过认证、鉴权和准入,等于把集群的安全边界和一致性约束一次性全部放弃。第二,直接写 etcd 会破坏乐观并发控制,resourceVersion 不再被正确推进,其他组件的 watch 会收到错误或缺失的事件,缓存与存储开始不一致。第三,也是最重要的一点,status 这类字段是 kubelet 上报的观测结果,人为改写它并不会改变真实世界,只会让控制循环基于错误的前提做决策,把一个可解释的问题变成一个不可解释的问题。
理解要落地
实战要跑通
排障要证据
架构要取舍
13、思考
回到最开始那个场景:一个 Pending 的 Pod,三个人给出三种判断。
问题从来不在工具,也不在谁的水平,而在于我们没有把「谁负责什么」变成一张可以共同指认的图。当每个人脑子里都有一套自己的组件模型,「我觉得」就成了唯一可用的推理方式,而「我觉得」在故障面前一文不值。
这次梳理给我留下的最实在的一条经验是:先把边界画出来,再谈排查。边界不清,再多的排查技巧都是在猜;边界清楚,一条命令就能把范围砍掉一大半。
第二条经验是关于顺序的。Kubernetes 的每个现象都有它的上游,Pending 的上游是调度决策,调度决策的上游是资源声明,资源声明的上游是准入规则。往前追一层,问题的可修复性就提升一个量级;往后追一层,往往只是在和症状纠缠。
第三条经验是关于代价的。把校验前移到准入,失去的是「随手提交」的便利,得到的是「错误不落地」的确定性。这个交换不是免费的,策略本身也会出错,所以要在严和松之间留出观察期。
最后一点:这一整套东西,本质上只是把 API Server 这个唯一入口的位置真正认清楚了。它是入口,也是边界,也是所有证据的出处。想明白这一层,剩下的组件不过是围着它转的配角,各司其职,各负其责。
一念辨边界
一证定根因
一改知取舍
一役见真章

浙公网安备 33010602011771号