⎈ K8s 进阶实战 · 第二册

第一册带你「看懂 K8s 怎么转」,这一册解决另一半问题:把服务配得稳、跟团队协作、用对工具链—— 这些是入职一个云原生组之后,你每天真正会碰的活。

🩺 探针 / 资源 / 优雅上下线 🔐 多租户 / RBAC / 网络隔离 🛠 Helm / Kustomize / GitOps 🤖 手写第一个 Operator
🧭
第一册的主线是「声明式 + 控制器调谐」一个思想贯穿始终。第二册你会发现:稳定性配置、HPA、GitOps、甚至你自己写的 Operator——全都在复用同一个思想。看到它反复出现,就说明你真的懂了。
第一部分
稳定篇 · 把服务配稳 —— 上线高频,配不好半夜被叫起来的就是你
第 1 章

健康探针:别让三种探针互相误杀

没有探针,K8s 只知道「进程在不在」,不知道「它能不能干活」。进程活着但连不上数据库 → 还在接流量 → 用户看到 500。探针就是让 K8s 从「看进程」升级到「看健康」。

是什么

三种探针各司其职:livenessProbe(存活)失败 → 重启容器;readinessProbe(就绪)失败 → 把 Pod 从 Service 的 Endpoints 摘除、不再给它流量(但不重启);startupProbe(启动)在它通过之前,暂时禁用另外两个探针,专门保护慢启动的应用。

为什么

「进程活着」≠「能对外服务」。Java 应用启动要 60 秒、依赖的数据库偶尔抖动、代码偶发死锁——这些状态都需要 K8s 区别对待:该等的等、该摘流量的摘、该重启的重启。

什么场景

几乎每个对外服务都该配 readiness(滚动更新不丢请求的关键);启动慢的配 startup;能通过重启自愈的死锁场景配 liveness

举个例子

三种探测方式:httpGet(调个健康接口)、tcpSocket(端口能连上就行)、exec(在容器里跑命令,退出码 0 算通过)。👇

关键区分

liveness 失败 = 重启(治「卡死」);readiness 失败 = 摘流量(治「暂时不可用」)。最致命的错误:把 liveness 指向数据库连接——数据库一抖,所有 Pod 同时被判死、集体重启 → 雪崩。liveness 只该探「我自己这个进程健不健康」。

YAML · Pod spec 里的三种探针
containers:
  - name: order
    image: shop/order:v1.2
    # ① 启动探针:给慢启动一个保护期,通过前另两个探针不生效
    startupProbe:
      httpGet: { path: /healthz, port: 8080 }
      failureThreshold: 30      # 最多等 30 次 × 5s = 150 秒启动
      periodSeconds: 5
    # ② 就绪探针:没就绪就不接流量(从 Service Endpoints 摘除)
    readinessProbe:
      httpGet: { path: /readyz, port: 8080 }
      periodSeconds: 5
      failureThreshold: 3       # 连续 3 次失败才摘除
    # ③ 存活探针:卡死了重启我。注意——只探自己,别探数据库!
    livenessProbe:
      httpGet: { path: /healthz, port: 8080 }
      periodSeconds: 10
      failureThreshold: 3
关键参数含义踩坑点
initialDelaySeconds容器启动后等多久开始第一次探测配了 startupProbe 就不用它兜启动时间了
periodSeconds每隔多久探一次太密集给应用加压,太稀疏故障发现慢
timeoutSeconds单次探测超时默认 1s,接口偶尔慢就误判,酌情调大
failureThreshold连续几次失败才算「坏了」设成 1 太敏感,一次网络抖动就重启/摘流量
successThreshold连续几次成功才算「好了」(readiness)恢复要稳,避免反复横跳
⚠️ 面试与事故高频:liveness 探针雪崩。/health 写成「顺便 ping 一下数据库」,数据库一慢,全体 Pod 的 liveness 连续失败 → 全部重启 → 重启后又连不上 → 死循环。规矩:liveness 只查进程自身,依赖健康交给 readiness。
📝

第 1 章自测

三种探针别用混
Q1.一个 Java 服务启动要 60 秒,运维给 liveness 配了 initialDelaySeconds: 10,结果 Pod 反复重启起不来。最优雅的修法是?
把 liveness 删掉,反正也没用
加一个 startupProbe 覆盖启动期,通过前 liveness 不生效;启动慢的问题交给它
把 periodSeconds 调到 120 秒
解析:startupProbe 就是为「慢启动」设计的:它没通过之前,liveness/readiness 都被挂起,应用有充足时间起来;一旦通过,liveness 才接管。比一味调大 initialDelaySeconds 更准,也不影响启动后的故障检测灵敏度。
Q2.下面哪种做法会导致「数据库一抖动,全站 Pod 集体重启」?
在 livenessProbe 的健康接口里检查数据库连接
在 readinessProbe 的接口里检查数据库连接
给数据库配了 StatefulSet
解析:liveness 失败会重启容器。把外部依赖塞进 liveness,依赖一挂就等于宣判所有 Pod 死刑 → 集体重启 → 雪崩。依赖健康应该放 readiness(失败只摘流量、不重启),等依赖恢复后自动重新接客。
第 2 章

资源与 QoS:requests 决定调度,limits 决定生死

requests / limits 这两个字段,决定了你的 Pod 能不能被调度上、跑起来稳不稳、集群缺资源时第一个被干掉的是不是它。不懂它,就等着莫名其妙的 OOMKilled 和 Pending。

是什么

requests(请求量)是调度依据——Scheduler 按它找「装得下」的节点,是给你预留的保证量;limits(上限)是硬约束——超了要挨罚。两者共同决定 Pod 的 QoS 等级

为什么

集群资源有限、多个应用挤一台机器。没有 requests,调度器不知道该把 Pod 放哪;没有 limits,一个内存泄漏的应用能把整台节点吃垮,拖死邻居。

举个例子

CPU 用 500m(半核)、内存用 512Mi 这样表达。👇

关键区分

CPU 超 limit = 被限流(throttle,减速但不杀)内存超 limit = OOMKilled(内核直接杀,exit code 137)。这是两种完全不同的惩罚——CPU 是可压缩资源,内存是不可压缩资源。

