⎈ Kubernetes 云原生交互式教程

你已经会写 Deployment / Service / ConfigMap 的 YAML 并部署过服务 —— 这份教程带你打开黑盒:每一条 kubectl apply 背后,各个组件到底做了什么。

🎬 大量可交互动画 🧩 统一概念结构:是什么 / 为什么 / 场景 / 例子 / 区分 📝 每章自测小题 📱 手机电脑通用
第 1 章

容器基础:一切的起点

K8s 是「容器编排系统」——想懂 K8s,先要真正理解容器解决了什么问题、它和虚拟机的本质区别。

1.1 容器 vs 虚拟机

🏠
类比:虚拟机像「独栋别墅」——每户自带地基、水电、全套设施(完整操作系统),隔离彻底但建造慢、占地大;容器像「精装公寓」——共用大楼的地基和水电(共享主机内核),每户只带自己的家具(应用+依赖),入住快、密度高。
是什么

容器是一种操作系统级的虚拟化技术:把应用和它的全部依赖(库、配置、运行时)打包在一个隔离的进程环境里运行,共享宿主机的内核,不需要模拟一台完整的机器。

为什么

解决「在我电脑上明明是好的」这一经典难题——环境不一致。同时相比虚拟机,容器启动秒级、资源开销小、单机可跑的实例密度高得多

什么场景

微服务部署、CI/CD 流水线、快速弹性扩缩容——凡是需要「快速、一致、大量」运行应用副本的场景。传统 VM 更适合需要不同内核/操作系统、或强安全隔离的场景。

举个例子

你的 Java 服务依赖 JDK 17 + 某版本字体库。打成容器镜像后,开发机、测试环境、生产 K8s 集群里跑的是完全相同的环境,不会再有「测试环境 JDK 版本不对」的问题。

关键区分

VM 虚拟化的是硬件(每台 VM 里跑完整 Guest OS,分钟级启动,GB 级开销);容器虚拟化的是操作系统(共享内核,只隔离进程视图,秒级启动,MB 级开销,性能接近原生)。隔离强度上 VM > 容器。

动画演示:VM 与容器的结构 & 启动速度对比
🏠 虚拟机(VM)
应用 A | 应用 B
依赖库 / 运行时 ×2 套
Guest OS ×2(每台VM一个完整操作系统)
Hypervisor 虚拟化层
宿主机操作系统
物理服务器硬件
📦 容器
应用 A | 应用 B | 应用 C | 应用 D
每个容器自带依赖库(很轻)
容器运行时(如 containerd)
宿主机操作系统(共享同一个内核)
点击播放:先看两者的「层级结构」差异,再比一比启动速度。

1.2 镜像与 Dockerfile

🍱
类比:Dockerfile 是菜谱(一步步写清怎么做),镜像是照菜谱做好并冷冻封装的预制菜(只读、可复制、可运输),容器是加热后端上桌的那份菜(运行中的实例)。一份预制菜可以同时加热出无数份。
是什么

镜像(Image)是一个只读的打包文件,内含应用+依赖+启动命令,按「分层」存储;Dockerfile 是构建镜像的脚本,每条指令生成一层。

为什么

实现「一次构建,随处运行」:镜像推到镜像仓库(Registry)后,任何装了容器运行时的机器 pull 下来都能跑出一模一样的环境。分层还能复用缓存——只改代码时,基础层不用重新构建/传输。

什么场景

所有要进 K8s 的应用第一步都是打镜像。CI 流水线里通常是:代码提交 → 构建镜像 → 打版本 tag → push 到仓库 → K8s 引用该镜像部署。

举个例子

下面是一个 Java 应用的典型 Dockerfile 👇

关键区分

镜像 vs 容器:镜像是静态模板(类),容器是运行实例(对象);一个镜像可以起 N 个容器。镜像 tag(如 myapp:v1.2)标识版本,K8s 滚动更新的本质就是「换个 tag 重新拉起」。

DOCKERFILE
FROM eclipse-temurin:17-jre        # 基础层:带 JDK17 运行时的官方镜像
WORKDIR /app                       # 设置工作目录
COPY target/app.jar app.jar        # 把构建好的 jar 拷进镜像(新的一层)
EXPOSE 8080                        # 声明服务端口
ENTRYPOINT ["java","-jar","app.jar"]  # 容器启动时执行的命令
动画演示:一次构建,随处运行
📄 Dockerfile (菜谱) build 基础层 JRE 依赖层 应用层 app.jar 镜像 myapp:v1 push 🏬 镜像仓库 Registry / Harbor 💻 开发机 docker run环境完全一致 ✓ 🧪 测试环境 K8s环境完全一致 ✓ 🚀 生产集群 K8s环境完全一致 ✓
Dockerfile 写一次 → 构建出分层镜像 → 推到仓库 → 任何环境拉取运行,环境 100% 一致。
📝

第 1 章自测

点击选项即可判分
Q1.容器启动能达到「秒级」而虚拟机通常是「分钟级」,最根本的原因是?
容器镜像文件比虚拟机镜像小
容器共享宿主机内核,无需引导一个完整的 Guest 操作系统
容器使用了更快的 SSD 存储
容器不需要分配 CPU 和内存
解析:核心区别在于内核。VM 要模拟硬件并完整引导一个 Guest OS(内核启动、系统服务拉起),所以慢;容器只是宿主机上一个被隔离的进程,共享宿主内核,本质上「启动容器 ≈ 启动一个进程」,所以是秒级甚至毫秒级。镜像大小只是次要因素。
Q2.关于「镜像」和「容器」的关系,下列哪个说法正确?
镜像是运行中的实例,容器是静态模板
一个容器可以同时对应多个不同镜像
镜像是只读模板,容器是它的运行实例;一个镜像可启动多个容器
修改运行中的容器会自动更新镜像
解析:类比面向对象:镜像 = 类(只读、静态),容器 = 对象(运行时实例)。一个镜像可以 run 出任意多个容器;容器里的改动默认不会写回镜像(除非显式 commit,生产中也不推荐这么做——应改 Dockerfile 重新构建)。
Q3.「一次构建、随处运行」主要靠什么机制保证?
K8s 会在每台机器上自动重新编译应用
镜像把应用及其全部依赖打包为不可变文件,各环境拉取同一镜像运行
所有服务器必须安装完全相同的操作系统版本
解析:关键是「不可变的自包含打包」——依赖、运行时、配置的默认值全部封进镜像并由 tag 标识版本。各环境(开发机/测试/生产)从同一个仓库拉同一个 tag,跑起来的环境就完全一致,与宿主机装了什么无关(只要内核兼容)。
第 2 章

K8s 整体架构与核心组件

这一章是全部原理的地基。K8s 集群 = 控制平面(大脑,做决策)+ 工作节点(干活的手,跑容器)。记住一个总原则:所有组件都只跟 API Server 通信,谁也不直接找谁

🏢
类比:把集群想成一家物流公司总部 + 各地仓库。API Server 是前台总机(所有指令必须经它);etcd 是档案室(存全公司的账本);Controller Manager 是各部门主管(盯着账本,发现「实际和计划不符」就下单纠正);Scheduler 是调度科(决定货放哪个仓库);Kubelet 是仓库管理员(真正把货上架并汇报);Kube-proxy 是仓库的物流路由(把访问请求转发到正确的货架)。
是什么

控制平面(Control Plane)是集群的决策中枢,负责「期望状态管理」;工作节点(Worker Node)是实际运行容器的机器。你写的 YAML 描述「期望状态」,控制平面负责让「实际状态」不断向它靠拢——这叫声明式 + 调谐循环(Reconcile Loop),是 K8s 最核心的设计哲学。

为什么

命令式运维(人肉执行「启动、重启、迁移」)在几百上千个容器规模下不可维护。声明式让你只说「我要 3 个副本」,挂了谁、补在哪,全部由控制器自动闭环处理。

关键区分

Scheduler 只「决定 Pod 放哪个节点」(写一条分配记录),它从不启动容器;真正拉镜像、启动容器的是目标节点上的 Kubelet。这是面试高频考点,也是理解第 3 章流程的关键。

2.1 可交互集群架构图(点击任意组件查看职责)

