尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

K8s资源管理与项目生命周期:从Requests到Pod驱逐的治理实践

K8s资源管理与项目生命周期:从Requests到Pod驱逐的治理实践 今天想接着K8S系列聊一个平时最容易“先跑起来再说”的部分K8s资源管理与项目生命周期。很多人已经把Pod、Deployment、Service这些玩得很顺了但一遇到集群里频繁出现Pod被驱逐、节点CPU被打满、某个项目悄悄吞掉了整个测试环境的资源就开始觉得K8s很“玄学”。其实大部分问题都不是K8s本身玄而是资源管理这一层没有跟项目的生命周期绑在一起从创建命名空间到发布版本、扩容缩容、最后回收环境资源策略应该是贯穿全程的线。这篇内容适合两类人一类是把K8s集群搭起来但不知道怎么给业务合理分配资源的运维/研发同学另一类是承担项目交付、需要在同一套K8s环境里管理多个应用的技术负责人。我会先把K8s资源体系的关键概念讲透再用一个LNMP类业务场景做实际拆解最后把POD资源异常时常用的故障排查链路和我的个人经验一起放出来。你会发现资源管理不是加几个YAML参数那么简单它决定的是一个项目从生到死能不能被可预期地控制。1. 资源管理在K8s里的真实含义从单机参数到集群调度的差别1.1 从Docker过来的人最容易被requests和limits坑到先说一个我经常被问到的问题K8s和Docker到底有什么区别如果只从资源管理角度回答我会说区别非常大。Docker里面写--memory512m --cpus1它做的是单机维度的事情启动容器时限制这个容器能吃得下多少内存和CPU超了就报错或者被杀掉。但K8s里同样出现requests和limits含义却完全不同因为在K8s中Pod是运行在“整个集群”里的不是运行在一台机器上的单机容器。kubectl run或者Deployment里的资源声明会同时影响调度器、kubelet、节点剩余资源判断、驱逐机制甚至会影响水平扩缩容HPA的计算。换句话说Docker参数是给容器运行时看的K8s资源声明是先给“大脑”kube-scheduler看的再给“手脚”kubelet执行的。很多从Docker一键脚本迁移到K8s的项目第一个坑就是把所有容器都扔进一个没有资源声明的Pod里等到节点上内存被某个业务Pod吃光其他Pod开始批量OOMKilled的时候才意识到资源管理不是可选项是K8s调度和稳定性的地基。1.2 一张图看清K8s中的核心资源对象在K8s里资源不只有CPU和内存两种。实际做容量规划时你会接触到的常见资源如下表资源类别配置文件里的常见字段集群如何处理CPUrequests.cpu/limits.cpu单位mrequests用于调度保障limits用于CPU时间片限制超过会节流而不是被杀内存requests.memory/limits.memory单位Mi/Girequests保障基本可用limits触发后进程有可能被OOM Killer杀死临时存储requests.ephemeral-storage控制emptyDir、容器可写层、镜像层占用的本地磁盘扩展资源nvidia.com/gpu等自定义资源需要有Device Plugin注册否则节点不会承认有这种资源网络带宽K8s原生不直接暴露一般通过CNI网络插件、Ingress Controller或者是QoS策略间接管理这张表建议存下来因为很多新人在做Pod配置的时候只写CPU和内存完全忘了临时存储。结果经常是某个Pod疯狂往emptyDir里写日志短时间内把节点根分区写满整个节点的kubelet开始报DiskPressure最后触发Pod驱逐。这里的核心逻辑是CPU属于可压缩资源内存和磁盘属于不可压缩资源。可压缩资源用量再高也只是让进程变慢不可压缩资源一旦超了系统必须用“牺牲”来换稳定而牺牲谁K8s有一套严格规则。2. Requests、Limits与QoS为什么系统优先丢掉“不确定性”2.1 Scheduler眼中的NodeK8s的调度器在给Pod找节点的时候看的并不是这个节点现在实际用了多少CPU和内存而是看这个节点上“已经被申请的requests总和”还剩多少。这一点特别关键。举个例子节点A是8核16G节点B也是8核16G。节点A上面跑了一堆没有写requests的Pod节点B上跑了一堆写了requests的Pod。调度器在调度新Pod时会认为节点B的剩余资源更少甚至可能把新Pod调度到已经实际快满的节点A。因为在K8s的模型里不写requests就等于你告诉集群不需要任何资源保障。这类Pod在节点上是“最不重要的”一旦节点内存开始紧张驱逐时先死的大概率就是它们。所以要做一个合理的Deployment至少要给每个容器都写上requests.memory和requests.cpulimits可以等于或略高于requests但不要只写limits不写requests。因为limits影响的是“单容器最多能用多少”requests影响的才是“调度器是否愿意把Pod放上来”。2.2 三个QoS等级Pod先被牺牲的顺序K8s根据容器是否设置requests和limits把Pod分成三种服务质量等级缩写是QoS Class。我做了个表方便你判断自己写的配置会被系统怎么对待QoS Class判断条件典型配置示例GuaranteedPod里每个容器都同时设置了CPU和内存的requests与limits且requests等于limitscpu: 500m/cpu: 500mmemory: 512Mi/memory: 512MiBurstable不满足Guaranteed但至少有一个容器设置了requests或limits只写requests不写limits或requests小于limitsBestEffort所有容器都没有设置requests和limits裸写image: nginx不加任何资源声明这三个等级的实际影响主要体现在节点资源紧张时。一般来说BestEffort的Pod会被最先考虑驱逐因为它没有声明任何资源保障接着是Burstable里实际用量超过requests的Pod最后才是Guaranteed Pod。但要提醒一点这不是说Guaranteed Pod就不会被驱逐如果整个节点的物理内存都被系统进程或某些非Pod占满Guaranteed Pod也可能被Kubelet强制清理。只是从设计意图上看K8s优先保护的是那些“守规矩”的Pod。写了资源声明就是明确告诉调度器和驱逐器这个Pod需要多少资源哪些Pod在压力下可以被牺牲。这个机制有点像紧急救灾时的分级保障你什么都不报系统默认你随时可以撤离。2.3 LimitRange和ResourceQuota给项目划边界如果说requests和limits是对Pod个体的要求那LimitRange和ResourceQuota就是对整个项目空间的要求。ResourceQuota可以限制一个命名空间下累加的CPU、内存请求总量、Pod数量、PVC数量等等。LimitRange则规定了命名空间内单个Pod或容器的最小、最大资源值并且能设置默认的requests和limits。我之前见过最典型的场景是一个研发团队在同一个命名空间里不断创建测试应用有人随手copy了一份Deployment但忘了写resources字段结果Pod每次都能被调度。后来生产环境出现内存吃紧排查时才发现某个命名空间里几十个Pod全都没有资源声明。用ResourceQuota兜底以后创建Pod时如果超过整个命名空间的配额API Server会直接拒绝报错信息会明确告诉你当前配额和已使用量。一个相对可用的ResourceQuota长这样apiVersion: v1 kind: ResourceQuota metadata: name: demo-namespace-quota namespace: demo-project spec: hard: requests.cpu: 8 requests.memory: 16Gi limits.cpu: 16 limits.memory: 32Gi persistentvolumeclaims: 10 pods: 50加上这个对象之后demo-project这个开发命名空间最多只能申请50个Pod、16Gi内存requests和32Gi内存limits。业务方再怎么误操作影响范围也被锁死在命名空间内部。3. 项目生命周期从Namespace规划到版本化交付与回收3.1 Namespace、Label、Annotation是资源治理的第一条线每个新项目进来我都会先和负责人确认三样东西项目代号、维护团队、资源上限。然后不会直接让开发自己创建命名空间而是由基础设施这边先定义好一套标准模板再apply。Namespace是项目生命周期的最外层边界。项目名、测试环境、生产环境建议都通过不同的Namespace隔离而不是靠Deployment名字区分。原因很直接资源配额是绑在Namespace上的NetworkPolicy也是绑在Namespace上的不同项目混在一个Namespace里后面不管做成本统计还是安全隔离都会非常痛苦。除了NamespaceLabel和Annotation也要在一开始就约定好。Label用于K8s的selectorDeployment如何选择Pod、Service如何路由流量全靠它。Annotation则适合放一些“给人类和运维系统看”的元数据比如项目负责人、维护群、创建时间、费用归属。我给生产Namespace打的标注一般类似metadata: annotations: owner: sre-platform project: user-center environment: prod sla: p1Label和Annotation不是资源配额那种强制约束但它们决定了项目在生命周期中能不能被快速检索、能不能被自动化工具识别重要性不低于CPU内核数。3.2 Deployment滚动升级、版本回滚与变更Resources项目运行起来之后最常见的生命周期动作是升级和回滚。而在K8s里升级不只是换一个镜像tag还涉及新的Deployment spec中资源声明会参与调度决策。我习惯把Deployment里的resources字段单独拿出来和镜像版本一起纳入代码仓库的版本管理。有人可能会问为什么连requests和limits都要跟着镜像版本一起走因为一个业务镜像从v1升到v2可能有新增线程池、增大Java堆内存、增加缓存模块等变化如果资源声明还在用旧方案很容易出现Pod能启动但运行一段时间就被OOMKilled的情况。更合理的做法是用kubectl rollout restart deployment/user-center触发一次普通重启或者用kubectl set image deployment/user-center user-centerregistry.local/user-center:v2来改镜像。如果想要回滚用kubectl rollout undo deployment/user-centerK8s会自动回到上一个有历史记录的ReplicaSet版本。注意rollout history可以看变更记录但如果你在Deployment里改了resources而没用新版本标签前面的记录会混在一起。这个坑我在一次上线时踩过旧Pod发现资源不够用但回滚后大家发现回滚的还是同一份资源文件折腾了半天。3.3 节点维护与项目下线cordon、drain和Finalizer项目生命周期不只有上线和升级还有下线。尤其是在公共服务集群上一个项目结束后如果没有把资源还回去就是在持续消耗真实成本。节点维护的场景中最常见的是要对某台物理机/虚拟机做重启、补丁或者排查异常。我一般先执行kubectl cordon node-name让它不接受新Pod调度再执行kubectl drain node-name --ignore-daemonsets --delete-emptydir-data把已有Pod平滑驱逐到其他健康节点。做完之后节点上的Pod会重新调度如果业务有PodDisruptionBudget驱逐过程还会参考这个约束避免一次性把多个副本全部杀掉。至于项目整体下线很多人执行了kubectl delete namespace demo-project之后发现命名空间一直卡在Terminating状态查了半天才发现是有其他API对象创建了Finalizer。Finalizer就像对象的一个“收尾回调”在真正删除资源前会等待关联资源清理完毕。常见的情况是PVC绑定到PVPV上有保护Finalizer如果你直接删NamespacePVC被占用的消息还没处理完删除流程就卡住了。我的经验是先找到并删除依赖资源再清Namespace。可执行下面几步# 先看看项目里还有哪些资源对象 kubectl get all -n demo-project kubectl get pvc -n demo-project kubectl get pv # 有状态服务先删StatefulSet再删PVC kubectl delete statefulset/mysql-demo -n demo-project kubectl delete pvc># 第1步创建Namespace和资源配额 kubectl apply -f 01-namespace.yaml kubectl apply -f 02-resourcequota.yaml # 第2步创建配置和存储 kubectl apply -f 03-configmap.yaml kubectl apply -f 04-pvc.yaml # 第3步创建工作负载 kubectl apply -f 05-deployment-nginx.yaml kubectl apply -f 06-deployment-php.yaml kubectl apply -f 07-statefulset-mysql.yaml # 第4步创建Service和Ingress kubectl apply -f 08-service.yaml kubectl apply -f 09-ingress.yaml这个顺序背后是有逻辑的。Namespace和ResourceQuota是一等公民必须先存在ConfigMap和PVC是工作负载运行所需的配置和存储如果缺失Deployment的Pod创建后很可能会找不到配置而CrashLoopBackOffMySQL这类StatefulSet比普通Deployment复杂需要保证PVC已经被正确创建。如果反着来比如先创建Deployment再创建PVCPVC的动态供给倒也能工作但异常恢复时逻辑会很难排查。4.3 扩容、缩容和HPA资源声明如何决定弹性上限LNMP项目运行一段时间后访问量上升Nginx和PHP的CPU使用率会先抬头。这时候通常有两个选择手动改Deployment副本数或者配置HorizontalPodAutoscalerHPA。有个细节非常关键HPA计算的是Pod的CPU利用率时是用实际CPU使用量除以Pod的requests.cpu。换句话说如果你的requests.cpu设得太高比如业务实际只用100m你却写了300mHPA会认为CPU利用率只有33%可能永远不会触发扩容。反之requests设得很低比如50m但实际瞬间就到500mHPA可能还没等到扩容完成POD已经因为延迟上升被拖垮了。所以想让HPA真正稳定工作至少要做到requests.cpu尽量贴近业务常规水位而不是贴近业务峰值limits.cpu可以大于requests但不能大得离谱否则会出现“扩容条件永远不满足”的状态。下面是一个相对常见的HPA配置示例apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: php-fpm-hpa namespace: demo-project spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-fpm minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70我只给php-fpm和nginx开了HPAMySQL是StatefulSet一般不太适合随意弹性缩容。数据库扩容靠的是垂直升级资源和增加缓存/读写分离架构而不是无脑把副本数调高否则会造成数据一致性问题。5. 故障排查资源告警和恢复手段5.1 Prometheus、Node Exporter、Grafana与磁盘告警资源管理的最后一道防线是监控。没有监控很多问题只能在用户反馈之后才被发现。现在多数团队用Prometheusnode-exporterGrafana这套组合node-exporter负责从节点上采集CPU、内存、磁盘、网络等指标Prometheus负责存储和告警判断Grafana负责展示。配置监控的时候有一个容易漏掉的地方Pod层面的资源指标需要metrics-server或者Prometheus Adapter来提供node-exporter本身只提供节点指标它不关心Pod。你要是想看每个Pod实际占用多少CPU先确认集群里有没有部署metrics-server否则kubectl top pod会直接报错。磁盘告警规则我自己常用的大概长这样groups: - name: kubernetes-node-exporter rules: - alert: NodeDiskUsageHigh expr: (1 - (node_filesystem_avail_bytes{mountpoint/} / node_filesystem_size_bytes{mountpoint/})) * 100 85 for: 5m labels: severity: warning annotations: summary: 节点磁盘使用率超过85% description: 节点 {{ $labels.instance }} 磁盘已用 {{ $value }}%这个表达式表示当根分区使用率超过85%并持续5分钟Prometheus就通过Alertmanager发告警。阈值不建议直接设到95%因为磁盘写满的连锁反应很严重90%以上往往已经来不及从容清理了。5.2 Pending、OOMKilled、CrashLoopBackOff的完整排查链路一个Pod创建后停留在Pending是最常见的资源调度问题。这时候先别愣着直接用describe看事件kubectl get pod pod-name -n demo-project kubectl describe pod pod-name -n demo-projectdescribe输出里如果Events中有0/6 nodes are available: 3 Insufficient memory, 2 Insufficient cpu...那说明Pod的requests超出了所有可用节点的剩余容量。这时候别急着调limitslimits不影响调度只有requests影响。要降就降requests或者扩容节点或者清理集群里已经不需要但还占着requests的Pod。如果Pod状态是OOMKilled往往不是调度问题而是运行期超了memory limit。可以执行kubectl logs pod-name --previous查看容器被杀前输出通常能看到Java、Node这种进程在内存不足时的错误日志。如果Pod一直是CrashLoopBackOff最容易被忽略的原因是它每次起来都因为livenessProbe失败而被K8s重启但另一个重要原因就是LimitRange给容器注入了一个过小的default limits导致应用进程还没起来就被OOM。这里提供一个我自己的排查顺序Pending先查调度事件OOMKilled先查limits和Pod进程CrashLoopBackOff先查启动命令和健康检查。你可以先执行kubectl get events --sort-by.lastTimestamp按时间排序查看事件流这样往往比单纯看Pod状态更早发现根因。5.3 只看进程还正常CPU却被节流该怎么判断有一种故障现象很隐蔽Pod状态是Running应用日志没有报错但接口延迟变得很高。查了半天才发现是CPU limit生效了。当容器CPU使用量超过limits.cpu时K8s不会杀掉容器而是会限制CPU时间片。换句话说容器被“节流”了它想跑但内核不给它分更多CPU所以请求只能排队等待。判断方法之一是进入容器查看cgroup统计信息比如kubectl exec -it pod-name -n demo-project -- /bin/sh # cgroup v2环境下可检查 cpu.stat cat /sys/fs/cgroup/cpu.stat # cgroup v1环境下可检查 cpu.stat cat /sys/fs/cgroup/cpu/cpu.stat如果nr_throttled一直增长说明容器确实在持续被限制CPU。更直观的方法是看服务所在的Deployment是否一直处于高延迟但CPU使用率打到limit上限。这类问题不能光加副本要审视是不是代码有死循环、线程池配置是否过大、以及limits本身是不是给得太低。6. 项目资源预算、成本换算与落地Checklist6.1 把资源请求翻译成成本和容量资源管理如果没有成本意识最后很容易变成“越加越多”。我在管理一套K8s集群时会把集群总容量和业务申请总量放在一起算一个资源分配率# 查看总节点容量 kubectl describe nodes | grep -E cpu|memory | head -20 # 查看命名空间累计配额和已使用量 kubectl describe resourcequota -n demo-project # 查看节点实际分配情况 kubectl describe nodes | grep -A4 Allocated resources假设一个节点是8核16G如果上面所有Pod的requests加起来已经达到6核12G我会至少保留2核4G给系统进程、kubelet和突发请求不建议把requests累到和节点总容量完全相等。因为系统运行本身也要占资源而且Pod实际使用量会偶尔高于requests。更科学的做法是给每个节点预留5%-10%的不可分配资源再结合节点上的DaemonSet比如日志、网络插件、监控Agent等算出一个实际可分配容量。成本换算时我更关注requests而不是实际占用因为requests代表了一段不可被其他Pod抢占的资源。如果你给10个Pod各申请了2Gi内存但是它们实际只用512Mi从成本计费角度你仍然应该按20Gi来算这个业务占用的量因为调度器已经给它们预留了20Gi。优化成本的最好方式就是尽量让requests贴合真实水位而不是为了防止OOM盲目调大。6.2 一套我沉淀下来的资源管理清单项目交付落到实际执行时我会在发布清单里反复检查下面这些项少一项都不放心Namespace是否创建并绑定ResourceQuota和LimitRange每个工作负载的resources字段必须同时存在requests和limits不允许裸奔StatefulSet/PVC在删除前要人工确认数据备份Deployment的strategy.rollingUpdate是否设置了maxUnavailable和maxSurge避免滚动更新把服务全部短暂停掉所有需要对外访问的Pod是否有对应的PodDisruptionBudget避免节点维护时副本被一次性驱逐HPA至少要接一个CPU或内存指标并且扩缩容阈值按requests算而不是按limits算Prometheus中有节点磁盘告警并按业务线和命名空间分组Annotation里记录项目负责人和紧急联系方式否则三个月后没人知道该找谁处理资源清理。这个清单不是写在规范文档里的漂亮话而是我实际踩过多次坑以后总结出来的。很多环境走到最后变得一团乱麻都是因为项目创建初期没有花十分钟把这些基础治理做起来。K8s能把一件复杂的事自动化但它不会替你判断一个项目该占多少资源、什么时候该释放资源。真正决定集群长期稳定的人仍然是那个在创建Namespace之前肯多想一步的工程师。
返回列表