云原生进阶必读:Kubernetes核心原理从Pod到存储全梳理 一门高级课之所以“高级”通常不是因为老师默认你连 kubectl 都不会而是默认你已经把基础工具的“使用”内化成了“原理级理解”。最近在给要上云原生进阶课的同学做前置摸底发现很多人卡住的不是命令记不住而是下面这几个底层环节没串起来Pod 到底怎么被“管”起来的、探针失败到底影响了什么、Service 为什么有时候地址在但访问不通、PV 和 PVC 到底是谁在消费谁。这篇就是把这些要在高级课开课前彻底盘清楚的知识点按我自己的复习思路过一遍。上一篇重点在命令行侧的工作负载快速回顾这篇把对象模型、调度、网络、存储、可观测性这几条主线一次捋顺适合准备上高级课的人、刚考完认证想补原理的人也适合带团队做排查培训时当提纲用。1. 声明式API和控制器循环云原生一切对象的地基1.1 为什么说声明式是云原生的“操作系统接口”很多人用 Kubernetes 用得很熟Deployment 一写、apply 一敲、Pod 起来了就以为自己在“云原生”了。但真正进入高级课程之后你会发现几乎所有进阶内容——自定义控制器、Operator、弹性伸缩、故障自愈——都建立在一个统一的心智模型上你只声明期望状态系统负责把实际状态收敛到期望状态。这是命令式和声明式的根本差异。命令式是“你告诉我下一步做什么”声明式是“你告诉我最后要变成什么样”。举个例子以前用脚本部署应用脚本里写的是“先停旧服务、再拉新镜像、然后启动容器、最后检查健康”这是一串命令。而 Deployment YAML 里写的是“我要 3 个副本镜像版本是 v2容器端口是 8080”至于怎么从 1 个副本扩到 3 个、哪个节点跑哪个副本、中间要不要滚动这些都不用你操心——控制器会想办法。所以读任何 Kubernetes 对象核心就是五个字段apiVersion、kind、metadata、spec、status。前三个是“身份信息”spec是你声明的期望状态status是系统回写的实际状态。spec是你写的status是控制器写的——这个区分如果没建立起来后面学自定义资源CRD和 Operator 会非常痛苦。1.2 控制器循环那个永不睡觉的“空调师傅”理解 Kubernetes 的运行时机制最贴切的类比是空调。你设定 25 度这是期望状态屋里实际 27 度这是当前状态。空调不会因为你“按了一下按钮”才工作它是不停地检测温度、对比差值、决定要不要制冷——这就是控制器循环观察实际状态 - 与期望状态对比 - 执行动作使状态收敛。Deployment 控制器干的事一模一样。它观察集群里某个 Deployment 对应的 Pod 数量发现实际只有 2 个、期望 3 个就创建 1 个发现某个 Pod 挂了就重新创建一个。这中间有个特别重要的细节控制器是电平触发level-triggered而不是边缘触发edge-triggered的。它不关心“变化”这个事件本身只关心“当前状态”和“期望状态”的差值。这就意味着即使控制器中途重启、错过了一些事件它醒来后重新对比一次全量状态依然能把系统拉回正确轨道。这个特性非常重要也是很多人在设计自己的控制器时容易犯迷糊的地方——过度关注事件而不是状态差。再往下拆一层Deployment 本身并不直接创建 Pod。Deployment 创建 ReplicaSetReplicaSet 创建 Pod。它们之间的父子关系是通过metadata.ownerReferences建立的。这个字段保证了垃圾回收的方向删掉 Deployment它名下的 ReplicaSet 和 Pod 会级联清理。很多初学者直接去删 Pod发现删完又起来了就是因为 ReplicaSet 还在真正要“停掉”这个工作负载应该删 Deployment。高级课程里你会频繁地接触“自定义控制器”和“Operator”它们的本质就是“自定义对象 针对这个对象的控制器循环”。前置阶段把“对象-控制器”这对关系吃透后面看 Operator 的代码才不会一头雾水。建议手写一个极小的 Deployment YAML然后用kubectl get deployment nginx -o yaml观察status字段如何被控制器逐步填满比背十遍概念都管用。2. Pod生命周期与三类探针排障第一道门槛2.1 从 Pending 到 RunningPod 的完整旅程Pod 是 Kubernetes 里最小的调度和运行单元但这不代表它“简单”。一个 Pod 从提交到运行要经过Pending - ContainerCreating - Running中间任何一个环节出问题都会卡住。Pending 意味着调度器还没找到合适的节点或者镜像在拉取ContainerCreating 意味着容器已经在节点上创建了但可能卡在拉镜像、挂载卷、初始化容器等环节。很多人在排查时第一反应是看应用日志这是错的。Pod 起不来的时候第一眼看的是事件Events不是日志。事件会明确告诉你调度失败的原因是资源不足还是节点亲和性不满足镜像拉取失败是 404 还是认证失败容器创建失败是磁盘压力还是权限问题。日志只有在容器真正跑起来之后才有意义。另外一个高频误区是 Pod 的restartPolicy。默认值是Always所以一个容器如果启动后立刻崩溃退出kubelet 会不断按退避策略重启它表现出来就是CrashLoopBackOff。很多人以为是 YAML 写错了其实这是正常的保护机制kubelet 检测到崩溃指数级退避重启10 秒、20 秒、40 秒……。要看得更细用kubectl logs pod --previous看上一次容器的退出日志往往会发现真正的原因。2.2 startupProbe / readinessProbe / livenessProbe 的职责边界三种探针是高级课和面试里必定会聊透的点。它们的区别用一句话概括探针类型回答的问题失败后果典型场景startupProbe容器是否已经启动完成容器被重启启动慢的应用Java、大数据组件readinessProbe容器是否能够接收流量从 Service 端点中摘除不重启依赖外部服务、启动后需要预热livenessProbe容器是否还活着容器被重启死锁、内存泄漏、长时间无响应最容易翻车的细节是initialDelaySeconds的设置。很多人给 Java 应用配 livenessProbeinitialDelaySeconds设成 10 秒结果应用启动要 40 秒探针在 10 秒后就开始打20 秒内连续失败容器被反复重启。正确做法是给慢启动应用配一个宽松的 startupProbe比如 60 秒、甚至 90 秒让 livenessProbe 等它启动完成后再开始工作——因为 livenessProbe 只要失败了几次就会杀掉容器而 startupProbe 只负责“告诉别人我已经起来了”。探针的类型也有讲究。exec是在容器内执行命令看退出码httpGet是请求指定 URL看 HTTP 状态码是否在 200-399tcpSocket是尝试建立 TCP 连接。我的建议是能上 HTTP 就上 HTTP因为健康检查应该是“业务层面”的不是“进程层面”的。TCP 端口通只能说明进程监听了不能说明业务可用。比如一个数据库连接池已满的应用端口照样能连上但 HTTP 健康检查已经返回 503 了——这两者对“是否该摘除流量”的判断完全不同。还有一个注意点readinessProbe 失败和 livenessProbe 失败的表现完全不同。readiness 失败只是把这个 Pod 从 Service 的 Endpoints 列表里摘掉别的 Pod 不受影响它自己也不会重启liveness 失败会直接 kill 容器并重启。所以排查时如果发现 Pod 一直 Running 但访问不到它先查 readinessProbe 是不是挂了而不是去看重启次数。配置探针时的常用参数也一并记住periodSeconds是多久探一次默认 10 秒timeoutSeconds是单次探针的超时默认 1 秒很多慢接口很容易超时需要调大failureThreshold是连续失败多少次才判失败默认 3 次。这几个参数要配合应用的响应时间一起设计不是抄默认值就完事。3. 调度器视角NodeSelector、亲和性与污点容忍3.1 默认调度流程过滤与打分当一个新的 Pod 创建出来调度器负责决定它落在哪个节点。这个决策过程分两步先过滤Filter再打分Score。过滤是把完全不满足条件的节点剔掉比如资源不足、端口冲突、磁盘压力打分是在剩余节点里排个序选出最合适的一个。过滤这一步最常被触发的就是资源请求requests和资源限制limits。调度器看的是requests不是limits。意思是说你声明这个 Pod 最低需要 500m CPU、512Mi 内存调度器就按这个值去找有足够空余的节点。limits是运行时限制不影响调度决策。很多人只写 limits 不写 requests调度器会认为这个 Pod 几乎不占资源于是疯狂把它调度到一小撮节点上结果那些节点运行时就全都超限——这是非常典型的线上事故源头。打分阶段涉及的因素就多了资源余量、节点亲和性、镜像是否已存在本地、Pod 之间是否分散。不过前置复习不用追到那么深记住“过滤保证可行性打分保证最优性”就够了。3.2 节点亲和性与 Pod 间亲和/反亲和把 Pod 放到该放的地方如果只是为了把 Pod 固定到某些节点nodeSelector是最简单的手段指定节点的标签Pod 只调度到带这些标签的节点上。它的问题是表达能力太弱——只能做“等于”匹配不能做“在这些节点中优先选哪些”。nodeAffinity就灵活得多支持In、NotIn、Exists、DoesNotExist等操作符还分硬性要求requiredDuringSchedulingIgnoredDuringExecution和软性偏好preferredDuringSchedulingIgnoredDuringExecution。前者是过滤条件不满足就不调度后者是打分条件能满足最好满足不了也接受。我把这两者简称为“必须”和“尽量”配置的时候先想清楚这个 Pod 到底是“必须”还是“尽量”。Pod 间亲和性podAffinity/podAntiAffinity解决的问题不一样它不关心节点标签只关心“其他 Pod 在哪”。典型场景有三个一是把两个延迟敏感的 Pod 放到同一个节点或同一个可用区podAffinity二是把同一应用的多个副本尽量分散到不同节点podAntiAffinity三是避免把不兼容的服务放到同一节点podAntiAffinity。这里有个容易被忽略的性能隐患当集群节点很多时Pod 间亲和性在调度时要做“与其他 Pod 集合做交集匹配”复杂度相当高。大规模集群里别滥用否则调度周期会明显变长。还有个更高频的坑是podAntiAffinity的requiredDuringScheduling会导致无解——比如集群只有 2 个节点副本数设 3每个节点只能放 1 个第 3 个 Pod 永远调度不上去。所以反亲和做“尽量”比做“必须”安全得多。3.3 污点与容忍反向思维的调度控制污点Taint是打在节点上的标记表示“我不欢迎某些 Pod 来”。容忍Toleration是打在 Pod 上的标记表示“我不介意这个污点”。这套机制和亲和性正好是反过来的亲和性是 Pod 主动挑节点污点是节点主动拒绝 Pod。污点有三种效果NoSchedule新 Pod 不调度上来已经在跑的不受影响、PreferNoSchedule尽量不调度是软性的、NoExecute不仅不调度新的还会把已经在跑的 Pod 驱逐走。NoExecute最危险加的时候要小心——一旦打上节点上所有没配容忍的 Pod 都会被赶走。配合tolerationSeconds还能实现“延迟驱逐”NoExecute污点打上后带容忍的 Pod 可以继续留在节点上但超过设置的时间后也会被驱逐。这套机制是节点维护的常用操作先给节点打上NoExecute污点过一段时间等 Pod 全部优雅迁移后再重启节点。我就见过一个生产事故某运维同学为了把新服务固定到新节点给新节点打了污点但没给核心数据库 Pod 配容忍结果节点重启后数据库 Pod 一直被调度到别的节点上而别的节点资源又不够整个核心链路雪崩。所以操作污点前先kubectl get pods -A -o wide看清楚哪些 Pod 正在这些节点上评估它们有没有容忍再动手。4. 网络模型与流量入口Service、Endpoint 和 Ingress 的完整链路4.1 Service 是怎么把流量转给 Pod 的Service 是 Kubernetes 里一个抽象层给一组 Pod 提供一个稳定的访问入口。关键机制是它的selectorService 通过标签选择器匹配一组 PodAPI Server 会实时维护这个匹配结果生成 Endpoints 对象在新版本里是 EndpointSlice。这里最常见的问题是Service 创建了ClusterIP 也有但服务就是访问不通。十有八九是selector写错了——标签稍微差一个 key/valueEndpoints 列表就是空的。我排查这类问题的固定顺序是kubectl get endpoints service-name如果返回的 ENDPOINTS 列是空的说明选择器没匹配到任何 Pod。kubectl get pods --show-labels看目标 Pod 的标签。比对 Service 的 selector 和 Pod 的标签找出差异。这个链路要刻在脑子里因为不管后面上 Istio 还是上各种网关底层“选择器-端点”模型一直存在。很多人一上来就查防火墙、查网络安全策略绕了一大圈最后发现就是标签少写了一个。Service 的类型也要区分清楚。ClusterIP只在集群内部可达NodePort会在每个节点上开一个端口把流量转发给 ClusterIPLoadBalancer是在 NodePort 的基础上再创建一个外部负载均衡器。很多人误以为 LoadBalancer 是“直接连到 Pod”的其实它只是把流量送到 NodePort 或 ClusterIP内部依然走一遍 kube-proxy 的转发链。4.2 kube-proxy 与 iptables/IPVS四层转发怎么做kube-proxy是负责把 Service 的虚拟 IP 转发到后端 Pod 的组件。它的实现模式最常用的是iptables和IPVS。iptables模式用一系列链式规则做转发。每个 Service 都会生成一堆规则Pod 数量大了以后规则数量爆炸性能下降明显。它还有一个让很多人困惑的特性转发是随机选中的不是按权重轮询所以后端扩容后你会发现流量分配不均匀。IPVS模式改用了内核态哈希表天然支持多种负载均衡算法轮询、最少连接等规模大时性能更好规则也更整洁。现在主流集群基本都用 IPVS。不过我要提醒一点不管哪种模式转发链路都发生在节点内核态跟 Pod 里的进程没关系。所以你在一台节点上curl ClusterIP通了不代表应用进程本身没问题只说明转发链路是通的。反过来如果你在 Pod 内部访问 ClusterIP 失败也别急着怀疑 Service先看这个 ClusterIP 是不是因为网络策略或其他原因被排除了。4.3 Ingress七层入口与常见的路径匹配误区Service 解决的是四层TCP/UDP转发但实际业务里我们更需要按域名、按 URL 路径来分发流量这就轮到 Ingress 出场了。Ingress 本身只是一个 API 对象真正干活的叫 Ingress Controller比如 NGINX Ingress Controller它负责读 Ingress 规则、生成实际的七层代理配置。一个最简 Ingress 配置长这样apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: web-ingress spec: ingressClassName: nginx rules: - host: app.example.com http: paths: - path: /api pathType: Prefix backend: service: name: api-service port: number: 8080 - path: / pathType: Prefix backend: service: name: web-service port: number: 80这里有三个特别容易踩的坑。第一个是pathType有Prefix和Exact两种很多人不写或者写错了导致路径匹配不符合预期。Prefix是前缀匹配/api会匹配/api、/api/v1但注意 NGINX 控制器默认还会做路径的重写处理——如果不加rewrite-target注解后端收到的路径可能还是带前缀的而后端如果没定义这个路由就会返回 404。第二个是ingressClassName集群里可能装了多个 Ingress Controller不指定 class 的话规则可能被忽略。第三个是 host 的匹配app.example.com和*.example.com是两回事泛域名的配置方式要单独看文档。掌握了 Service - Endpoints - Pod 这条链路之后再去学服务网格Istio 之类会顺很多。因为服务网格本质上也是“流量劫持 自定义转发规则”只是把转发层面从节点内核态挪到了应用旁路代理sidecar里。前置阶段把 kube-proxy 和 Ingress 这两层的职责边界分清后面看网格数据面就不容易晕。5. 配置、密钥与存储ConfigMap、Secret、PV/PVC5.1 ConfigMap 和 Secret挂载方式的差异决定热更新行为应用配置不进镜像是云原生实践里最基本的一条。ConfigMap 管普通配置Secret 管敏感信息密码、Token、证书两者的用法几乎一样要么作为环境变量注入要么作为文件挂载。它们最容易被忽略的区别是热更新行为。通过env方式注入的环境变量在 Pod 运行期间不会更新因为环境变量是容器启动时一次性设置的改 ConfigMap 后必须重建 Pod 才能生效。而通过volume方式挂载的文件kubelet 会定期同步改 ConfigMap 后过一小会儿容器里就能看到新内容前提是应用自己会重新读文件而不只是启动时读一次。所以如果你的应用不支持动态加载配置改了配置后别傻等直接滚动重启如果应用支持热加载就用 volume 挂载方式。有一次我在某项目里看到同事改完 ConfigMap等了半小时线上配置还是旧的后来发现应用把这个 ConfigMap 放到了环境变量里——这种问题排查起来费劲本质上就是“挂载方式选错了”。Secret 还有一个安全细节默认情况下 Secret 只是 Base64 编码存储在 etcd 里不是加密的。高级点的集群都会开启 etcd 加密存储Encryption at Rest。权限上Secret 和 ConfigMap 一样通过 RBAC 控制但 Secret 泄露的后果严重得多生产环境建议配合外部密钥管理服务比如云厂商的 KMS来做密钥的自动轮换。5.2 PV、PVC、StorageClass存储供给的三层结构一个应用要持久化数据得搞清楚三件套PVPersistentVolume是集群里的一块存储资源PVCPersistentVolumeClaim是用户对存储的申请StorageClass 是存储的“模板”或“驱动”负责按需动态创建 PV。理解它们的关系最贴切的类比是PV 是一块“硬盘”PVC 是一张“领用单”StorageClass 是“硬盘生产流水线”。管理员可以提前备好 PV静态供给用户提交 PVC 时系统去匹配现成的 PV如果没有管理员预制 PV而集群里有 StorageClassPVC 会触发动态供给——云厂商的存储驱动收到请求后自动创建一块云盘并生成 PV。匹配 PVC 和 PV 的条件有三个accessModes读写模式、storageClassName存储类名、resources.requests.storage容量。其中accessModes最容易出问题。ReadWriteOnce意味着这块存储只能被一个节点挂载这在多副本的 StatefulSet 里要特别注意——如果 3 个副本共用一个 PVC挂载会失败。ReadWriteMany则适合多节点共享的场景但这类存储如 NFS通常性能和一致性都不是最优的。回收策略也是复习重点。Retain表示删除 PVC 后 PV 保留但不能再被复用需要管理员手动处理Delete表示删除 PVC 时底层的云盘也会被删掉——这对测试环境很友好但生产环境如果 PV 被误删数据全没了。生产环境的数据库存储建议用Retain或者至少定期做快照不能让 Delete 一把梭。习惯上我会在 YAML 里同时写 requests 和 storageClassName下面是动态供给的一个最小示例apiVersion: v1 kind: PersistentVolumeClaim metadata: name:>apiVersion: apps/v1 kind: Deployment metadata: name: app-with-storage spec: replicas: 1 selector: matchLabels: app: demo template: metadata: labels: app: demo spec: containers: - name: app image: nginx:1.25 volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName:>