ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kubernetes Pod管理与资源清单实战:生命周期、探针与故障排查

Kubernetes Pod管理与资源清单实战:生命周期、探针与故障排查

1. 先搞清楚Pod在K8s里到底是个什么角色

很多人学Kubernetes,第一关就卡在Pod这个概念上。这很正常,因为Pod确实是整个K8s体系里最基础也最核心的抽象单位。说直白点,Pod是K8s里最小的调度单元,一个Pod里面可以跑一个容器,也可以跑多个容器,这些容器共享同一个网络命名空间、共享同一个存储卷,可以说是生死与共的一家人。

我在实际项目里遇到的最常见误解,是有人把Pod和容器混为一谈,觉得"Pod就是容器"。这个说法不全错,但不准确。容器是实际跑进程的东西,而Pod是为容器提供运行环境的"壳"。打个比方,容器是住客,Pod就是一套房子。房子可以是单间(单容器Pod),也可以是大户型(多容器Pod),但无论几个住客,他们共享同一套水电网络基础(网络命名空间和存储卷)。

企业项目里为什么需要引入Pod这一层?直接跑容器不行吗?这个问题我在给团队培训时被问过很多次。答案在于,有些场景下多个进程需要紧密协作,比如一个进程写日志文件,另一个进程负责把日志转发出去,这两个进程如果各自跑在独立的容器里,互相之间要通信就得走网络,还得考虑IP变化、端口冲突这些问题。但如果把它们放在同一个Pod里,它们可以直接通过localhost互相访问,共享同一个文件系统,配合起来就像本机进程一样自然。这种设计就是K8s给出的"进程级编排"方案。

从企业实战的角度来说,我们操作K8s,99%的场景都是在操作Pod。无论是Deployment、StatefulSet、DaemonSet这些控制器,最终都是在管理Pod的声明周期。所以,理解了Pod,你就理解了K8s的半个天下。这篇我就结合企业项目里的实际经验,把Pod管理和资源清单这两块内容彻底掰开揉碎讲清楚。

2. 多容器Pod的设计逻辑与适用场景

2.1 什么情况下必须用多容器Pod

这里我直接给结论:多容器Pod不是用来凑数或者图省事的,它是一个"强耦合"的设计模式。判断标准就一条——这些容器需不需要共享生命周期?

举个例子。我参与过的某支付系统项目里,核心业务容器负责处理交易请求,旁边必须有一个Sidecar容器专门负责收集业务日志并推送到日志中心。这两个容器的关系是这样的:

  • 它们需要部署在同一台机器上(因为要共享本地磁盘写日志文件)
  • 它们的生命周期需要保持一致(业务容器挂了,日志容器跟着挂也没意义)
  • 它们之间的通信要走本地回环(性能好,不走网络栈)

这种场景,就是多容器Pod的教科书式需求。Sidecar模式是目前多容器Pod最典型的应用场景。还有两外两种常见模式,我也一并说了:

  • Adapter模式:把业务容器输出的数据,通过一个适配容器转换成统一格式。比如业务容器输出的是自定义格式日志,旁边挂一个适配器容器负责转成JSON格式再输出。
  • Ambassador模式:业务容器通过本地回环访问代理容器,由代理容器负责连接外部服务。这就像业务容器身边配了一个"外交官",专门替它跟外部打交道。

这三种模式,在企业项目里最常用的就是Sidecar,后面两种看到能认出来即可。

2.2 多容器Pod的资源共享机制

多容器Pod内部的容器,共享的东西其实不少。具体来说有三块:

  • 共享网络命名空间:所有容器共享同一个IP和端口空间,通过localhost就能互相通信。但注意,端口就不能随便用了,同一个Pod里的两个容器不能同时监听同一个端口,否则会冲突。
  • 共享存储卷:Pod级别的存储卷可以挂载到多个容器里,实现文件级别的共享。如果容器之间要传文件,走这种本地共享卷比走网络要快得多、可靠得多。
  • 共享同一份Pod配置:包括环境变量(通过Pod级配置注入)、标签、注解等。

这块有一个常见的坑:很多人以为同一个Pod里的容器可以直接用localhost访问对方端口,其实确实可以,但是你得确认对方确实在监听那个端口。如果对方没起来,那访问就失败,这个和普通本机进程通信没有任何区别。我在项目里就遇到过,Sidecar容器日志转发器启动得比主容器慢,主容器已经写入了一批日志,结果Sidecar起来的时候只读到了部分文件。解决办法有几个,比较土的办法是让Sidecar启动时先扫描已有文件;比较优雅的办法是在主容器里加启动延迟,或者在Sidecar里做增量监听。