点击下图中的任一组件 → 下方显示它的「是什么 / 干什么 / 类比」
🧠 控制平面 Control Plane(大脑:只做决策,不跑业务容器) API Server 集群唯一入口 · 总机 etcd 集群状态数据库 · 档案室 Controller Manager 运行各种控制器 · 部门主管们 Scheduler 决定 Pod 放哪个节点 · 调度科 读写 watch / 上报(节点组件也只和 API Server 通信) 💪 工作节点 Node-1(真正跑容器的机器) Kubelet 节点管理员 Kube-proxy 网络规则 containerd 容器运行时 📦 Pod A 📦 Pod B 💪 工作节点 Node-2 Kubelet节点管理员 Kube-proxy网络规则 containerd容器运行时 📦Pod C

👆 点击上方任意组件

比如先点最中间的 API Server——它是理解一切流程的钥匙。

⚠️ 再强调一次分工(高频考点):Scheduler 干的活只是「在 Pod 对象上写下 nodeName: node-2 这条分配决定」,写完它的任务就结束了;真正把容器启动起来的是 node-2 上的 Kubelet——它 watch 到「有个 Pod 分给我了」,才去调用 containerd 拉镜像、起容器。决策与执行完全分离。
💡 为什么所有组件只跟 API Server 通信?① 统一鉴权审计入口,安全边界清晰;② 组件间彻底解耦——Scheduler 挂了不影响已运行的 Pod,Kubelet 断连节点上容器照跑;③ 所有状态变化都落 etcd,任何组件重启后从「账本」恢复视角即可(watch 机制:组件订阅自己关心的对象变化,而不是轮询)。
📝

第 2 章自测

组件分工搞清了吗?
Q1.你执行 kubectl get pods,这条请求首先到达哪个组件?
etcd —— 数据存在它那里,直接查它
API Server —— 它是集群唯一入口,再由它去读 etcd
Kubelet —— Pod 在节点上,问节点最准
Scheduler —— 它管理所有 Pod
解析:任何组件/用户都不允许绕过 API Server 直接读写 etcd。kubectl 本质上就是一个 REST 客户端,所有命令都是发给 API Server 的 HTTP 请求,由它统一做认证、鉴权、校验,再操作 etcd。
Q2.Scheduler 完成调度后,实际「启动容器」这件事由谁执行?
Scheduler 自己远程登录节点启动
API Server 下发启动指令并执行
目标节点上的 Kubelet 调用容器运行时(containerd)启动
Controller Manager 中的 Pod 控制器
解析:Scheduler 只负责「决策」:给 Pod 挑一个节点并把 nodeName 写回 API Server。目标节点的 Kubelet 通过 watch 发现「这个 Pod 分给我了」,才调用 containerd 拉镜像、创建容器。决策(Scheduler)与执行(Kubelet)分离。
Q3.如果 etcd 的数据全部丢失且无备份,会发生什么?
没影响,各节点上保存有完整副本
集群「失忆」:所有资源对象定义(Deployment/Service 等)丢失,等于集群状态清零
只丢监控数据,业务无感知
解析:etcd 是集群唯一的状态存储——所有 Deployment、Service、ConfigMap、Secret 等对象都只存在这里。丢了它,控制平面就不知道「期望状态」是什么了。已在节点上运行的容器短期还会跑(Kubelet 缓存),但集群已无法管理它们。所以生产环境 etcd 必须多副本部署 + 定期备份。
第 3 章

一个 Pod 从创建到运行的完整流程

面试高频题:「kubectl apply 一个 Deployment 之后发生了什么?」下面的动画把 10 步链路逐一点亮——注意观察:每一步都是某个组件 watch 到变化后各干各的活,没有谁在统一指挥,这就是 K8s 的「编排」。

🍽️
类比:像餐厅点单:你把订单交给前台(API Server),前台记入系统(etcd)。后厨主管(Deployment/RS 控制器)看到新订单就开出制作工单(创建 Pod 对象);排班员(Scheduler)决定哪个灶台做;灶台厨师(Kubelet)看到分给自己的工单才真正开火(起容器);传菜员(Kube-proxy)配好上菜路线。每个人只盯系统里跟自己有关的单子。
分步动画:kubectl apply -f deployment.yaml 之后的 10 步(可播放 / 上一步 / 下一步)
$ kubectl apply -f ... 控制平面 API Server 唯一入口 etcd 状态账本 Controller Manager Deployment 控制器 · RS 控制器 (都在 watch API Server) Scheduler 只做「放哪」决定 🏬 镜像仓库 Registry 工作节点 Node-2(被 Scheduler 选中) Kubelet 节点管理员 containerd 容器运行时 Kube-proxy 网络规则 📦 Pod: myapp-xxx 未创建
未开始(共 10 步)
点击「自动播放」或「下一步」,看每个组件在链路中各干了什么。
💡 看完动画请记住三句话:① kubectl 之后你就「下班」了,剩下全是组件间通过 watch 自动接力;② Pending 状态 = Pod 对象已存在于 etcd,但还没被调度或没起来;③ 整条链没有任何组件直接互相调用——API Server + etcd 是唯一的「协作黑板」
📝

第 3 章自测

链路顺序记住了吗?
Q1.Pod 长时间处于 Pending 状态,说明流程卡在了哪一环?
镜像还在下载中
Pod 对象已创建,但 Scheduler 还没能把它调度到任何节点(常见原因:资源不足)
容器已启动但健康检查未通过
API Server 拒绝了请求
解析:Pending = 「对象已写入 etcd,但尚未成功调度/启动」。最常见是没有节点能满足资源请求(CPU/内存 requests)或亲和性约束,Scheduler 找不到落点。镜像下载阶段状态是 ContainerCreating;健康检查失败通常表现为 Running 但 not ready 或重启。排查第一步:kubectl describe pod 看 Events。
Q2.Deployment 控制器直接创建的是什么对象?
直接创建 Pod
创建 ReplicaSet,再由 ReplicaSet 控制器创建 Pod
直接调用 Kubelet 启动容器
解析:层级是 Deployment → ReplicaSet → Pod。Deployment 控制器只管理 RS(滚动更新时正是靠新旧两个 RS 交替);具体「保证 N 个 Pod 存在」由 RS 控制器完成。第 4 章会展开这个关系。
Q3.Kubelet 是如何知道「有个新 Pod 要我来跑」的?
Scheduler 直接发送 RPC 命令给 Kubelet
Controller Manager 会 SSH 到节点通知它
Kubelet 持续 watch API Server,发现有 Pod 的 nodeName 是自己所在节点
解析:又是那个总原则——组件之间从不直接通信。Scheduler 只是把 nodeName 写到 Pod 对象上(经 API Server 存入 etcd);Kubelet 通过 watch 订阅「分配给本节点的 Pod」,发现新条目后主动开始干活。这种「写黑板 + 订阅」的松耦合设计让任何组件宕机重启都不丢指令。
第 4 章

工作负载控制器全景 ⭐ 最重点

所有控制器的本质是同一句话:「盯着期望状态,不断把现实掰成期望的样子」。它们层层包一层,但最终全部落到 Pod。先看总图建立地图感,再逐个拆开讲。

4.1 控制器关系总图(点击任意控制器,高亮它管到 Pod 的完整链路)

一张图看懂谁管谁:点击节点试试
Deployment 无状态应用首选 管理 ReplicaSet 保证 N 个副本 · 打工人 📦 📦 📦 Pod ×3(可随意替换) StatefulSet 有状态应用(数据库等) 直接管 Pod (不经过 RS) mysql-0 mysql-1 mysql-2 固定名字 · 有序创建 💾 PVC ×3(每 Pod 独立存储) volumeClaimTemplates 自动创建 DaemonSet 每个节点各跑一个 Node1📦 Node2📦 Node3📦 日志采集 / 监控 agent / 网络插件 CronJob 定时触发 · K8s 的 crontab 按时生成 Job 一次性任务 · 跑完即止 📦 ✅ Pod 成功退出后不再重建 🤖 Operator = 自定义资源(CRD) + 自定义控制器 把「领域运维知识」写成控制器,像管 Deployment 一样 管理复杂中间件,如 kind: MysqlCluster(自动主从/备份/故障切换) 与上面所有控制器同样的原理:watch 期望状态 → 调谐现实 📦 Pod = 最小调度单元,脆弱不自愈 → 所以才需要上面这些控制器

👆 点击任意控制器节点

被点中的控制器会连同它管理的整条链路一起高亮,其余变暗;下方同步显示它的定位和关键区分点。

4.2 Pod:最小单元,但从不「裸奔」