YAML · resources
resources:
  requests:            # 调度按这个预留,也是被驱逐时的"护身符"
    cpu: "250m"
    memory: "256Mi"
  limits:              # 上限:CPU 超了限流,内存超了 OOMKill
    cpu: "500m"
    memory: "512Mi"

2.1 QoS 三个等级 —— 决定谁先被驱逐

当节点内存告急,kubelet 会按 QoS 等级从低到高驱逐 Pod:

QoS 等级怎么才算节点压力时
Guaranteed每个容器 requests == limits,且 CPU 和内存都设了最后才动它 —— 最稳
Burstable设了 requests,但不满足 Guaranteed(如只设了 requests、或 limits > requests)中间,超出 requests 的部分优先被回收
BestEffort啥都没设第一个被杀 —— 最脆
JVM / Go 的容器内存坑:老版本 JVM 不感知容器 limits,会按宿主机物理内存算堆大小 → 堆开太大 → 一跑就 OOMKilled。修法:加 -XX:MaxRAMPercentage=75.0 让 JVM 读容器的 limits。Node/Python 同理要注意别让运行时误判可用内存。
生产经验值:requests 按日常 P50 用量设(保证调度合理),limits 内存留够余量(防 OOM),CPU 的 limit 很多团队索性不设或设得很宽松——因为 CPU 限流会让延迟毛刺,而 CPU 超用只是「借用」邻居的空闲,危害远小于内存。
📝

第 2 章自测

资源与驱逐
Q1.容器 Last State 显示 OOMKilled、Exit Code 137,说明?
节点整体内存不足,是节点的问题
容器内存用量触到了自己的 memory limit,被内核 OOM Killer 处决
CPU 超过了 limit 被限流致死
解析:OOMKilled 是「碰到自己的 limits」被杀,不一定是节点没内存。CPU 超限只会被限流(throttle),不会杀进程。修法:调大 limits 或排查内存泄漏;JVM 应用重点核对堆参数与 limits 是否匹配。
Q2.节点内存告急,kubelet 要驱逐 Pod,最先被干掉的是?
Guaranteed(requests==limits 都设了)的 Pod
BestEffort(requests/limits 都没设)的 Pod
副本数最多的那个 Deployment
解析:驱逐顺序 BestEffort → Burstable → Guaranteed。什么都不设 = 你在告诉 K8s「我不重要,缺资源先杀我」。给关键服务设好 requests/limits(尤其做到 Guaranteed),是稳定性的基本功。
第 3 章

优雅上下线:滚动更新为什么会丢请求

「明明配了滚动更新,为什么每次发版都有几个 5xx?」十有八九是没处理好 Pod 的。这一章讲清 initContainer(起之前)和优雅停机(停之时)。

是什么

initContainer(初始化容器)在主容器启动之前按顺序执行,全部成功了主容器才开始跑;优雅停机是 Pod 被删除时的一套收尾流程,让它「处理完手头的活再走」。

为什么

起之前常要做准备:等依赖就绪、拉配置、跑数据库迁移。停之时如果一刀切断,正在处理的请求就会失败——这就是发版丢请求的根因。

关键区分

initContainer 管「起之前」(顺序、一次性、成功即退出);preStop + SIGTERM 管「停之时」。别把「等依赖」的逻辑塞进主容器的启动脚本,用 initContainer 更清晰。

3.1 Pod 终止的完整流程(丢请求就出在这)

1收到删除信号,Pod 进入 Terminating
下面两件事几乎同时发生,注意它们是异步的、有传播延迟。
2a从 Service 的 Endpoints 摘除
API Server 通知各节点更新 kube-proxy 规则——但这需要时间传播。在传播完成前,仍有新流量被转发过来。这就是为什么需要下面的 preStop sleep。
2b执行 preStop hook,然后发 SIGTERM
先跑 preStop(常放一个 sleep 5,等 Endpoints 传播完),再给主进程发 SIGTERM。应用应监听 SIGTERM:停止接新请求、把存量请求处理完、关闭连接。
3等待 terminationGracePeriodSeconds(默认 30s)
给应用的收尾时间窗。这个倒计时包含 preStop 的执行时间。
4还没退?发 SIGKILL 强杀
宽限期到了进程还赖着,内核直接 SIGKILL。所以优雅停机的逻辑要能在宽限期内跑完。
YAML · initContainer + 优雅停机
spec:
  terminationGracePeriodSeconds: 30
  initContainers:
    - name: wait-db                    # 主容器起之前,先等数据库通
      image: busybox:1.36
      command: ['sh','-c','until nc -z mysql 3306; do sleep 2; done']
  containers:
    - name: order
      image: shop/order:v1.2
      lifecycle:
        preStop:
          exec:
            # 先睡几秒,等 Endpoints 摘除传播到所有节点,再让应用退出
            command: ['sh','-c','sleep 5']
发版丢请求的 Top1 原因:应用没监听 SIGTERM(收到就立刻硬退出),或者没加 preStop sleep。结果是「流量还在打过来,进程已经没了」。两个动作缺一不可:应用侧优雅处理 SIGTERM + preStop 兜住 Endpoints 传播延迟。
📝

第 3 章自测

起与停
Q1.为什么很多团队在 preStop 里放一个 sleep 5
Pod 从 Endpoints 摘除有传播延迟,先睡几秒等各节点 kube-proxy 规则更新完,避免流量还在打进来就退出
让 Pod 优雅地休息一下,减少 CPU 峰值
等 initContainer 执行完毕
解析:「摘除 Endpoints」和「发 SIGTERM」几乎同时发生,但前者要传播到集群每个节点的转发规则、有延迟。这段延迟里若进程已退出,在途流量就 5xx。preStop sleep 就是拿一小段时间兜住这个窗口。
Q2.关于 initContainer,正确的是?
和主容器同时启动,谁先起来看运气
在主容器之前按顺序执行,全部成功退出后主容器才启动,适合「等依赖 / 初始化」
主容器崩溃后自动重启 initContainer
解析:initContainer 是「起之前」的一次性准备工作,顺序执行、成功即退出。等数据库就绪、跑迁移、下载配置都适合放这里,比塞进主容器启动脚本更干净、职责更清晰。
第 4 章

弹性伸缩 HPA:副本数交给指标自己定

