⎈ Kubernetes 云原生交互式教程
你已经会写 Deployment / Service / ConfigMap 的 YAML 并部署过服务 —— 这份教程带你打开黑盒:每一条 kubectl apply 背后,各个组件到底做了什么。
容器基础:一切的起点
K8s 是「容器编排系统」——想懂 K8s,先要真正理解容器解决了什么问题、它和虚拟机的本质区别。
1.1 容器 vs 虚拟机
容器是一种操作系统级的虚拟化技术:把应用和它的全部依赖(库、配置、运行时)打包在一个隔离的进程环境里运行,共享宿主机的内核,不需要模拟一台完整的机器。
解决「在我电脑上明明是好的」这一经典难题——环境不一致。同时相比虚拟机,容器启动秒级、资源开销小、单机可跑的实例密度高得多。
微服务部署、CI/CD 流水线、快速弹性扩缩容——凡是需要「快速、一致、大量」运行应用副本的场景。传统 VM 更适合需要不同内核/操作系统、或强安全隔离的场景。
你的 Java 服务依赖 JDK 17 + 某版本字体库。打成容器镜像后,开发机、测试环境、生产 K8s 集群里跑的是完全相同的环境,不会再有「测试环境 JDK 版本不对」的问题。
VM 虚拟化的是硬件(每台 VM 里跑完整 Guest OS,分钟级启动,GB 级开销);容器虚拟化的是操作系统(共享内核,只隔离进程视图,秒级启动,MB 级开销,性能接近原生)。隔离强度上 VM > 容器。
🏠 虚拟机(VM)
📦 容器
1.2 镜像与 Dockerfile
镜像(Image)是一个只读的打包文件,内含应用+依赖+启动命令,按「分层」存储;Dockerfile 是构建镜像的脚本,每条指令生成一层。
实现「一次构建,随处运行」:镜像推到镜像仓库(Registry)后,任何装了容器运行时的机器 pull 下来都能跑出一模一样的环境。分层还能复用缓存——只改代码时,基础层不用重新构建/传输。
所有要进 K8s 的应用第一步都是打镜像。CI 流水线里通常是:代码提交 → 构建镜像 → 打版本 tag → push 到仓库 → K8s 引用该镜像部署。
下面是一个 Java 应用的典型 Dockerfile 👇
镜像 vs 容器:镜像是静态模板(类),容器是运行实例(对象);一个镜像可以起 N 个容器。镜像 tag(如 myapp:v1.2)标识版本,K8s 滚动更新的本质就是「换个 tag 重新拉起」。
FROM eclipse-temurin:17-jre # 基础层:带 JDK17 运行时的官方镜像 WORKDIR /app # 设置工作目录 COPY target/app.jar app.jar # 把构建好的 jar 拷进镜像(新的一层) EXPOSE 8080 # 声明服务端口 ENTRYPOINT ["java","-jar","app.jar"] # 容器启动时执行的命令
第 1 章自测
点击选项即可判分run 出任意多个容器;容器里的改动默认不会写回镜像(除非显式 commit,生产中也不推荐这么做——应改 Dockerfile 重新构建)。K8s 整体架构与核心组件
这一章是全部原理的地基。K8s 集群 = 控制平面(大脑,做决策)+ 工作节点(干活的手,跑容器)。记住一个总原则:所有组件都只跟 API Server 通信,谁也不直接找谁。
控制平面(Control Plane)是集群的决策中枢,负责「期望状态管理」;工作节点(Worker Node)是实际运行容器的机器。你写的 YAML 描述「期望状态」,控制平面负责让「实际状态」不断向它靠拢——这叫声明式 + 调谐循环(Reconcile Loop),是 K8s 最核心的设计哲学。
命令式运维(人肉执行「启动、重启、迁移」)在几百上千个容器规模下不可维护。声明式让你只说「我要 3 个副本」,挂了谁、补在哪,全部由控制器自动闭环处理。
Scheduler 只「决定 Pod 放哪个节点」(写一条分配记录),它从不启动容器;真正拉镜像、启动容器的是目标节点上的 Kubelet。这是面试高频考点,也是理解第 3 章流程的关键。
2.1 可交互集群架构图(点击任意组件查看职责)
👆 点击上方任意组件
比如先点最中间的 API Server——它是理解一切流程的钥匙。
nodeName: node-2 这条分配决定」,写完它的任务就结束了;真正把容器启动起来的是 node-2 上的 Kubelet——它 watch 到「有个 Pod 分给我了」,才去调用 containerd 拉镜像、起容器。决策与执行完全分离。第 2 章自测
组件分工搞清了吗?kubectl get pods,这条请求首先到达哪个组件?nodeName 写回 API Server。目标节点的 Kubelet 通过 watch 发现「这个 Pod 分给我了」,才调用 containerd 拉镜像、创建容器。决策(Scheduler)与执行(Kubelet)分离。一个 Pod 从创建到运行的完整流程
面试高频题:「kubectl apply 一个 Deployment 之后发生了什么?」下面的动画把 10 步链路逐一点亮——注意观察:每一步都是某个组件 watch 到变化后各干各的活,没有谁在统一指挥,这就是 K8s 的「编排」。
第 3 章自测
链路顺序记住了吗?Pending 状态,说明流程卡在了哪一环?ContainerCreating;健康检查失败通常表现为 Running 但 not ready 或重启。排查第一步:kubectl describe pod 看 Events。nodeName 写到 Pod 对象上(经 API Server 存入 etcd);Kubelet 通过 watch 订阅「分配给本节点的 Pod」,发现新条目后主动开始干活。这种「写黑板 + 订阅」的松耦合设计让任何组件宕机重启都不丢指令。工作负载控制器全景 ⭐ 最重点
所有控制器的本质是同一句话:「盯着期望状态,不断把现实掰成期望的样子」。它们层层包一层,但最终全部落到 Pod。先看总图建立地图感,再逐个拆开讲。
4.1 控制器关系总图(点击任意控制器,高亮它管到 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 层。
4.4 Deployment:三层套娃与滚动更新
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 看到)。
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 章细讲)。
4.6 DaemonSet:每个节点站一个岗
DaemonSet 确保每个(或指定标签的)节点上恰好运行一个该 Pod 的副本。副本数不用你定——节点数决定。
有些能力是「节点级」的:采集这台机器的日志、监控这台机器的指标、给这台机器配网络。这类 Pod 必须一台不落。
日志采集(Filebeat/Fluentd)、监控 agent(node-exporter)、网络插件(Calico/Flannel)、节点安全 agent。
集群扩容加了一台新节点 → DaemonSet 控制器自动在新节点上补一个采集 agent,无需任何人工操作(下面动画)。
vs Deployment:Deployment 关心「总共 N 个,放哪随意」;DaemonSet 关心「每节点 1 个,跟着节点走」。副本数一个由 replicas 字段定,一个由节点数量定。
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 |
| Operator | CRD → 任意资源 | 自定义控制器管自定义资源 | 复杂中间件全生命周期 | 是控制器模式的扩展,不是新机制 |
第 4 章自测
控制器选型是面试和实战的核心能力kubectl get rs 能看到),正是 rollout undo 一键回滚的凭据:回滚就是把旧 RS 重新扩容。存储线:PV 与 PVC
Pod 会死、会漂移,但数据必须活着。K8s 用「PV + PVC」把存储的提供和存储的使用解耦——这也是 StatefulSet「有状态」能力的地基。
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,绑定是排他的。
storageClassName: alicloud-disk,存储插件就自动创建一块匹配的云盘和 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(防误删的安全设计)。
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(互相独立!)第 5 章自测
存储解耦的思想抓住了吗?kubectl delete pvc data-mysql-2。数据也不会自动迁移(K8s 不懂你的业务数据怎么合并,那是 Operator 这类领域控制器的活)。网络与配置:Service · Ingress · ConfigMap · Secret
Pod 的 IP 说变就变,怎么稳定地访问它?外部流量怎么进集群?配置怎么和镜像解耦?这一章补齐「让应用真正可用」的最后几块拼图。
6.1 Service:给一群善变的 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。
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:按域名和路径分流的七层网关
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——两者是上下游而非替代关系。
6.3 ConfigMap:配置与镜像分手
ConfigMap 是存非敏感配置的键值对象(数据库地址、功能开关、整个配置文件的内容都行),Pod 以环境变量或挂载成文件两种方式使用它。
配置烧进镜像 = 改个数据库地址都要重新 build + push + 发版;而且同一镜像没法在测试/生产用不同配置。「一次构建随处运行」要成立,配置必须外置。
环境差异化配置(测试/生产不同的下游地址)、应用配置文件(application.yaml、nginx.conf)。
两种注入方式的完整写法(点击 tab 切换)👇 Java 应用常用「挂载成文件」放 application.yaml,简单开关用环境变量。
① 改 ConfigMap 后:环境变量方式不会自动更新(env 在容器启动时注入,需重启 Pod 生效);挂载文件方式会延迟同步更新(但应用得支持热加载才有意义)。② 敏感信息(密码/密钥)不要放 ConfigMap → 用 Secret。
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 是启动时注入的)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 章自测
网络与配置四件套kubectl rollout restart deployment/xxx 滚动重启。挂载文件方式内容会延迟自动同步,但也要求应用自己支持热加载。不需要重建镜像——这正是配置外置的意义。完整部署流程对比:Deployment vs StatefulSet ⭐
前面所有知识在这一章拼成两条完整的实战流水线。每一步都可点开,看对应的 YAML(关键字段逐个注释)和 kubectl 命令。建议两列对照着看,差异点已用 ⭐ 标出。
docker build -t registry.example.com/shop/order:v1 . docker push registry.example.com/shop/order:v1
apiVersion: v1 kind: ConfigMap metadata: name: order-config data: DB_HOST: "mysql-0.mysql" LOG_LEVEL: "info"
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 步的配置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
apiVersion: v1
kind: Service
metadata:
name: order-svc
spec:
selector:
app: order # ★ 和 Deployment Pod 模板的标签对上,才能找到 Pod
ports:
- port: 80 # Service 端口
targetPort: 8080 # 容器端口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 }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
# 升级:改 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 # 查看历史版本
docker build -t registry.example.com/db/mysql:8.0 . docker push registry.example.com/db/mysql:8.0
clusterIP: None = 不要统一虚拟 IP,而是给每个 Pod 一个独立稳定的 DNS 名:mysql-0.mysql、mysql-1.mysql…(从库要精确连主库 mysql-0,负载均衡反而是干扰)。apiVersion: v1
kind: Service
metadata:
name: mysql # Pod DNS 后缀由它决定:mysql-0.mysql
spec:
clusterIP: None # ★ Headless 的标志:不分配虚拟 IP、不做负载均衡
selector:
app: mysql
ports:
- port: 3306serviceName 关联 Headless Service;volumeClaimTemplates 每 Pod 独立存储。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 各自独立的 10Gkubectl 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
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
# 在集群内任意 Pod 里: nslookup mysql-0.mysql # 精确解析到 0 号 Pod 的 IP nslookup mysql-1.mysql # 精确解析到 1 号 # 从库连主库就配:mysql-0.mysql:3306(Pod 重建 IP 变了,域名不变)
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 差异总表(面试可直接背)
| 维度 | Deployment | StatefulSet |
|---|---|---|
| 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 → Pod | StatefulSet → Pod(不经过 RS) |
| Pod 可替换性 | 完全等价,可随意替换 | 各有身份,重建后必须"还是原来那个" |
| 适用场景 | Web / API / 微服务(无状态) | MySQL / Redis / Kafka / ES(有状态) |
第 7 章自测
两条流水线的差异点kubectl get endpoints order-svc 显示为空,最可能的原因是?排错实战:像 SRE 一样思考 ⭐
排错不是背命令,是沿着「Pod 创建链路」和「流量转发链路」逐段验证。下面两棵可点击的决策树,就是把第 3 章和第 6 章的链路倒过来走。
kubectl get 看状态)→ 再「查体」(describe 看 Events,K8s 把它观察到的异常都写在这)→ 需要时「化验」(logs 看应用自己说了什么)→ 最后「开腹探查」(exec 进容器现场检查)。顺序几乎总是这四步。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 State | limits 给小了,或应用内存泄漏(Java 注意堆参数与 limits 匹配) |
第 8 章自测
给你三个真实故障现场kubectl logs order-xxx 却几乎什么都没有。下一步最应该?--previous 正是看它的。exec 也进不去(容器活不了几秒)。「删 Pod 重建」对崩溃循环无效:控制器本来就在不停重建它。kubectl get endpoints order-svc 显示 ENDPOINTS 为 <none>,但 get pods 显示 3 个 Pod 全部 Running 且 1/1 Ready。问题最可能出在?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 但流量不通,且是节点级异常。0/5 nodes are available: 5 Insufficient memory。正确的处理方向是?进阶了解层:生态一瞥
这一章只求「知道它是什么、解决什么问题」,为以后深入留个路标。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。
第 9 章自测
生态路标🎓 全部 9 章完成!
你现在拥有的不只是命令,而是一张完整的原理地图:声明式 + 控制器调谐这一个思想,贯穿了从 Deployment 到 Operator 再到 GitOps 的一切。下一步建议:在本地 minikube/kind 上把第 7 章两条流水线亲手跑一遍,再故意制造第 8 章的每一种故障并修好它——那时候你就真的「懂」了。