是什么

Pod 是 K8s 的最小部署/调度单元:一组共享网络(同一 IP、localhost 互通)和存储卷的容器(通常一个 Pod 就一个业务容器)。

为什么

容器粒度太细:有些进程必须「同生共死、共享 localhost」(如业务容器 + 日志 sidecar)。Pod 把它们捆成一个原子单位来调度。

什么场景

一切负载最终都以 Pod 形式运行;但现实中几乎不手写裸 Pod——只在临时调试时用(如起个 busybox 排查网络)。

举个例子

你部署的 Deployment 副本数为 3,最终就是 3 个 Pod 在不同节点上运行,每个 Pod 有自己的 IP。

关键区分

裸 Pod 脆弱、不自愈:节点宕机或进程被杀后没人重建它,IP 也会变。「保证它活着」是控制器的职责——这就是为什么下面所有控制器存在。

4.3 ReplicaSet:只管数量的「打工人」

是什么

ReplicaSet(RS)用标签选择器盯住一组 Pod,保证任何时刻恰好有 N 个副本在运行。多了杀、少了补。

为什么

解决裸 Pod 不自愈的问题:Pod 挂了自动补,实现故障自愈和水平扩缩。

什么场景

你几乎从不直接创建 RS——它是 Deployment 雇来的「打工人」。因为 RS 只管数量、完全不管版本更新:改了镜像它不会滚动替换旧 Pod。

举个例子

下面的动画:3 副本的 RS,你亲手「杀死」一个 Pod,看它秒速补齐 👇

关键区分

RS vs Deployment:RS = 数量保证;Deployment = 版本管理 + 指挥 RS。滚动更新、回滚这些能力都在 Deployment 层。

交互实验:故障自愈 —— 点击任意 Pod 把它「杀死」
期望副本数 3 | 实际运行 3 ✅(点击上面的 Pod 试试)
RS 控制器持续对比「期望 3 个 vs 实际几个」,一旦少了立刻创建新 Pod 补齐——注意新 Pod 名字后缀是新的:补的是数量,不是原来那个 Pod

4.4 Deployment:三层套娃与滚动更新

🎖️
类比:Deployment 是项目经理(管版本、管发布策略),ReplicaSet 是班组长(保证在场人数),Pod 是工人。发新版本时,经理不是给老班组换人,而是新建一个新版本班组,逐步把人从旧组调到新组——旧班组编制保留(0人),随时可以整组调回(回滚)。
是什么

Deployment 管理 ReplicaSet、再由 RS 管理 Pod 的三层结构,提供滚动更新、版本回滚、暂停/恢复发布能力。

为什么

光保证数量不够,生产还要「不停机换版本、出问题一键回滚」。Deployment 用「新旧两个 RS 此消彼长」优雅解决。

什么场景

无状态应用的默认选择:Web 服务、API 网关、微服务——任何「Pod 之间完全等价、可随意替换重建」的应用。

举个例子

把镜像从 v1 改到 v2 后 kubectl apply:Deployment 新建一个 v2 的 RS,逐步扩 v2、缩 v1(下面动画)。回滚 = kubectl rollout undo,本质是把旧 RS 再扩回来。

关键区分

改镜像时绝不是修改旧 RS,而是创建新 RS——旧 RS 缩到 0 但保留(这就是回滚凭据,可用 kubectl get rs 看到)。

动画演示:滚动更新(v1 → v2)与一键回滚
旧 ReplicaSet(myapp-v1)· 副本 3
新 ReplicaSet(myapp-v2)· 副本 0
点击升级:注意全程始终有 Pod 在服务(不停机),新起一个 v2、才停一个 v1;旧 RS 缩到 0 后仍保留。
💡 为什么说「Pod 可随意替换 = 无状态应用首选」?滚动更新时 v1 的 Pod 被杀掉、v2 的 Pod 是全新的(新名字、新 IP、无本地数据)。如果你的应用把数据存在 Pod 本地磁盘或依赖固定身份,这套机制就会弄丢数据——那就该用 StatefulSet。

4.5 StatefulSet:给 Pod 发「身份证」

是什么

StatefulSet 与 Deployment 平级、直接管理 Pod(不经过 ReplicaSet),给每个 Pod 三样「不变的身份」:固定名字(mysql-0/1/2)、固定网络标识(独立 DNS 名)、绑定的独立存储(每 Pod 一块自己的 PVC)。

为什么

数据库、消息队列这类应用的实例不等价:主从有角色之分、各自有本地数据。Deployment「随意替换」的哲学会破坏它们;StatefulSet 保证 Pod 重建后还是原来那个身份、挂回原来那块盘

什么场景

MySQL / Redis / Kafka / Zookeeper / Elasticsearch 等一切「有本地状态、实例间有身份区别」的应用。

举个例子

3 副本 MySQL:mysql-0 是主库、mysql-1/2 是从库。mysql-1 所在节点宕机后,新 Pod 仍叫 mysql-1、仍挂载原来的 PVC,数据不丢、主从关系不乱。

关键区分

vs Deployment 四连:① 名字固定序号 vs 随机后缀;② 有序创建(0 就绪才建 1)vs 并行;③ 每 Pod 独立 PVC vs 通常无状态不挂盘;④ 需要配 Headless Service 提供稳定 DNS(第 7 章细讲)。

动画对比:StatefulSet 有序创建(0 就绪 → 才建 1),每个 Pod 绑独立存储
对比记忆:Deployment 的 3 个 Pod 是「一把全撒出去」并行创建;StatefulSet 像排队点名——mysql-0 完全就绪后才开始创建 mysql-1,且每人领到自己专属的储物柜(PVC)。

4.6 DaemonSet:每个节点站一个岗

是什么

DaemonSet 确保每个(或指定标签的)节点上恰好运行一个该 Pod 的副本。副本数不用你定——节点数决定。

为什么

有些能力是「节点级」的:采集这台机器的日志、监控这台机器的指标、给这台机器配网络。这类 Pod 必须一台不落。

什么场景

日志采集(Filebeat/Fluentd)、监控 agent(node-exporter)、网络插件(Calico/Flannel)、节点安全 agent。

举个例子

集群扩容加了一台新节点 → DaemonSet 控制器自动在新节点上补一个采集 agent,无需任何人工操作(下面动画)。

关键区分

vs Deployment:Deployment 关心「总共 N 个,放哪随意」;DaemonSet 关心「每节点 1 个,跟着节点走」。副本数一个由 replicas 字段定,一个由节点数量定。

交互实验:DaemonSet 跟着节点走 —— 点「新增节点」看 agent 自动补位
当前 3 个节点,每个节点自动运行 1 个日志采集 agent(绿色)。新节点加入集群后,DaemonSet 控制器 watch 到节点列表变化,自动补一个。

4.7 Job 与 CronJob:干完就走的临时工

是什么

Job:运行一次性任务的控制器,Pod 成功退出(exit 0)后即完成,不再重建;失败可按策略重试。CronJob:按 cron 表达式定时创建 Job 的控制器,链路是 CronJob → Job → Pod。

为什么

Deployment 的世界观是「永远活着」——进程退出就拉起,根本不适合「跑完就该结束」的任务。Job 的成功标准恰好相反:正常退出 = 任务完成。

什么场景

Job:数据库迁移脚本、批量数据处理、离线计算。CronJob:每日凌晨备份、定时对账、周期性报表——相当于 K8s 里的 crontab。

举个例子

每天 2 点备份数据库:CronJob(schedule: 0 2 * * *)到点生成一个 Job,Job 拉起 Pod 执行 mysqldump,成功退出后 Pod 保留记录但不再运行。

关键区分

Job vs CronJob:Job 是「跑一次」,CronJob 是「按时间表反复生成 Job」。Job vs Deployment:容器退出后,Deployment 视为故障要拉起,Job 视为完成不拉起——选错会出现「迁移脚本被无限重复执行」的事故。

4.8 Operator / CRD:把运维专家写成代码

是什么

CRD(自定义资源定义)让你给 K8s 扩展新的资源类型(如 kind: MysqlCluster);Operator = CRD + 自定义控制器,用与内置控制器完全相同的「watch → 调谐」模式管理这种新资源。

为什么

StatefulSet 只解决「稳定身份和存储」,但不懂业务:MySQL 主挂了怎么选新主?备份怎么做?扩容后怎么重新分片?这些领域知识以前靠运维专家人肉操作,Operator 把它们写成控制器代码,7×24 自动执行。