第一册里副本数 N 是你手写死的。真实流量有波峰波谷——大促要自动扩、深夜要自动缩省钱。HPA 就是那个「根据指标自动加减 Pod」的控制器(又一次调谐循环!)。

是什么

HPA(HorizontalPodAutoscaler)根据指标(CPU/内存利用率,或自定义的 QPS 等)自动增减 Deployment/StatefulSet 的副本数。

为什么

手动定 N 应付不了波动:定高了平时浪费钱,定低了高峰扛不住。HPA 让副本数跟着负载自动呼吸。

什么场景

流量有明显波动的无状态服务(Web/API)。有状态服务扩缩要谨慎(涉及数据再平衡),一般不直接 HPA。

关键区分

HPA = 加 Pod(水平,最常用)VPA = 调单个 Pod 的 requests/limits(垂直,与 HPA 基于 CPU 时会打架,少用)Cluster Autoscaler / Karpenter = 加节点(当 Pod 因资源不足 Pending 时)。三者是不同层次,常配合:HPA 加 Pod → 节点不够 → Cluster Autoscaler 加节点。

YAML · 一个按 CPU 扩缩的 HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: order }
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60   # 目标:平均 CPU 用到 requests 的 60%
⚠️ HPA 不工作的头号原因:Pod 没设 CPU requests。HPA 算的是「实际用量 ÷ requests」的利用率,没有 requests 就算不出百分比,HPA 直接摆烂。所以 HPA 的前提是被扩对象配好了 requests(回到第 2 章)。另外还需要集群装了 metrics-server(提供 CPU/内存指标);要按 QPS 等自定义指标扩缩,则要 Prometheus Adapter。
冷启动追不上突发流量:如果单个 Pod 启动要 1 分钟,HPA 发现高负载→拉起新 Pod→就绪,这中间的空窗流量还是打在老 Pod 上。应对:把 startupProbe/预热做好、给足 minReplicas 余量、或用预测式扩缩。
📝

第 4 章自测

自动扩缩
Q1.配好了 HPA(目标 CPU 60%),但它始终不扩容,最可能的原因?
Pod 没设 CPU requests,HPA 算不出利用率百分比
maxReplicas 设得太大了
Deployment 用了滚动更新策略
解析:利用率 = 实际用量 / requests。没有 requests 这个分母,HPA 无从计算,形同虚设。此外还要确认 metrics-server 已安装、能 kubectl top pod 看到数据。
Q2.HPA 想扩到 15 个副本,但新 Pod 一直 Pending,describe 显示「Insufficient cpu」。谁来救场?
VPA,自动调小每个 Pod 的 requests
Cluster Autoscaler / Karpenter,检测到 Pending Pod 后自动给集群加节点
再配一个 HPA
解析:HPA 只负责「加 Pod」,Pod 没地方放是「节点不够」的问题,归 Cluster Autoscaler / Karpenter 管(加节点)。这三层配合才是完整的弹性:HPA(Pod)↔ CA(Node)↔(VPA 调单 Pod 资源,另一维度)。
第 5 章

调度控制:让 Pod 落在对的地方

默认调度器会自己找节点,但很多时候你需要干预:GPU 任务只能上 GPU 机器、同一应用的副本要打散到不同可用区抗故障、升级节点时别一次干掉太多副本。这一章是这些手段的地图。

手段谁挑谁典型用途
nodeSelectorPod 挑节点(最简单)按标签选:只上 disktype=ssd 的节点
nodeAffinityPod 挑节点(能力更强)硬要求(required)/软偏好(preferred),如「优先放 zone-a」
pod(Anti)AffinityPod 挑 Pod同应用副本互斥打散 / 让缓存挨着业务放
taint + toleration节点挑 Pod节点打「污点」排斥,只有带「容忍」的 Pod 能上(GPU 专用机、master 节点)
topologySpreadConstraints按拓扑均匀铺副本在各 zone/node 均匀分布,抗单点/单区故障
PodDisruptionBudget限制"同时挂几个"节点排空/升级时,保证最少可用副本数,别一次全干掉
🎯
一句话记住方向:nodeSelector / affinity 是「Pod 主动挑节点」;taint / toleration 是「节点主动排斥 Pod」。两者常配合——比如给 GPU 节点打 taint(把普通 Pod 挡在外面),再给 AI 任务同时配 toleration + nodeAffinity(让它专门去 GPU 节点)。
YAML · 副本打散 + 容忍 GPU 污点
spec:
  # 同一应用的副本尽量分到不同节点(软反亲和)
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector: { matchLabels: { app: order } }
            topologyKey: kubernetes.io/hostname
  # 按可用区均匀铺,最多差 1 个
  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: DoNotSchedule
      labelSelector: { matchLabels: { app: order } }
  # 容忍 GPU 节点的污点(否则上不去)
  tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
PDB 是升级救命符:运维排空节点(kubectl drain)做升级时,如果不配 PodDisruptionBudget,可能一下驱逐掉你 3 个副本里的 2 个,短暂只剩 1 个扛全部流量。配上 minAvailable: 2,K8s 会保证驱逐过程中始终有至少 2 个可用。
📝

第 5 章自测

放哪、别放哪、别一起挂
Q1.你想让 GPU 机器「默认谁都别上,只有 AI 任务能上」,该怎么组合?
给 GPU 节点打 taint(排斥所有 Pod)+ 给 AI 任务配对应的 toleration(+ nodeAffinity 指向 GPU 节点)
只给 AI 任务配 nodeSelector: gpu=true 就够了
给 GPU 节点配 HPA
解析:只用 nodeSelector 能让 AI 任务去 GPU 机器,但挡不住普通 Pod 也被调度过去。要「排斥」必须靠 taint——节点打了污点,没有对应 toleration 的 Pod 一律上不去。这就是「节点挑 Pod」和「Pod 挑节点」要配合使用的经典场景。
Q2.运维要 drain 一个节点做升级,你担心自己的服务被一次性干掉太多副本。配什么?
HPA,让它自动补副本
PodDisruptionBudget,声明 minAvailable,限制主动驱逐时最多同时挂几个
给 Pod 加 liveness 探针
解析:PDB 专门约束「主动干扰」(drain、节点升级等自愿驱逐)时的可用性下限。它不管节点宕机这种「非自愿」故障——那是 ReplicaSet 补副本的活。两者互补。
第二部分
隔离篇 · 多租户与安全 —— 多个团队共享一个集群时,怎么互不干扰、互不越权
第 6 章

