
你的服务明明只用了 200MB 内存Pod 却因为 OOMKilled 被反复重启明明集群还有大量空闲节点新 Pod 却一直 Pending。这两个现象放在一起基本可以断定Pod 资源管理没做好。Kubernetes 里讲的 Pod 资源管理核心就一句话——用requests和limits告诉调度器你的应用需要多少 CPU 和内存让节点上的资源分配有据可依。它解决的是两个问题一是节点上多个 Pod 互相抢 CPU、抢内存谁都不好过二是资源预留过多导致集群整体利用率低下钱花了用不上。这篇文章我会从 requests/limits 的设计逻辑讲起把调度器怎么用这些值做决策、怎么配置资源配额、遇到 OOMKilled 和 Pending 怎么排查一次性说清楚最后再聊聊 2c4g 这种常见规格到底能撑多少并发。1. 资源管理的两条主线requests与limits1.1 CPU和内存两种完全不同的资源想弄懂资源管理先得知道 CPU 和内存的根本区别。CPU 是可压缩资源内存是不可压缩资源。这六个字决定了它们在被“用完”之后的表现完全不一样。CPU 被用完时进程只是被暂时搁置任务排队等待最多就是变慢不会崩。这就像一条单车道车多了就排队但车不会消失。内存被用完时系统没有地方存放新数据只能强制杀掉进程来腾地方。这就像宿舍满了新来的同学没地方住只能把某些人赶出去。所以 Kubernetes 对内存的限制远比 CPU 苛刻因为内存超限的直接后果是进程死亡而不是变慢。这个区别直接体现在 limits 的落实机制上。CPU limits 在底层是依靠 CFS 配额Completely Fair Scheduler quota来做的。比如你给容器设置了limits.cpu: 1默认情况下它在一个时间周期通常 100ms内最多能用满 1 个核心的配额100ms用完就节流等下一个周期恢复。所以 CPU 可以“超卖”节点上所有 Pod 的 CPU limits 总和可以超过节点实际核数只要不会长期同时打满问题不大。但内存不行limits.memory一旦设置cgroup 会直接限制容器的内存使用上限超过就触发 OOM Killer容器被杀没有商量余地。所以你看很多人在配置资源时习惯“CPU 随便设内存也随便设”这很危险。CPU 设错最多性能受影响内存设错直接线上故障。1.2 requests的“双重身份”调度依据与运行保障requests这个字段经常被误解成“我希望用的资源量”这么想就偏了。它真正的含义是调度器为这个 Pod 预留的资源量也是节点上资源分配账本里的固定支出。举个例子。一个节点有 4 个 CPU上面已经跑了三个 Pod它们的requests.cpu分别为 0.5、0.5、1合计 2。现在来了一个新 Podrequests.cpu是 3。调度器一算4 减去 2 只剩 2放不下 3直接跳过这个节点。哪怕这三个 Pod 实际占用都很低哪怕这个节点 CPU 使用率只有 10%调度器也不会把这个新 Pod 放上来。这就是所谓的“为未来预留”。所以requests的第一个身份是调度依据Kubernetes 调度器通过把所有容器的 requests 累加来判定节点是否有足够的“余量”容纳新 Pod。第二个身份是运行保障cgroup 会根据 requests 的值来设置 CPU 份额权重比如两个 Pod 的 CPU requests 分别是 1 和 2当节点 CPU 紧张时前者获得 CPU 时间的比例大约是后者的二分之一。这意味着 requests 设得越高Pod 在竞争中得到的时间片越多。这也是常见的资源浪费源头。很多人怕 Pod 被调度到差节点把 requests 设置得虚高比如实际只用 200m CPU却请求 2 个核。结果节点上堆了一大堆空转的预留资源真实负载不高新 Pod 却进不来整个集群的利用率被拉低。requests 不是越高越好而是越贴近真实使用量越好留一点合理的余量即可。1.3 limits一旦设置错后果完全不同与 requests 不同limits是硬性上限。它的作用不是预留而是限制——容器最多能用多少超了就采取强制措施。CPU 的 limits 超额后会节流这还能接受但内存的 limits 超额就直接杀进程。我见过太多线上事故应用本身正常只是因为内存 limits 设得比实际使用量还低运行一段时间内存涨上来直接 OOMKilled然后 Kubernetes 按照重启策略拉起容器再跑一会儿又被杀形成无限重启循环。这种问题隐蔽性极强因为看监控时容器一直 Running但业务时不时报错。反过来limits 设得过高也有问题。内存 limits 高意味着节点上所有容器的内存上限总和可能远超节点实际内存一旦多个容器同时冲高节点内存耗尽内核开始杀进程这时候谁在该节点上谁倒霉。而且杀的顺序还不一定是你想保留的那些容器。我个人的建议是先测出应用的真实资源画像再设置 requests 和 limits。具体来说先用不设 limits 的方式运行一段时间通过监控记录峰值然后按“峰值 × 1.2 到 1.5”作为 limitsrequests 设为“稳态均值 × 1.1 到 1.3”。这样既不会因为 limits 过紧被杀也不会因为 requests 虚高造成浪费。2. 调度器如何避免资源争抢从节点筛选到抢占机制2.1 调度器怎么判断节点“放不放得下”Kubernetes 的调度器做决策时不是看节点当前的实际使用率而是看节点上已经分配的 requests 总量。这个逻辑很关键它是“避免争抢”的第一道屏障。调度过程大体分两步。第一步是过滤Filtering把所有节点过一遍去掉那些不满足条件的。比如节点剩余可分配资源小于 Pod 的 requests直接剔除Pod 有节点选择器、亲和性、污点容忍等条件也要一一匹配。第二步是打分Scoring对剩下满足条件的节点打分按分数排序。默认的评分策略里节点上剩余资源越多的得分越高这样调度器倾向于把 Pod 放到负载较低的节点上实现资源均衡。这里要特别注意“剩余可分配资源”的计算方式。节点的总资源要减去系统预留如 kubelet 预留、驱逐阈值预留还要减去节点上所有已存在 Pod 的 requests 之和。所以如果你有几个 Pod 的 requests 设置得特别大它们所在的节点即使 CPU 使用率很低在调度器眼里也是“接近满载”的状态。排障时最直接的命令是kubectl describe node输出里有一段Allocated resources能看到 CPU 和内存的 requests/limits 的累计情况很快就能定位是不是 requests 设置不合理导致节点假性满载。2.2 “no preemption victims found”到底在说什么你可能在日志里见过这样一条事件no preemption victims found for incoming pod。这行字看起来像报错但其实是调度器的“日常自言自语”意思是一个高优先级 Pod 找不到能塞下的节点调度器决定尝试抢占preemption也就是赶走某些低优先级 Pod 给高优先级 Pod 腾地方结果发现无“受害者”可杀。什么时候会触发抢占当一个 Pod 因为资源不足而无法调度且它的优先级足够高时调度器会进入抢占流程。它先从节点上找优先级低于该 Pod 的 Pod然后设想把这些低优先级 Pod 全部驱逐后节点能否腾出足够资源容纳这个高优先级 Pod。如果能就执行驱逐如果不能就寻找下一个节点。no preemption victims found表示调度器遍历了一圈发现所有节点即使驱逐低优先级 Pod 也腾不出足够资源。最常见的原因是要调度的 Pod 太大比如 requests 直接要了 8 个 CPU而集群里所有节点即便全部空出来也塞不下或者节点上已有的 Pod 优先级都不低没有“软柿子”可捏又或者集群处于资源碎片化状态每个节点剩余一点点但凑不齐一个大 Pod 的需求。遇到这种情况排查方向很明确先把高优先级 Pod 的 requests 改小或者把它拆成多个小 Pod让它能落在不同节点上再检查节点规格是否真的满足需求最后看节点是否被系统预留占用了过多资源。记住预热词里那个“no preemption victims”它本身不代表你的集群坏了只代表当前调度器“想帮忙但帮不上”。2.3 QoS等级决定“先杀谁”节点内存不够时总得有 Pod 被牺牲。Kubernetes 用一个叫 QoS Class 的机制来决定“先杀谁”这个机制和资源管理直接挂钩。QoS 分三档Guaranteed、Burstable、BestEffort。最稳的是 Guaranteed。要求 Pod 内所有容器都同时设置了 requests 和 limits并且两者的值相等。这类 Pod 被系统认为是最重要的OOM 时的分数最低基本最后才被杀。其次是 Burstable只要容器设置了 requests 或 limits但不完全相等就算这一类。最惨的是 BestEffort所有容器都不设置任何 requests 和 limits完全没有资源保障节点一紧张第一个被清退的就是它们。这三档的差异很多人平时感知不到真到节点内存告急的时候才开始后悔。所以我每次跟别人强调如果你希望一个 Pod 长期稳定运行至少把它配到 Burstable 以上的等级关键业务直接上 Guaranteed。这不仅是资源设置的正确姿势也是运维上的安全线。优先级高低也值得留意。即使同一个 QoS 等级内Pod 的 OOM 分数还会受实际内存使用量影响用得多的人更早被杀。这符合直觉都到了生死关头先杀那个吃得最多的。所以别以为设置了相同 QoS 就完全一样实际内存占用越接近 limits 的 Pod越危险。3. 实操配置从Pod到命名空间的资源防线3.1 为Pod设置requests和limits的完整示例纸上谈兵到此为止直接上一个生产环境的配置示例。下面是一个 Deployment 的 YAML里面包含了完整的资源请求和限制配置。apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: production spec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.example.com/order-service:v1.2.0 ports: - containerPort: 8080 resources: requests: cpu: 250m memory: 512Mi limits: cpu: 500m memory: 1Gi readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 10几个关键点说一下。250m表示 0.25 个 CPU 核512Mi是 512 兆内存MiB。这里的 requests 是调度依据limits 是硬性上限。我给这个服务设置 requests 是实际使用量的中位数limits 大概是峰值的 1.5 倍留出足够缓冲。从上到下看这个 YAML我会额外关注两点。一是不要只给容器设置 limits 而不设置 requests因为这样可能导致调度器把 Pod 放到一个资源已经比较紧张的节点但运行时又限制得很死容易出问题。二是Pod 的 requests/limits 是所有容器之和如果一个 Pod 里有多个容器任何单个容器的配置不影响整体判断最终用于调度计算的是所有容器的总和。3.2 用LimitRange和ResourceQuota卡住集群资源滥用给单个 Pod 设置资源是基本功但一个团队很多人在用集群时总有新人忘了配资源或者一上来就写个 32Gi 内存。这时候需要两道“闸门”LimitRange和ResourceQuota。LimitRange 的作用范围是命名空间它给命名空间内所有 Pod 定义一个默认值和边界。比如下面的配置可以为命名空间内未显式设置 requests/limits 的容器自动填充默认值并限制单个容器不能超过 4Gi 内存。apiVersion: v1 kind: LimitRange metadata: name: default-resource-limits namespace: production spec: limits: - default: cpu: 500m memory: 512Mi defaultRequest: cpu: 250m memory: 256Mi max: cpu: 4 memory: 4Gi min: cpu: 50m memory: 64Mi type: ContainerResourceQuota 则是从命名空间总量上卡。比如限制 production 命名空间所有 Pod 的 CPU requests 总和不超过 10 个核内存 requests 总和不超过 20Gi。这样即使单个 Pod 配置得再夸张命名空间整体也有限。apiVersion: v1 kind: ResourceQuota metadata: name: production-quota namespace: production spec: hard: requests.cpu: 10 requests.memory: 20Gi limits.cpu: 20 limits.memory: 40Gi这两样配合使用基本能堵住大部分资源滥用口子。我自己在管理多租户集群时每个命名空间必配 ResourceQuota这是性命攸关的防线。没有它一个命名空间的无节制 Pod 就能把整个集群拖垮。3.3 2c4g的Pod到底能撑多少并发搜索热词里“2c4g的pod支持的并发量”被问得很多但这问题其实没有标准答案。2c4g 指 2 个 CPU 核、4GB 内存它能撑多少并发取决于应用是 CPU 密集型、IO 密集型还是内存密集型。给你一个估算思路。如果是典型的 Web 服务假设每个请求平均耗时 50ms并且主要是等待下游返回IO 密集。单个 CPU 核每秒大约能串行处理 20 个这样的请求1000ms 除以 50ms2 个核就是 40 QPS。这只是一个线程的算力如果服务是异步非阻塞模型或者使用了多线程并发量会成倍增加。但实际中请求不可能完全均摊在 CPU 上还有 JVM 或运行时开销通常打五折到七折。内存方面更要小心。4GB 内存系统预留和堆外内存占掉约 1GB剩下 3GB 给业务。如果每个请求产生 10MB 的对象300 个并发就可能把内存打满触发 OOMKilled。所以你会发现2c4g 的 Pod 有的服务能撑几千并发有的几百就崩核心在于应用的内存模型和响应时间。别迷信“2c4g 能扛多少”最好是压测。拿测试工具比如 wrk、k6、JMeter在生产规格的 Pod 上跑一轮打到内存使用率稳定在 limits 的 70% 左右记录此时的 QPS这就是靠谱的容量数字。3.4 一个Pod多个容器的资源分配细节Pod 是 Kubernetes 里最小的调度单元一个 Pod 里可以有多个容器它们共享网络命名空间、共享存储卷生命周期也被绑定在一起。但资源分配上有个细节值得单独强调调度时看的是 Pod 内所有容器的 requests 总和而不是单个容器。假设有个 Pod 里有两个容器一个业务容器 requests 512Mi一个辅助容器 requests 256Mi那调度器会为这个 Pod 预留 768Mi 内存。运行时限也是分开的两个容器各自的 limits 独立生效一个容器超限被杀不会因为另一个容器还有余量而幸免。另外多容器 Pod 的启动是串行的不是并行。kubelet 会按照 YAML 里的容器列表顺序逐个启动如果第一个容器因为镜像拉不下来或启动失败卡住了后面的容器会一直等待。生产环境里如果一个 Pod 因为容器启动失败长时间处于 ContainerCreating 状态从外面看就像整个 Pod 都起不来。这也是多容器 Pod 的一个痛容错性差一个容器出问题整个 Pod 的服务能力都会受影响。如果你在用 Kubernetes Dashboard 创建 Pod界面上确实有“创建”按钮可以粘贴 YAML 或填写表单来创建资源。但我要提醒一句别通过 Dashboard 直接创建裸 Pod。裸 Pod 没有 Deployment、StatefulSet 这类控制器管理节点故障后不会被自动重建。正确做法是通过 Dashboard 创建 Deployment或是直接粘贴 Deployment 的 YAML让控制器去管理 Pod 的生命周期。4. 故障排查实录争抢与浪费的典型表现4.1 Pod被反复OOMKilled问题出在哪OOMKilled 是资源管理里出现频率最高的故障现象是 Pod 一直 Running但业务间歇性报错或者容器反复重启。排查时第一步先看容器上次退出的原因kubectl describe pod pod-name重点关注Last State字段如果显示TerminatedReason为OOMKilledExit Code为 137那基本就是内存超限被杀。这时候去调内存 limits而不是去调代码。先看当前容器实际内存使用量kubectl top pod pod-name --containers如果实际使用量已经接近 limits说明 limits 设得太紧需要上调。如果实际使用量远低于 limits但日志里还出现 OOM那问题在节点层面很可能是节点本身内存压力太大内核在选择牺牲者时选中了这个容器虽然它没用满自己的 limits。这种情况下要查看节点内存情况kubectl top node node-name kubectl describe node node-namekubectl describe node里有一段Allocated resources和节点内存总量对比如果系统预留加上所有 Pod 的 limits 已经超过节点总内存那这个节点就是典型的“超卖过度”状态一旦多个 Pod 同时冲高内存必然有人被杀。解决思路是调整节点上 Pod 的 limits或者给内存压力大的节点上的部分 Pod 做迁移。4.2 Pod一直Pending到底卡在哪Pod 处于 Pending 状态最常见的原因就是调度器找不到合适的节点。先执行kubectl describe pod pod-name看 Events 段。如果是0/3 nodes are available: insufficient cpu, insufficient memory说明节点剩余资源不满足 Pod 的 requests。此时有两个方向一是看 Pod 的 requests 是不是设高了二是看节点是不是真的没资源了。用kubectl describe node看一下每个节点的Allocated resources通常你会发现两种情况。第一种是节点总资源确实用完了比如 CPU requests 合计已经打满这时要扩容节点或者把部分 Pod 的 requests 调低。第二种是节点资源明明很多但 Pod 还是 Pending这就不是资源问题而是调度约束问题比如这个 Pod 设置了nodeSelector选中的节点恰好没资源或者节点上有污点Pod 没有对应容忍又或者 PVC 一直没绑定成功调度器认为不满足条件。还有一类隐蔽的 Pending 原因命名空间配额。如果你配了 ResourceQuota并且命名空间内已有的 requests 总量已经达到配额上限新 Pod 即使节点上有资源也会被配额卡住事件里会提示exceeded quota。这种问题从节点层面看不出来得去查命名空间的 quota 使用情况kubectl get resourcequota -n namespace kubectl describe resourcequota quota-name -n namespace4.3 容器启动失败会拖累整个Pod吗回到热词里的那个问题一个 Pod 里如果有多个容器其中一个没成功启动会影响其他容器吗答案是会影响而且影响的方式很直观。首先Pod 内容器的启动是串行的顺序按 YAML 里的容器列表排列。第一个容器如果因为镜像拉取失败ImagePullBackOff或启动后崩溃CrashLoopBackOff后续容器不会开始启动。从外观看 Pod 会一直卡在 ContainerCreating 或 PodInitializing 状态探针也不会执行。其次就算是已经启动成功的容器如果后续某个容器一直失败整个 Pod 的生命周期也由失败容器主导。因为 Pod 的重启策略是作用于整个 Pod 的只要有一个容器反复失败就会按 restartPolicy 触发整个 Pod 的重启。已经正常运行的容器也会被一起重启造成不必要的抖动。所以在 Pod 里塞多个容器要非常克制只有在“逻辑上不可分割、必须同生共死”的场景下才适合多容器。比如主业务容器 sidecar 日志收集、主业务容器 网络代理。至于那些可以独立运行的组件拆成独立的 Deployment 更好隔离故障域互不影响。4.4 几个排查资源问题的高效命令最后分享几个我平时排查资源问题的高频命令都是实测好用的。# 查看所有节点的资源使用情况 kubectl top nodes # 查看所有 Pod 的资源使用情况 kubectl top pods -A # 查看节点资源分配账本requests/limits 累计 kubectl describe node node-name # 查看事件按时间排序 kubectl get events --sort-by.lastTimestamp # 查看 Pod 被调度到哪个节点及容器实际使用量 kubectl describe pod pod-name如果你嫌命令行麻烦可以装一个 k9s终端里的可视化工具按:输入资源类别就能快速切换查看资源使用、事件、日志都很方便。另外metrics-server 一定要装否则kubectl top是跑不出来的。Dashboard 上也能看到基本的 CPU/内存使用趋势但排查深层问题还是命令行更直接。写在最后资源管理这事儿说难不难说简单也不简单。核心就是把 requests 当成“占座位”limits 当成“饭量上限”两者的设置既要贴近真实使用量又要留出冗余。踩过几次 OOMKilled 和 Pending 的坑之后我现在的习惯是先压测拿到资源基线再落到 YAML 里最后用 LimitRange 和 ResourceQuota 兜底基本能把大部分资源问题挡在发生之前。希望这篇能帮你少走点弯路。