做了几年 Kubernetes 相关的运维和平台开发,经常有人拿着入门教程问我要进阶路线。市面上讲 Pod 怎么建、Service 怎么暴露的入门资料一抓一大把,但真到了生产环境,遇到 Pod 频繁重建、流量不通、存储挂载失败这些问题,光会敲 kubectl 命令是远远不够的。这份指南不是什么“七天精通”的捷径,而是把我自己从会用 Kubernetes、到能排查问题、再到敢动源码这条路线上踩过的坑和沉淀下来的方法整理出来。目标是帮你建立一套真正能用于实战的 Kubernetes 进阶认知框架,适合已经能独立部署集群、但面对复杂故障和架构选型还心里没底的工程师。
1. 先把“进阶”这件事定义清楚——入门和进阶段的分水岭不在知识量
很多人觉得进阶就是多背几个资源对象、多看几篇源码解析,于是疯狂囤资料,结果一打开《深入理解 Kubernetes 源码》这类书就卡在第一篇的 kube-apiserver 启动流程里出不来。我见过太多这样的学习者,问题不在不够努力,而是没搞懂进阶到底在进阶什么。
1.1 入门阶段的三个标志性能力
先回顾一下你已经有的东西。能熟练使用 kubectl 操作常用资源、能照着官方示例写 Deployment 和 Service、能把集群装起来跑通一个应用,这三个能力凑齐了,算入门。但注意,入门阶段最大的特点是“知其然不知其所以然”:你知道 Pod 会重启,但不知道为什么重启;你知道 Service 有 ClusterIP,但不知道流量是怎么从 Service 转发到 Pod 的;你知道要配资源请求,但不知道配多少的底层逻辑是什么。
这时候最危险的事情是开始追逐“高级词汇”。很多人一上来就研究 Operator、Service Mesh、Serverless,聊起来头头是道,但让他解释一下一个 Node 上同时运行了 20 个 Pod,其中 3 个频繁 OOM,为什么 Node 本身没有挂,他就说不清楚了。这不是进阶,这是空中楼阁。
1.2 进阶的本质:建立“因果链条”思维
在我看来,从入门到进阶的分水岭,是你有没有在脑海里把 Kubernetes 的每个操作、每个现象背后那条因果链串起来。比如等等,Kubernetes 为什么要这么做?答案藏在历史里:早期 Docker 本身有网络模式,但多机编排需要统一的三层网络模型,于是 CNI(Container Network Interface)被抽象出来,kubelet 通过调用 CNI 插件完成网卡创建和 IP 分配。理解了这条因果链,你遇到网络插件相关的问题时,就不会只是去网上搜“calico 报错怎么办”,而是能顺着 kubelet 日志、CNI 插件事件、网络策略的路劲一步步排查。
进阶思维模型大致包含三层:
- 第一层,资源对象之间的关联与约束,比如 Deployment 如何控制 ReplicaSet,ReplicaSet 如何控制 Pod,GC(垃圾回收)机制又是怎么清理孤儿资源的。
- 第二层,控制循环(control loop)的运行机制。Kubernetes 的核心哲学是声明式管理,每个 controller 都在做“期望状态 vs 当前状态”的对比和趋近。
- 第三层,关键组件之间的信息流。apiserver 是唯一的状态入口,etcd 存状态,kubelet 上报节点和 Pod 状态,controller-manager 负责调和,scheduler 决定 Pod 放到哪台机器。
这三层链条通了,你才算是真正“看着源码心里不慌”。所以,后面所有内容我都会顺着这个思维去展开。
2. 进阶第一课:吃透 Pod 的生命周期与调度逻辑
Pod 是 Kubernetes 里最小的调度单位,这句话谁都会说,但大多数人没有认真把 Pod 的完整生命周期和调度逻辑揉碎了看。这一节是后面所有排障的基础,必须扎扎实实过一遍。
2.1 从 Pod 生命周期看故障排查:Pending、Running、Failed 背后的真相
一个 Pod 从提交到 apiserver 到最终 Running,实际上经历了至少五个阶段:Pending、ContainerCreating、Running、Succeeded/Failed,中间还可能穿插 Terminating 状态。每个状态背后都有明确的原因,排查故障的第一步不是去看日志,而是先搞清楚 Pod 卡在哪个阶段。
先看 Pending。Pending 意味着 Pod 还没有被调度器成功绑定到某个节点上。常见原因有:节点资源不足(requests 配得太大)、节点有 taint 而 Pod 没有对应 toleration、节点亲和性选择器没匹配上、或者调度器本身出了问题。实操时先kubectl describe pod,看 Events 段有没有FailedScheduling,如果看到类似 “0/3 nodes are available: 1 Insufficient cpu, 2 node(s) had taint ...”,答案已经写在里面了。这里有个很容易忽视的细节:如果集群里只有一个节点池且所有节点都有同一个 taint,而你新建的 Pod 没有容忍这个 taint,它就会永远 Pending。
再看 ContainerCreating。这个状态其实是 Pod 已被绑定到节点,但容器还没起来。一般卡在这里是因为镜像拉取失败(网络不通、tag 写错、私有仓库没配 imagePullSecret)、存储卷挂载超时(比如 NFS 存储没就绪)、或者入口探针/初始化容器还没完成。看到 ContainerCreating 卡住的时间超过 1 分钟,先kubectl describe pod,再看 kubelet 日志。很多人在这一步会漏掉一个神器:kubectl get pod -o wide,能直接看到 Pod 被调度到了哪台节点,然后ssh上节点用crictl ps -a查看容器创建失败的具体原因。这个命令比反复kubectl logs高效得多,因为容器可能根本都没被创建出来。
再说 Terminating 卡住。正常删除一个 Pod 只需要几秒钟,但如果 Pod 里有容器不响应 SIGTERM 信号、或者 finalizers 没被清理、或者由于节点失联导致 apiserver 无法确认删除完成,Terminating 状态就会卡很长时间。解决思路:先检查这个词 — Pod 的优雅终止时长设定的是默认 30 秒,如果应用不处理 SIGTERM,强制 kill 的 SIGKILL 会在超时后发出。如果确认应用确实来不及处理 SIGTERM,应该在 Deployment 的spec.terminationGracePeriodSeconds里加长这个值。
提示:Pod 生命周期排障的思路不应该是“先看日志”,而应该是“先看状态、再查事件、最后查日志”,顺序反了会浪费大量时间。日志只反映容器内应用的运行情况,而 Pod 在容器启动之前的绝大多数故障,都需要靠 describe 的事件和节点上的 CRI 命令来定位。
2.2 调度器背后那点事:从 NodeSelector 到拓扑分布
调度器可能是整个 Kubernetes 里被低估得最严重的组件。很多人只会用nodeSelector来挑节点,结果生产环境一旦需要精细控制多 AZ 容灾、或者处理跨节点的 GPU 分配,就不知道怎么动了。
nodeSelector是最朴素的调度方式,属于硬性约束,它的逻辑是“只调度到带这些标签的节点上”。但它有两个硬伤:第一,它只能做等值匹配,无法表达“标签在这组值之一”的情况,这得靠nodeAffinity解决;第二,它没有“软性偏好”的能力,没法表达“尽量调度到这些节点,但实在不行就算了”。所以进阶之后,要逐步过渡到“亲和性 + 反亲和性 + 拓扑分布”这套组合拳。
节点亲和性分硬性(requiredDuringSchedulingIgnoredDuringExecution)和软性(preferredDuringSchedulingIgnoredDuringExecution)。硬性亲和的写法是:
affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - us-east-1a - us-east-1b软性亲和则给调度器打了一个偏好分值,比如 “尽量把 Pod 放在 SSD 节点上,分值为 100”,调度器会在满足约束的前提下尝试满足这个偏好。理解了这两个词,再看 Pod 反亲和性就顺了。Pod 反亲和的典型场景是让同一个应用的多个副本尽量分散到不同节点或不同可用区,避免单点故障导致整个服务不可用。
但反亲和支持的topologyKey一定要设计好。kubernetes.io/hostname代表节点维度,topology.kubernetes.io/zone代表可用区维度,topology.kubernetes.io/region代表地域维度。反亲和性的表达方式是:
affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: my-app topologyKey: topology.kubernetes.io/zone这段意思是:在同一个逻辑拓扑域(这里是同一个可用区)里,不能有两个带着app=my-app标签的 Pod。实际部署时这个能力能避免“所有副本挤在一个可用区”的尴尬。但要提醒一句:强反亲和(required)在节点池很小的时候会导致 Pod 一直 Pending,因为集群满打满算也只有 3 个节点,你要跑 10 个副本且每个节点只能放一个,剩下的就永远等在那里。生产环境建议优先用软反亲和preferredDuringScheduling...,调度器会尽量分散,实在放不下也不会卡死。
再往后,如果你的集群规模到了几百节点的量级,就要开始关注topologySpreadConstraints。它解决的是“反亲和只能管同一个标签的 Pod 之间的分布,但没法管不同工作负载之间在拓扑域上的均匀性”这个问题。例如用maxSkew: 1让同一副本集的 Pod 在不同可用区之间的数量差异最多为 1。这个机制比较精细,配置时要注意nodeAffinityPolicy和nodeTaintsPolicy这些可选字段的默认行为,建议先在 staging 集群实验一轮再上生产。
3. 进阶第二课:Controller 与工作负载的选型智慧
Deployment、StatefulSet、DaemonSet、Job、CronJob,谁的控制器是干什么的,网上有无数表格。但进阶重点不是背表格,而是理解为什么要有这么多 Controller,以及在面对一个具体应用时该怎么选。
3.1 无状态优先还是坚持有状态?——Deployment、StatefulSet、DaemonSet 的选型逻辑
先给一个最核心的判断标准:你的应用有没有“身份”和“存储”两个强需求?无状态应用,Web 后端、API 服务、计算任务,直接用 Deployment,多个副本之间完全对等,任何一个 Pod 挂着新拉起一个就行。Deployment 的版本管理、滚动更新策略、回滚能力都很成熟,日常 80% 的工作都围着它转。
但当应用需要稳定的网络标识和持久化存储,比如数据库主从、消息队列、分布式缓存,这时候就要认真考虑 StatefulSet 了。StatefulSet 和其他 Controller 最根本的差异在于,它给每个 Pod 一个持久且有序的标识:名字从-0开始依次递增,PVC 和 Pod 的名字绑定,重启之后 Pod 名字不变、存储卷也保持不变。这样在设计主从选举等场景时,节点身份可以作为稳定的输入。
举个例子,部署一套三副本的 ZooKeeper,如果用 Deployment,Pod 名字是随机串(如zk-5f8d7g7b9c-abcde),重启之后名字变化,节点角色和身份的稳定就无从谈起。而 StatefulSet 保证zk-0、zk-1、zk-2三个名字长期恒定,配合 headless Service,可以通过zk-0.zk-svc.namespace.svc.cluster.local这样的 DNS 名称直接访问固定实例。要注意 StatefulSet 的更新顺序是反序的,从最后一个副本开始逐个升级,这样能保证主节点最后才更新,减少集群可用性风险。滚动更新参数podManagementPolicy默认是OrderedReady,如果不需要严格顺序,改成Parallel能提升更新速度,但别指望官方保证过程中的可用性。
DaemonSet 的使用场景则是“每个节点上都必须跑且只能跑一个副本”。典型的有节点监控 Agent、日志采集器、网络插件(如 Calico 的 felix)、存储插件(如 CSI 驱动)。这类组件的特点是和节点强相关,副本数量随节点数量变化,而不是由用户指定。
在实际选型时还有一个常被忽略的判断因素:这个应用是否需要在节点间移动?Deployment 的 Pod 可以随时被调度到任意满足条件的节点,而 StatefulSet 的 Pod 因为依赖固定的 PVC 和 DNS,通常不能随意跨节点移动(除非存储是共享型且网络允许跨节点访问),所以你的集群如果需要频繁缩容扩容,建议尽量把应用设计成无状态,把有状态部分独立出来。
实操心得:我见过不少人一上来就把所有服务都用 StatefulSet 部署,理由是“以后可能需要持久化”,结果反而给自己挖了坑——StatefulSet 的删除、更新、扩缩容都比 Deployment 麻烦,命名、PVC 管理、headless Service 都是额外的心智负担。我的建议是“默认 Deployment,只有在明确需要稳定身份或严格顺序时才切 StatefulSet”。存储需求如果只是卷的持久化,可以用 Deployment + 独立 PVC,前提是你不介意副本不共享 PVC。
3.2 资源请求、HPA 与放缩容之间那笔账
进阶学习中一个很重要的能力,是能算清“资源账”。很多人直接把测试环境里的 CPU request 搬到生产,结果集群的整体资源利用率惨不忍睹,或者一个 Pod 的 request 设置得过高导致其他 Pod 无法调度。
先理清 request 和 limit 的区别:request 是调度器用来做容量估算的字段,表示“这个 Pod 至少需要这么多资源”;limit 是运行时层面(kubelet/cri)用来做资源限制的字段,表示“这个 Pod 最多能用这么多资源”。一个容易忽略的点是,如果只设置 limit 不设置 request,Kubernetes 会默认把 request 设为 limit 的值,这样调度时占用的资源就变成和 limit 一样大,非常浪费。
HPA(HorizontalPodAutoscaler)的逻辑,就是根据指标(默认是 CPU 利用率)自动调整 Deployment/StatefulSet 的副本数。默认行为是计算“当前副本数 * 当前指标利用率 / 目标利用率”,然后向上取整。举例说明,目标利用率是 50%,当前有 3 个副本,平均 CPU 利用率是 90%,那么新的副本数就是 3 * 0.9 / 0.5 = 5.4,向上取整后变成 6。这里有个进阶技巧:如果你的应用启动耗时比较长,HPA 扩出来一个新副本后,它要过很久才开始实际处理流量,但 HPA 基于的 metrics 已经包含了这段时间的 CPU 峰值。结果就是“扩容永远慢半拍”,用默认参数在流量突增时很容易出现服务过载。这时要么把spec.behavior.scaleUp的stabilizationWindowSeconds调短,要么增加pods策略的限制,允许一步多扩几个副本,同时配合 PDB(PodDisruptionBudget)保证缩容时不至于把可用副本数压得太低。
还要注意 HPA 在目标工作负载是 StatefulSet 时的区别:StatefulSet 的缩容是反序删 Pod,如果你同时设置了pv有且仅有一个副本挂载某块盘,那么你需要检查你在缩容时会不会删除那个主节点。HPA 本身不会感知角色,所以对于主从架构,一般不用 HPA 自动缩容主节点,最多只对从节点做弹性。
资源配比这块我建议把 request 压到实际使用量的 70%-80%,留出 buffer,limit 再往上留 30%-50%,避免直接 OOMKilled。压测阶段用metrics-server观察实际用量,比拍脑袋靠谱得多。
4. 进阶第三课:网络模型与 Service 流量路径
网络是 Kubernetes 里最容易出问题、也最容易“玄学”的部分。很多人在集群里curl一下 Service IP 能通,就觉得自己已经搞定网络了,但真要排查一个问题时,总是得重新查资料。进阶核心是把数据包的完整路径画在脑子里。
4.1 Service、Endpoint 与 kube-proxy:一条流量的完整旅行
从客户端请求一个 Service 的 ClusterIP 开始,流量大致经过这几个环节:
- DNS 解析到 Service 的 ClusterIP(或通过环境变量注入)。
- kube-proxy 在宿主机上维护的 iptables/ipvs 规则接收这个 IP 的流量。
- 根据规则,把请求 DNAT 到某个后端 Pod 的 IP(由 Endpoint 列表提供)。
- 请求通过节点的路由转发到 Pod 所在节点的 Pod 网络。
- 最终到达 Pod 内的容器,经过容器网卡进入应用进程。
这里的核心结构是 Service 的后端集合来自 EndpointSlice,它由 Kubernetes 的 EndpointSlice controller 自动维护。当你给 Service 写了 selector 之后,controller 就会把匹配到的 Pod IP 都放进 EndpointSlice。一个极常见的故障是:Service 配置正常,后端 Pod 也正常,但流量就是不通。查法很简单:kubectl get endpointslices,看端点列表里有没有 Pod 的 IP。如果没有,基本就是 selector 写错了或者 Pod 的 readiness 探针一直失败,导致 Pod 被从后端列表里移除。很多人kubectl describe svc只看到 “Endpoints: ”,第一反应是查防火墙,其实大概率就是 selector 不匹配。
再往下,kube-proxy 有两种工作模式,iptables 和 ipvs。iptables 模式是默认的,它是通过一条条规则做 DNAT,且规则顺序复杂,在大规模 Service 场景下,内核会消耗大量时间在规则匹配上,性能瓶颈非常明显。ipvs 模式则是在内核里用哈希表做转发,规则数量和匹配成本大幅下降,适合 Service 数量超过几百个的集群。切换方式是在 kube-proxy 的启动参数或 ConfigMap 里把mode设为ipvs。但 ipvs 模式有个小坑:它对 kube-proxy 的启动参数--ipvs-scheduler有要求,默认是rr(轮询),如果你的后端 Pod 长短不一,轮询会让慢的 Pod 拖垮整体延迟,可以考虑改成wrr或lc(最少连接)。不过大部分场景用rr+ 合理的 Pod 副本和压力分布就够用了。
还有一个很多人忽略的关键点:Service 的sessionAffinity。默认是 None,也就是每次请求都可能打到不同的后端 Pod;如果应用有本地会话状态(比如内存里的 session 缓存),需要设置为ClientIP,这样同一个客户端 IP 的请求就会固定落在同一个 Pod 上。靠“默认 None”去排查一个“用户刷新几次就掉线”的问题时,这个字段值得先看一眼。
注意:Service 的
externalTrafficPolicy有两个取值,Cluster 和 Local。默认是 Cluster,也就是即便请求到达的节点上没有该 Service 的后端 Pod,kube-proxy 也会把请求转发给集群内其他节点的后端 Pod,这样每个节点上负载会相对均匀,但也保留了中间一跳 SNAT,客户端拿到的源 IP 会变成节点 IP。如果你做的是“系统想拿到真实用户 IP”的场景,需要把externalTrafficPolicy设置为Local,它只在本地节点有后端 Pod 时才转发,避免二次转发,从而保留真实源 IP。代价是流量只分布在有后端 Pod 的节点上,需要考虑负载均衡器和 ingress controller 的配合。
4.2 Ingress 与南北向流量:从 Ingress 到 Service 的落地姿势
Service 解决了集群内部的南北向流量入口问题,但由 LoadBalancer 直接暴露给外部不够灵活、成本也高,所以 Ingress 成了事实标准。Ingress 本身是一个 API 对象,真正干活的是 Ingress Controller,比如 NGINX Ingress Controller、Traefik、HAProxy。Ingress 对象定义“根据什么 host/path 转发到什么 Service”,Ingress Controller 负责监听这些规则并生成对应的负载均衡配置。
生产实践里我给你的建议是:尽量不要在 Ingress 规则里写太复杂的 path 重写和正则,复杂的转发逻辑会大幅降低排障效率。Inngress Controller 的配置里,ingress-nginx的 annotationnginx.ingress.kubernetes.io/rewrite-target是一个高频重灾区。比如你写了一个/api路径的转发规则,结果访问/api/users时后端收到的路径却变成了/users,这就是因为 rewrite-target 指定了去掉前缀。你的期望是把/api/users原样传到后端,就去掉这个 annotation,或者把 rewrite-target 设为/api加一个后面的捕获组。但注意 pathType 要改成Prefix,如果用Exact,/api之外的子路径根本匹配不上。
Ingress 控制器的另外一个常踩的坑是proxy-body-size默认限制在 1m,如果后端的接口允许上传较大的文件,需要在 annotation 里调大这个值。所在我建议在搭建 Ingress 层时,一次性把proxy-body-size、proxy-connect-timeout、proxy-read-timeout这几个参数都按业务默认值配置到 controller 的 ConfigMap 中,而不是每次在单独 Ingress 对象上加 annotation,这样规则会整洁很多。
再深入一点,Ingress Controller 本身也是一个 Deployment,通常托管在集群内部,通过 NodePort 或 LoadBalancer 方式暴露。这里的流量路径是:外部负载均衡器 -> 某个节点的 NodePort -> Ingress Controller Pod -> Service ClusterIP -> 后端 Pod。如果你发现 Ingress 在公网访问时延迟忽高忽低,大概率是这个链路里节点跳转太多,且externalTrafficPolicy: Cluster导致请求从一个节点转到另一个节点,跨节点转发多了延迟和 SNAT 损耗。优化方式是给 Ingress Controller 打上externalTrafficPolicy: Local和反亲和性,让流量只落在运行了 Controller Pod 的节点上。
5. 进阶第四课:存储与配置的进阶玩法
存储是 Kubernetes 里最容易“翻车”的领域,因为牵涉到驱动、网络、权限、数据一致性多个层面。同时 ConfigMap 和 Secret 看似简单,但细节处分分钟埋雷。
5.1 PV/PVC/StorageClass 三件套的动态供给和静态供给
有状态应用的存储需求绕不开这三件套:PV(PersistentVolume)是集群资源,PVC(PersistentVolumeClaim)是用户的存储申请,StorageClass 是动态供给的模板。进阶的关键是理解“动态供给”四个字到底在做什么。
集群管理员创建磁盘类型并定义好 StorageClass(比如 SSD 盘、HDD 盘,或者在云平台上绑定某个云盘类型),应用开发者只需要写一个 PVC 声明需要多大的空间、什么读写模式、引用哪个 StorageClass,系统会自动帮他创建一个 PV 并绑定。这个过程的底层调用是 CSI 插件,例如csi.storage.k8s.io这类 driver 会调用云平台的 createDisk API 或基础设施的卷创建 API,之后再通过节点的 kubelet 把磁盘挂载到宿主机指定目录,并 bind-mount 到容器内。
动态供给看起来方便,但在生产上线的第一件事,你务必要评估“删除 PVC 时是否连带删 PV 和底层磁盘”。默认情况下,如果 StorageClass 的reclaimPolicy是Delete,PVC 被删除后 PV 和底层磁盘都会被销毁,数据无法找回。这等于把生产数据库的命脉交给了用户手滑。我的建议是:对于核心应用(特别是数据库),把 StorageClass 的reclaimPolicy改为Retain,这样即便 PVC 被误删,管理员还能手动处理底层卷和 PV。这是我在一次测试环境中不小心删掉了一个 Grafana 的 PVC,连带整个监控数据盘被删掉之后得到的深刻教训。数据无价,该保守时绝不要贪图省事。
StorageClass 里有几个参数需要逐个过一下:provisioner指定谁去创建卷,reclaimPolicy决定卷的回收策略,volumeBindingMode决定 PVC 和 PV 的绑定时机。volumeBindingMode默认是Immediate,即 PVC 创建后立刻找一个可用的 PV 做绑定。但有个坑,如果底层存储是“只在某些节点可用”(比如本地盘、指定可用区的云盘),Immediate模式下绑定的 PV 可能落在和调度到目标节点不匹配的存储上,导致 Pod 调度失败。这种情况要改成WaitForFirstConsumer:先等有 Pod 来用这个 PVC 的时候,调度器先确定 Pod 要去哪台节点,再在那个节点对应的存储范围内创建 PV。所以,凡是“本地存储、拓扑感知存储”,就优先用WaitForFirstConsumer。
还有一个经常被忽略的访问模式:ReadWriteOnce(RWO)只能被一个节点挂载,适合单副本应用;ReadOnlyMany(ROX)可以被多个节点同时挂载;ReadWriteMany(RWX)是“多节点可读可写”,通常只有 NFS 这类网络文件系统能支持,云上块存储通常不支持 RWX。如果你把 NFS 装到一个只有 RWO 的驱动上,挂载可能会成功,但生产上很容易出现数据不一致或并发锁问题。所以 RWX 一定要确认存储驱动真的支持。
5.2 ConfigMap 与 Secret 的“小”问题大坑
ConfigMap 和 Secret 的底层其实都是 etcd 里的键值数据,挂载到容器时被转换成文件。它们分别应付环境配置和敏感信息的传递。进阶者需要注意的不只是“会写”,还要注意几个容易踩雷的细节。
第一,ConfigMap 更新了,但容器内挂载的文件并不会自动更新。很多人以为“改了 ConfigMap,容器里立刻生效”,实际上,只有你重新部署了引用该 ConfigMap 的 Pod,容器里的文件才会被刷新。有些应用支持监听文件变化的热加载,kubelet 会定期同步挂载卷里的 ConfigMap 数据,但这个同步周期不是实时的,通常有几秒到几十秒的延迟。SO,如果业务要求“配置变更秒级生效”,一般不会通过 ConfigMap 挂载文件来实现,而是用环境变量 + 应用自身的配置中心。
第二,Secret 的默认存储方式其实只是 base64 编码,不是加密。Kubernetes 默认把 Secret 对象存到 etcd 中,etcd 里的数据是明文(只是外层 base64 编码)。如果对安全要求高,需要开启 etcd 静态加密(Encryption at Rest)或者引入外部的 Key Management Service,比如云上 KMS。很多人以为 Secret 天然安全,把它当成安全的钥匙链,这是高环境敏感度的团队必须注意的。生产环境切忌把database-password以明文写在 YAML 里提交到 Git,至少要用sops或者sealed-secrets这类工具做加密,再提交到仓库。
第三,Secret 和 ConfigMap 一旦被存储到 etcd,如果之后从集群里删除,并不会立即从所有节点缓存中抹掉,因为 kubelet 可能会有缓存。所以删除敏感信息时,务必要额外检查有没有残余的 watchdog 或其他组件在转发它。特别是当你在 K8s 集群里运行第三方应用时,不要用默认的defaultServiceAccount,而是单独创建带最小 RBAC 权限的 ServiceAccount。
6. 进阶实战:用 Kubernetes 部署 Nginx 并暴露服务——一份可以抄作业的完整流程
理论堆了一大堆,最后必须用一顿实操把前面讲的网络、存储、工作负载串起来。这里我选一个最经典的案例:在 Kubernetes 里部署一个 Nginx,并通过 Service 和 Ingress 对外暴露。这个案例网上有无数教程,但我要走一遍带着排障视角的进阶版流程。
6.1 写好 Deployment:从复制粘贴到理解细节
第一版 Deployment 简单到只包含容器镜像和端口声明,但要让它在生产里经得起推敲,至少要关注三个细节:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo labels: app: nginx-demo spec: replicas: 3 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx image: nginx:1.27-alpine ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 250m memory: 256Mi readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 3 periodSeconds: 5 livenessProbe: httpGet: path: / port: 80 initialDelaySeconds: 10 periodSeconds: 10第一,亲和性。虽然 Nginx 无状态,但如果你的集群是跨可用区部署,建议在上面的 spec 里加上 Pod 反亲和性,让 3 个副本分散到不同可用区。如果没有跨可用区需求,可以不加。
第二,探针。readinessProbe 决定这个容器什么时候算“可接受流量”,livenessProbe 决定容器什么时候需要被重启。Nginx 对探针的响应很快,所以这里选 HTTP GET 探针就行。要注意的是,不要用command探针去执行nginx -t,因为nginx -t只检查配置是否有效,不检查进程是否真的在监听。如果你的应用本身启动慢,要适当调大initialDelaySeconds,否则探针连续失败几次,kubelet 会直接把它杀掉。
第三,资源请求。这里cpu: 100m的意思是 0.1 个 vCPU。对于纯静态文件服务,这个值偏低,Nginx 在默认 worker_processes 下可能跑得更快。但生产上更合理的做法是先给一个中等值跑一轮压测,再看 metrics-server 的实际用量去调。
6.2 用 Service 暴露:从 ClusterIP 到 NodePort 和 LoadBalancer
Deployment 创建了 Pod,但这些 Pod 的 IP 是临时的,重建后就变了,而且对于集群外部没有任何意义。Service 是稳定的抽象入口。
apiVersion: v1 kind: Service metadata: name: nginx-demo-svc spec: selector: app: nginx-demo ports: - protocol: TCP port: 80 targetPort: 80 type: ClusterIP这里要理解port和targetPort的区别:port是 Service 对外暴露的端口,targetPort是后端 Pod 上实际监听应用的端口。很多人写成port: 8080, targetPort: 80也能通,因为有 iptables DNAT 会把10.96.x.x:8080转换到某个 Pod 的10.244.x.x:80,但外部访问也得跟着调整为:8080。维护成本会上升,所以建议保持 Service 端口和后端应用端口一致。
如果想让外部直接通过 NodeIP 访问,把type改为NodePort,系统会自动分配一个 30000-32767 之间的端口。在云环境下想直接用云负载均衡器,可以改成LoadBalancer(需要集群里装好 cloud-controller-manager)。三种类型的流量路径差异:
- ClusterIP:只能在集群内部访问。
- NodePort:外部通过任意节点 IP + 30000+ 端口访问,通过 kube-proxy 转发。
- LoadBalancer:外部通过云负载均衡器访问,负载均衡器后端指向各节点的 NodePort,再转到 Pod。
6.3 用 Ingress 暴露:一套更完整的流量入口
在生产环境,直接用 LoadBalancer 暴露每个服务成本很高,所以一般只给 Ingress Controller 建一个 LoadBalancer,剩下所有 HTTP 服务都通过 Ingress 规则转发。
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx-demo-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: ingressClassName: nginx rules: - host: k8s.demo.local http: paths: - path: /nginx pathType: Prefix backend: service: name: nginx-demo-svc port: number: 80这里需要先确认集群里是否安装了 Ingress Controller。如果没有,ingressClassName: nginx会指向一个不存在的 controller class,Ingress 对象创建成功但不会有任何实际效果。检查方法:kubectl get ingressclass,看有没有名为 nginx 的类。
创建完以后怎么验证?先kubectl get ingress,看 ADDRESS 字段是否有值;再kubectl get svc -n ingress-nginx,确认 LoadBalancer 的 external IP。最后本机加上 hosts 映射k8s.demo.local -> LB_IP,用curl http://k8s.demo.local/nginx测试。如果返回 404,先看路径的 rewrite-target 是否符合预期,再看后端 Service 的 Endpoint 有没有内容。
排障技巧:Ingress 转发失败时,不要只盯着 Ingress 对象,要去看 Ingress Controller 的日志,里面有每一次请求的转发细节,比如
"status":404会告诉你它当时把请求转给了哪个 upstream。kubectl logs -f deploy/nginx-ingress-controller -n ingress-nginx里搜upstream关键字,基本能在十秒内定位到是转发路径问题还是后端 404。
7. 进阶尾声:向源码级理解迈进——我的心得与建议
学到这里,你已经有了实践层面的完整认知框架,那么接下来的进阶方向,我建议就是往源码方向走。很多人问《深入理解 Kubernetes 源码》这本书到底要怎么读,我的经验是用“模块 + 需求”的方式去读,而不是从第一页按顺序啃。
7.1 源码学习的三个有效切入口
第一个有效入口是 apiserver。它是所有请求的入口,理解它的 HTTP 处理链、认证授权、准入控制(Admission Control)、以及和 etcd 的交互,你就抓住整个系统的骨架。读源码时不要从头读,先从你熟悉的场景切进去:比如当你在命令行执行kubectl create deployment时,这个 POST 请求到 apiserver 之后经历了什么?顺着这个请求路径往下读,自然就能把 REST 存储、解码、默认值注入、验证、持久化到 etcd 这条线串起来。
第二个有效入口是 controller-manager 里的某个具体 controller,比如 Deployment controller。它本身就是一个调协循环:watch Deployment 对象变化,看到期望的 ReplicaSet 数量和当前不一致时,就创建或删除 ReplicaSet。这里有意思的部分是“队列是怎么做去重和延迟的”,以及“一次调协循环里为什么有这么多syncHandler和expectations”。这些机制设计之所以存在,是因为调度和控制事件可能重复,必须有幂等设计。理解了这些,你写自己的 Operator 时就能少踩很多并发坑。
第三个有效入口是 kubelet 的 Pod 同步逻辑。kubelet 自己维护了一个 Pod 的 cache,通过 watch apiserver 上分配给本节点的 Pod 列表,然后 reconcile 到底层容器运行时(通过 CRI 调用)。很多“Pod 状态和实际容器状态不一致”的问题,答案都藏在 kubelet 的syncLoop里。这个模块的逻辑比较复杂,建议配合kubelet 日志的 verbose level 一起来看。
7.2 我从磕磕绊绊里提炼的几条实践原则
最后分享几条我自己的学习原则,希望你能少走弯路:
第一,深入一个组件之前,先自己动手解决过它引发的问题。没有真实故障场景的驱动,源码读起来都是走马观花。比如你从来没有排查过一个“Pod 卡在 ContainerCreating 一整晚”的问题,你是无法真正理解 kubelet 的 CRI 调用链的。
第二,带着问题上路。我会在笔记本上维护一个“Kubernetes 未解之谜”清单,每当看到一个奇怪的现象,就记录成问题,然后去源码或社区里找答案。比如我记录过“为什么滚动更新时新旧 ReplicaSet 几乎会同时存在”“为什么 Service 的 ClusterIP 在某些网络插件下 ping 不通”,这些问题的答案都让我对自己的系统理解加深了一大块。
第三,不要吝啬输出。我之前在团队内部做过几场内部分享,就是把“为什么 Pod 会卡在 Terminating”这种看似简单的问题拆给同事听。讲解的过程,是逼着自己把每一个“熟视无睹”的细节重新审视一遍。说实话,很多东西是自己讲完之后才真正搞明白的。
再讲一个小技巧,实用且立竿见影:在你觉得“这地方太难了”的时候,先别急着去啃那一块,跳回上一层的架构视角,想象如果让你用白纸设计一个能承受节点宕机的编排系统,你会怎么设计?然后打开 Kubernetes 的对应模块,看看它和你设计的差异在哪里。这种“先设计,再对照”的学习方式,比从头读源码有效率得多。
这份进阶指南到这里没有终点。Kubernetes 生态迭代非常快,今天写下的某些最佳实践,可能明年就被新机制取代了。但底层的那套控制循环思维、链路排查思路、资源账计算能力,是不会过时的。你可以带着这份框架,去迎接你生产环境里那一堆还没冒出来的疑难杂症了。