Namespace 与配额:给团队划地盘

一个集群往往被多个团队、多个环境共用。Namespace 是逻辑分区,ResourceQuota 和 LimitRange 是防止某个团队「吃独食」把资源占光的护栏。

是什么

Namespace把集群逻辑切分成多个空间(如 dev / staging / prod,或按团队);ResourceQuota限制一个 ns 的资源用量;LimitRange给 ns 内单个 Pod/容器设默认值和上下限。

为什么

共享集群里,一个失控的应用能把 CPU/内存吃光、拖垮所有邻居。配额就是给每个团队圈一块地,越界就被拒绝。

关键区分

ResourceQuota = ns 的总量天花板(这个 ns 所有 Pod 加起来最多用 100 核);LimitRange = 单个对象的默认/边界(没写 resources 的容器自动补默认、单容器最多 4 核)。两者配合用。

Namespace 不隔离什么:它是逻辑隔离,不是安全边界。默认情况下 ns 之间网络是互通的(要隔离得靠第 8 章 NetworkPolicy),也不隔离节点(不同 ns 的 Pod 可能跑在同一台机器)。别把 Namespace 当防火墙。
YAML · 给 team-a 划定总量 + 默认值
apiVersion: v1
kind: ResourceQuota
metadata: { name: team-a-quota, namespace: team-a }
spec:
  hard:
    requests.cpu: "20"        # 这个 ns 所有 Pod 的 requests 总和上限
    requests.memory: 40Gi
    limits.cpu: "40"
    pods: "50"
---
apiVersion: v1
kind: LimitRange
metadata: { name: team-a-defaults, namespace: team-a }
spec:
  limits:
    - type: Container
      default: { cpu: "500m", memory: 512Mi }        # 没写 limits 时的默认
      defaultRequest: { cpu: "100m", memory: 128Mi }  # 没写 requests 时的默认
经典坑:一旦给 ns 配了限制 requests/limits 的 ResourceQuota,那些没写 resources 的 Pod 会被直接拒绝创建(因为 Quota 无法核算)。解法就是同时配 LimitRange 给默认值,让老 YAML 也能被自动补齐、正常创建。这两个通常成对出现。
📝

第 6 章自测

地盘与护栏
Q1.给团队 ns 配了 ResourceQuota 后,同事说他没改 YAML,Pod 却创建失败提示需要 requests。为什么?该怎么救?
配了 Quota 后,没写 resources 的 Pod 无法核算配额会被拒;配一个 LimitRange 给默认 requests/limits 即可
Quota 配错了,删掉就好
是 namespace 网络不通导致的
解析:ResourceQuota 要求进入该 ns 的对象都能被计量。没写 resources 就没法计量 → 拒绝。LimitRange 负责给这些对象自动补默认值,两者是标配搭档:Quota 定总量,LimitRange 兜单体默认。
Q2.关于 Namespace,下列说法正确的是?
不同 namespace 的 Pod 之间网络默认是隔离、不通的
Namespace 是逻辑分区,默认不隔离网络也不隔离节点,要网络隔离得用 NetworkPolicy
每个 namespace 独占一批物理节点
解析:Namespace 只是逻辑边界(用于分组、配额、权限范围),不是安全/网络边界。跨 ns 默认可以互相访问,不同 ns 的 Pod 也可能同居一台节点。真正的网络隔离靠 NetworkPolicy(下一章)。
第 7 章

RBAC 与 ServiceAccount:谁能干什么

「你能不能删这个 Deployment」「这个 Pod 能不能读 Secret」——这些都由 RBAC 说了算。它是集群安全的第一道闸,也是很多入侵事故的突破口。

是什么

请求到 API Server 要过两关:认证(你是谁)授权(你能干啥)。RBAC 就是最常用的授权机制,用「角色 + 绑定」描述权限。

为什么

集群是共享的,不能人人都是管理员。最小权限原则:每个人/每个程序只拿它干活必需的权限。

举个例子

Role/ClusterRole 定义「能对哪些资源做哪些动作」,RoleBinding/ClusterRoleBinding 把角色绑给具体的 user / group / ServiceAccount。👇

关键区分

Role = namespace 内ClusterRole = 集群级(或跨 ns 复用)。User(人)由集群外部管理、K8s 里没有对应对象;ServiceAccount(SA)是 K8s 对象,专给 Pod / 程序访问 API 用。人用 kubeconfig,程序用 SA。

YAML · 让一个 Pod 只能「读」本 ns 的 Pod
apiVersion: v1
kind: ServiceAccount
metadata: { name: reader-sa, namespace: team-a }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role                    # 只在 team-a 这个 ns 内有效
metadata: { name: pod-reader, namespace: team-a }
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]   # 只读,不给 delete/create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding             # 把 pod-reader 角色绑给 reader-sa
metadata: { name: bind-reader, namespace: team-a }
subjects:
  - kind: ServiceAccount
    name: reader-sa
roleRef: { kind: Role, name: pod-reader, apiGroup: rbac.authorization.k8s.io }
YAML/BASH · Pod 引用 SA + 自查权限
# Pod spec 里指定用哪个 SA(不指定就是 ns 的 default SA)
spec:
  serviceAccountName: reader-sa
  automountServiceAccountToken: false   # 不需要访问 API 的 Pod,干脆别挂 token
---
# 排查权限神器:我(或某个 SA)到底能不能做某件事?
# kubectl auth can-i delete pods --as=system:serviceaccount:team-a:reader-sa
⚠️ 安全事故高发区:很多入侵链是「容器被攻破 → 读取 Pod 挂载的 SA token → 拿它打 API Server」。如果这个 SA 权限过大(甚至 cluster-admin),攻击者就接管了集群。铁律:给 SA 最小权限、绝不给 default SA 大权限、不访问 API 的 Pod 关掉 token 自动挂载
📝

第 7 章自测

