Repository Reading Site
第二课:`kubectl apply` 之后,到底发生了什么
如果只能选一条主线来理解 Kubernetes,那就应该是这条: 如果这条链路你没学透,那么后面你学: 都会变成一个个互不相连的知识点。 但如果这条链路学透了,你会发现后面几乎所有 Kubernetes 机制,都只是围绕这条主链路增加: 所以这节课是整个 K8s 学习中的“总纲”。 --- 我在你的集群里创建了一组隔离实验对象,放在: 实验文件放在仓库里:
第二课:kubectl apply 之后,到底发生了什么
这节课为什么是 Kubernetes 最重要的一课
如果只能选一条主线来理解 Kubernetes,那就应该是这条:
你执行一条
kubectl apply -f deployment.yaml,为什么最后会变成某两个节点上真的跑起了两个容器,而且 Service 还能把流量转过去?
如果这条链路你没学透,那么后面你学:
- Deployment
- Service
- Ingress
- HPA
- StatefulSet
- CRD
- Operator
都会变成一个个互不相连的知识点。
但如果这条链路学透了,你会发现后面几乎所有 Kubernetes 机制,都只是围绕这条主链路增加:
- 更多规则
- 更多控制器
- 更多状态
- 更多约束
所以这节课是整个 K8s 学习中的“总纲”。
这次我们不是空讲,而是拿你的真实集群做实验
我在你的集群里创建了一组隔离实验对象,放在:
- Namespace:
learn-k8s
实验文件放在仓库里:
实验对象是:
- 一个 2 副本的 Deployment:
hello-deploy - 一个 ClusterIP Service:
hello-svc
最终实际结果是:
- ReplicaSet:
hello-deploy-79646d9bdc - Pod 1:
hello-deploy-79646d9bdc-fx6s9 - Pod 2:
hello-deploy-79646d9bdc-x5ldg - Service Endpoints:
10.244.119.226:80和10.244.147.106:80
这意味着我们不是在讨论抽象图,而是在讨论一条你已经亲手执行、并且在真实集群里留下了证据链的对象生命周期。
先给你最终版答案
执行:
kubectl apply -f deployment.yaml
之后,核心链路是:
kubectl读取 YAML,向 API Server 发 HTTPS 请求- API Server 完成认证、授权、准入校验、对象校验
- API Server 把 Deployment 对象持久化到 etcd
- Deployment Controller watch 到新的 Deployment
- Deployment Controller 创建 ReplicaSet
- ReplicaSet Controller 发现“期望 2 个 Pod,但当前 0 个”
- ReplicaSet Controller 创建 2 个 Pod 对象
- Scheduler watch 到 Pending Pod,为它们挑节点
- API Server 记录绑定结果,Pod 获得
.spec.nodeName - 目标节点上的 kubelet watch 到“有 Pod 分配给我了”
- kubelet 通过 CRI 调 containerd 拉镜像、创建容器
- CNI 给 Pod 分配 IP,容器启动
- 探针通过,Pod 进入 Ready
- Endpoint Controller 把这些 Ready Pod 的 IP 写进 Endpoints
- Service 开始把流量转发到这些 Pod
你要记住:
Kubernetes 不是“命令执行器”,而是“状态驱动的持续调和系统”。
你提交的不是“动作脚本”,而是“期望状态”。
用你这次实验的真实对象,把链路画出来
真实对象链
Deployment
hello-deploy
|
v
ReplicaSet
hello-deploy-79646d9bdc
|
v
Pods
hello-deploy-79646d9bdc-fx6s9 -> node us590068728056 -> podIP 10.244.119.226
hello-deploy-79646d9bdc-x5ldg -> node wk-1 -> podIP 10.244.147.106
|
v
Service
hello-svc
|
v
Endpoints
10.244.119.226:80
10.244.147.106:80
这个图到底在说明什么
它说明 Kubernetes 不会“直接把 Deployment 变成容器”。
中间经过了几层抽象:
- Deployment 管发布策略和期望副本
- ReplicaSet 管具体副本个数
- Pod 是最小调度单元
- Service 管稳定访问入口
- Endpoints 维护真正的后端 Pod IP 列表
专家思维的关键,就是始终分得清这些层次。
第一步:kubectl 其实不是在“操作集群”,而是在“请求 API Server”
很多初学者会误以为:
- 我运行
kubectl apply - 然后 kubectl 帮我把容器启动了
这是错的。
kubectl 的真实身份
kubectl 只是客户端。
它做的事是:
- 读取 kubeconfig
- 找到 API Server 地址
- 带上证书/Token 身份
- 把 YAML 转换为 HTTP 请求
- 发给 API Server
所以:
kubectl不负责调度kubectl不负责起容器kubectl不负责分配 Pod IP
它只是把你的“期望状态”告诉集群大脑。
为什么必须这样设计
因为如果所有客户端都直接和每个组件通信,会很乱:
- 安全边界不好控制
- 权限难统一
- 数据一致性难保证
- 组件耦合会很高
所以 Kubernetes 的设计是:
所有外部和内部操作,统一经由 API Server。
这就是它为什么被称为集群的“唯一入口”。
第二步:API Server 收到请求后,先不是“执行”,而是“审查”
你把 YAML 发给 API Server 之后,不是立刻创建成功。
中间至少要经过几个关卡:
1. 认证 Authentication
先判断你是谁。
例如:
- 证书用户
- ServiceAccount
- Bearer Token
2. 授权 Authorization
再判断你有没有权限做这件事。
例如:
- 你能不能在
learn-k8s创建 Deployment - 你有没有权限读 Secret
3. 准入 Admission
再判断这个对象本身是否允许进入系统。
例如:
- 命名空间策略
- Pod Security Admission
- 资源格式是否符合要求
4. 校验 Validation
API schema 会检查字段是否合法。
例如:
apiVersion对不对selector和template.labels是否匹配- 数值字段是否合法
5. 持久化 Persistence
通过校验后,Deployment 对象被写入 etcd。
这里你要建立一个关键认知:
API Server 负责“接收和记录期望状态”,并不直接负责“把容器启动起来”。
第三步:为什么说 etcd 是集群状态中枢
Deployment 被创建成功后,它会被持久化到 etcd。
etcd 里存的不是“正在运行的进程”,而是:
- 对象定义
- 对象状态
- 期望与当前状态相关数据
换句话说,etcd 存的是:
集群世界观。
谁会依赖它?
- API Server 从这里读写对象
- Controller 通过 API Server watch 对象变化
- Scheduler 通过 API Server watch Pending Pod
所以:
- etcd 挂了,集群大脑基本失忆
- 已运行 Pod 可能还能继续跑
- 但集群无法可靠继续变更
第四步:Deployment Controller 并不会创建容器,它先创建 ReplicaSet
这是很多人第一次真正理解 Controller 思想的地方。
你的真实证据
实验中最终生成了:
ReplicaSet: hello-deploy-79646d9bdc
owner=Deployment/hello-deploy
replicas=2
ready=2
这说明:
- Deployment Controller watch 到
hello-deploy - 它没有直接变出 Pod
- 它先变出一个 ReplicaSet
为什么要多这一层
因为 Deployment 的职责不是“直接维持 Pod 个数”,而是:
- 管版本
- 管滚动更新
- 管回滚
- 管发布过程
而真正负责“我要几个 Pod”的,是 ReplicaSet。
你可以把它理解成:
- Deployment:发布总指挥
- ReplicaSet:副本执行官
这就是 Kubernetes 典型的“分层控制”思想。
第五步:ReplicaSet Controller 才是真正创建 Pod 的人
当 ReplicaSet 被创建后,ReplicaSet Controller 会去看:
- 期望副本数是多少
- 当前自己名下的 Pod 有多少
如果期望是 2,当前是 0,它就会补 2 个 Pod。
你的真实证据
事件里很清楚:
SuccessfulCreate replicaset/hello-deploy-79646d9bdc Created pod: hello-deploy-79646d9bdc-fx6s9
SuccessfulCreate replicaset/hello-deploy-79646d9bdc Created pod: hello-deploy-79646d9bdc-x5ldg
这句非常重要。
它证明:
- 创建 Pod 的直接操作者不是 kubectl
- 也不是 Deployment 本身
- 而是 ReplicaSet 对应的控制循环
Pod 一开始是什么状态
一开始它只是一个 API 对象。
还没有真正绑定节点时,通常是 Pending。
所以创建 Pod 对象 ≠ 容器已经跑起来。
这两个阶段必须分开理解。
第六步:Scheduler 负责“挑节点”,不是“起容器”
当 Pod 对象出现后,如果它还没有 .spec.nodeName,Scheduler 就会接手。
Scheduler 真正做的事
它不创建容器,不拉镜像,不做网络。
它只做一件事:
给这个 Pod 选一个最合适的 Node。
你的真实证据
事件里有:
Scheduled pod/hello-deploy-79646d9bdc-fx6s9 Successfully assigned ... to us590068728056
Scheduled pod/hello-deploy-79646d9bdc-x5ldg Successfully assigned ... to wk-1
同时,Pod 对象里能看到:
hello-deploy-79646d9bdc-fx6s9 owner=ReplicaSet/hello-deploy-79646d9bdc node=us590068728056
hello-deploy-79646d9bdc-x5ldg owner=ReplicaSet/hello-deploy-79646d9bdc node=wk-1
为什么 Scheduler 只绑定,不执行
因为职责分离更清晰:
- Scheduler 专注于“决策”
- kubelet 专注于“执行”
这和现代系统设计的经典模式一致:
- 控制面做决定
- 数据面执行动作
如果把调度和执行绑死在一起,扩展性和稳定性都会更差。
第七步:kubelet 才是真正把 Pod 变成容器的人
Pod 被分配到某个 Node 之后,对应节点上的 kubelet 会 watch 到:
- “有一个 Pod 分给我了”
然后 kubelet 才会开始干活:
- 读取 PodSpec
- 调用容器运行时接口 CRI
- 让 containerd 拉镜像
- 创建 sandbox
- 配置网络
- 启动容器
- 回报状态给 API Server
你的真实证据
事件里有:
Pulling image "nginx:1.27.0"
Pulled Successfully pulled image ...
Created container: hello-web
Started container hello-web
这说明在目标节点上,执行链已经走到了:
- 拉镜像
- 建容器
- 起进程
这里 kubelet 和 containerd 怎么分工
很多人会混淆这两个组件。
你要这样理解:
- kubelet:节点代理,理解 Kubernetes 的 PodSpec
- containerd:容器运行时,理解镜像和容器生命周期
类比一下:
- kubelet 像“施工队队长”
- containerd 像“具体施工机械”
kubelet 决定“这个 Pod 该怎么落地”,containerd 负责“真的把容器跑起来”。
第八步:Pod Running 还不等于 Pod Ready
这是初学者最容易忽略的一点。
Running 代表什么
容器进程已经启动。
Ready 代表什么
这个 Pod 已经通过 Readiness 检查,可以接收流量。
这两个不是一回事。
你的实验为什么很适合讲这个
因为我们在 deployment.yaml 里明确加了:
- readinessProbe
- livenessProbe
这两个探针的作用完全不同:
Readiness Probe
回答问题:
这个容器现在能不能接收业务流量?
如果没通过:
- Pod 可以是 Running
- 但不会进入 Service Endpoints
Liveness Probe
回答问题:
这个进程是不是已经坏掉,需要重启?
如果失败太多:
- kubelet 会杀掉容器并重启
为什么生产环境离不开它们
没有 readiness:
- 新 Pod 还没准备好就被送流量
没有 liveness:
- 应用死锁、假活、线程卡死可能长期不被重启
第九步:Service 不会自己找 Pod,它依赖 Endpoints
很多人以为:
- Service 直接“连着” Pod
其实中间还有一个非常关键的对象:
- Endpoints
你的真实证据
我们看到:
service/hello-svc ClusterIP 10.98.52.186
endpoints/hello-svc 10.244.119.226:80,10.244.147.106:80
这意味着:
hello-svc的稳定入口是 ClusterIP10.98.52.186- 真正的后端目标是两个 Pod IP
- 这两个 Pod IP 被写进了 Endpoints
为什么必须有 Endpoints 这层
因为 Service 的 ClusterIP 是稳定的,但 Pod IP 是动态变化的。
Pod 可能:
- 重建
- 迁移
- 扩缩容
如果 Service 直接写死 Pod IP,它就失去意义了。
所以 Kubernetes 的做法是:
- Service 提供稳定入口
- Endpoints 保存当前活着且可接流量的后端列表
这就是“稳定抽象 + 动态实现”的典型设计。
第十步:事件时间线是理解链路最好的证据
你的实验事件顺序大致是:
ScalingReplicaSet deployment/hello-deploy Scaled up replica set hello-deploy-79646d9bdc to 2
SuccessfulCreate replicaset/hello-deploy-79646d9bdc Created pod: hello-deploy-79646d9bdc-fx6s9
Scheduled pod/... Successfully assigned ...
Pulling pod/... Pulling image "nginx:1.27.0"
Pulled pod/... Successfully pulled image ...
Created pod/... Created container: hello-web
Started pod/... Started container hello-web
这个时间线非常有价值,因为它把各组件职责串起来了。
你要学会从事件里读什么
有 ScalingReplicaSet
说明 Deployment Controller 已经开始工作
有 SuccessfulCreate on ReplicaSet
说明 ReplicaSet Controller 创建了 Pod
有 Scheduled
说明 Scheduler 已完成节点绑定
有 Pulling / Pulled / Created / Started
说明 kubelet + containerd 执行成功
这就是为什么专家排障时特别爱看 Events:
因为它几乎是“控制链路流水账”。
第十一步:为什么这条链路体现的是“声明式”,不是“命令式”
我们这次写的是:
spec:
replicas: 2
你没有写:
- 创建 Pod A
- 创建 Pod B
- 放到哪个节点
- 什么时候拉镜像
- 什么时候加入 Service
这些动作都是 Kubernetes 自己做的。
你写的到底是什么
你写的是:
我希望存在一个叫
hello-deploy的 Deployment,它应该维持 2 个符合模板的 Pod。
这和命令式最大的区别
命令式是:
- “帮我执行一步动作”
声明式是:
- “我希望系统维持这个状态”
所以 Kubernetes 的核心不是“执行一次成功”,而是:
持续观察当前状态,并不断把它拉回你声明的期望状态。
这就是 Reconciliation Loop。
第十二步:你这次实验里还有一个很好的“幂等性”样本
第一次执行:
kubectl apply -f manifests/02-apply-chain
结果是:
- Namespace 创建成功
- Service 创建成功
- Deployment 因 namespace not found 失败
第二次再执行同样命令:
- Deployment 创建成功
- Namespace 和 Service 显示
unchanged
这为什么很重要
这说明声明式系统强调的是:
- 安全重试
- 幂等执行
- 最终状态收敛
不是“每一步都必须一次性绝对成功,否则世界毁灭”。
这也是 GitOps、自动化平台特别依赖的能力。
控制面为什么本身也是 Pod:你这次在 Master 上看到了证据
我在 Master 上直接看了:
/etc/kubernetes/manifests
里面有:
etcd.yamlkube-apiserver.yamlkube-controller-manager.yamlkube-scheduler.yaml
同时 kubelet 配置里有:
staticPodPath: /etc/kubernetes/manifests
这说明了什么
这说明控制面组件是以 Static Pod 方式运行的。
也就是说:
- kubelet 直接监控这个目录
- 看到 YAML 就拉起对应 Pod
为什么控制面要这样启动
因为在集群最开始启动时,API Server 还没起来。
如果控制面组件还依赖“先有 API Server 才能创建自己”,就会陷入死循环:
- API Server 没起来,不能创建 Pod
- 但 API Server 自己又得先被创建
所以 kubeadm 的做法是:
先把控制面写成静态 Pod 文件,交给本机 kubelet 直接拉起。
这就绕开了“先有鸡还是先有蛋”的问题。
你在 kube-apiserver.yaml 里看到了哪些关键参数
我们实际看到了:
--advertise-address=10.10.0.1--authorization-mode=Node,RBAC--etcd-servers=https://127.0.0.1:2379--service-cluster-ip-range=10.96.0.0/12hostNetwork: true
它们分别说明什么
--advertise-address=10.10.0.1
API Server 对外告诉别的组件:
- 我在这个地址上可达
在你的集群里,就是 WireGuard 地址。
--authorization-mode=Node,RBAC
说明 API Server 会做授权控制,节点权限和 RBAC 都参与。
--etcd-servers=https://127.0.0.1:2379
说明 API Server 会通过本机回环地址访问 etcd。
这和你当前单控制面部署方式一致:
- API Server 和 etcd 在同一台 Master 上
--service-cluster-ip-range=10.96.0.0/12
说明 Service ClusterIP 都从这个地址段分配。
这也正好解释了为什么你的 hello-svc 得到了:
10.98.52.186
hostNetwork: true
说明控制面 Pod 不走普通 Pod 网络,而是直接使用宿主机网络。
为什么重要?
因为控制面组件必须稳定、尽早启动、且不依赖 CNI 完成后才可用。
你在 etcd.yaml 里看到了哪些关键参数
我们实际看到了:
--data-dir=/var/lib/etcd--listen-client-urls=https://127.0.0.1:2379,https://10.10.0.1:2379--initial-cluster=us480851516617a=https://10.10.0.1:2380hostNetwork: true
这说明了什么
--data-dir=/var/lib/etcd
etcd 的核心数据存在宿主机这个目录里。
这也就是为什么 etcd 备份、恢复都非常关键。
--listen-client-urls=...
说明 etcd 会同时监听:
- 本机回环地址
- Master 的工作地址
--initial-cluster=...
说明当前 etcd 集群配置里只有一个成员。
这再次证明:
- 你的控制面是单点
- 不是 HA etcd 集群
这节课最容易被误解的 8 个点
1. kubectl apply 不是“在本机执行部署”
它只是把期望状态发给 API Server。
2. Deployment 不是直接创建容器
Deployment 先创建 ReplicaSet。
3. Pod 创建出来不代表已经运行
先是 API 对象,再调度,再执行。
4. Scheduler 不负责起容器
它只负责选节点。
5. kubelet 不负责长期存储集群状态
集群状态中枢是 API Server + etcd。
6. Running 不等于 Ready
Ready 需要通过 readinessProbe。
7. Service 不直接等于后端 Pod 列表
真正后端列表在 Endpoints。
8. 控制面不是神秘黑盒
它本身也是 Pod,而且在 kubeadm 集群里通常是 Static Pod。
用一句话重新讲完这条链路
如果面试官让你 30 秒讲清楚这件事,你可以这么说:
kubectl apply本质是把声明式对象提交给 API Server。API Server 做认证、授权、准入和校验后,把 Deployment 写入 etcd。Deployment Controller watch 到新对象后创建 ReplicaSet,ReplicaSet Controller 再创建 Pod。Scheduler 给 Pending Pod 选节点并完成绑定,目标节点的 kubelet 通过 containerd 拉镜像、创建容器并启动。Pod 通过 readiness 后,Endpoint Controller 把 Pod IP 加入 Service Endpoints,Service 才开始把流量转过去。
如果你能把这段话讲顺,你已经超过很多只会记命令的人了。
你现在必须能回答的 12 个问题
kubectl在这个链路里到底扮演什么角色?- 为什么 API Server 必须是唯一入口?
- Deployment 为什么不直接管 Pod?
- ReplicaSet 的职责是什么?
- Scheduler 和 kubelet 的职责边界是什么?
- 为什么 Pod 在 Running 前必须先绑定 Node?
- kubelet 和 containerd 是什么关系?
- 为什么 Readiness 和 Liveness 不能混为一谈?
- Service 为什么还需要 Endpoints?
- 为什么控制面组件在 kubeadm 里是 Static Pod?
hostNetwork: true对控制面意味着什么?- 为什么说 Kubernetes 的本质是“持续调和”,而不是“执行命令”?
下一课预告
下一课最值得继续往下钻的是:
Pod 为什么会被调度到这两个节点,而不是另外两个?
也就是:
- Scheduler 过滤与打分
- requests / limits
- nodeSelector / affinity
- taint / toleration
- topology spread
到了那一课,你会真正开始理解“为什么某些 Pod 会 Pending”。
第二课总结
你这次不是只看了一篇解释文,而是亲手在真实集群里走完了一条最核心的 Kubernetes 生命周期:
- 写 Deployment
- 提交给 API Server
- 看到 ReplicaSet 生成
- 看到 Pod 被调度
- 看到容器启动
- 看到 Service 获得 Endpoints
- 看到控制面本身作为 Static Pod 存在
这节课最重要的结论只有一句:
Kubernetes 的本质不是“帮你执行命令”,而是“接收你声明的目标状态,然后由一组控制循环持续把真实世界调和到这个状态”。