2.3 多容器Pod的现状与演进

这些年K8s社区对多容器Pod的态度也在变化。早期大家一股脑往Pod里塞容器,一个Pod恨不得塞五六个。后来发现,Pod里的容器数量越多,排障就越麻烦。比如你想看日志,Pod里有三个容器,你得指定-c容器名才能看到对应容器的日志。再比如kubectl exec进去执行命令,默认是进入第一个容器,你要操作第二个容器还得加-c参数。

所以现在社区有个趋势,把本来属于"强耦合"的多个容器尽量拆分到不同的Pod里,利用服务发现机制做解耦。但Sidecar这类强绑定的场景依然保留。像Service Mesh领域,每个业务Pod里都要注入一个Envoy代理容器,这个就是Sidecar模式的典型应用。

我个人在实际项目里的原则是:能用单容器Pod解决的事情,绝不用多容器Pod。多容器Pod不是不行,而是会带来额外的排障复杂度和安全风险(容器面大了,攻击面也大)。只有业务和Sidecar之间的绑定关系确确实实是"一荣俱荣一损俱损"的时候,才放到同一个Pod里。

3. Pod生命周期全解析

3.1 从创建到销毁的五种状态

Pod的状态是整个生命周期管理的核心观察窗口。用kubectl get pods看到的状态,其实是Pod的"阶段"(Phase),一共五种:

状态含义说明
Pending等待中Pod已被API Server接受,但容器还没运行。可能是镜像在拉取,也可能是调度器还没找到合适的节点
Running运行中Pod已经绑定到节点,至少有一个容器在运行或处于启动状态
Succeeded成功退出Pod中所有容器都正常执行完成退出,且没有重启
Failed失败退出至少有一个容器以非零状态退出
Unknown未知API Server无法获取Pod状态,通常是网络问题导致

Pending状态是实际项目中卡得最久的一个状态。镜像拉取慢、节点资源不足、持久卷没有绑定成功、调度器找不到满足亲和性要求的节点,都可能导致Pod长期停在Pending。我在某次项目上线时,有个Pod在Pending里卡了十分钟,排查下来是镜像仓库没有配置拉取密钥,导致镜像一直拉不下来。这个后面会详细说。

3.2 容器重启策略的三种选择

Pod级别的restartPolicy字段,控制的是容器退出后要不要重启,以及怎么重启。三种策略:

  • Always:不管容器以什么方式退出(正常退出还是异常崩溃),都会重启。这是默认值,Deployment管理的Pod通常用这个。
  • OnFailure:只有容器以非零状态码退出时才重启。正常退出(退出码0)不重启。
  • Never:容器退出后绝对不重启。

这里要特别注意一个误区:很多人以为重启策略是Pod崩溃了就换一个新的Pod重新拉起,其实不是。重启是在同一个节点上、同一个Pod里、重新创建容器实例,不是重新调度。如果你需要"Pod挂了就换个节点重新调度",那是控制器(比如Deployment)的职责,靠的是ReplicaSet的副本管理能力,不是restartPolicy。

用一句话总结:restartPolicy管的是Pod内部容器的"复活",控制器管的是整个Pod的"换人"。

3.3 探针机制——让K8s真正"感知"业务健康状态

这是Pod管理里最实用也最容易被忽略的内容。K8s有两种探针,解决不同的问题:

  • 存活探针(livenessProbe):判断容器是不是活着的。如果探针检测失败,K8s就会认为容器已经"死"了,按策略重启容器。这解决的是"进程还在但业务已挂"的问题,比如Java应用OOM后进程僵死。
  • 就绪探针(readinessProbe):判断容器是不是准备好接收流量了。如果检测失败,K8s会把Pod从Service的端点列表中踢出去,不再向它转发流量。这解决的是"刚启动但还没加载完配置"的问题。

实际项目中,就绪探针用的频率远高于存活探针。原因很简单:就绪探针失败只是暂时不转发流量,不会导致重启;存活探针失败就直接重启容器,如果配置的参数不合理,很容易引发"重启风暴"——容器还没起来又被探测到存活异常,再次重启,循环往复。