身份与权限
Q1.User 和 ServiceAccount 的核心区别是?
User 是给「人」用的、由集群外部管理、K8s 里没有对象;ServiceAccount 是 K8s 对象、给 Pod/程序用
User 权限一定比 ServiceAccount 大
两者完全一样,只是叫法不同
解析:K8s 没有「User」这种资源对象——人的身份来自证书/OIDC 等外部体系,通过 kubeconfig 使用。ServiceAccount 才是集群内的对象,专给工作负载(Pod)一个身份去访问 API Server。搞清这点才能正确设计权限。
Q2.一个只对外提供 HTTP、根本不需要访问 K8s API 的业务 Pod,安全上最好怎么做?
给它绑一个 cluster-admin,省得以后要权限
设 automountServiceAccountToken: false,不挂载 token,减少被攻破后的横向移动面
给它单独建一个 namespace
解析:默认每个 Pod 会挂载 default SA 的 token。如果 Pod 压根不调 API,把 token 关掉能显著缩小攻击面——容器被攻破也拿不到可用凭据去打 API Server。最小权限从「根本不给不必要的凭据」开始。
第 8 章

NetworkPolicy:给 Pod 之间装防火墙

K8s 默认网络是「全通」的——任意 Pod 能访问任意 Pod,哪怕跨 namespace。生产合规常要求「数据库只允许后端访问」,这就得靠 NetworkPolicy 做 Pod 级的网络分段。

是什么

NetworkPolicy用标签选择器定义某组 Pod 的入站(ingress)/出站(egress)白名单:谁能连我、我能连谁。

为什么

默认全通意味着一个被攻破的前端 Pod 能直连数据库。零信任要求「非必要不放行」,把爆炸半径压到最小。

关键区分

一个 Pod 只要被任意一条 NetworkPolicy 选中,它就从「默认全通」翻转为「默认全禁,只放行规则里写的」。没被任何 policy 选中的 Pod 仍然全通。这个「翻转」语义是最容易搞错的地方。

前提先确认:NetworkPolicy 要 CNI 支持才生效。Calico、Cilium 支持;而有些环境的默认网络插件不支持——这时你 apply 了 policy 也静默不生效(不报错,但根本没拦住)。入职干活第一件事就是问清楚:我们集群的 CNI 是什么、支不支持 NetworkPolicy?
YAML · 后端只允许前端访问(先默认拒绝,再放行)
# ① 本 ns 默认拒绝所有入站(一旦有这条,未被放行的入站全断)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny-ingress, namespace: shop }
spec:
  podSelector: {}          # 选中本 ns 所有 Pod
  policyTypes: ["Ingress"]
---
# ② 只放行:带 app=frontend 的 Pod → 访问 app=backend 的 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-frontend-to-backend, namespace: shop }
spec:
  podSelector: { matchLabels: { app: backend } }
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - podSelector: { matchLabels: { app: frontend } }
      ports:
        - { protocol: TCP, port: 8080 }
能力边界:NetworkPolicy 工作在 L3/L4(IP、端口、标签),管不了 L7——「只允许 GET /api、禁止 DELETE」这种按 HTTP 方法/路径的控制,得上 Service Mesh(Istio 等)。别指望 NetworkPolicy 干七层的活。
📝

第 8 章自测

网络分段
Q1.你给某个 Pod 加了一条只放行前端的 NetworkPolicy,结果它其它所有入站流量都断了。这是 bug 吗?
不是。Pod 一旦被任意 NetworkPolicy 选中,就从「默认全通」变成「默认全禁、只放行规则里写的」
是 bug,NetworkPolicy 应该只加不减
是 CNI 崩了
解析:这正是 NetworkPolicy 的核心语义:白名单模型。没被任何 policy 选中的 Pod 保持全通;一旦被选中,未显式放行的一律拒绝。所以设计时通常「default-deny 打底 + 逐条放行」,心里要清楚这个翻转。
Q2.同事 apply 了 NetworkPolicy 却发现完全没拦住流量,最该先查什么?
集群的 CNI 是否支持 NetworkPolicy(有些默认插件不支持,policy 会静默失效)
是不是忘了重启所有 Pod
是不是 namespace 写错了 —— 但这会报错
解析:NetworkPolicy 是「声明」,真正执行它的是 CNI 插件。若 CNI 不支持(部分 Flannel 配置等),策略被接受但无人执行——不报错、也不拦截。所以先确认 CNI 能力,这也是入职摸清「你司技术栈」的一部分。
第三部分
工具篇 · 干活刚需 —— 第一册完全没教,但入职第一天就会撞上
第 9 章

Helm:K8s 的包管理器

真实项目里没人手写十几个裸 YAML。多环境(dev/prod)常常只有几个值不同——用模板 + 一份 values 覆盖,一条命令搞定安装/升级/回滚。Helm 之于 K8s,就像 apt 之于 Linux、npm 之于 Node。

是什么

Helm 用「Chart」打包一个应用:templates/(带占位符的 YAML 模板)+ values.yaml(默认值)+ Chart.yaml(元信息)。安装一次得到一个 Release(有版本历史)。

为什么

同一套应用要部署到 dev/staging/prod,差异往往只是副本数、镜像 tag、域名。模板 + 分环境 values,避免复制粘贴维护 N 份 YAML。

关键区分

Chart = 模板包(静态制品)Release = 装到集群里的一个实例(运行态,可升级/回滚)。同一个 Chart 可以装出多个 Release。

BASH · Helm 日常命令
# 用 prod 的值安装或升级(--install:没装过就装,装过就升级)
helm upgrade --install order ./order-chart -f values-prod.yaml

# 只改个别值
helm upgrade order ./order-chart --set image.tag=v1.3 --set replicaCount=5

# 上线前先渲染成纯 YAML 看看它到底会生成什么(读别人 chart 的利器)
helm template order ./order-chart -f values-prod.yaml

# 看历史、一键回滚到上一个版本
helm history order
helm rollback order 3

helm list                 # 当前集群装了哪些 Release
helm uninstall order      # 卸载
干活真相:你多半不写 Chart,而是改 values.yaml 里那几个值再 helm upgrade。看不懂一个复杂 chart 会生成什么?用 helm template 把它渲染成最终 YAML,一目了然。上线前 helm template | kubectl diff -f - 预览变更,能救命。

📝

第 9 章自测

