阶段一 · Kubernetes 核心

从 Compose 到 K8s / Pod 与第一个集群

前置回顾

阶段一最关键的认知基础:

  • 容器 = 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-deps Deployment Controller 逐批替换,保证可用副本数
怎么让 API 和 DB 部署在一起? 单机天然在一起 Pod 概念:共享 Network/Mount NS 的一组容器
10 台机器的日志怎么看? 每台登上去看 统一的日志/监控体系(阶段四/五的主题)

Compose = 公寓管家,K8s = 城市管理者.

  • 管家只管一栋楼(一台机器)里的水电维修,城市管理者要管几百栋楼的供水供电,交通调度,紧急疏散.
  • K8s 面对的是一整个数据中心,每台机器都可能挂,每个容器都可能被迁移--它必须把这些不确定性交给自动化系统来处理.

Pod 是什么:最小调度单元,不是容器

k8s 以 Pod 为最小调度单元,Pod 是一组容器的集合,它们共享部分 Namespace,必须一起调度到同一台机器上.

Docker 的原子单元是容器, K8s 的原子单元是 Pod.

Docker 世界:                         K8s 世界:

┌──────────┐ ┌──── Pod ────────────────┐
│ Container│ ← 最小单元 │ ┌──────┐ ┌──────┐ │
│ │ │ │ C1 │ │ C2 │ │ ← 一组容器
│ nginx │ │ │nginx │ │fluentd│ │
│ │ │ └──────┘ └──────┘ │
└──────────┘ │ 共享 NS + Volume │
└────────────────────────┘

最小调度单元

为什么要在一组容器外面再包一层?
因为存在紧密耦合的容器组--比如 nginx 写 access.log, 然后 fluentd 读取并转发日志.它们必须:

  1. 共享文件系统(看到同一份日志文件)
  2. 部署在同一台机器上(否则文件访问会有网络延迟)

Pod 就是为这类场景设计的抽象.

Pod = 豆荚,容器 = 豆子.
豆荚是一个整体(调度单位),里面的豆子紧密挨在一起,共享同一片"荚壁"(Network Namespace).

不过大多数场景下,一个 Pod 里可以只跑一个容器.多容器 Pod(Sidecar 模式)是进阶用法,后面篇章再展开.现在只需要记住:Pod 是 K8s 调度的最小单位, K8s 不会只调度 Pod 里的某一个容器.

K8s YAML 字段速览

在动手之前,先认识 K8s 资源的四个必填字段.所有 K8s 资源(Pod,Deployment,Service...)的 YAML 都遵循同一个骨架:

apiVersion: v1            # API 版本
kind: Pod # 资源类型
metadata: # 元数据(名字,标签,命名空间)
name: nginx
labels:
app: nginx
spec: # 期望状态(不同资源内容完全不同)
containers:
- name: nginx
image: nginx:1.25
字段 含义
apiVersion 资源所属的 API 组和版本.
- 核心资源(Pod,Service)用 v1
- apps 组资源(Deployment,StatefulSet)用 apps/v1
- batch 组(Job,CronJob)用 batch/v1
kind 资源类型:Pod,Deployment,Service,ConfigMap
metadata 资源的标识信息.至少包含 name;labels 用于被 selector 选中;namespace 不写则用当前上下文默认值
spec 资源的"期望状态"--你告诉 K8s 这个东西应该长什么样.每种 kind 的 spec 结构不同,这是最核心也最复杂的部分

各字段在不同资源中的取值:

资源 apiVersion spec 里有什么(概要)
Pod v1 containers(容器列表),volumes,restartPolicy...
Deployment apps/v1 replicas + selector(选 Pod 的规则)+ template(Pod 模板,就是一个 Pod spec)
Service v1 selector(选哪些 Pod 作为后端)+ ports + type
ConfigMap v1 data(键值对)
Job batch/v1 completions + parallelism + Pod template

后面的内容会反复出现这些字段

03 篇 Deployment 比 Pod 多了 selectortemplate 两个字段--selector 定义"哪些 Pod 归我管",template 就是 Pod 的 spec.
04 篇 Service 用 selector 定义"流量发给哪些 Pod".

本质都是同一个骨架,只是 spec 里的内容随资源类型变化.

动手:启动第一个 Pod

准备集群环境

你需要一个本地 K8s 集群.以下三种方式任选其一:

方式 适合场景 安装方式
Docker Desktop 内置 K8s macOS/Windows 已装 Docker Desktop Settings → Kubernetes → Enable
k3d 轻量,多节点模拟 brew install k3d && k3d cluster create mycluster
kind CI 友好,用 Docker 容器模拟节点 brew install kind && kind create cluster

确认集群就绪:

$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
docker-desktop Ready control-plane 10m v1.28.2

# 看到 Ready 就说明集群跑起来了

写一个最简 Pod YAML

创建一个文件 nginx-pod.yaml:

apiVersion: v1
kind: Pod
metadata:
name: my-nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80

就这么短--声明"我要一个 Pod,里面跑一个 nginx 容器,监听 80 端口".

启动 Pod

# 提交给 K8s
$ kubectl apply -f nginx-pod.yaml
pod/my-nginx created

# 查看 Pod 状态
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-nginx 1/1 Running 0 12s

# STATUS 从 Pending → ContainerCreating → Running
# 如果卡在 ContainerCreating,多半是在拉镜像(第一次会慢)

查看详情

# 查看 Pod 的完整信息(调度到了哪个节点,IP 是什么,经历了哪些事件)
$ kubectl describe pod my-nginx
Name: my-nginx
Namespace: default
Node: docker-desktop/192.168.65.4 ← 被调度到了哪台机器
IP: 10.1.0.15 ← Pod 的集群内 IP
Status: Running
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 30s default-scheduler Successfully assigned default/my-nginx to docker-desktop
Normal Pulling 29s kubelet Pulling image "nginx:1.25"
Normal Pulled 20s kubelet Successfully pulled image
Normal Created 20s kubelet Created container nginx
Normal Started 20s kubelet Started container nginx

# Events 从下往上读:调度 → 拉镜像 → 创建容器 → 启动
# 这就是后面要讲的"Pod 创建全流程"的用户视角

访问与调试

# 查看容器日志
$ kubectl logs my-nginx
# (nginx 的 access log 会出现在这里)

# 进入容器(类似 docker exec -it)
$ kubectl exec -it my-nginx -- /bin/bash

root@my-nginx:/# curl localhost:80
<!DOCTYPE html>
<html>
<head><title>Welcome to nginx!</title></head>
...

# 临时端口转发到本机(不需要 Service 也能访问)
$ kubectl port-forward pod/my-nginx 8080:80
Forwarding from 127.0.0.1:8080 -> 80

# 另开终端访问
$ curl localhost:8080
# → 看到 nginx 欢迎页

删除 Pod

$ kubectl delete pod my-nginx
pod "my-nginx" deleted

$ kubectl get pods
No resources found in default namespace.

体验"自愈":用 Deployment 管理 Pod

裸 Pod 有一个致命问题:删了就没了,没有人帮你重建. 这和 docker run 一样--进程挂了就挂了.
K8s 的核心价值在于 Deployment:

# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
spec:
replicas: 3 # 期望 3 个副本
selector:
matchLabels:
app: nginx
template: # Pod 模板(和前面的 Pod spec 几乎一样)
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
$ kubectl apply -f nginx-deployment.yaml
deployment.apps/nginx-deploy created

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deploy-7fb96c846b-2xk4n 1/1 Running 0 5s
nginx-deploy-7fb96c846b-9m2dv 1/1 Running 0 5s
nginx-deploy-7fb96c846b-kx7pq 1/1 Running 0 5s

# 3 个副本跑起来了.现在试试手动杀掉一个:
$ kubectl delete pod nginx-deploy-7fb96c846b-2xk4n
pod "nginx-deploy-7fb96c846b-2xk4n" deleted

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deploy-7fb96c846b-9m2dv 1/1 Running 0 30s
nginx-deploy-7fb96c846b-kx7pq 1/1 Running 0 30s
nginx-deploy-7fb96c846b-r8wtz 1/1 Running 0 3s ← 自动重建了!

# 你删一个,K8s 立刻补一个.始终维持 3 个副本.
# 这就是"声明式 + 控制循环"的威力--后面会详细讲原理.

# 清理
$ kubectl delete deployment nginx-deploy

现在你已经亲手:创建了一个 Pod,看到了它的生命周期事件,体验了 Deployment 的自愈能力.接下来我们深入底层--这些操作背后,K8s 的各个组件是怎么协作完成的?

集群架构全景