什么场景

在 K8s 上运行复杂中间件基本都用现成 Operator:Prometheus Operator、Redis Operator、Elastic Operator、各种数据库 Operator。

举个例子

装好 MySQL Operator 后,你只需提交一份 kind: MysqlCluster, replicas: 3 的 YAML,Operator 自动完成:建 StatefulSet、配主从复制、设定时备份、主库故障自动切换。

关键区分

Operator 不是新机制,而是「控制器模式」的用户级复用——K8s 把自己最核心的设计模式开放给了所有人,这也是它能成为「平台的平台」的原因。

4.9 一表总结:到底选哪个控制器?

控制器管理链路一句话职责典型场景最容易混淆的点
Deployment→ RS → Pod无状态应用 + 滚动更新/回滚Web / API 服务改镜像是「新建 RS」,不是改旧 RS
ReplicaSet→ Pod只保证副本数量(被 Deployment 使用)不管更新,所以不直接用
StatefulSet→ Pod(直接)固定身份 + 独立存储 + 有序MySQL / Kafka / ES不经过 RS;需 Headless Service
DaemonSet→ Pod(每节点1个)副本数 = 节点数日志/监控 agent、网络插件没有 replicas 字段
Job→ Pod跑完即止,成功退出不重建数据迁移、批处理与 Deployment 对「退出」的理解相反
CronJob→ Job → Pod定时生成 Job每日备份、定时对账它创建的是 Job,不直接创建 Pod
OperatorCRD → 任意资源自定义控制器管自定义资源复杂中间件全生命周期是控制器模式的扩展,不是新机制
📝

第 4 章自测

控制器选型是面试和实战的核心能力
Q1.Deployment 滚动更新(v1→v2)的底层机制是?
逐个修改旧 Pod 里的容器镜像并重启容器
修改旧 ReplicaSet 的镜像字段,由它替换 Pod
创建一个 v2 的新 ReplicaSet,逐步扩大新 RS、缩小旧 RS,旧 RS 缩到 0 后保留
删除整个旧 RS 再创建新 RS(会短暂停机)
解析:Pod 和 RS 的模板都是不可变思想——换版本 = 换一批新 Pod,而不是改造旧 Pod。旧 RS 缩到 0 但保留在集群里(kubectl get rs 能看到),正是 rollout undo 一键回滚的凭据:回滚就是把旧 RS 重新扩容。
Q2.要在 K8s 上部署一个 3 节点 Kafka 集群(每个 broker 有自己的数据盘和固定 ID),应选哪个控制器?
Deployment —— 3 副本加个 PVC 就行
StatefulSet —— 固定名字 + volumeClaimTemplates 给每个 broker 独立 PVC
DaemonSet —— 保证每个节点一个 broker
Job —— 启动 3 次 Kafka 进程
解析:Kafka broker 之间身份不等价(各有 broker.id 和本地数据),Pod 重建后必须找回自己的数据盘和网络身份——这正是 StatefulSet 三件套(固定名字/稳定 DNS/独立 PVC)的用武之地。Deployment 的 Pod 共用一个 PVC 定义且随意替换,会造成数据错乱。DaemonSet 副本数被节点数绑架,也不对。
Q3.为什么实际工作中几乎不直接创建 ReplicaSet?
因为 RS 性能差,Deployment 是它的高性能版本
因为 RS 只保证副本数量、不管版本更新;用 Deployment 能白得滚动更新和回滚能力
因为 RS 已被废弃,新版本 K8s 不再支持
解析:RS 没被废弃也没有性能问题——它至今仍是 Deployment 底下真正干活的「打工人」。但你直接用 RS 的话,改镜像它不会替换存量 Pod(新模板只对之后补的 Pod 生效),更没有回滚。Deployment 在 RS 之上白送发布管理能力,所以无状态应用永远从 Deployment 起步。
Q4.「每天凌晨 3 点执行一次数据对账脚本」应该用什么?
Deployment:replicas=1,脚本里自己 sleep 到 3 点
Job:每天手动 kubectl create 一次
CronJob:schedule 设为 0 3 * * *,到点自动生成 Job 执行
DaemonSet:保证每个节点都对一次账
解析:周期性任务 = CronJob 的定义本身。用 Deployment 跑脚本是常见错误:脚本执行完退出,Deployment 认为容器「挂了」会不停重启,触发 CrashLoopBackOff(或者反复执行脚本)。链路记牢:CronJob 按时间 → 生成 Job → Job 拉起 Pod → 成功退出即完成。
第 5 章

存储线:PV 与 PVC

Pod 会死、会漂移,但数据必须活着。K8s 用「PV + PVC」把存储的提供存储的使用解耦——这也是 StatefulSet「有状态」能力的地基。

🅿️
类比:PV 是小区里实际存在的停车位(物业/开发商提供,规格固定);PVC 是你交给物业的租车位申请单("我要一个能停 SUV 的、长租的");物业撮合后你们绑定——之后不管你换什么车(Pod 重建),车位还是那个车位(数据还在)。
是什么

PV(PersistentVolume,持久卷):集群里一块实际的存储资源——可能是云盘(阿里云盘/EBS)、NFS 目录、Ceph 块设备。由管理员手动创建,或由存储系统动态创建。它是集群级资源,不属于任何命名空间。

为什么

Pod 里容器的文件系统是临时的——容器重建即清空。数据必须落到独立于 Pod 生命周期的地方。而且「存储怎么提供」(云盘?NFS?)不该让开发者操心。

什么场景

一切需要持久化的场景:数据库文件、上传的用户文件、消息队列的日志段。

举个例子

管理员在集群里注册了一块 100G 的云盘作为 PV:「容量 100G,支持单节点读写(RWO)」。

关键区分

PV 描述的是「供给侧」:这块存储是什么、在哪、多大。它和 Pod 没有直接关系——中间隔着 PVC。

是什么

PVC(PersistentVolumeClaim,持久卷申领):Pod 使用存储前提交的申请单:「我要 10G、要能读写」。K8s 找到满足条件的 PV 后把两者绑定(Bound),Pod 再挂载这个 PVC。

为什么

让开发者只描述需求、不关心实现:你写「我要 10G」,至于给你的是阿里云盘还是 NFS,由管理员/StorageClass 决定。供需解耦,YAML 可以跨集群移植。

什么场景

Pod 的 YAML 里永远引用 PVC(而不是直接引用 PV 或云盘 ID)。

举个例子

下方动画:Pod 挂载 PVC → PVC 绑定 PV → PV 对应真实云盘 👇

关键区分

PV vs PVC:PV = 资源本身(供给,管理员视角);PVC = 使用申请(需求,开发者视角)。一个 PVC 同一时间只绑定一个 PV,绑定是排他的。

💡 StorageClass(动态供给)一句话:手动建 PV 太累,StorageClass 是「PV 自动售货机」——PVC 里写上 storageClassName: alicloud-disk,存储插件就自动创建一块匹配的云盘和 PV 来绑定。生产环境基本都这么用。
动画演示:Pod → PVC → PV → 实际存储 的申领与绑定链路
📦 Pod: mysql-0 volumeMounts: /var/lib/mysql ① 挂载 📋 PVC(申请单) 要 10Gi · ReadWriteOnce 状态:Pending ② 匹配绑定 💾 PV(真实资源) 容量 10Gi · RWO 状态:Available ③ 落盘 ☁️ 云盘 / NFS 物理世界的存储 开发者只看得见左边两个;右边两个是管理员/存储系统的事 —— 这就是解耦
Pod 只认 PVC(需求),PVC 再去撮合 PV(供给),PV 背后才是真实的盘。

5.1 volumeClaimTemplates:StatefulSet「有状态」的关键一环

是什么

StatefulSet 里的 volumeClaimTemplates(PVC 模板):按模板为每个 Pod 自动创建一个专属 PVC,命名规则固定为 模板名-Pod名(如 data-mysql-0)。

为什么

Deployment 的 Pod 共用 YAML 里写死的同一个 PVC——3 个副本挤一块盘,数据库必乱。有状态应用需要「每个实例一块自己的盘」,且 Pod 重建后能按名字找回原来那块

举个例子