包与实例
Q1.Chart 和 Release 的关系是?
Chart 是模板包(像安装包),Release 是用它装到集群里的一个实例(可升级/回滚),一个 Chart 能装多个 Release
Chart 是运行中的实例,Release 是它的备份
两者是同一个东西
解析:类比:Chart ≈ 安装包/类,Release ≈ 装好的实例/对象。同一个 Chart 配不同 values 可以在不同 ns 装出多个 Release,各自有独立的版本历史,能独立 rollback。
Q2.接手一个别人写的复杂 chart,想搞清它到底会往集群里塞哪些资源,最快的办法?
直接 helm install 到生产看效果
helm template 本地把模板 + values 渲染成最终 YAML,直接读
挨个打开 templates/ 里的文件用肉眼脑补变量替换
解析:helm template 在本地就把模板和 values 渲染成实际会 apply 的纯 YAML,不连集群、无副作用。读渲染结果远比对着带 {{ }} 的模板脑补靠谱,也是上线前配合 kubectl diff 做变更预览的基础。
第 10 章

Kustomize:不用模板的「覆盖式」配置

和 Helm 走两条路:Helm 靠模板变量,Kustomize 靠「一份基础 YAML + 各环境打补丁覆盖」,全程保持纯 YAML、不引入模板语言。而且它内置在 kubectl 里(kubectl apply -k),零额外安装。

是什么

Kustomizebase/(一套基础 manifests)+ overlays/{dev,prod}/(各环境的差异补丁)组织配置,用覆盖/合并的方式生成最终 YAML。

为什么

不想学模板语法、想让 YAML 始终是能被工具校验的纯 YAML;只需表达「prod 相对 base 改了哪几处」。

关键区分

Helm:模板 + 变量 + 包管理 + 版本回滚,生态现成 chart 多;Kustomize:纯 YAML 覆盖、原生集成、无变量、无「Release/回滚」概念。两者可以组合(Helm 出包、Kustomize 做环境覆盖),很多团队 Argo CD 里两种都在用。

YAML · prod overlay 覆盖 base 的副本数和镜像
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base            # 引入基础 manifests
replicas:
  - name: order
    count: 5              # base 里是 2,prod 覆盖成 5
images:
  - name: shop/order
    newTag: v1.3          # 覆盖镜像 tag
patches:
  - path: prod-resources.yaml   # 用补丁改 resources 等更复杂的字段
BASH · 应用与预览
kubectl apply -k overlays/prod       # 直接应用 prod overlay
kubectl kustomize overlays/prod      # 只渲染出最终 YAML 看看(不 apply)
怎么选?需要打包分发、版本化、生态里有现成 chart(如装 Prometheus、Redis)→ Helm;只是「同一套应用在几个环境改几个值」、想保持纯 YAML → Kustomize。干活时你更多是「读懂 overlays 结构、改对某一层」,而不是从零搭。
📝

第 10 章自测

两条配置路线
Q1.Kustomize 相比 Helm 最本质的不同是?
它不用模板变量,而是「base + overlays 打补丁覆盖」,全程保持纯 YAML,且内置于 kubectl
它只能用于生产环境
它比 Helm 性能更高、部署更快
解析:Helm = 模板 + 变量 + 包/版本管理;Kustomize = 无模板、纯 YAML 覆盖、原生集成(kubectl apply -k)。没有谁绝对更好,取决于团队偏好,两者甚至能组合。理解这条分界,读懂项目里用的是哪套即可。
第 11 章

GitOps 工作流:改生产 = 提 PR,不是 kubectl apply

第一册第 9 章讲过 GitOps 的思想(Git 是唯一事实来源)。这一章落到「你每天怎么操作」——在成熟团队里,你改生产的方式很可能不是敲 kubectl,而是提一个 Pull Request。

是什么

GitOps:集群该长什么样,以 Git 仓库里的 YAML 为唯一事实来源。Argo CD / Flux 这类工具持续对比「Git 里的期望」和「集群的实际」,有差异就自动同步。

为什么

可审计(谁改的、改了啥,git log 全有)、可回滚(git revert)、防漂移(有人偷偷 kubectl 改了,会被自动纠回)、防人肉误操作(生产变更强制走 review)。

关键区分

CI(构建、测试、打镜像)负责「产出变化」,是 push 式;CD/GitOps(Argo CD 把期望状态同步进集群)负责「让集群变成期望的样子」,是 pull 式。发现没有——这又是控制器调谐:Git 扮演 etcd,Argo 扮演控制器

11.1 你的一次日常变更长这样

BASH · 一次 GitOps 变更的动作序列
git checkout -b bump-order-v1.3          # ① 开分支
# ② 改配置库里的 values / manifests(比如把 image tag 改成 v1.3)
git commit -am "order: v1.2 -> v1.3"
git push origin bump-order-v1.3          # ③ 推分支
# ④ 开 Pull Request,CI 自动校验(lint / diff / 策略检查)
# ⑤ 同事 review 通过 → 合并到 main
# ⑥ Argo CD 检测到 Git 变了 → 自动同步进集群 → 在 Argo UI 看健康状态
# 出问题?git revert 那个 commit,Argo 自动把集群同步回旧版本
🗂
两个仓库别搞混:通常有「代码库」(业务源码,CI 在这里构建测试、打出镜像)和「配置库」(部署用的 YAML/values,Argo 盯的是这个)。常见流水线:代码库 CI 打完镜像后,自动去配置库改一行 image tag,触发 Argo 部署。
GitOps 铁律:生产变更走 PR,别 kubectl apply / edit 生产。你手动改的,Argo 会检测到「集群 ≠ Git」并自动纠正回去(自愈)——你的改动白改,还可能引发困惑。要改就改 Git。这也是入职后最容易踩的规矩。
📝

第 11 章自测

