控制平面 / 数据平面 / Pod 创建全流程 — 建立 K8s 的完整心智模型
前置回顾
阶段一最关键的认知基础:① 容器 = Namespace + Cgroups + OverlayFS 隔离的进程(第 02/03/04 篇);② docker CLI → dockerd → containerd → runc → kernel(第 05 篇);③ Compose 管理单机多容器,但无法跨机器调度(第 07 篇);④ CRI/CNI/CSI 三接口让容器运行时、网络、存储可插拔(第 09 篇)。K8s 就是站在这些基础之上,解决多台机器上大规模容器的自动调度与管理问题。
回顾第 07 篇 Compose 的生产边界表——Compose 不能跨机器调度。你的应用从 1 台机器扩展到 10 台时,会出现一连串 Compose 解决不了的问题:
| 场景 | Compose 的局限 | K8s 的答案 |
|---|---|---|
| 容器该跑在哪台机器上? | 你手动选 | Scheduler 自动根据资源、亲和性等策略分配 |
| 机器挂了怎么办? | 手动恢复 | Controller 检测到 Pod 消失,自动在新机器上重建 |
| 流量怎么分散到多台机器? | 需要外部负载均衡器 + 手动配 | Service + kube-proxy 自动负载均衡 |
| 怎么滚动更新不中断服务? | 无滚动更新,手动 up --no-deps | Deployment Controller 逐批替换,保证可用副本数 |
| 怎么让 API 和 DB 部署在一起? | 单机天然在一起 | Pod 概念:共享 Network/Mount NS 的一组容器 |
| 10 台机器的日志怎么看? | 每台登上去看 | 统一的日志/监控体系(阶段四/五的主题) |
Compose = 公寓管家,K8s = 城市管理者。管家只管一栋楼(一台机器)里的水电维修,城市管理者要管几百栋楼的供水供电、交通调度、紧急疏散。K8s 面对的是一整个数据中心,每台机器都可能挂,每个容器都可能被迁移——它必须把这些不确定性交给自动化系统来处理。
K8s 集群的所有组件分成两大类:控制平面管决策,数据平面管执行:
┌──────────── Control Plane(控制平面)───────────────────────────┐
│ 这些组件决定"集群应该长什么样" │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │ API Server │ │ Scheduler │ │Controller Manager│ │
│ │ (唯一入口) │ │ (调度决策) │ │ (维持期望状态) │ │
│ └──────┬───────┘ └──────┬──────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └─────────────────┴────────────────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ etcd │ ← 所有配置和状态的唯一存储 │
│ │ (分布式KV) │ │
│ └─────────────┘ │
└───────────────────────────────────────────────────────────────────┘
│
│ 网络
│
┌──────────── Data Plane(数据平面)───────────────────────────────┐
│ 这些组件执行"集群实际在做什么" │
│ │
│ ┌─────────────── Worker Node 1 ───────────────┐ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ │
│ │ │ kubelet │ │kube-proxy│ │Container │ │ │
│ │ │(节点代理) │ │(流量转发) │ │Runtime │ │ │
│ │ └──────────┘ └──────────┘ │(containerd)│ │ │
│ │ └────────────┘ │ │
│ │ │ │
│ │ ┌── Pod A ──┐ ┌── Pod B ──┐ │ │
│ │ │ nginx │ │ api + │ │ │
│ │ │ │ │ sidecar │ │ │
│ │ └───────────┘ └───────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌─────────────── Worker Node 2 ───────────────┐ │
│ │ (同样的 kubelet + kube-proxy + 容器运行时 + Pod) │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌─────────────── Worker Node 3 ───────────────┐ │
│ │ ... │ │
│ └──────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
API Server 是 K8s 集群的中央枢纽。任何对集群的操作——不管是 kubectl apply、Scheduler 做调度决策、还是 kubelet 汇报 Pod 状态——全部通过 API Server。没有任何组件之间直接通信,全部走 API Server 中转。
┌── kubectl ──┐
│ │
▼ ▼
┌─────────────────────────┐
│ API Server │ ← 所有读写都经过它
│ ┌───────────────────┐ │
│ │ Authentication │ │ ← 你是谁?(证书/Token/OIDC)
│ │ Authorization │ │ ← 你能做什么?(RBAC)
│ │ Admission │ │ ← 你的请求合规吗?(校验 + 变更)
│ │ Validation │ │ ← 数据格式对吗?
│ └───────────────────┘ │
└──────────┬──────────────┘
│ │ │
▼ ▼ ▼
etcd Scheduler Controller Manager
(持久化存储) (watch 新 Pod)(watch 所有资源)
关键设计:所有组件通过"watch"机制工作,而非轮询。 Scheduler 不是在问 "有什么新 Pod 吗?"——它打开一个到 API Server 的长连接,API Server 有新 Pod 分配请求时主动推送给它。这意味着 Scheduler 几乎实时响应,且不会浪费 CPU 做空轮询。
# 各组件的典型 watch 行为:
Scheduler: watch Pods (spec.nodeName == "") → 我来调度!
Controller Manager: watch Deployments → 实际副本够不够?
watch Pods → 有 Pod 挂了?重建!
kubelet: watch Pods (spec.nodeName == "node-1") → 这是我的活!
kube-proxy: watch Services + Endpoints → iptables 要更新了
etcd(读作 et-see-dee)是一个分布式键值存储。你可以把它理解为一个支持高可用的 Redis——但设计目标比 Redis 更极端:强调一致性(任何时刻所有节点读到同样的值)和持久性(写入的数据绝不丢失)。
K8s 集群中 etcd 存着什么?
/registry/
├── pods/default/nginx-abc123 ← 每个 Pod 的定义和状态
├── deployments/default/nginx ← Deployment 的期望副本数
├── services/default/nginx-svc ← Service 的 ClusterIP
├── configmaps/default/app-config ← ConfigMap 内容
├── secrets/default/db-password ← Secret(base64 编码)
├── nodes/node-1 ← Node 的状态(可调度?CPU/内存剩余?)
├── namespaces/default ← Namespace 定义
└── ...
# 集群里的一切(除了容器运行时状态和网络规则)都在 etcd 里
# etcd 挂 = 集群失忆 = 虽然已有容器还能跑,但任何新操作都做不了
etcd 和 Redis 的关键区别
Redis 是缓存(丢数据可以接受),etcd 是唯一真相来源(Source of Truth)——丢了就丢了集群的完整状态。所以 etcd 默认用 Raft 共识算法保证多节点数据一致,写操作必须被超过半数节点确认才算成功。生产环境 etcd 至少 3 节点且奇数个(防脑裂)。
Scheduler 的职责只有一句话:给每个新创建的 Pod 找一个最合适的 Node。它不做别的——不监控已运行的 Pod、不重启崩溃的容器,只负责"选节点"这一个决策。
Scheduler 的决策流程:
1. watch API Server → 发现一个新 Pod,spec.nodeName 为空
("这个 Pod 还没分配节点,该我了")
2. 过滤(Filtering)——排除不合格的节点:
❌ 节点不可调度(cordoned)
❌ 节点资源不够(剩余 CPU < Pod 请求的 CPU)
❌ 节点不满足亲和性/反亲和性规则
❌ 节点不满足污点/容忍规则
→ 得到"候选节点列表"
3. 打分(Scoring)——对候选节点排名:
✓ 剩余资源最多的加分
✓ Pod 间亲和性匹配的加分
✓ 避免把同一 Deployment 的 Pod 全放一个节点
→ 选出最优节点
4. 更新 API Server → 设置 Pod.spec.nodeName = "node-3"
("这个 Pod 我给安排到 node-3 了")
→ Scheduler 的任务到此结束。后续是 node-3 上 kubelet 的事了。
Controller Manager 是 K8s 的"自动驾驶仪"——它持续对比"用户想要的"和"实际存在的",发现差异就自动修正。
它是多个独立 Controller 的集合体,每个 Controller 管一种资源:
| Controller | 管什么 | 做什么 |
|---|---|---|
| Deployment Controller | Deployment 资源 | 期望 3 个副本,实际只有 2 个 → 创建一个新 Pod |
| ReplicaSet Controller | ReplicaSet 资源 | 保证 Pod 数量 = replicas 字段值 |
| Node Controller | Node 资源 | 某节点 5 分钟没心跳 → 标记为 Unknown,把上面的 Pod 驱逐走,在其他节点重建 |
| Service Controller | Service + Endpoints | Service 选中的 Pod 变了 → 更新 Endpoints 列表 |
| Job Controller | Job 资源 | 创建一个 Pod 执行任务,失败了重试,成功了记录完成 |
K8s 最核心的设计模式:声明式 + 控制循环
每个 Controller 都运行同一个逻辑:观察(observe)→ 比对(diff)→ 修正(reconcile)。用户声明"我要 3 个副本"(写 YAML),Controller 持续保证"真的有 3 个副本在跑"。挂了一个?重建。多了一个?删除。这不是一次性的命令("创建 3 个"然后就不管了),而是持续的保证("始终有 3 个")。这是 K8s 和传统运维脚本最根本的区别。
每个 Worker Node 上都跑着一个 kubelet。它是该节点上 K8s 的唯一代理人——控制平面只和 kubelet 说话,kubelet 负责让本机上的 Pod 按期望运行。
kubelet 的工作循环:
1. 向 API Server 注册自己:"我是 node-3,我有 4 核 CPU、8G 内存"
持续汇报心跳 + 节点资源使用情况
2. watch API Server → 获知有哪些 Pod 被分配到自己
(Scheduler 已经给 Pod 写上了 nodeName=node-3)
3. 同步 Pod 状态:
Pod 应该在跑但没跑?→ 调用 CRI 创建容器
Pod 不应该跑了? → 调用 CRI 停止容器
Pod 配置更新了? → 重新创建容器
4. 持续监控:
容器退出了? → 重启(根据 restartPolicy)
探针失败了? → 标记 Pod 不健康,根据需要重启或标记 NotReady
5. 把 Pod 的状态回报给 API Server
# kubelet 不负责的事情:
# • 不决定 Pod 去哪台机器(那是 Scheduler 的事)
# • 不处理 Service 的网络转发(那是 kube-proxy 的事)
# • 不跨节点调度(只管自己这台机器)
回顾第 01 篇的 iptables 和第 07 篇 Compose 的 DNS 服务发现。kube-proxy 做的是类似的事——但规模大得多:
# 用户创建一个 Service:
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
# kube-proxy 做的事:
1. watch API Server → 发现新的 nginx-svc
2. 查到 nginx-svc 的 ClusterIP(如 10.96.0.1)和它选中的 Pod 列表
3. 在本机写 iptables 规则:
# 任何访问 10.96.0.1:80 的流量
# → 随机转发到以下 Pod 的 IP 之一:
# • 10.244.1.5:80 (Pod A, node-1)
# • 10.244.2.3:80 (Pod B, node-2)
# • 10.244.3.7:80 (Pod C, node-3)
当一个 Pod 挂了 → kube-proxy watch 到 Endpoints 变化
→ 更新 iptables 规则,移除死 Pod,加入新 Pod
这就回到第 09 篇的内容了。K8s 不自己创建容器——它通过 CRI(Container Runtime Interface)调用 containerd 或 CRI-O,然后 containerd 再调用 runc/crun/kata:
kubelet
│ CRI (gRPC)
▼
containerd / CRI-O ← 高级运行时(管理镜像 + 容器生命周期)
│ OCI Runtime Spec (config.json)
▼
runc / crun / kata ← 低级运行时(调系统调用,创建 Namespace/Cgroup/rootfs)
│ clone/unshare/mount
▼
Linux Kernel
# 这条链和 docker run 的链几乎一样——只是把 dockerd 换成了 kubelet
# docker: docker CLI → dockerd → containerd → runc → kernel
# K8s: kubectl → API Server → kubelet → containerd → runc → kernel
# ↑ ↑
# 中间多了两层调度(因为有集群维度)
这是理解 K8s 架构的"终极测试"——把前面所有组件串起来走一遍。假设你已经搭好了一个三节点集群:
$ kubectl apply -f nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
下面是一次完整的 Pod 创建过程(12 步):
┌─ 第 1 步:认证 & 授权 ──────────────────────────────────────┐
│ kubectl → API Server (HTTPS) │
│ │
│ API Server 收到 POST /api/v1/namespaces/default/pods │
│ → Authentication:检查你的 kubeconfig 里的证书/Token │
│ → Authorization (RBAC):你有创建 Pod 的权限吗? │
│ → Admission:Webhook 校验(合规检查、注入 Sidecar 等) │
│ → 通过 ✓ │
└──────────────────────────────────────────────────────────────┘
┌─ 第 2 步:写入 etcd ────────────────────────────────────────┐
│ API Server → etcd │
│ │
│ 将 Pod 对象序列化为 JSON → 写入 /registry/pods/default/nginx │
│ → etcd 通过 Raft 协议复制到其他 etcd 节点 │
│ → 写入确认,API Server 返回 kubectl "created" │
│ │
│ 此时 Pod 在 etcd 里了,但 spec.nodeName 是空的 │
│ 没有容器在运行,也没有节点被分配 │
└──────────────────────────────────────────────────────────────┘
┌─ 第 3 步:Scheduler 发现未分配的 Pod ───────────────────────┐
│ Scheduler 通过 watch 机制发现: │
│ "有个新 Pod nginx,spec.nodeName 为空!" │
│ │
│ → 过滤(Filtering):3 个节点中哪些满足条件? │
│ node-1: 剩余 CPU 够 ✓ 可调度 ✓ → 候选 │
│ node-2: 剩余 CPU 够 ✓ 可调度 ✓ → 候选 │
│ node-3: 剩余 CPU 不够 ✗ → 淘汰 │
│ │
│ → 打分(Scoring):node-1 vs node-2 │
│ node-1: 剩余资源多,分数高 → 胜出! │
│ │
│ → Scheduler 更新 API Server: │
│ Pod.spec.nodeName = "node-1" │
│ │
│ Scheduler 的工作到此结束 │
└──────────────────────────────────────────────────────────────┘
┌─ 第 4 步:kubelet 发现分配给自己的 Pod ──────────────────────┐
│ node-1 上的 kubelet watch API Server 发现: │
│ "有个 Pod nginx,nodeName=node-1,这是我的活!" │
│ │
│ → kubelet 调用 CRI (gRPC): │
│ containerd,拉镜像 nginx:1.25 │
│ (如果本地没有 → 去 Registry pull,按层下载) │
│ │
│ → 镜像拉取完成 │
└──────────────────────────────────────────────────────────────┘
┌─ 第 5 步:kubelet 创建 Pod 的沙箱(Sandbox)─────────────────┐
│ kubelet → CRI → containerd → runc │
│ │
│ 1. containerd 生成 OCI Runtime Spec 的 config.json │
│ 2. containerd 调用 runc: │
│ → 创建 Network Namespace(Pod 内所有容器共享此 NS) │
│ → 创建 PID Namespace(可选,通过 shareProcessNamespace 控制)│
│ → 创建 Mount Namespace │
│ → 创建 UTS/IPC Namespace │
│ │
│ 3. runc 启动 Pause 容器(Pod 的"占位符") │
│ → Pause 容器是一个极小的进程(sleep 永久),作用是: │
│ • 持有 Pod 的 Network NS(其他容器共享它的 NS) │
│ • 作为 PID 1 收割僵尸进程 │
│ → Pause 容器在宿主机上是一个极小的进程,永远不退出 │
│ │
│ 4. CNI 插件此时介入: │
│ → 给 Pod 分配一个 IP(如 10.244.1.5) │
│ → 创建 veth pair,一端插入 Pod 的 Network NS │
│ → 另一端接到宿主机的 CNI 网桥上 │
│ (这和你在第 01 篇手动搭的 veth + bridge 原理一样) │
└──────────────────────────────────────────────────────────────┘
┌─ 第 6 步:kubelet 启动用户容器 ──────────────────────────────┐
│ kubelet → CRI → containerd → runc │
│ │
│ 1. containerd 生成第二个 OCI Runtime Spec: │
│ → 加入 Pause 容器的 Network NS(共享网络!) │
│ → rootfs = nginx 镜像的 OverlayFS merged 目录 │
│ → 限制 CPU/内存根据 Pod spec 配置 │
│ │
│ 2. runc 创建 nginx 容器进程: │
│ → clone/unshare(加入已有的 Network NS) │
│ → mount OverlayFS rootfs │
│ → pivot_root │
│ → exec nginx │
│ │
│ 3. nginx 进程开始运行 │
│ → 它看到 eth0: 10.244.1.5(和 Pause 容器共享的网络) │
│ → 它的 PID:在 Pod NS 内是某个数字,宿主机上是真实的 PID │
└──────────────────────────────────────────────────────────────┘
┌─ 第 7 步:kubelet 回报状态 ─────────────────────────────────┐
│ kubelet → API Server: │
│ Pod nginx 的状态更新为 Running │
│ Pod IP = 10.244.1.5 │
│ │
│ kubectl get pods 现在显示 Running │
│ │
│ Controller Manager 中的各种 Controller 持续 watch: │
│ → 这个 Pod 是健康的吗? → 不影响,继续监控 │
│ → 这个 Pod 的副本数对吗? → 不影响,继续监控 │
│ │
│ 全流程结束 ✓ │
└──────────────────────────────────────────────────────────────┘
每一步 = 发快递的流程。你(kubectl)在快递单(YAML)上写了目的地,送到快递站(API Server)。快递站把单子存档(etcd)。调度员(Scheduler)决定哪个配送站负责。配送站站长(kubelet)收到通知,准备好运输工具(CRI → runc 创建容器),快递装车出发(容器运行)。整个过程每一步都有明确的负责人,没有谁需要同时做两件事。
生产环境的 K8s 集群不会只跑一个控制平面——那叫单点故障。标准的高可用拓扑:
┌────────── High Availability K8s ──────────────────────────────┐
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Load Balancer │ │
│ │ (HAProxy / Nginx / 云 LB) │ │
│ └────┬────────────────────┬────────────────────┬──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─ API Server 1 ─┐ ┌─ API Server 2 ─┐ ┌─ API Server 3 ─┐ │
│ │ │ │ │ │ │ │
│ │ Scheduler │ │ Scheduler │ │ Scheduler │ │
│ │ Ctrl Manager │ │ Ctrl Manager │ │ Ctrl Manager │ │
│ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ etcd Cluster │ │
│ │ (3 或 5 节点) │ │
│ │ Raft 共识协议 │ │
│ └─────────────────┘ │
│ │
│ API Server:无状态,可横向扩展,前端放 LB │
│ Scheduler + Controller Manager:通过 Leader Election 选主 │
│ 同时只有一个在工作(避免冲突),其他的 standby │
│ etcd:奇数节点,Raft 协议保证一致性,需 ≥半数的节点存活 │
│ │
│ Worker Nodes:每个 Worker 跑 kubelet + kube-proxy + CRI │
│ 一个 Worker 挂 → 上面的 Pod 被调度到其他 Worker 重建 │
│ 只要还有足够的 Worker,应用就不中断 │
└────────────────────────────────────────────────────────────────┘
| 要点 | 一句话概括 |
|---|---|
| 控制平面 vs 数据平面 | 控制平面管决策(API Server/etcd/Scheduler/Controller Manager),数据平面管执行(kubelet/kube-proxy/CRI) |
| API Server | 集群唯一入口,所有组件通过它通信;watch 机制实现实时推送而非轮询;认证→授权→准入→校验四层把关 |
| etcd | 分布式 KV 存储,集群的"唯一真相来源",存所有资源对象;用 Raft 保证一致性,挂掉集群失忆 |
| Scheduler | 只做一件事:给未分配 Pod 选最合适的 Node。过滤→打分→写入 nodeName |
| Controller Manager | 多个 Controller 的集合,做同一件事:观察→比对→修正,持续把实际状态推向期望状态 |
| kubelet | 节点代理人:watch 分配给自己的 Pod → 调 CRI 拉镜像/创建容器 → 监控进程+探针 → 回报状态 |
| kube-proxy | watch Service/Endpoints → 写 iptables/IPVS 规则 → 让 Service ClusterIP 的流量随机分发到后端 Pod |
| Pod 创建全流程 | kubectl → API Server → etcd → Scheduler → kubelet → CRI → CNI → runc → Pause 容器 → 用户容器 → 回报状态 |
| 高可用 | API Server 多副本+LB(无状态)、Scheduler/CM 选主(同时一个工作)、etcd 奇数节点 Raft(一致性与容灾) |
k3d cluster create 搭一个本地集群,用 kubectl get nodes 确认节点就绪kubectl get pods -n kube-system 查看控制平面组件(它们在 kube-system 命名空间里以静态 Pod 方式运行)