K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

第二课:`kubectl apply` 之后,到底发生了什么

如果只能选一条主线来理解 Kubernetes,那就应该是这条: 如果这条链路你没学透,那么后面你学: 都会变成一个个互不相连的知识点。 但如果这条链路学透了,你会发现后面几乎所有 Kubernetes 机制,都只是围绕这条主链路增加: 所以这节课是整个 K8s 学习中的“总纲”。 --- 我在你的集群里创建了一组隔离实验对象,放在: 实验文件放在仓库里:

Markdown02-第二课-kubectl-apply-之后到底发生了什么.md2026年4月9日 17:42

第二课: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:8010.244.147.106:80

这意味着我们不是在讨论抽象图,而是在讨论一条你已经亲手执行、并且在真实集群里留下了证据链的对象生命周期。


先给你最终版答案

执行:

kubectl apply -f deployment.yaml

之后,核心链路是:

  1. kubectl 读取 YAML,向 API Server 发 HTTPS 请求
  2. API Server 完成认证、授权、准入校验、对象校验
  3. API Server 把 Deployment 对象持久化到 etcd
  4. Deployment Controller watch 到新的 Deployment
  5. Deployment Controller 创建 ReplicaSet
  6. ReplicaSet Controller 发现“期望 2 个 Pod,但当前 0 个”
  7. ReplicaSet Controller 创建 2 个 Pod 对象
  8. Scheduler watch 到 Pending Pod,为它们挑节点
  9. API Server 记录绑定结果,Pod 获得 .spec.nodeName
  10. 目标节点上的 kubelet watch 到“有 Pod 分配给我了”
  11. kubelet 通过 CRI 调 containerd 拉镜像、创建容器
  12. CNI 给 Pod 分配 IP,容器启动
  13. 探针通过,Pod 进入 Ready
  14. Endpoint Controller 把这些 Ready Pod 的 IP 写进 Endpoints
  15. 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 只是客户端。

它做的事是:

  1. 读取 kubeconfig
  2. 找到 API Server 地址
  3. 带上证书/Token 身份
  4. 把 YAML 转换为 HTTP 请求
  5. 发给 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 对不对
  • selectortemplate.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 才会开始干活:

  1. 读取 PodSpec
  2. 调用容器运行时接口 CRI
  3. 让 containerd 拉镜像
  4. 创建 sandbox
  5. 配置网络
  6. 启动容器
  7. 回报状态给 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 的稳定入口是 ClusterIP 10.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.yaml
  • kube-apiserver.yaml
  • kube-controller-manager.yaml
  • kube-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/12
  • hostNetwork: true

它们分别说明什么

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:2380
  • hostNetwork: 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 个问题

  1. kubectl 在这个链路里到底扮演什么角色?
  2. 为什么 API Server 必须是唯一入口?
  3. Deployment 为什么不直接管 Pod?
  4. ReplicaSet 的职责是什么?
  5. Scheduler 和 kubelet 的职责边界是什么?
  6. 为什么 Pod 在 Running 前必须先绑定 Node?
  7. kubelet 和 containerd 是什么关系?
  8. 为什么 Readiness 和 Liveness 不能混为一谈?
  9. Service 为什么还需要 Endpoints?
  10. 为什么控制面组件在 kubeadm 里是 Static Pod?
  11. hostNetwork: true 对控制面意味着什么?
  12. 为什么说 Kubernetes 的本质是“持续调和”,而不是“执行命令”?

下一课预告

下一课最值得继续往下钻的是:

Pod 为什么会被调度到这两个节点,而不是另外两个?

也就是:

  • Scheduler 过滤与打分
  • requests / limits
  • nodeSelector / affinity
  • taint / toleration
  • topology spread

到了那一课,你会真正开始理解“为什么某些 Pod 会 Pending”。


第二课总结

你这次不是只看了一篇解释文,而是亲手在真实集群里走完了一条最核心的 Kubernetes 生命周期:

  • 写 Deployment
  • 提交给 API Server
  • 看到 ReplicaSet 生成
  • 看到 Pod 被调度
  • 看到容器启动
  • 看到 Service 获得 Endpoints
  • 看到控制面本身作为 Static Pod 存在

这节课最重要的结论只有一句:

Kubernetes 的本质不是“帮你执行命令”,而是“接收你声明的目标状态,然后由一组控制循环持续把真实世界调和到这个状态”。