ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Kubernetes进阶指南:从Pod调度到网络排障的实战方法论

Kubernetes进阶指南:从Pod调度到网络排障的实战方法论 做了几年 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 本身有网络模式但多机编排需要统一的三层网络模型于是 CNIContainer Network Interface被抽象出来kubelet 通过调用 CNI 插件完成网卡创建和 IP 分配。理解了这条因果链你遇到网络插件相关的问题时就不会只是去网上搜“calico 报错怎么办”而是能顺着 kubelet 日志、CNI 插件事件、网络策略的路劲一步步排查。进阶思维模型大致包含三层第一层资源对象之间的关联与约束比如 Deployment 如何控制 ReplicaSetReplicaSet 如何控制 PodGC垃圾回收机制又是怎么清理孤儿资源的。第二层控制循环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这段意思是在同一个逻辑拓扑域这里是同一个可用区里不能有两个带着appmy-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如果用 DeploymentPod 名字是随机串如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 不设置 requestKubernetes 会默认把 request 设为 limit 的值这样调度时占用的资源就变成和 limit 一样大非常浪费。HPAHorizontalPodAutoscaler的逻辑就是根据指标默认是 CPU 利用率自动调整 Deployment/StatefulSet 的副本数。默认行为是计算“当前副本数 * 当前指标利用率 / 目标利用率”然后向上取整。举例说明目标利用率是 50%当前有 3 个副本平均 CPU 利用率是 90%那么新的副本数就是 3 * 0.9 / 0.5 5.4向上取整后变成 6。这里有个进阶技巧如果你的应用启动耗时比较长HPA 扩出来一个新副本后它要过很久才开始实际处理流量但 HPA 基于的 metrics 已经包含了这段时间的 CPU 峰值。结果就是“扩容永远慢半拍”用默认参数在流量突增时很容易出现服务过载。这时要么把spec.behavior.scaleUp的stabilizationWindowSeconds调短要么增加pods策略的限制允许一步多扩几个副本同时配合 PDBPodDisruptionBudget保证缩容时不至于把可用副本数压得太低。还要注意 HPA 在目标工作负载是 StatefulSet 时的区别StatefulSet 的缩容是反序删 Pod如果你同时设置了pv有且仅有一个副本挂载某块盘那么你需要检查你在缩容时会不会删除那个主节点。HPA 本身不会感知角色所以对于主从架构一般不用 HPA 自动缩容主节点最多只对从节点做弹性。资源配比这块我建议把 request 压到实际使用量的 70%-80%留出 bufferlimit 再往上留 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 的后端 Podkube-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 三件套的动态供给和静态供给有状态应用的存储需求绕不开这三件套PVPersistentVolume是集群资源PVCPersistentVolumeClaim是用户的存储申请StorageClass 是动态供给的模板。进阶的关键是理解“动态供给”四个字到底在做什么。集群管理员创建磁盘类型并定义好 StorageClass比如 SSD 盘、HDD 盘或者在云平台上绑定某个云盘类型应用开发者只需要写一个 PVC 声明需要多大的空间、什么读写模式、引用哪个 StorageClass系统会自动帮他创建一个 PV 并绑定。这个过程的底层调用是 CSI 插件例如csi.storage.k8s.io这类 driver 会调用云平台的 createDisk API 或基础设施的卷创建 API之后再通过节点的 kubelet 把磁盘挂载到宿主机指定目录并 bind-mount 到容器内。动态供给看起来方便但在生产上线的第一件事你务必要评估“删除 PVC 时是否连带删 PV 和底层磁盘”。默认情况下如果 StorageClass 的reclaimPolicy是DeletePVC 被删除后 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。还有一个经常被忽略的访问模式ReadWriteOnceRWO只能被一个节点挂载适合单副本应用ReadOnlyManyROX可以被多个节点同时挂载ReadWriteManyRWX是“多节点可读可写”通常只有 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 和 LoadBalancerDeployment 创建了 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 classIngress 对象创建成功但不会有任何实际效果。检查方法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 生态迭代非常快今天写下的某些最佳实践可能明年就被新机制取代了。但底层的那套控制循环思维、链路排查思路、资源账计算能力是不会过时的。你可以带着这份框架去迎接你生产环境里那一堆还没冒出来的疑难杂症了。
返回列表