我见过最惨烈的案例是这么来的:某服务设置了存活探针,但忽略了应用启动时需要加载大量数据,启动过程超过30秒,探针设置的超时时间只有5秒。结果容器每次启动到一半就被探针判定为失败,触发重启,永远起不来。这就是"探针配置不当引发的雪崩"。

这里给一套我个人总结的探针配置安全参数:

  • 初次探测延迟(initialDelaySeconds):一定要大于应用的最长启动时间,建议在极端情况下测得的启动时间基础上再加30%的冗余。
  • 探测间隔(periodSeconds):默认10秒够用,别设太短,太短会放大误判风险。
  • 失败阈值(failureThreshold):至少设3次,给应用一点"喘息"的机会,避免单次网络抖动就触发重启或摘除。
  • 超时时间(timeoutSeconds):一般1到3秒,如果业务接口响应特别慢,可以适当放宽。

写Pod清单的时候,这三个探针相关的字段一定要反复对照检查。我在团队里定了一条规矩:凡是修改探针参数,必须走代码评审,不允许直接改线上配置。原因很简单,探针参数就是"生死开关",改错了比改代码还危险。

4. 资源清单的核心字段与手写实践

4.1 从零手写一个Pod资源清单

资源清单就是K8s的"施工图纸",写明白你要什么,K8s帮你做什么。我从一个最简的Pod清单开始拆解:

apiVersion: v1 kind: Pod metadata: name: web-pod namespace: default labels: app: web env: prod spec: containers: - name: web-container image: nginx:1.25 ports: - containerPort: 80 protocol: TCP env: - name: TZ value: "Asia/Shanghai" resources: requests: memory: 256Mi cpu: "0.5" limits: memory: 512Mi cpu: "1" livenessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: 80 initialDelaySeconds: 10 periodSeconds: 5 restartPolicy: Always

这份清单麻雀虽小五脏俱全。逐字段说:

  • apiVersion定义为v1,Pod是核心对象,这种版本最稳定。
  • metadata里给Pod打上标签(labels),后面Service、Deployment筛选Pod全靠这些标签。
  • spec.containers是核心,定义一个容器,用nginx镜像,监听80端口。
  • 容器里的env设置了时区,这是个细节但很重要,容器的默认时区是UTC,不做设置的话业务日志时间和本地时间对不上,排查问题非常痛苦。
  • resources定义了资源请求和限制,前半部分后面专门讲。
  • 探针两个,存活和就绪,都用HTTP请求方式。

这里有个必须记住的细节:spec字段里可以定义多个容器,只要containers列表后面接着写就行。但绝大多数新手写清单时会忘了一个东西——spec底下没有改template,这是直接用kubectl create -f创建裸Pod的场景。如果你用Deployment管理(实际项目中90%如此),需要把Pod定义包在spec.template里,这个后面会演示。

4.2 如何选择镜像拉取策略

镜像拉取策略imagePullPolicy有三个值:Always(总是拉取)、IfNotPresent(本地没有才拉取)、Never(绝不拉取)。它的默认逻辑是这样的:

  • 镜像标签是latest,默认Always。
  • 镜像标签不是latest,默认IfNotPresent。
  • 显式指定了imagePullPolicy,以显式值为准。

生产环境我会主动把imagePullPolicy设为Always。为什么?假设镜像标签是v1.0.2,看起来这是个固定版本,应该用IfNotPresent省点拉取时间。但问题是,如果你重新推送了一个镜像,且标签还是v1.0.2,同时节点上已经缓存了旧镜像,那IfNotPresent策略直接就用旧的,不拉新的了,这就会导致你发了个"假上线",服务跑的其实还是旧代码。这种问题极其隐蔽,等发现问题时,第一个排查方向往往还不在镜像拉取策略上。

我经历过的真实教训:某次发布新功能后,线上行为没变化,查了半天,发现是镜像标签没变,节点上缓存了旧镜像,拉取策略是IfNotPresent,K8s直接用了本地缓存。所以从那次以后,凡是涉及业务代码更新的镜像,全部用Always策略,从源头杜绝这种问题。

4.3 资源请求与限制的计算思路

这一小块是资源清单的重中之重,也是日常Review时最常发生争论的地方。

requests是"最低保障",告诉调度器这个Pod至少要这么多资源才会被调度到某节点。limits是"上限天花板",容器最多用到这么多资源,超过就会被限制或杀掉。