协作方式
Q1.在 Argo CD 管理的 GitOps 集群里,你 kubectl edit 了生产的 Deployment 副本数,过一会儿发现被改回去了。为什么?
Argo CD 持续对比集群与 Git,发现「实际 ≠ 期望」就自动同步回 Git 声明的状态(自愈/防漂移)
是别的同事手动改回去的
kubectl edit 在生产被禁用了
解析:这正是 GitOps 的核心机制——Git 是唯一事实来源,工具持续调谐。手动改属于「漂移」,会被纠正。要真正改副本数,得去改 Git 仓库里的 YAML 并合并。这也是「声明式 + 调谐」思想在发布流程上的体现。
Q2.CI 和 CD(GitOps) 的分工是?
CI 负责构建/测试/打镜像(产出变化,push 式);CD/GitOps 负责把期望状态同步进集群(pull 式)
CI 部署到测试环境,CD 部署到生产,其它没区别
CI 和 CD 是同一个工具的两个名字
解析:CI 把「代码 → 可部署制品(镜像 + 更新后的配置)」这一段自动化;GitOps/CD 则以 Git 为准,把集群拉齐到期望状态。回滚 = git revert,审计 = git log,一致性 = 禁止绕过流程手改。
第 12 章

kubectl 排错手艺:日常 90% 都在这

干活时你敲得最多的不是写 YAML,而是排错。把这套命令练成肌肉记忆——看到症状条件反射出命令——比背原理更能决定你上手快不快。(第一册第 8 章的决策树是「怎么想」,这一章是「用什么命令」。)

12.1 排错命令速查表

命令看什么什么时候用
kubectl get pod -o widePod 状态、在哪个节点、IP第一眼总览,看 STATUS / READY / RESTARTS
kubectl describe pod XEvents、调度/拉镜像/探针/挂载详情Pod 起不来、Pending、CrashLoop 先看它
kubectl logs X -f应用自己的输出应用报错;--previous 看上次崩溃前的日志
kubectl exec -it X -- sh进容器里手动查验证配置、连通性、文件是否存在
kubectl get events --sort-by=.lastTimestamp全局事件时间线「刚才集群发生了什么」
kubectl port-forward X 8080:8080本地直连某个 Pod/Service绕过 Ingress 单独验证某个服务
kubectl top pod / node实时 CPU/内存用量查 OOM、资源打满(需 metrics-server)
kubectl rollout status/undo deploy X发布进度 / 一键回滚滚动更新卡住或发错版本

12.2 进阶:镜像没 shell 怎么进去查?

是什么

现代精简镜像(distroless)连 sh 都没有,kubectl exec 进不去。用 kubectl debug 往目标 Pod 注入一个临时容器(ephemeral container),带着工具进去、和目标容器共享进程/网络命名空间。

举个例子

👇 还能 debug 整个节点、复制文件出来。

BASH · debug 与常用姿势
# 往正在跑的 Pod 注入一个带工具的临时容器(不用改镜像)
kubectl debug -it order-xxx --image=busybox --target=order

# 把文件从容器里捞出来
kubectl cp shop/order-xxx:/app/logs/err.log ./err.log

# 用 kubectl 生成 YAML 模板(不真的创建),改改再 apply
kubectl create deploy demo --image=nginx --dry-run=client -o yaml > demo.yaml

# 查某个字段怎么写
kubectl explain deployment.spec.strategy
⚠️ 生产事故之王:敲错集群 / 敲错 namespace。kubectl 默认作用在当前 context 和 default ns。以为在测试环境、其实连着生产,一个 delete 下去就出事。养成习惯:操作前 kubectl config current-context 确认集群,命令里显式带 -n <namespace>,高危操作先 --dry-run
提效小配置:alias k=kubectl;装 kubectx / kubens 快速切集群和 ns;给高危集群的 context 起个刺眼的名字(如 PROD-DANGER)。这些小习惯长期省下大量时间、避掉大事故。
📝

第 12 章自测

手艺活
Q1.一个 Pod 在 CrashLoopBackOff 反复重启,此刻 kubectl logs 却看不到报错。最该加什么?
加 --previous,看「上一次崩溃前」的日志(当前容器是刚重启的,还没来得及报错)
加 -f 一直跟着看
直接删了 Pod 让它重建
解析:CrashLoop 时你看到的往往是「刚拉起、还没崩」的新容器,日志自然干净。kubectl logs X --previous 才是上一个已崩溃容器的临终遗言,配合 kubectl describe 的 Events / Last State 定位死因。
Q2.目标是 distroless 精简镜像,kubectl exec -it -- sh 报「executable not found」进不去,怎么办?
用 kubectl debug 注入一个带工具的临时容器(ephemeral container),共享目标容器的命名空间来排查
没办法,只能把镜像换成带 shell 的重新发一遍
重启这个 Pod
解析:精简镜像没有 shell 是故意为之(更小更安全)。kubectl debug 通过临时容器把调试工具「贴」进去,和目标共享进程/网络视图,不必改动或重发业务镜像——这是排查现代镜像的标准姿势。
第四部分
进阶篇 · 通往专家 —— 把「看懂调谐」变成「亲手实现调谐」,以及更远的路标
第 13 章

写你的第一个 Operator:从看懂调谐到实现调谐

这是「从会用到精通」最能体现功力的一跃:把第一册那句「声明式 + 控制器调谐」从看懂,变成亲手写一个。这一章不教你写完整代码,而是讲清写一个 controller 必须懂的机制和坑——不然写出来的东西会有一堆隐蔽 bug。

是什么

Operator = CRD(自定义资源)+ 自定义控制器。CRD 让 K8s API 认识一种新资源(如 kind: RedisCluster),控制器则 7×24 地把「实际状态」推向这个资源声明的「期望状态」——把资深运维的操作手册写成了代码。

为什么

StatefulSet 只给「稳定身份」,不懂业务:主从切换、备份恢复、扩容重分片这些领域知识它管不了。Operator 就是来装这些「懂业务的自动化」。

关键区分

90% 的需求用现成的就行(Prometheus Operator、Redis Operator…);只有当你有独特的领域运维逻辑、现成工具满足不了时,才自己写。动手前先问一句:能不能用 Helm / 现成 Operator 解决?

13.1 写 controller 必须懂的 6 个机制(都是坑)