3 副本 MySQL:自动生成 data-mysql-0 / data-mysql-1 / data-mysql-2 三个 PVC,各绑各的 PV。mysql-1 所在节点宕机后,新的 mysql-1 Pod 会重新挂上 data-mysql-1——数据完好如初(下面动手试试)。

关键区分

缩容不删 PVC:把副本从 3 缩到 2,mysql-2 的 Pod 没了,但 data-mysql-2 这个 PVC 和数据默认保留——再扩回 3 时数据还在。删数据必须手动删 PVC(防误删的安全设计)。

YAML · StatefulSet 存储模板片段
volumeClaimTemplates:            # PVC 模板:每个 Pod 自动生成一份
- metadata:
    name: data                   # PVC 名 = data-mysql-0 / data-mysql-1 / data-mysql-2
  spec:
    accessModes: ["ReadWriteOnce"]   # RWO:只允许单节点读写(数据库标配)
    storageClassName: fast-ssd       # 用哪台"PV 自动售货机"动态建盘
    resources:
      requests:
        storage: 10Gi            # 每个 Pod 各要 10G(互相独立!)
交互实验:每个 Pod 独立存储 · 宕机重建数据不丢
三个 MySQL Pod 各自绑定独立 PVC,各存各的数据分片。点按钮杀掉 mysql-1,看它重建后是否还能找回自己的数据。
📝

第 5 章自测

存储解耦的思想抓住了吗?
Q1.PV 和 PVC 的关系,哪个说法最准确?
PVC 是 PV 的备份副本
PV 是实际存储资源(供给),PVC 是使用申请(需求),两者匹配后一对一绑定
PV 在命名空间内,PVC 是集群级资源
Pod 直接挂载 PV,PVC 只是记录用途的标签
解析:记「供需」二字:PV = 管理员/存储系统提供的实际资源;PVC = 开发者提交的需求申请。绑定是一对一排他的。另外方向反了——PV 是集群级、PVC 属于命名空间;Pod 永远挂 PVC 而不是直接挂 PV。
Q2.StatefulSet 用 volumeClaimTemplates 而不是直接写一个 PVC,是为了?
节省存储空间,让所有 Pod 共享同一块盘
给每个 Pod 自动创建各自独立的 PVC,Pod 重建后按名字挂回自己原来的盘
加快 Pod 启动速度
解析:恰恰是「不共享」:模板为每个 Pod 生成专属 PVC(data-mysql-0/1/2)。因为 StatefulSet 的 Pod 名字固定,重建后的 mysql-1 仍叫 mysql-1,自然挂回 data-mysql-1——「身份 + 专属存储」共同构成了「有状态」。若像 Deployment 那样写死一个 PVC,所有副本会挤同一块盘。
Q3.把 StatefulSet 从 3 副本缩容到 2,mysql-2 的数据会怎样?
PVC 和数据随 Pod 一起被自动删除
Pod 被删,但 PVC data-mysql-2 和数据默认保留;再扩回 3 副本时数据还在
数据自动迁移合并到 mysql-0 和 mysql-1
解析:K8s 对数据的态度是「宁可留着占空间,不可替你做删除决定」。缩容只删 Pod 不删 PVC——这是防止误操作丢数据的安全设计。真要释放存储,需要手动 kubectl delete pvc data-mysql-2。数据也不会自动迁移(K8s 不懂你的业务数据怎么合并,那是 Operator 这类领域控制器的活)。
第 6 章

网络与配置:Service · Ingress · ConfigMap · Secret

Pod 的 IP 说变就变,怎么稳定地访问它?外部流量怎么进集群?配置怎么和镜像解耦?这一章补齐「让应用真正可用」的最后几块拼图。

6.1 Service:给一群善变的 Pod 一个不变的门牌

☎️
类比:Service 像客服中心的总机号码:客服(Pod)会换班、离职、新人入职(IP 变化),但你手里的号码永远是那一个;每次拨打,系统自动把你转给一位当前在线的客服(负载均衡 + 只转给就绪的 Pod)。
是什么

Service 是一组 Pod 的稳定访问入口:一个不变的虚拟 IP(ClusterIP)+ DNS 名,背后通过标签选择器动态跟踪匹配的 Pod 列表(Endpoints),流量在它们之间负载均衡。

为什么

Pod 重建 IP 就变(第 4 章自愈实验里你亲眼见过)。调用方不可能追着 IP 跑;Service 把「谁在提供服务」这个动态问题收敛成一个静态地址。

什么场景