CPU资源这里有个和直觉相反的细节:CPU的requests和limits的单位是核,但写法上用的是"毫核"。0.5表示500毫核,1表示1核,100m表示100毫核。K8s调度的时候,节点上的CPU可分配总量除以所有Pod的requests总和,就是用率。

内存就好理解了,单位可以是Mi(二进制兆字节)或M(十进制兆字节),实际项目里统一用Mi。我强烈建议团队里统一规范,不要一会儿Mi一会儿M,排查资源问题时很容易算错。

给资源参数定值,关键不在于"参数怎么填",而在于"业务到底需要多少"。我的经验流程是这样:

  1. 压测阶段记录对应用例的CPU和内存峰值,取峰值作为limits的参考值。
  2. requests设为压测峰值的70%到80%,留出一定的调度余量。
  3. limits设为压测峰值的1.2到1.5倍,防止突发流量把容器打死。
  4. 上线后持续观察一周监控,根据实际数据再回头调参。

这里有个风险一定要讲:limits和requests设得太接近,会很危险。比如CPU limits设了1核,requests也设了1核,那么Pod被调度到节点后,节点上所有其他Pod的CPU请求加起来一旦达到节点总CPU,这个Pod就会被判定为"超卖",调度器会优先驱逐它。内存就更直接,超过limits直接被内核OOM杀掉,不会有任何商量余地。

所以我给团队定的基准是:requests和limits之间至少留20%的弹性空间,内存尤其如此,因为内存不像CPU可以压缩,一旦超限就是进程被杀。CPU的limits超过requests其实是允许的,在CPU竞争不激烈时Pod可以burst到limits值,这种弹性是K8s调度策略的一部分。

4.4 Labels与Selector——K8s世界的寻址系统

标签(Labels)是挂载在所有对象上的键值对,选择器(Selector)是"按标签筛选对象"的查询机制。这两兄弟是整个K8s组件解耦的基石。

比如我管理的系统里有多个业务模块,每个模块的Pod都打上了不同的标签:

labels: app: payment-api version: v1.0.2 env: production tier: backend

Service选择Pod的时候,就靠selector.app=payment-api来匹配。如果将来要做蓝绿发布,只要临时创建一个selector带version=v2.0.0的新Service,流量就能从v1切到v2,老Service还在,回滚也简单——把selector改回去就行。

有几个标签规范我建议团队必须遵守:

  • 标签命名统一用DNS子域前缀,比如app.kubernetes.io/name这类,避免后续工具链(比如Helm)识别不了。
  • 每个Pod至少要有app、env、version三个标签,环境区分(dev/staging/prod)靠env,发布版本定位靠version。
  • selector上不要写太多条件,写多了看着精确,但后续扩容、发布时稍微改个标签就把服务匹配断了。

5. Deployment控制器的使用与滚动更新策略

5.1 为什么企业项目几乎不用裸Pod

裸Pod(直接用kind: Pod创建的Pod)有一个致命缺陷:节点宕机或者Pod被误删,它不会自动重建。所有依赖Pod的"自我修复"能力,都来自控制器。企业项目里最常见的就是Deployment,它管理的是无状态应用。

Deployment的职责有三个:

  • 保证指定数量的Pod副本时刻在线(少了就自动补,多了就自动收)。
  • 支持滚动更新,发布新版本时逐渐替换旧版本。
  • 支持快速回滚,发布出问题时秒级回退到旧版本。

从运维角度说,裸Pod适合调试、验证配置,Deployment适合上生产。哪怕是跑一个最简单的nginx测试环境,我也建议用Deployment,因为这样才能真正体验K8s的"声明式管理"——你告诉它最终状态是什么样,它自己去搞定。

5.2 Deployment资源清单的完整解读

apiVersion: apps/v1 kind: Deployment metadata: name: web-app namespace: default spec: replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web env: prod spec: containers: - name: web image: nginx:1.25 ports: - containerPort: 80 resources: requests: memory: 256Mi cpu: "0.5" limits: memory: 512Mi cpu: "1"

注意几个关键点:

  • selector.matchLabels必须和template.metadata.labels匹配,这里的app: web是Deployment管理Pod的依据。如果两者不一致,Deployment创建出来的Pod根本管不住。
  • replicas声明目标副本数,Deployment会保证集群里永远有3个匹配标签的Pod在运行。
  • 滚动更新策略默认是RollingUpdate,还有一种是Recreate(先全部杀掉旧Pod再创建新Pod)。生产环境默认用滚动更新,因为它能做到不停机发布。

