Kubernetes 集群架构

控制平面 / 数据平面 / Pod 创建全流程 — 建立 K8s 的完整心智模型

阶段二 · 01 · 返回阶段大纲

前置回顾

阶段一最关键的认知基础:① 容器 = Namespace + Cgroups + OverlayFS 隔离的进程(第 02/03/04 篇);② docker CLI → dockerd → containerd → runc → kernel(第 05 篇);③ Compose 管理单机多容器,但无法跨机器调度(第 07 篇);④ CRI/CNI/CSI 三接口让容器运行时、网络、存储可插拔(第 09 篇)。K8s 就是站在这些基础之上,解决多台机器上大规模容器的自动调度与管理问题。

从 Compose 到 K8s:为什么需要编排系统

回顾第 07 篇 Compose 的生产边界表——Compose 不能跨机器调度。你的应用从 1 台机器扩展到 10 台时,会出现一连串 Compose 解决不了的问题:

场景Compose 的局限K8s 的答案
容器该跑在哪台机器上?你手动选Scheduler 自动根据资源、亲和性等策略分配
机器挂了怎么办?手动恢复Controller 检测到 Pod 消失,自动在新机器上重建
流量怎么分散到多台机器?需要外部负载均衡器 + 手动配Service + kube-proxy 自动负载均衡
怎么滚动更新不中断服务?无滚动更新,手动 up --no-depsDeployment 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 ───────────────┐                  │
│  │  ...                                         │                  │
│  └──────────────────────────────────────────────┘                  │
└───────────────────────────────────────────────────────────────────┘

控制平面(Control Plane)

API Server — 集群的唯一入口

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 — 集群的"唯一真相来源"

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 去哪台机器

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 — 维持集群的"期望状态"

Controller Manager 是 K8s 的"自动驾驶仪"——它持续对比"用户想要的"和"实际存在的",发现差异就自动修正。

它是多个独立 Controller 的集合体,每个 Controller 管一种资源:

Controller管什么做什么
Deployment ControllerDeployment 资源期望 3 个副本,实际只有 2 个 → 创建一个新 Pod
ReplicaSet ControllerReplicaSet 资源保证 Pod 数量 = replicas 字段值
Node ControllerNode 资源某节点 5 分钟没心跳 → 标记为 Unknown,把上面的 Pod 驱逐走,在其他节点重建
Service ControllerService + EndpointsService 选中的 Pod 变了 → 更新 Endpoints 列表
Job ControllerJob 资源创建一个 Pod 执行任务,失败了重试,成功了记录完成

K8s 最核心的设计模式:声明式 + 控制循环

每个 Controller 都运行同一个逻辑:观察(observe)→ 比对(diff)→ 修正(reconcile)。用户声明"我要 3 个副本"(写 YAML),Controller 持续保证"真的有 3 个副本在跑"。挂了一个?重建。多了一个?删除。这不是一次性的命令("创建 3 个"然后就不管了),而是持续的保证("始终有 3 个")。这是 K8s 和传统运维脚本最根本的区别。

数据平面(Data Plane)

kubelet — 节点上的"车间主任"

每个 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 的事)
# • 不跨节点调度(只管自己这台机器)

kube-proxy — 把 Service 翻译成网络规则

回顾第 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
#                                ↑          ↑
#                            中间多了两层调度(因为有集群维度)

Pod 创建全流程:从 kubectl apply 到容器运行

这是理解 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-proxywatch 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(一致性与容灾)

动手练习

  1. 画出 K8s 集群架构图(不参照任何资料,凭记忆画),标注每个组件的职责和它与其他组件的通信关系
  2. 默写 Pod 创建全流程的 12 个步骤,特别标注出哪几步涉及阶段一学过的 Namespace/Cgroup/OverlayFS
  3. 如果你有 Docker Desktop,启用内置的 K8s 集群;或者用 k3d cluster create 搭一个本地集群,用 kubectl get nodes 确认节点就绪
  4. kubectl get pods -n kube-system 查看控制平面组件(它们在 kube-system 命名空间里以静态 Pod 方式运行)
  5. 思考题:为什么所有组件都必须通过 API Server 通信,而不是直接互相调用?如果 Scheduler 直接告诉 kubelet "你去跑这个 Pod",会有什么问题?