集群内服务互调(http://order-svc 直接当域名用);配合 NodePort/LoadBalancer 暴露到集群外。

举个例子

订单服务 3 副本,前端只管访问 order-svc:80;滚动更新期间 Pod 换了一整批,前端毫无感知。

关键区分

Service 匹配 Pod 靠的是 selector 和 Pod 标签的对应关系——写错标签 = Endpoints 为空 = 访问不通(第 8 章排错的头号常客)。另外记得:真正让流量转发生效的是各节点的 kube-proxy。

交互实验:负载均衡 + Pod 换血但 Service 地址不变
Service: order-svc
ClusterIP 10.96.8.20:80
↑ 这个地址永远不变
点「连发请求」:流量被轮流分发到 3 个 Pod(计数器 +1)。再点「杀死一个 Pod」:新 Pod 的 IP 完全不同,但 Service 地址纹丝不动,请求自动只发给活着的 Pod。
YAML · Service
apiVersion: v1
kind: Service
metadata:
  name: order-svc            # 集群内 DNS 名:http://order-svc
spec:
  selector:
    app: order               # ★ 匹配所有带 app=order 标签的 Pod(必须和 Pod 标签一致!)
  ports:
  - port: 80                 # Service 自己对外的端口
    targetPort: 8080         # 转发到 Pod 里容器监听的端口
  type: ClusterIP            # 默认类型:仅集群内可访问

6.2 Ingress:按域名和路径分流的七层网关

🏨
类比:Service(NodePort/LoadBalancer)像酒店的各个楼层直达电梯——一个入口只通一个地方;Ingress 像大堂前台:所有客人都从正门进(一个入口),前台看你要去餐厅还是健身房(域名/URL 路径),指给你对应的电梯(转发到不同 Service)。
是什么

Ingress 是 HTTP/HTTPS 层(七层)的路由规则集:「shop.com/api → api-svc、shop.com/web → web-svc」。规则本身只是配置,真正干活的是 Ingress Controller(如 nginx-ingress)——一个跑在集群里的反向代理。

为什么

如果每个服务都用 LoadBalancer 暴露,一个服务一个公网 IP,又贵又乱。Ingress 让一个入口承载全部 HTTP 服务,还统一管 TLS 证书、域名。

什么场景

对外提供 HTTP 服务基本都走 Ingress:多个微服务共享一个域名按路径分流,或不同子域名对应不同服务。

举个例子

下面动画:外部用户访问 shop.com/web/api,Ingress 按路径分发到两个不同 Service 👇

关键区分

Service(四层)vs Ingress(七层):Service 只认 IP:端口,不懂 HTTP,进来什么转发什么;Ingress 能读懂 HTTP 的域名、路径、Header,按内容路由。Ingress 的转发目标是 Service——两者是上下游而非替代关系。

动画演示:七层路由——同一个入口,按路径分发
👩‍💻 外部用户 Ingress (nginx controller) /web → web-svc /api → api-svc web-svc 商城页面服务 api-svc 订单接口服务 📦 📦 📦 📦
两个按钮走的是同一个入口(同一个域名、同一个公网 IP),Ingress 读取 URL 路径后分发到不同 Service。

6.3 ConfigMap:配置与镜像分手

是什么

ConfigMap 是存非敏感配置的键值对象(数据库地址、功能开关、整个配置文件的内容都行),Pod 以环境变量挂载成文件两种方式使用它。

为什么

配置烧进镜像 = 改个数据库地址都要重新 build + push + 发版;而且同一镜像没法在测试/生产用不同配置。「一次构建随处运行」要成立,配置必须外置

什么场景

环境差异化配置(测试/生产不同的下游地址)、应用配置文件(application.yaml、nginx.conf)。

举个例子

两种注入方式的完整写法(点击 tab 切换)👇 Java 应用常用「挂载成文件」放 application.yaml,简单开关用环境变量。

关键区分

① 改 ConfigMap 后:环境变量方式不会自动更新(env 在容器启动时注入,需重启 Pod 生效);挂载文件方式会延迟同步更新(但应用得支持热加载才有意义)。② 敏感信息(密码/密钥)不要放 ConfigMap → 用 Secret。

YAML · ConfigMap → 环境变量
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-config
data:
  DB_HOST: "mysql-0.mysql"     # 键值对形式的配置
  LOG_LEVEL: "info"
---
# Deployment 的容器部分这样引用:
containers:
- name: order
  image: order:v1
  envFrom:
  - configMapRef:
      name: order-config       # 整个 ConfigMap 的键值全部变成环境变量
  # 应用里直接读 System.getenv("DB_HOST") 即可
  # ⚠️ 改 ConfigMap 后需重启 Pod 才生效(env 是启动时注入的)
YAML · ConfigMap → 挂载成文件
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-config-file
data:
  application.yaml: |          # 一个键对应一整个文件的内容
    spring:
      datasource:
        url: jdbc:mysql://mysql-0.mysql:3306/orders
    logging:
      level.root: info
---
# Deployment 的 Pod 模板部分这样引用:
spec:
  containers:
  - name: order
    image: order:v1
    volumeMounts:
    - name: config-vol
      mountPath: /app/config   # 容器内出现 /app/config/application.yaml
  volumes:
  - name: config-vol
    configMap:
      name: order-config-file  # 把 ConfigMap 变成一个"虚拟文件夹"挂进去

6.4 Secret:敏感信息专用柜

是什么

Secret 与 ConfigMap 用法几乎一样(也是环境变量/挂载文件两种注入方式),专门存敏感数据:数据库密码、API 密钥、TLS 证书、拉私有镜像的凭证(imagePullSecrets)。

为什么

敏感信息需要区别对待:单独的资源类型才能单独控制权限(RBAC 可以只允许运维读 Secret)、支持 etcd 落盘加密、避免在日志和 kubectl describe 里明文露出。

举个例子

把 MySQL 密码存成 Secret,Deployment 里用 secretKeyRef 注入为环境变量 DB_PASSWORD——YAML 和镜像里从此不出现明文密码。

关键区分

⚠️ Secret 里的值只是 base64 编码,不是加密——任何有读权限的人一条命令就能解出来。它的安全性来自「权限控制 + etcd 加密配置」,而不是编码本身。高安全场景配合外部方案(如 Vault、云厂商 KMS)。

📝

第 6 章自测

网络与配置四件套
Q1.Service 能持续把流量发给正确的 Pod(哪怕 Pod 全换了一批),靠的是?
Service 记录了每个 Pod 的固定 IP
标签选择器动态维护 Endpoints 列表:谁带着匹配标签且就绪,谁就在名单上
Pod 重建后会主动向 Service 注册自己
DNS 会自动把域名解析到最新的 Pod IP
解析:Service 从不记「固定 IP」——它只声明「我要 app=order 的 Pod」。Endpoints 控制器持续监视:新 Pod 带此标签且 readiness 探针通过 → 加入名单;Pod 死了 → 移出名单。kube-proxy 再把名单落实为转发规则。所以标签写错 = 名单永远为空 = 访问不通。
Q2.「Service 是四层、Ingress 是七层」,这句话的实际含义是?
Service 只认 IP 和端口转发;Ingress 能读懂 HTTP 的域名/路径并按内容分流
Ingress 比 Service 快,因为层数越高性能越好
Service 在第四个机房,Ingress 在第七个机房
两者是替代关系,用了 Ingress 就不需要 Service
解析:OSI 四层 = 传输层(TCP,只见 IP:端口);七层 = 应用层(HTTP,能见域名、路径、Header)。所以 Ingress 能做「/api 给 A、/web 给 B」的内容路由,Service 做不到。两者是上下游:Ingress 分流的目标就是各个 Service。
Q3.你改了 ConfigMap 里的 LOG_LEVEL(以环境变量方式注入),发现应用没变化,为什么?
ConfigMap 修改后需要 24 小时才生效
环境变量方式必须重新构建镜像
环境变量在容器启动时一次性注入,改 ConfigMap 不影响已运行的容器,需重启 Pod(如 rollout restart)
解析:进程的环境变量是启动瞬间从父进程继承的,之后外界怎么改都与它无关——这是操作系统的规矩,不是 K8s 的限制。所以 env 方式改配置 = kubectl rollout restart deployment/xxx 滚动重启。挂载文件方式内容会延迟自动同步,但也要求应用自己支持热加载。不需要重建镜像——这正是配置外置的意义。
第 7 章

完整部署流程对比:Deployment vs StatefulSet ⭐

前面所有知识在这一章拼成两条完整的实战流水线。每一步都可点开,看对应的 YAML(关键字段逐个注释)和 kubectl 命令。建议两列对照着看,差异点已用 ⭐ 标出。

🌊 Deployment 部署一个无状态 Web 服务
1打镜像并推到仓库
所有部署的起点(第 1 章讲过的 build → push)。
BASH
docker build -t registry.example.com/shop/order:v1 .
docker push registry.example.com/shop/order:v1
2写 ConfigMap / Secret(配置先行)
配置与镜像分离(第 6 章)。先建配置,Deployment 才能引用。
YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-config
data:
  DB_HOST: "mysql-0.mysql"
  LOG_LEVEL: "info"
3写 Deployment YAML(核心)
副本数、镜像、资源限制、两种探针、引用配置——生产必备字段全在这。
YAML · deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order
spec:
  replicas: 3                    # 期望 3 个副本
  selector:
    matchLabels:
      app: order                 # ★ 必须与下面 Pod 模板的标签一致
  template:                      # ↓↓ Pod 模板:每个副本长这样 ↓↓
    metadata:
      labels:
        app: order               # Pod 的标签(Service 也靠它来找 Pod)
    spec:
      containers:
      - name: order
        image: registry.example.com/shop/order:v1
        ports:
        - containerPort: 8080    # 容器监听端口
        resources:
          requests:              # 调度依据:至少给我这么多
            cpu: "250m"          # 0.25 核
            memory: "512Mi"
          limits:                # 上限:超内存会被 OOMKill
            cpu: "1"
            memory: "1Gi"
        readinessProbe:          # 就绪探针:没通过 → 不接流量
          httpGet: { path: /health, port: 8080 }
          initialDelaySeconds: 10
        livenessProbe:           # 存活探针:连续失败 → 重启容器
          httpGet: { path: /health, port: 8080 }
          initialDelaySeconds: 30
        envFrom:
        - configMapRef:
            name: order-config   # 引用第 2 步的配置
4kubectl apply 部署
触发第 3 章的完整链路。⭐ 3 个 Pod 并行创建、名字带随机后缀。
BASH
kubectl apply -f configmap.yaml -f deployment.yaml
kubectl get pods
# NAME                     READY   STATUS    AGE
# order-7d9f8b5c4-x7k2p    1/1     Running   15s   ← 随机后缀
# order-7d9f8b5c4-m3q9r    1/1     Running   15s   ← 三个同时创建
# order-7d9f8b5c4-v5t8w    1/1     Running   15s
5写 Service 提供稳定入口
⭐ 普通 ClusterIP Service:selector 必须匹配 Pod 标签。
YAML · service.yaml
apiVersion: v1
kind: Service
metadata:
  name: order-svc
spec:
  selector:
    app: order          # ★ 和 Deployment Pod 模板的标签对上,才能找到 Pod
  ports:
  - port: 80            # Service 端口
    targetPort: 8080    # 容器端口
6(可选)Ingress 对外暴露
要给集群外用户用,加一条七层路由规则。
YAML · ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ing
spec:
  rules:
  - host: shop.example.com
    http:
      paths:
      - path: /api/order
        pathType: Prefix
        backend:
          service:
            name: order-svc   # 转发目标是第 5 步的 Service
            port: { number: 80 }
7验证三连
Pod 跑起来了?Service 挂上 Pod 了?——Endpoints 非空是关键。
BASH
kubectl get pods -l app=order          # Pod 都 Running/Ready?
kubectl get svc order-svc              # Service 建好了?
kubectl get endpoints order-svc        # ★ 必查!有 Pod IP 才说明 selector 匹配成功
# NAME        ENDPOINTS                             AGE
# order-svc   10.244.1.5:8080,10.244.2.8:8080,...   1m
8滚动更新与回滚
第 4 章动画演示过的新旧 RS 交替,这是对应的命令。
BASH
# 升级:改 yaml 里的镜像 tag 再 apply(推荐,可进 git 管理)
kubectl apply -f deployment.yaml
kubectl rollout status deployment/order    # 盯着滚动进度
# 出问题一键回滚(把旧 RS 扩回来):
kubectl rollout undo deployment/order
kubectl rollout history deployment/order   # 查看历史版本
🗄 StatefulSet 部署一个 3 副本 MySQL
1打镜像并推到仓库
与左侧完全相同(或直接用官方 mysql 镜像)。
BASH
docker build -t registry.example.com/db/mysql:8.0 .
docker push registry.example.com/db/mysql:8.0
2⭐ 先建 Headless Service
⭐ 独有步骤!clusterIP: None = 不要统一虚拟 IP,而是给每个 Pod 一个独立稳定的 DNS 名mysql-0.mysqlmysql-1.mysql…(从库要精确连主库 mysql-0,负载均衡反而是干扰)。
YAML · headless-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql              # Pod DNS 后缀由它决定:mysql-0.mysql
spec:
  clusterIP: None          # ★ Headless 的标志:不分配虚拟 IP、不做负载均衡
  selector:
    app: mysql
  ports:
  - port: 3306
3写 StatefulSet YAML(核心)
⭐ 两个独有字段:serviceName 关联 Headless Service;volumeClaimTemplates 每 Pod 独立存储。
YAML · statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet          # ★ 类型不同
metadata:
  name: mysql
spec:
  serviceName: mysql       # ★ 独有:关联第 2 步的 Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: registry.example.com/db/mysql:8.0
        ports:
        - containerPort: 3306
        volumeMounts:
        - name: data                 # 挂载下面模板生成的 PVC
          mountPath: /var/lib/mysql  # 数据库文件目录
        readinessProbe:
          exec:
            command: ["mysqladmin","ping"]
          initialDelaySeconds: 15
  volumeClaimTemplates:    # ★ 独有:PVC 模板(第 5 章)
  - metadata:
      name: data           # 生成 data-mysql-0/1/2
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 10Gi    # 每个 Pod 各自独立的 10G
4apply → ⭐ 有序创建
⭐ 与左侧「三个同时出现」不同:mysql-0 完全就绪后才创建 mysql-1,名字是固定序号。
BASH
kubectl apply -f headless-svc.yaml -f statefulset.yaml
kubectl get pods -w        # -w 持续观察,能看到依次出现:
# mysql-0   0/1   ContainerCreating
# mysql-0   1/1   Running          ← 0 就绪了
# mysql-1   0/1   ContainerCreating  ← 才开始建 1
# mysql-1   1/1   Running
# mysql-2   0/1   ContainerCreating
5⭐ 每 Pod 自动生成独立 PVC
⭐ 无需手写 PVC,模板自动创建并各自绑定 PV。
BASH
kubectl get pvc
# NAME           STATUS   VOLUME     CAPACITY   AGE
# data-mysql-0   Bound    pv-a1b2    10Gi       2m   ← 各绑各的
# data-mysql-1   Bound    pv-c3d4    10Gi       1m
# data-mysql-2   Bound    pv-e5f6    10Gi       40s
6验证固定身份(DNS)
⭐ 每个 Pod 有独立稳定域名——从库配置里可以放心写死主库地址。
BASH
# 在集群内任意 Pod 里:
nslookup mysql-0.mysql     # 精确解析到 0 号 Pod 的 IP
nslookup mysql-1.mysql     # 精确解析到 1 号
# 从库连主库就配:mysql-0.mysql:3306(Pod 重建 IP 变了,域名不变)
7有序扩缩容与滚动更新
⭐ 扩容顺序 0→1→2→3…;缩容倒序 3→2…且 PVC 默认保留;滚动更新也是从最大序号倒着来(先更新从库、最后动主库,风险最小)。
BASH
kubectl scale statefulset mysql --replicas=4   # 有序扩:新建 mysql-3
kubectl scale statefulset mysql --replicas=3   # 倒序缩:先删 mysql-3
kubectl get pvc                                # data-mysql-3 还在!数据保留
# 滚动更新:改镜像后 apply,按 2 → 1 → 0 倒序逐个替换
# 每个 Pod 重建后按名字挂回自己原来的 PVC,数据不丢

7.1 差异总表(面试可直接背)

维度DeploymentStatefulSet
Pod 命名随机后缀:order-7d9f8b5c4-x7k2p固定序号:mysql-0 / mysql-1 / mysql-2
创建顺序并行,一起创建严格有序:0 就绪才建 1;缩容/更新倒序
网络身份普通 Service:统一入口 + 负载均衡需 Headless Service(clusterIP: None):每 Pod 独立稳定 DNS
存储通常无盘;写 PVC 也是全体共用一个volumeClaimTemplates:每 Pod 独立 PVC,重建后按名挂回
管理链路Deployment → ReplicaSet → PodStatefulSet → Pod(不经过 RS)
Pod 可替换性完全等价,可随意替换各有身份,重建后必须"还是原来那个"
适用场景Web / API / 微服务(无状态)MySQL / Redis / Kafka / ES(有状态)
📝

第 7 章自测

两条流水线的差异点
Q1.StatefulSet 为什么必须配 Headless Service(clusterIP: None)而不是普通 Service?
Headless Service 性能更高,省去了负载均衡开销
有状态实例需要被"点名"访问(如从库精确连主库 mysql-0),Headless 给每个 Pod 独立稳定 DNS,而普通 Service 会把请求随机分给任意 Pod
StatefulSet 的 Pod 没有 IP,只能靠 DNS 访问
解析:普通 Service 的负载均衡对无状态服务是优点,对主从数据库是灾难——写请求被随机发给从库就出错了。Headless Service 不做负载均衡,而是为每个 Pod 注册独立 DNS(mysql-0.mysql),让「精确访问某个特定实例」成为可能。StatefulSet 的 serviceName 字段就是用来关联它的。
Q2.Deployment 部署后 kubectl get endpoints order-svc 显示为空,最可能的原因是?
Ingress 还没有创建
Service 的 selector 与 Pod 的标签没有匹配上(或 Pod 都没通过就绪探针)
没有给 Service 分配公网 IP
解析:Endpoints 为空 = Service「按标签找 Pod」一无所获。两大原因:① selector 和 Pod 标签对不上(拼写、大小写、少个键);② Pod 存在但 readiness 探针没通过(未就绪的 Pod 不进 Endpoints)。这是服务访问不通排查的第一站,第 8 章的决策树会用到。
Q3.StatefulSet 滚动更新的顺序是?
与 Deployment 一样,随机挑 Pod 替换
从 0 号开始正序更新,0 是主库最重要要先升
从最大序号倒序逐个更新(先 mysql-2,最后 mysql-0),每个就绪后才更新下一个
解析:倒序 + 逐个是刻意设计:编号大的通常是从库/次要实例,先拿它们试新版本;主库(mysql-0)最后更新,风险最小。而且严格「一个就绪才动下一个」,任何一步失败更新就停住,不会把整个集群一起带崩。对比 Deployment:并发度由 maxSurge/maxUnavailable 控制、无顺序概念。
第 8 章

排错实战:像 SRE 一样思考 ⭐

排错不是背命令,是沿着「Pod 创建链路」和「流量转发链路」逐段验证。下面两棵可点击的决策树,就是把第 3 章和第 6 章的链路倒过来走。

🩺
类比:像医生看病:先「问诊」(kubectl get 看状态)→ 再「查体」(describe 看 Events,K8s 把它观察到的异常都写在这)→ 需要时「化验」(logs 看应用自己说了什么)→ 最后「开腹探查」(exec 进容器现场检查)。顺序几乎总是这四步
BASH · 排错四板斧(先记住这个顺序)
kubectl get pods                        # ① 问诊:状态是什么?(Pending/CrashLoop/ImagePull...)
kubectl describe pod order-xxx          # ② 查体:看最下面的 Events!K8s 眼中的异常全在这
kubectl logs order-xxx                  # ③ 化验:应用自己打印了什么
kubectl logs order-xxx --previous       # ③′ 崩溃重启过?看"上一次"崩溃前的日志(关键!)
kubectl exec -it order-xxx -- sh        # ④ 探查:进容器里看环境变量/配置/网络连通

8.1 决策树一:Pod 起不来 / 不正常(点击你看到的症状)

8.2 决策树二:服务访问不通(沿流量链路排查)

8.3 症状速查表

症状 / 状态它在说什么第一条该敲的命令最常见原因
Pending还没调度到任何节点kubectl describe pod 看 Events资源不足(requests 超过节点余量)、污点/亲和性不满足
ImagePullBackOff镜像拉不下来,正在退避重试kubectl describe pod 看 Events镜像名/tag 拼错、私有仓库没配 imagePullSecrets、网络不通
CrashLoopBackOff容器反复崩溃-重启-崩溃kubectl logs --previous应用启动报错(配置错/依赖连不上)、liveness 探针过严、OOMKilled
ContainerCreating 卡住卡在启动准备阶段kubectl describe pod 看 Events挂载失败:PVC 没绑定、引用的 ConfigMap/Secret 不存在
Running 但 0/1(Not Ready)进程活着但没通过就绪检查kubectl describe pod 看探针配置readiness 探针路径/端口错、应用启动慢、依赖没就绪
Service 访问不通流量链路某一段断了kubectl get endpoints <svc>Endpoints 为空 = selector 与 Pod 标签不匹配(头号原因)
OOMKilled(describe 可见)内存超过 limits 被内核杀死kubectl describe pod 看 Last Statelimits 给小了,或应用内存泄漏(Java 注意堆参数与 limits 匹配)
📝

第 8 章自测

给你三个真实故障现场
Q1.Pod 状态 CrashLoopBackOff,你执行 kubectl logs order-xxx 却几乎什么都没有。下一步最应该?
删掉 Pod 让它重建,说不定就好了
kubectl logs order-xxx --previous,看上一次崩溃前的日志
直接 exec 进容器查看
重启整个节点
解析:CrashLoop 时你 logs 到的往往是「刚重启、还没来得及报错」的新容器,真正的死因在上一个容器的临终日志里——--previous 正是看它的。exec 也进不去(容器活不了几秒)。「删 Pod 重建」对崩溃循环无效:控制器本来就在不停重建它。
Q2.服务访问不通,kubectl get endpoints order-svc 显示 ENDPOINTS 为 <none>,但 get pods 显示 3 个 Pod 全部 Running 且 1/1 Ready。问题最可能出在?
Service 的 selector 与 Pod 的标签不匹配(拼写/键名对不上)
kube-proxy 挂了
镜像有问题需要重新构建
Ingress 规则写错了
解析:Pod 健康 + Endpoints 为空,几乎可以锁定 selector 匹配问题:用 kubectl get pods --show-labels 对照 kubectl describe svc order-svc 里的 Selector,一比就出来(常见:app=order 写成 app=orders)。如果 Pod not ready,Endpoints 也会空,但题干说了 1/1。kube-proxy 故障的表现是 Endpoints 有 IP 但流量不通,且是节点级异常。
Q3.Pod 一直 Pending,describe 的 Events 显示:0/5 nodes are available: 5 Insufficient memory。正确的处理方向是?
把 liveness 探针的 initialDelaySeconds 调大
配置 imagePullSecrets
调小 Pod 的 memory requests、扩容节点、或清理低优先级负载——这是调度资源不足
解析:这条 Events 是 Scheduler 在说话:「5 个节点没有一个装得下你要的内存」。注意看的是 requests(调度依据)而不是实际使用量——requests 写 8Gi 而节点只剩 4Gi 就调不上去,哪怕应用实际只用 1Gi。方向:合理化 requests / 加节点 / 腾资源。探针和镜像凭证与 Pending 无关(那是启动之后的事)。
第 9 章

进阶了解层:生态一瞥

这一章只求「知道它是什么、解决什么问题」,为以后深入留个路标。K8s 只是地基,云原生生态在它上面盖了整座城。

🤖 Operator(回顾+深化)

解决:StatefulSet 只管「稳定身份」,不懂业务——主从切换、备份恢复、扩容重分片这些「领域运维知识」谁来做?

思路:CRD 定义新资源(kind: MysqlCluster),配一个自定义控制器 7×24 调谐。把资深 DBA 的操作手册写成了代码。用现成的:Prometheus Operator、Redis Operator、Elastic Operator。

🕸 服务网格 Service Mesh(Istio)

解决:微服务间通信的「共性需求」——加密(mTLS)、重试、超时、熔断、灰度分流、调用链追踪——难道每个服务都自己写一遍?

思路:给每个 Pod 自动注入一个 sidecar 代理(Envoy),进出流量全被它接管,上述能力在代理层统一实现,业务代码零改动。代价是架构复杂度和资源开销,中小规模慎重上。

📊 可观测性(Prometheus + 日志)

解决:几百个 Pod 生生灭灭,出了问题去哪看?三大支柱:指标(Metrics)、日志(Logs)、链路(Traces)。

思路:Prometheus 定时「拉取」各服务暴露的 /metrics 指标 + Grafana 出图 + Alertmanager 告警;日志由 DaemonSet 部署的采集器(第 4 章讲过为什么用 DaemonSet)统一收到 ES/Loki。Pod 会消失,所以日志和指标必须集中存储

🔁 CI/CD 与 GitOps

解决:「改代码 → 上线」怎么全自动且可审计、可回滚?

思路:CI(构建测试打镜像)之后,GitOps 主张:集群该长什么样,以 Git 仓库里的 YAML 为唯一事实。Argo CD 之类的工具持续对比「Git 里的期望 vs 集群实际」并自动同步——发现了吗,这就是控制器调谐思想搬到了发布流程上。回滚 = git revert。

GitOps 流水线一图流(点播放看代码如何自动到达生产)
👨‍💻git push 代码 CI 流水线测试→打镜像→push改配置库镜像 tag 📜 配置 Git 仓库全部 YAML 在这里= 唯一事实来源 Argo CD对比 Git vs 集群有差异 → 自动同步 生产集群 发布 = 改 Git;回滚 = git revert;谁改的、改了啥,git log 全有 —— 可审计的自动化
注意最后一环:Argo CD 干的事(对比期望与实际、自动调谐)正是 K8s 控制器的思想——整个云原生生态都在复用这一个模式。
📝

第 9 章自测

生态路标
Q1.Istio 能给微服务加上 mTLS 加密、重试、熔断而「业务代码零改动」,靠的是?
在每个服务里自动生成并编译进相关代码
给每个 Pod 注入 sidecar 代理容器,进出流量都经过代理,能力在代理层实现
修改 Linux 内核模块实现
解析:sidecar 模式:Pod 里除了业务容器再塞一个 Envoy 代理容器(还记得吗——Pod 内容器共享网络,天然适合做这件事),流量被透明劫持经过它。加密、重试、熔断、指标采集全在代理层完成,对业务进程完全透明。这也是 Pod「一组容器」设计的经典应用。
Q2.GitOps 的核心主张是?
所有开发者都必须会用 Git 才能上岗
Git 仓库里的 YAML 是集群状态的唯一事实来源,工具持续把集群同步成 Git 描述的样子
用 Git 存储容器镜像以省去镜像仓库
解析:「唯一事实来源 + 自动调谐」——是不是很耳熟?这就是 K8s 声明式思想(etcd 存期望状态、控制器调谐)在发布流程上的翻版:Git 扮演 etcd,Argo CD 扮演控制器。好处:每次变更有 commit 记录(可审计)、回滚 = revert(可靠)、禁止人肉 kubectl 改生产(一致性)。
Q3.为什么 K8s 集群的日志方案强调「集中收集存储」,而不是登录到 Pod 里看文件?
Pod 随时可能被重建/漂移,本地日志会随容器消失;且几百个 Pod 无法逐个登录排查
因为 kubectl logs 命令有 1MB 大小限制
集中存储比本地磁盘便宜
解析:核心还是那句话——Pod 是易逝的。滚动更新、自愈重建、节点故障漂移,任何一次都会带走容器本地的日志文件。所以用 DaemonSet 在每个节点跑采集 agent(第 4 章的经典场景),实时把日志送进 ES/Loki 集中存储。排查昨天挂掉的那个 Pod 的日志?只有集中存储里才找得到。

🎓 全部 9 章完成!

你现在拥有的不只是命令,而是一张完整的原理地图:声明式 + 控制器调谐这一个思想,贯穿了从 Deployment 到 Operator 再到 GitOps 的一切。下一步建议:在本地 minikube/kind 上把第 7 章两条流水线亲手跑一遍,再故意制造第 8 章的每一种故障并修好它——那时候你就真的「懂」了。