知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 01 - 核心对象:从 Pod 到 Deployment > 本篇目标:扭正三个容易搞错的心智模型,看清集群架构,然后**真正把一个镜像跑起来**,并让它具备自愈、扩容、滚动升级和回滚能力。 > 环境已按 00 篇准备好(`kubectl get nodes` 全部 `Ready`)。 --- ## 1. 三个必须扭过来的观念 概念记不住,往往不是名词不认识,而是**脑子里的模型还停留在"操作服务器"**。先扭这三点,后面都顺。 ### 1.1 声明式:你不是在下命令,你是在填表  *图:命令式与声明式:前者关心步骤与过程,后者只声明期望状态* ```text 命令式(传统运维) 声明式(K8s) ────────────────────── ────────────────────── "启动 3 个 nginx" "我要 3 个 nginx 一直存在" → 执行完就结束了 → 集群持续盯着这件事 → 挂掉一个,还是 2 个 → 挂掉一个,立刻补到 3 个 → 想改就得再下一条命令 → 改副本数,重新 apply 即可 ``` 所以你写 YAML 时的心态应该是**描述终态**,而不是"第一步做什么、第二步做什么"。这也是为什么 K8s 里 `apply` 一个已经存在的资源不会报错 —— 它只是把你的期望又声明了一遍。 ### 1.2 一切都是对象:K8s 本质是个 API 系统 `kubectl` 不是"运维工具",它是一个 **HTTP 客户端**。你敲的每条命令,最终都是往 API Server 发一个 REST 请求:  *图:kubectl 只是客户端:每条命令最终都是一次到 API Server 的 REST 请求* ```text kubectl get pods │ └─→ GET https://<apiserver>/api/v1/namespaces/default/pods kubectl apply -f deploy.yaml │ └─→ POST/PATCH https://<apiserver>/apis/apps/v1/namespaces/default/deployments ``` 理解这一点,你就明白为什么: - 所有资源都能用 `-o yaml` 导出来看,因为**它就是一份 JSON/YAML** - RBAC 能精确到"某人对某类资源有 get/list 权限",因为**权限就建在 API 上** - `kubectl explain deployment.spec` 能当文档用 ### 1.3 控制器循环:所有"自动"都来自它 K8s 里的自动重启、自动扩容、自动补副本,全都来自同一个循环:  *图:控制器循环:比对期望与现状、把差异拉平,K8s 的自愈都来自它* ```text ┌──────────────────────────────┐ │ │ v │ 读取期望状态 ──→ 读取实际状态 ──→ 差异?──否──→ 等待/重新循环 (你写的 YAML) (集群真实情况) │ │是 v 执行动作把它拉平 ``` 每种资源背后都有一个控制器在做这件事。Deployment 控制器管副本数,Node 控制器管节点状态,Ingress 控制器管路由规则。**"自愈"不是什么魔法,就是这个循环在不停地跑。** --- ## 2. 架构:一条 `kubectl apply` 命令经历了什么  *图:Kubernetes 集群架构:控制面与工作节点,以及一条 kubectl apply 的流向* ```text ┌─────────────────────────── 控制面(Control Plane)───────────────────────────┐ │ │ │ ┌──────────────┐ 你敲的命令 │ │ │ kubectl │──────┐ │ │ └──────────────┘ │ HTTPS │ │ v │ │ ┌────────────────────────────┐ ┌───────────────────────────────┐ │ │ │ API Server │<────>│ etcd │ │ │ │ (唯一入口 / 鉴权 / 校验) │ │ (集群唯一的"真相来源"存储) │ │ │ └───────┬────────────────────┘ └───────────────────────────────┘ │ │ │ 写入期望状态 │ │ v │ │ ┌────────────────────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ Controller Manager │ │ Scheduler │ │ Cloud Controller│ │ │ │ (各种控制器循环,补副本) │ │ (Pod 放哪台)│ │ (对接云 LB/磁盘)│ │ │ └────────────────────────────┘ └──────┬───────┘ └──────────────────┘ │ └──────────────────────────────────────────┼──────────────────────────────────┘ │ 调度结果写回 API Server v ┌────────────────────────── 工作节点(Node,可以有很多台)──────────────────────┐ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ kubelet ← 从 API Server 拉取"我该跑哪些 Pod",负责真正启动容器 │ │ │ │ │ │ │ │ │ ├─ 容器运行时(containerd / CRI-O)→ 拉起容器 │ │ │ │ │ │ │ │ │ └─ kube-proxy → 维护 Service 的转发规则(iptables / IPVS) │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ Pod A Pod B Pod C │ └─────────────────────────────────────────────────────────────────────────────┘ ``` | 组件 | 一句话职责 | 挂了会怎样 | |---|---|---| | **API Server** | 唯一的读写入口,所有命令都经过它 | 集群管理瘫痪(正在跑的容器不受影响) | | **etcd** | 存全集群状态的数据库 | 最要命的组件,**必须备份** | | **Scheduler** | 决定 Pod 该放到哪个节点 | 新 Pod 一直 Pending | | **Controller Manager** | 跑各种控制器循环,负责"补副本" | 副本数不自动收敛 | | **kubelet** | 节点上的"工头",真正管容器生死 | 该节点的 Pod 失管 | | **kube-proxy** | 实现 Service 的虚拟 IP 转发 | Service 访问不通 | > 注意 **kubelet 才是真正启动容器的人**,Scheduler 只是"填志愿",kubelet 才"录取并报到"。这个分工决定了:**Pod 调度到某节点后,即使控制面挂了,容器照样跑。** --- ## 3. 最小可运行对象:Pod ### 3.1 Pod 是什么 Pod 是 K8s 里**最小的调度单位**,一个 Pod 里可以有一个或多个容器。同一个 Pod 内的容器共享**网络命名空间**和**存储卷**:  *图:Pod 内部:多容器共享网络命名空间与存储卷* ```text ┌──────────────── Pod(共享一个 IP,共享网络栈)────────────────┐ │ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ 容器 A │ │ 容器 B │ │ │ │ (主应用) │ │ (sidecar) │ │ │ │ 监听 8080 │ │ 监听 9090 │ │ │ └───────┬────────┘ └───────┬────────┘ │ │ │ │ │ │ └────────┬────────────────┘ │ │ │ localhost 互相能通 │ │ ┌───────v────────┐ ┌──────────────┐ │ │ │ 共享网络命名空间 │ │ 共享 Volume │ │ │ │ Pod IP: 10.1.2.7│ │ (日志/缓存) │ │ │ └────────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` **为什么不是"直接跑容器"?** 因为现实里经常需要"主应用 + 日志收集器 + 配置代理"这种捆绑关系,它们需要共享网络和文件。Pod 就是这个捆绑包。 ### 3.2 写一个最小 Pod ```yaml # pod.yaml apiVersion: v1 kind: Pod metadata: name: nginx-demo labels: app: nginx-demo spec: containers: - name: nginx image: nginx:1.27 ports: - containerPort: 80 ``` ```bash kubectl apply -f pod.yaml kubectl get pods -o wide # -o wide 能看到 Pod 落在哪个节点、Pod IP 是多少 kubectl describe pod nginx-demo # 出问题时第一个该看的命令 kubectl logs -f nginx-demo # 看日志 kubectl exec -it nginx-demo -- sh # 进容器 kubectl delete pod nginx-demo ``` ### 3.3 一个必须知道的坑 > **裸 Pod 不要用在生产。** 你删掉它,它就真的没了 —— 不会有任何东西把它重新拉起来。 > > 因为它没有"期望副本数"这回事。要自愈和扩容,必须用下一节的 Deployment。 --- ## 4. 自愈与扩容:Deployment ### 4.1 三层关系:Deployment → ReplicaSet → Pod  *图:Deployment → ReplicaSet → Pod 三层关系,以及滚动升级时新旧 ReplicaSet 的交替* ```text ┌──────────────────────────────────────────────────────────────┐ │ Deployment (我要 3 个副本,用镜像 nginx:1.27) │ │ ← 你写这个,它管版本和滚动更新 │ │ │ │ │ │ 创建并管理 │ │ v │ │ ┌────────────────────────────────────────┐ │ │ │ ReplicaSet (revision 1) ← 保证副本数 │ │ │ │ ← 你一般不直接碰它,升级时它会被替换 │ │ │ │ │ │ │ │ │ ├── Pod (nginx:1.27) │ │ │ │ ├── Pod (nginx:1.27) │ │ │ │ └── Pod (nginx:1.27) │ │ │ └────────────────────────────────────────┘ │ │ │ │ 升级镜像时:新建 ReplicaSet (revision 2),逐步把 Pod 迁过去 │ │ → 旧 ReplicaSet 保留(副本数 0),这就是"回滚"能一键完成的原因 │ └──────────────────────────────────────────────────────────────┘ ``` **这三个对象的存在意义,就是"版本管理"**:Deployment 记录历史版本,ReplicaSet 负责当前版本的副本数,Pod 是最终跑起来的实例。 ### 4.2 完整 Deployment ```yaml # deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: blog-system labels: app: blog-system spec: replicas: 3 # 期望副本数 selector: # 管理哪些 Pod(必须和 template 的 labels 对上) matchLabels: app: blog-system strategy: type: RollingUpdate # 默认就是滚动更新 rollingUpdate: maxSurge: 1 # 更新时最多多出 1 个 Pod maxUnavailable: 0 # 更新时不允许有 Pod 不可用 → 零停机 template: # Pod 模板 metadata: labels: app: blog-system spec: containers: - name: app image: registry.example.com/blog-system:1.0.0 ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: "prod" resources: # 这两项非常重要,见 4.4 requests: cpu: "250m" memory: "512Mi" limits: cpu: "1" memory: "1Gi" readinessProbe: # 就绪探针:通过后才接入流量 httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 20 periodSeconds: 10 livenessProbe: # 存活探针:失败就重启容器 httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 60 periodSeconds: 20 ``` ```bash kubectl apply -f deployment.yaml kubectl get deploy,rs,pods # 三层一起看,关系一目了然 kubectl get deploy blog-system -o wide ``` ### 4.3 扩容、升级、回滚(日常三件事) ```bash # 扩容:改声明即可 kubectl scale deployment blog-system --replicas=5 # 升级镜像(会触发滚动更新) kubectl set image deployment/blog-system app=registry.example.com/blog-system:1.0.1 # 盯住滚动更新的过程 kubectl rollout status deployment/blog-system # 看历史版本 kubectl rollout history deployment/blog-system # 回滚到上一个版本 kubectl rollout undo deployment/blog-system # 回滚到指定版本 kubectl rollout undo deployment/blog-system --to-revision=2 ``` > `maxUnavailable: 0` 是"零停机发布"的关键:它保证升级过程中可用的 Pod 数**永远不会低于**期望副本数。代价是需要多一份资源余量(`maxSurge`)。 ### 4.4 一定要设 resources ```text 不设 requests/limits 的后果: ├─ 不设 requests → Scheduler 不知道这个 Pod 要多少资源 → 可能把 10 个大服务塞到同一台机器 │ → 互相抢 CPU/内存,一起变慢 └─ 不设 limits → 某个服务内存泄漏 → 把节点吃干 → 拖垮节点上所有 Pod 设了 limits 的后果(要知道): └─ 容器超过 memory limits → 被 OOMKilled(kubectl describe 能看到 Reason: OOMKilled) ``` **经验值**:`requests` 按"稳定运行时的用量"设,`limits` 按"峰值 + 余量"设;内存的 `limits` 建议别超过 `requests` 太多,否则 OOM 判断会很突兀。 ### 4.5 探针为什么要两个 ```text readinessProbe(就绪探针) └─ 决定"这个 Pod 能不能接流量" ├─ 应用启动慢(Spring Boot 要 20 秒)→ 不设就绪探针 → 流量打进来全是 503 └─ 应用临时卡住(线程池满)→ 摘掉流量,等它缓过来,不重启 livenessProbe(存活探针) └─ 决定"这个容器该不该重启" └─ 死锁、内存泄漏导致彻底没响应 → 重启容器是唯一的出路 startupProbe(启动探针,可选) └─ 给"启动特别慢"的应用用:启动期间不跑 liveness,避免被误杀 ``` **设错探针是最常见的自找麻烦**:把 liveness 的路径设成一个会查数据库的接口,数据库一抖动,你的所有 Pod 就开始集体重启,反而放大故障。 --- ## 5. 本篇小结 ```text 你写 Deployment(声明期望) │ v Deployment 管 ReplicaSet,ReplicaSet 管 Pod 数量 │ v Pod 挂掉 → ReplicaSet 补一个 → 控制器循环持续拉平差异 │ v 升级镜像 → 新 ReplicaSet 逐步接管 → 旧的留着当回滚点 ``` 到这里,服务已经"活着"了,但**还访问不到** —— Pod IP 是一次性的,你没法拿它对外提供服务。 下一篇解决这件事:Service 与 Ingress。
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入