这里有个实际项目中特别容易踩的坑:修改Deployment的template字段才会触发滚动更新,修改replicas不会触发更新。很多人改了下副本数,发现Pod没有按预期滚动,一脸懵。其实滚动更新的触发条件是spec.template里的内容(镜像、环境变量、资源参数等)发生变化。副本数变化是"水平伸缩",不需要滚动。

5.3 滚动更新参数与发布策略

Deployment的滚动更新由这两个参数控制:

  • maxUnavailable:更新过程中最多允许多少个Pod不可用。
  • maxSurge:更新过程中最多允许新增多少个Pod。

这两个参数配合起来决定了发布速度和可用性。原则是这样的:

  • 如果希望发布速度快一点,把maxSurge调大,比如50%,意思是先多拉起一半的新Pod,确保新版本验证通过后再慢慢摘旧。
  • 如果希望发布过程中尽量稳定,把maxUnavailable调小,比如0%,意思是任何一个时刻都要保证所有旧Pod可用。

我建议生产环境用这两个值:maxUnavailable: 0,maxSurge: 25%。这样任何一个时刻都不存在Pod不可用(旧版全部在线),同时只多拉起四分之一的Pod做新版本验证,平滑度最好。

滚动更新真正发布出去的流程是:

  1. 用户修改镜像版本,提交Deployment。
  2. K8s创建一个新的ReplicaSet,副本数从0开始增长。
  3. 新ReplicaSet的Pod越来越多,旧ReplicaSet的Pod越来越少(由maxUnavailable和maxSurge共同控制节奏)。
  4. 等新ReplicaSet达到目标副本数、旧ReplicaSet缩减到0,更新完成。

如果发布过程中发现新版本有问题,一条命令回滚:

kubectl rollout undo deployment/web-app

这个命令会回到上一次的部署版本。如果要回滚到更早的版本,可以先看历史:

kubectl rollout history deployment/web-app

然后用指定版本号回滚:

kubectl rollout undo deployment/web-app --to-revision=2

我在项目里强烈建议团队在发布前先看一眼滚动更新历史,至少保存最近5个版本(通过spec.revisionHistoryLimit控制),别把历史清空,否则回滚时就抓瞎了。

5.4 Deployment的扩容与缩容操作

扩容缩容就一句话:改replicas。

kubectl scale deployment/web-app --replicas=5

但这种方式改的只是当前Deployment的配置,如果有CI/CD流水线,流水线重新部署时会按代码仓库里的版本覆盖掉手动扩容的副本数。所以生产环境的操作规则是:临时扩容(比如大促、流量高峰)用kubectl scale,长期变更必须改代码仓库里的YAML,保证"代码即真相"。

还有一个细节:如果Deployment的Pod经常因为资源紧张被杀或者调度失败,你要先看的不是replicas,而是节点资源利用率。扩容只能解决流量问题,解决不了资源瓶颈问题。资源不够,扩再多的副本也只是把问题转移到"调度失败"上。

6. Pod调试与故障排查的实战技巧

6.1 查看Pod状态和关键信息的正确姿势

排查Pod问题,第一步永远是看整体状态:

kubectl get pods -n <namespace>

结果里如果出现CrashLoopBackOff,说明容器启动后立马崩溃,反复重启。这个状态基本是代码问题(启动就抛异常)或者配置问题(环境变量不对、依赖服务没起来)。

接下来要看详细信息:

kubectl describe pod <pod-name> -n <namespace>

describe输出里最有价值的是Events部分,里面记录了这个Pod从创建开始经历的所有关键事件:调度到哪个节点、拉取什么镜像、什么时候容器启动、什么时候探针失败、什么时候被驱逐。很多问题看Events一眼就知道根因。

再深入一层看日志:

kubectl logs <pod-name> -n <namespace>

如果Pod里有多个容器,需要指定容器名:

kubectl logs <pod-name> -c <container-name> -n <namespace>

如果是容器启动后立即崩溃,可以加--previous看上一次崩溃前的日志:

kubectl logs <pod-name> --previous -n <namespace>

这个命令在CrashLoopBackOff状态下非常好用,能看到上一次容器实例在崩溃前输出了什么。

6.2 进入Pod内部排查的方法