K8s 集群的所有组件分成两大类:控制平面管决策,数据平面管执行:

┌────────────────────────────────────────┐
│ Control Plane(控制平面) │
├────────────────────────────────────────┤
│ • API Server(唯一入口) │
│ • Scheduler(调度决策) │
│ • Controller Manager(维持期望状态) │
│ • etcd(分布式KV存储) │
└────────────────────────────────────────┘
┌────────────────────────┐ ┌────────────────────────┐
│ Worker Node 1 │ │ Worker Node 2 │
├────────────────────────┤ ├────────────────────────┤
│ • kubelet │ │ • kubelet │
│ • kube-proxy │ │ • kube-proxy │
│ • containerd │ │ • containerd │
│ • Pod A / Pod B │ │ • Pod C / Pod D │
└────────────────────────┘ └────────────────────────┘

回忆你刚才 kubectl apply 的体验:提交 YAML → Pod 出现在某个节点上 → 容器开始运行.
这背后涉及控制平面的调度决策和数据平面的实际执行.

控制平面(Control Plane)

API Server - 集群的唯一入口

API Server 是 K8s 集群的中央枢纽.任何对集群的操作--不管是 kubectl apply,Scheduler 做调度决策,还是 kubelet 汇报 Pod 状态--全部通过 API Server.没有任何组件之间直接通信,全部走 API Server 中转.

请求进入 API Server 后的处理链:

Authentication(你是谁?)


Authorization(你能做什么?RBAC)


Admission(请求合规吗?校验+变更)


Validation(数据格式对吗?)


写入 etcd

关键设计:所有组件通过"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 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 和传统运维脚本最根本的区别.

回忆你刚才的实验:删掉一个 Pod 后 Deployment 立刻补回来--这就是 Controller Manager 在起作用.

数据平面(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 到容器运行

回忆你之前执行 kubectl apply -f nginx-pod.yaml 时,kubectl describe 显示了一串 Events.现在来还原完整链路--把前面所有组件串起来走一遍:

$ kubectl apply -f nginx-pod.yaml

apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.25


下面是一次完整的 Pod 创建过程(7 步):

┌─ 第 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 的副本数对吗? → 不影响,继续监控 │
│ │
│ 全流程结束 ✓ │
└──────────────────────────────────────────────────────────────┘

高可用部署

生产环境的 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,应用就不中断 │
└────────────────────────────────────────────────────────────────┘

小结

本篇要点回顾

要点 一句话概括
Pod K8s 最小调度单元,不是"一个容器"而是"一组共享 NS 的容器集合"
YAML 骨架 apiVersion + kind + metadata + spec;所有 K8s 资源都遵循这四个必填字段
kubectl 基本操作 apply(创建/更新) / get(查看) / describe(详情) / logs(日志) / exec(进入) / delete(删除)
Deployment 管理 Pod 副本;你声明"要 3 个",Controller 持续保证"真的有 3 个在跑"
控制平面 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 规则 → 让 ClusterIP 的流量分发到后端 Pod
Pod 创建全流程 kubectl → API Server → etcd → Scheduler → kubelet → CRI → CNI → runc → Pause → 用户容器 → 回报
高可用 API Server 多副本+LB,Scheduler/CM 选主,etcd 奇数节点 Raft

动手练习

  1. kubectl run hello --image=nginx:1.25 --port=80 快速创建一个 Pod(不写 YAML 也能跑),然后用 kubectl get pod hello -o yaml 查看 K8s 自动生成的完整 YAML--观察哪些字段是你没写但被补上的
  2. 写一个 Deployment(replicas=2),启动后执行 kubectl delete pod <其中一个>,观察新 Pod 的自动重建;再试 kubectl scale deployment <name> --replicas=5,观察扩容
  3. 执行 kubectl get pods -n kube-system 查看控制平面组件(它们在 kube-system 命名空间里以静态 Pod 方式运行)
  4. 画出 K8s 集群架构图(不参照任何资料,凭记忆画),标注每个组件的职责和它与其他组件的通信关系
  5. 默写 Pod 创建全流程的 7 个步骤,特别标注出哪几步涉及阶段一学过的 Namespace/Cgroup/OverlayFS
  6. 思考题:为什么所有组件都必须通过 API Server 通信,而不是直接互相调用?如果 Scheduler 直接告诉 kubelet "你去跑这个 Pod",会有什么问题?