1Informer / Lister:本地缓存 + watch,别猛拉 API Server
控制器不是每次都去 API Server 拉数据,而是通过 Informer watch 变更、在本地维护一份缓存(Lister 从缓存读)。写错成「每次 reconcile 都 List 全量」会瞬间压垮 API Server。
2WorkQueue:限速队列,去重 + 失败退避重试
事件先进一个限速工作队列:同一对象的多次变更会去重、处理失败会按指数退避重新入队。你的 reconcile 只管处理队列里取出的 key。
3Reconcile 必须幂等:从任意状态都能收敛
reconcile 可能因为重试、重启被重复调用任意多次。绝不能假设「上次执行到哪」。正确写法:读期望 → 看实际 → 缺就建、多就删、不对就改,每次都能从当前实际收敛到期望。
4OwnerReference:子资源认爹,级联删除
控制器创建的子资源(Pod、Service…)要标记 OwnerReference 指向你的 CR。这样删掉 CR 时,K8s 会级联删除它的子资源,不留垃圾。
5Finalizer:删除前先清理外部资源
如果你的 CR 在集群外还建了东西(云上的负载均衡、外部 DB…),直接删 CR 会漏清理。加 Finalizer:删除请求先卡住,等你的清理逻辑跑完、移除 finalizer,对象才真正消失。
6Status 子资源:spec 是期望,status 是实际
用户写 spec(我要 3 个副本),控制器把观察到的实际情况写回 status(当前就绪 2 个、conditions…)。spec/status 分离是 K8s API 的通用范式,也是别人 kubectl get 你的 CR 时看到的信息来源。
GO(伪代码)· 一个 reconcile 的骨架
func Reconcile(req) {
  cr := get(req)                 // 读期望状态(我的 CRD 对象)
  if cr.deleting {               // 处理删除:先跑 finalizer 清理外部资源
    cleanupExternal(); removeFinalizer(cr); return
  }
  want := cr.spec.replicas       // 期望
  have := countPods(cr)          // 实际(从 Lister 缓存读)
  if have < want { create(want - have) }   // 缺就补
  if have > want { delete(have - want) }    // 多就删
  cr.status.ready = have; update(cr.status) // 把实际写回 status
  // 返回后,任何新变更会再次触发 Reconcile —— 这就是调谐循环
}
工具链:别从零手撸,用 kubebuilderOperator SDK(都基于 controller-runtime)——它们帮你把 Informer、WorkQueue、Manager、Webhook 脚手架都生成好,你只要填 Reconcile 的业务逻辑。理解上面 6 个机制,就能看懂它生成的代码在干嘛。
📝

第 13 章自测

实现调谐
Q1.为什么 Reconcile 函数必须写成幂等的?
它会因重试、重启、事件重复而被调用任意多次,只能靠「读期望-看实际-收敛」,不能假设上次做到哪
因为 Go 语言要求函数幂等
为了让代码看起来更整洁
解析:调谐循环的本质是「无论当前实际状态如何,都朝期望状态收敛一步」。工作队列会重试、控制器会重启、同一对象会有多次事件——若你的逻辑依赖「上次执行到第几步」,重复调用就会建重复资源或状态错乱。幂等是 controller 正确性的地基。
Q2.你的 CR 在云上还创建了一个外部负载均衡。为保证删除 CR 时不遗留这个 LB,应该用?
OwnerReference,让 LB 认 CR 当爹
Finalizer:删除请求先被卡住,等你的清理逻辑删掉外部 LB、移除 finalizer 后对象才真正消失
在 status 里记一下就行
解析:OwnerReference 只能级联删除集群内的 K8s 子资源,管不了集群外的东西。Finalizer 专门解决「删除前的清理钩子」:它拦住删除,直到你确认外部资源已释放。二者常一起用——集群内靠 OwnerRef,集群外靠 Finalizer。
第 14 章

进阶轴一览:成熟专家的五条路

这一章只求「知道有这条轴、它解决什么问题」,为你入职后按方向深入留路标。你不需要全精通——不同角色(平台 / 业务 / SRE / 安全)往不同轴使劲。

🔐 安全深入

解决:光有 RBAC 和 NetworkPolicy 不够——镜像可信吗?谁能创建高危资源?密钥怎么管?

供应链cosign 签名SBOMTrivy 扫描OPA/GatekeeperKyvernoPod SecurityVault/External SecretsFalco

📊 可观测性深入

解决:几百个易逝的 Pod,出了问题去哪看、怎么提前告警、怎么定义「多稳算稳」?

PrometheusPromQLGrafanaAlertmanagerThanos/MimirLoki/ELKOpenTelemetryJaeger/TempoeBPF/HubbleSLO/错误预算

🕸 网络深入

解决:Pod 网络到底怎么通的?kube-proxy 在干嘛?七层治理谁来做?

CNICalico/Cilium(eBPF)kube-proxy iptables/IPVSCoreDNSGateway APIIstio/Linkerd

🛰 集群运维 / 规模化

解决:集群本身怎么搭、怎么升级不出事、怎么备份灾备、怎么控成本、多集群怎么管?

kubeadm集群升级/版本 skewetcd 备份VeleroCluster Autoscaler/KarpenterKubecostCluster API/Karmada

🏗 平台工程

解决:把上面这些能力包装成「业务团队自助使用」的内部平台,让开发者不必懂 K8s 细节也能上线。

Argo 全家桶CrossplaneKEDA(事件驱动扩缩)Knative(Serverless)Backstage/IDP

14.1 按你的角色指路

你进的组日常重心优先补哪几轴
平台 / 基础架构组搭&运维给别人用的集群、写 Operator、管 CI/CD 底座第 13 章 Operator + 网络深入 + 集群运维 + 平台工程
业务 / 应用组把自己的服务配好、部署上去、排自己的故障稳定篇(1-5)+ 工具篇(9-12)练熟即可
SRE / 稳定性组盯监控、容量、on-call、事故复盘可观测性深入 + 集群运维 + 稳定篇
安全组准入策略、供应链、权限治理隔离篇(6-8)+ 安全深入这一整轴

🎓 两册读完,你手里有什么

第一册给你一张原理地图(K8s 怎么转),第二册给你一套干活的手艺(怎么配稳、怎么协作、怎么用工具、怎么扩展)。而贯穿两册的,始终是同一个思想——声明式 + 调谐:从 Deployment 到 HPA,从 GitOps 到你自己写的 Operator,都是它。

入职前 30 天的两个动作:① 在 kind/minikube 上把稳定篇每个配置项都亲手配一遍、故意搞坏再修好;② 上班第一周画出两张图——「一个外部请求怎么进到你的 Pod」和「一次代码提交怎么变成生产上的新版本」。画得出来,你就真的上手了。