kubectl exec -it <pod-name> -n <namespace> -- /bin/sh

如果Pod里有多个容器,同样要加-c指定容器名:

kubectl exec -it <pod-name> -c <container-name> -n <namespace> -- /bin/sh

很多基础镜像里没有bash,只有sh,所以进入容器常用的是/bin/sh。如果连sh都没有,那就需要先用kubectl cp把一个调试工具包拷贝进去,这种场景虽然少见,但我确实遇到过——某个极简镜像里连curl都没有,探针用的还是tcpSocket方式。

6.3 常见故障与排查思路速查表

故障现象可能原因排查方向
Pod一直Pending节点资源不足、调度约束无法满足用kubectl describe pod看Events里的调度失败原因
镜像拉取失败镜像仓库认证失败、标签不存在、网络不通手动docker pull验证,检查imagePullSecrets
CrashLoopBackOff启动命令错误、依赖服务没就绪、配置错误用kubectl logs --previous看崩溃前日志
探针失败导致重启initialDelaySeconds太短、探针路径不对检查探针配置和业务接口,确认启动时间
容器时间不对镜像默认UTC时区通过env设置TZ环境变量,或挂载时区文件
节点NotReady节点负载过高、kubelet异常上节点看kubelet日志,检查系统资源
Evicted节点内存不足触发驱逐调大requests或增加副本数,降低单Pod负载

这张表我贴在我们团队故障排查文档里,新同学上手遇到Pod异常,照着表查,大部分问题五分钟内能定位。

6.4 一个完整的排查过程实录

某次项目里,前端服务突然报502。我第一反应就是看Pod状态:

kubectl get pods -n frontend

(此处案例基于常见实践补充)

结果发现有个Pod状态是Running,但就绪探针一直失败,Service不往这个Pod转发流量。看下来源,是后端依赖接口响应变慢,前端服务的就绪探针请求后端接口超时,连续3次失败后被判定为未就绪,被踢出Service端点。

这个问题的根因其实不在前端服务本身,而在后端的性能瓶颈。但就绪探针的设计逻辑就是这样——你的服务如果依赖的下游有问题,它就应该被摘掉,因为此时的请求大概率也会失败。所以这个行为是正确且合理的。

处理方式分两步:

  1. 先解决后端瓶颈(扩容后端实例、优化SQL、加缓存)。
  2. 后端恢复后,就绪探针自动通过,Pod自动重新接入Service流量。

整个过程有一个关键点:就绪探针失败不会重启容器,所以业务不受影响,只是暂时不进流量。这就是为什么我强调生产环境就绪探针要配好,它本质上是一个"优雅摘除"机制。

7. 多环境资源清单的管理技巧

7.1 用Namespace隔离环境

企业项目通常有dev、staging、prod三套环境。最朴素的做法是三个Namespace隔离:

kubectl create namespace dev kubectl create namespace staging kubectl create namespace prod

然后每个环境的资源清单里,metadata.namespace写上对应环境名。

Namespace是逻辑隔离,不是硬隔离。同一集群里不同的Namespace之间可以互相访问(除非你用NetworkPolicy做了限制),所以不要以为Namespace就是安全边界。它主要是管理视角的隔离——资源配额、权限控制、按环境分组查看这些,依赖Namespace就很方便。

7.2 通过Kustomize管理不同环境变量

我不想推荐太复杂的方案,这里讲一种企业项目里用得最多的轻量方案——Kustomize。它的核心理念是:写一套基础YAML,用overlay去覆盖不同环境的差异。

举个例子。基础配置里是这样:

# base/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: web-app spec: replicas: 2 template: spec: containers: - name: web image: nginx:1.25 resources: requests: cpu: "0.5"

生产环境的overlay:

# overlays/prod/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: web-app spec: replicas: 5 template: spec: containers: - name: web resources: requests: cpu: "1"

通过kustomize build overlays/prod | kubectl apply -f -,生产的副本数变成5,CPU请求变成1核,其他配置跟基础一致。

这种方式的好处是:环境差异一目了然,不需要维护三套完全不同的YAML。新同学上手也能很快理解"base是共同部分,overlay是环境差异"的逻辑。

7.3 资源配额与限额管理

大团队里不同业务线共享一个K8s集群,如果不对每个Namespace做资源配额,很容易出现某个业务线把集群资源吃光的情况。需要用ResourceQuota对Namespace做限制:

apiVersion: v1 kind: ResourceQuota metadata: name: quoa namespace: dev spec: hard: requests.cpu: "4" requests.memory: 8Gi limits.cpu: "8" limits.memory: 16Gi pods: "20"

(此处yaml中name改为更安全的占位示例:quoa仅为示意,实际请按业务命名)

这样dev环境最多跑20个Pod,CPU请求总量不超过4核,内存请求不超过8Gi。超了怎么办?新建Pod会卡在Pending状态,并提示配额不足。这种"硬约束"在共享集群里是必须的,能避免不少运维事故。

另外还有个LimitRange,可以给Namespace里的Pod设置默认的requests和limits。这样就算有人写清单时忘了写resource字段,LimitRange也会自动补上默认值,防止"裸奔"Pod出现。

8. 我在企业项目里攒下的避坑经验

写到这里,把我在K8s Pod管理和资源清单方面踩过的重要坑,专门列一份清单。这些都是用真金白银换来的教训,比任何文档都有说服力。

第一,Pod里的进程必须以PID 1运行。容器镜像里如果用了类似entrypoint.sh这种包装脚本,脚本里用exec去拉起真正的进程,保证信号能传给业务进程。如果不用exec,脚本作为PID 1,永远收不到SIGTERM信号,K8s想优雅停止Pod时会超时然后强制kill,业务进程会被突然杀掉,数据一致性受影响。

第二,不要在Pod里配置公网IP或依赖固定IP。Pod的IP是动态的,重启一次可能就换了。如果业务里有写死某个Pod IP的场景,这代码迟早要出问题。正确做法是通过Service做服务发现,永远不关心Pod的IP。

第三,探针配置一定不要照抄网上的模板。网上的模板都是通用场景,你的业务启动时间、接口响应时间、依赖组件各不相同,必须实测后调参。我见过太多次"直接抄模板导致探针误杀"的案例。

第四,发布前一定要看kubectl rollout status。很多人发布完就走了,等到用户反馈才回头查。正确姿势是:

kubectl rollout status deployment/web-app -n <namespace>

(此处以常见操作补充)

这个命令会阻塞等待,直到滚动更新完成或者超时。滚动更新失败会返回非零状态码,这时候再回滚,可以把故障时间控制到最小。实际项目里,这块能帮你把发布事故的处置时间从半小时压缩到3分钟。

第五,命名规范要统一到"强迫症"级别。Pod名、容器名、Service名、Namespace名,所有资源命名都要一致规范。我在某次事故排查中遇到过,同一个服务在不同环境里的Pod名风格不一样,脚本里匹配Pod名的正则还要兼容N种格式,平白无故浪费了40分钟。从一开始就定好规范,后面能省非常多的排查时间。

第六,环境变量管理要集中。不要把敏感信息(数据库密码、API密钥)直接明文写在资源清单里。正确做法是用Secret保存敏感信息,用ConfigMap保存普通配置,然后通过envFrom或valueFrom引用。环境变量直接暴露在镜像清单里,任何一个能查看Pod配置的人都能看到,安全风险很大。

9. 从Pod到生产集群的最后一公里

Pod管理和资源清单,本质上是在回答两个问题:"你的服务以什么方式跑起来"和"K8s以什么方式管理它"。把这两个问题搞清楚了,后面学Service、Ingress、存储、监控就有了扎实的基础。

我在给团队新人做培训时,通常会让每个人都完整走一遍这个流程:

  1. 手写一个Deployment清单,包含两个环境变量、一份资源配额、一个就绪探针。
  2. 通过kubectl apply在测试环境部署。
  3. 改镜像版本号,观察滚动更新过程。
  4. 故意写错一个参数,触发回滚。
  5. 用kubectl describe和kubectl logs排查一个自己制造的问题。

这套流程走下来,对Pod管理和资源清单的理解基本就到位了。

最后再分享一个我个人的小习惯:每次写资源清单的时候,先写resources字段,再写探针,最后才写镜像版本。因为这个顺序对应的是K8s管理一个应用最重要的三个维度——资源保障、健康检查和版本发布。把这三个维度想清楚了,清单本身反而不容易错。

K8s的Pod管理和资源清单,看着枯燥,实际是日常操作里最频繁接触的部分。这篇从设计思路到故障排查都尽量讲了,完全按我的项目经验来写的,希望能帮你把这块基础打扎实。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进