☰
K8s压测资源不足怎么处理:分层排查与容量规划实战
2026/10/26 5:03:52 网站建设 项目流程

上周三凌晨两点,压测群里有人贴了一张截图:脚本里的并发从 500 往上推,推到 800 就推不动了,QPS 曲线像撞了天花板一样平掉,可服务端各项监控指标看起来还算健康,唯独kubectl get pods里横七竖八躺着六个 Pending。这就是「压测 k8s 服务资源不足怎么处理」这个问题的经典开局——注意,现象叫压不上去,原因却未必是资源不足。我见过太多次把压测机自己跑满 CPU、或者把数据库先打穿,然后一群人围着应用 Pod 加副本的情况。

这篇东西想讲清楚三件事:一是把「资源不足」这四个字拆成可定位的层次,二是从压测口径出发把容量算出来而不是拍脑袋,三是把现场救火和事后治理分开做。适合正在做全链路压测、容量规划、服务上容器之后遇到扩容瓶颈的同学,无论你是刚开始接手 k8s 业务,还是已经踩过几轮坑,应该都能从里面找到能直接抄的东西。

1. 先别急着加机器:把「资源不足」拆成四个层次

1.1 调度层的不足和运行层的不足,症状完全不同

我刚接触这块的时候,最大的误区是把所有「压不上去」都归成资源不够。实际上在 k8s 上,资源相关的故障至少分布在四个层次,每一层的表征、排查命令、解法都不一样。分清楚层次,能让排查时间从半天压缩到十分钟。

层次典型现象第一手排查手段常见解法
调度层Pod 卡在 Pending,副本数上不去kubectl describe pod看 Events补 requests 匹配的节点、拆碎片、去亲和约束
运行层Pod 起来了但反复重启,Exit Code 137kubectl describe pod看 Last State、kubectl top pod调内存 limit、调 CPU limit、修探针
节点层节点 NotReady、Pod 被驱逐、DiskPressurekubectl describe node看 Conditions清磁盘、扩节点池、调 kubelet 预留
链路层Pod 一切正常,就是 QPS 上不去、错误率高容器内看 conntrack、fd、端口、内核参数调 sysctl、改长连接、加中间层

调度层的不足是最容易识别的,因为 k8s 直接把答案写在 Events 里了。运行层麻烦一点,因为它表现为「Pod 明明在跑但一直被干掉」。节点层最隐蔽,因为可能只是某一个节点触发了磁盘压力,然后 kubelet 开始驱逐 Pod,你在应用侧只看到副本数忽上忽下。链路层最容易被误判,因为 CPU、内存监控全都正常,但吞吐就是上不去。

我一般会按这个顺序过一遍:先看 Pod 是不是全部 Running 且 Ready,再看副本数有没有达到期望值,再看有没有重启记录,最后才去看内核和网络层。这个顺序能把 80% 的问题在前两步定位掉。

1.2 压测口径没对齐,资源永远算不准

这是我最想强调的一点。压测目标如果只写了「扛住 5000 QPS」,那这个数字几乎没有工程意义。同样叫 5000 QPS,短连接和长连接对服务端的资源消耗能差出三四倍,请求体 200 字节和 200KB 差得更离谱。

做容量计算之前,至少要把下面这几件事定死:

  • 流量模型:读写比例、缓存命中率、请求体与响应体的平均大小和 P99 大小。
  • 连接模型:长连接还是短连接,是否复用连接池,是否走 TLS。
  • 并发模型:是固定并发还是固定 RPS,有没有思考时间(think time)。
  • 数据状态:冷数据还是热数据,缓存预热做没做。这一条坑最多,没预热就开压,第一波流量直接穿透到数据库。
  • 爬坡曲线:是阶梯加压还是瞬间打满。瞬间打满对自动扩容类机制极不友好。
  • 持续时间:跑 3 分钟和跑 3 小时,内存泄漏、连接泄漏、日志堆积的暴露程度完全不是一个量级。

这些参数定完,才能用利特尔法则(Little's Law)把并发和 QPS 互转:并发数 ≈ QPS × 平均响应时间。举个例子,目标 2000 QPS,平均响应时间 50ms,那稳定状态下同时在处理的请求大约就是 100 个,这是你配置线程池、连接池下限的参考值。但注意这个公式只适用于稳定态,压测爬坡阶段和排队积压阶段是不成立的,别拿它当万能钥匙。

还有一个经常被忽略的口径问题:压测流量打的是不是同一批实例。如果你的服务在 Service 后面挂了 20 个 Pod,压测只打到其中 5 个(比如因为会话保持或者长连接没有重新负载),那你测出来的单副本能力和集群总能力会严重失真。我习惯在压测前先确认每个副本的 QPS 分布是否均匀,kubectl top pod扫一眼就能看出来谁在摸鱼。

2. 压测前的容量基线怎么算出来

2.1 从单副本能力反推集群总资源

跳过单副本能力测试直接上集群压测,是我见过最浪费时间的做法。因为集群压测出来的数字是一锅粥,你不知道瓶颈在应用、在调度、还是在某个中间件。

正确的做法是先把单副本压到拐点。具体操作是:只保留一个副本(或者固定路由到一个副本),逐步加压,记录每一档的 QPS、P99、CPU usage、内存 RSS。所谓拐点,就是 P99 开始非线性上涨的那个点。比如实测数据是这样:

并发QPSP99 (ms)CPU 使用 (核)内存 RSS (MiB)
50180281.6620
100340312.9680
150380453.4720
2003951203.8780
2504003803.9840

从这个表能读出来的信息比一行「单副本 400 QPS」多得多:400 QPS 之后 CPU 已经接近 4 核上限,P99 从 45ms 跳到 120ms,说明瓶颈在 CPU 而不是在内存。这时候如果目标是 3000 QPS,粗算就是 3000 / 380 ≈ 8 个副本,再按 25% 余量取整到 10 个副本。

对应的资源账是这样的:CPU request 每个副本 2 核,10 个副本就是 20 核 request;CPU limit 每个 4 核,总 limit 40 核。调度看的是 request 总和,节点实际承载看的是 usage,这两笔账要分开算。集群里必须有至少 20 核可分配的 CPU,同时物理核数要能兜住峰值 usage,按 65% 水位反推,物理核数最好在 30 核以上。

内存这边同理。单副本 RSS 峰值 840MiB,但要注意这只是 RSS,不含 JVM 的堆外、线程栈、直接内存缓冲区。如果是 Java 服务,容器内存 limit 一般按「堆大小 ÷ 0.7」来估,比如堆设 2G,limit 给 3G 左右比较稳。压测期间如果看到 137 退出码,十有八九是这一步估少了。

2.2 requests 和 limits 到底怎么取值

这两个字段的区别值得单独讲,因为很多资源不足的问题根源就在这里。requests 决定调度,limits 决定运行时。调度器只看 requests,它把 Pod 往节点上塞的时候,只关心节点剩余的可分配资源够不够 requests 之和。而 kubelet 和内核在运行时约束的是 limits。

这就产生了两个方向的坑。requests 设太小,调度器会把太多 Pod 塞到同一台机器上,节点实际负载爆表,所有 Pod 一起变慢。requests 设太大,节点明明还有余量却调度不上去,白白浪费。我一般的取法是:

  • CPU request:按压测实测的稳定态 CPU 使用量 × 1.2 来设。上面那个例子稳定态 2.9 核,request 设 3 核偏保守,设 2 核更贴近实际但节点会超卖。压测环境我倾向设 2 核,因为要测出真实上限。
  • CPU limit:延迟敏感的服务建议设成 request 的 1.5 到 2 倍,不要设等值。原因是 CFS 的节流是按 100ms 周期算的,一个短时突发(比如 GC、批量请求)很容易在单个周期内超额,然后被强制等到下个周期,表现就是 P99 抖动。
  • 内存 request:按稳定态 RSS 设,不用乘系数。
  • 内存 limit:按峰值 RSS × 1.3 到 1.5 估,同时要覆盖堆外内存。

CPU limit 的节流问题值得再多说两句。我见过一个服务,CPU usage 常年在 limit 的 60%,但 P99 就是不稳定。后来查container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total这个比值,超过 25%。原因就是请求处理有突发性,100ms 周期里前 30ms 用掉了大半配额,后 70ms 全在等。所以如果你的压测目标是稳定的低延迟,CPU limit 要么放宽,要么干脆不设(但前提是节点上跑的都是可信负载,否则一个 Pod 能把整台机器抢干)。

顺带提一下 QoS 类别,它会直接影响节点资源紧张时谁先被驱逐:

  • Guaranteed:所有容器的 requests 等于 limits,最后被驱逐。
  • Burstable:至少一个容器设置了 requests 但没设 limits,中间优先级。
  • BestEffort:完全没设,第一个被干掉。

压测期间如果你的服务是 BestEffort,节点一紧张它就先没,副本数忽上忽下,压测数据根本没法看。所以至少保证 Burstable,核心服务尽量上 Guaranteed。

2.3 给调度预留足够的余量

集群水位这个话题,我的观点是压测环境也别跑满。原因有三个。

第一,滚动更新会瞬时叠加资源。假设你有 10 个副本,maxSurge设了 25%,那更新过程中会短暂出现 13 个副本,多出来的 3 个副本的 request 需要节点能接住。如果集群已经跑到 95%,滚动更新就会卡在 Pending,压测直接中断。

第二,DaemonSet 类的组件也要吃资源。日志采集、监控代理、网络插件,这些在每个节点上都有一份,小节点上一扣就是几百毫核。节点 Allocatable 的算法是:Capacity 减去 kubeReserved,再减去 systemReserved,再减去 evictionHard 阈值。一台 4 核 8G 的节点,扣完之后可分配可能只有 3.2 核 6.5G,和标称容量差了不少。

第三,资源碎片是真实存在的。假设集群总共有 6 核空闲,分散在 5 台机器上,每台剩 1.2 核,而你的 Pod 需要 2 核,那就一个都调度不上。kubectl describe node里每个节点的 Allocated resources 那一栏能看到详细的碎片分布。缓解办法有几种:把单个 Pod 的资源调小一点(比如把 2 核拆成 1.8 核加一个小的 sidecar),用更小规格的节点,或者用拓扑分布约束把同类 Pod 打散。

集群整体水位我一般控制在 60% 到 70% 之间,留 30% 给突发和滚动更新。压测期间可以适当提高,但别超过 85%,再往上各种边缘问题会集中爆发。

3. 现场排查:从 Pending 到 OOMKilled 的完整链路

3.1 调度侧:三分钟定位卡在哪一环

压测现场最需要的是速度,所以我习惯用一条命令先看清全局:

kubectl get pods -n <namespace> -o wide --field-selector status.phase=Pending

锁定 Pending 的 Pod 之后,describe出来的 Events 段落基本就把答案写明了:

kubectl describe pod <pod-name> -n <namespace> | sed -n '/Events:/,$p'

常见的几条信息和解法对应如下:

  • 0/12 nodes are available: 5 Insufficient cpu—— 节点 CPU request 不够了,要么降 request,要么扩节点。
  • 3 node(s) had untolerated taint—— 节点有污点,检查是不是压测专用节点池没打上对应的容忍。
  • 2 node(s) didn't match Pod's node affinity/selector—— 亲和性写死了,Pod 被绑在特定标签的节点上。
  • 2 node(s) didn't have free ports for the requested pod ports—— 用了 hostPort,同节点端口冲突,这个坑很隐蔽。
  • pod has unbound immediate PersistentVolumeClaims—— PVC 没绑上,常见于跨可用区存储。

资源碎片的情况要多做一步,看节点的实际可分配情况:

kubectl describe node <node-name> | sed -n '/Allocated resources/,/Events/p'

同时确认节点上还有没有其他「隐性消耗」,比如已被驱逐但还在终止中的 Pod、被 DaemonSet 占掉的份额、以及 kubelet 的预留资源。

我还遇到过一种情况:Events 里明确写着资源不足,但集群总的空闲资源明明够。查下来是topologySpreadConstraints配得太严,要求同类 Pod 在每个可用区数量差不超过 1,而某个可用区的节点刚好满了。这类约束在压测扩容时特别容易翻车,因为扩容是瞬间加很多副本,约束条件会直接卡死一半。压测期间我建议临时放宽分布约束,或者干脆去掉,压完再恢复。

3.2 运行侧:怎么确认是 OOMKilled 还是 CPU 节流

Pod 调度上去了,但压测数据还是上不去,那就看运行侧。先确认有没有重启:

kubectl get pods -n <namespace> -o custom-columns=NAME:.metadata.name,RESTARTS:.status.containerStatuses[*].restartCount

有重启就看原因:

kubectl describe pod <pod-name> -n <namespace> | grep -A 5 "Last State"

如果是Reason: OOMKilled, Exit Code: 137,那就是内存超了 limit。这里有个细节:OOMKilled 有可能是容器主进程被杀,也有可能是整个 cgroup 被内核干掉,现象一样但排查方向不同。前者通常伴随应用日志里的堆栈,后者则完全静默。

判断内存到底用了多少,可以进容器看 cgroup 的计数:

# cgroup v1 cat /sys/fs/cgroup/memory/memory.max_usage_in_bytes # cgroup v2 cat /sys/fs/cgroup/memory.peak

这个峰值会比kubectl top pod看到的采样值更准,因为 top 是按周期采样的,抓不到瞬时尖峰。很多 OOM 就是这么来的——采样看是 1.8G,limit 给 2G,看起来很安全,但实际峰值冲到 2.1G 就被杀了。

Java 服务要特别注意堆外内存。堆设 1.5G,limit 给 2G,看起来留了 500M 余量,但 Metaspace、线程栈(每个线程默认 1M 栈空间)、DirectByteBuffer、JIT 代码缓存加起来很容易吃掉 400M 以上,再算上 glibc 的 malloc arena 碎片,2G 根本不够。我的经验是limit = 堆 × 1.4 到 1.5,压测期间再往上加一档。

CPU 节流不像 OOM 那么暴烈,它表现为「服务没挂,但延迟上不去」。判断指标是:

container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total

这个比值超过 20% 就说明有明显节流了。同步看一下container_cpu_cfs_throttled_seconds_total的增速,如果压测期间持续增长,那就是 limit 卡得太死。

第三种运行侧问题是探针误杀。压测时响应变慢,readiness 探针的timeoutSeconds设得太短(很多人设 1 秒),探针失败,Pod 被摘出 Endpoints。摘流之后剩余副本压力更大,响应更慢,更多探针失败,直接雪崩。liveness 探针更狠,失败直接重启容器,压测中途重启一次,前面测的数据全废。

我的做法是压测期间把探针参数放宽一档:timeoutSeconds给 3 到 5 秒,failureThreshold从 3 提到 5,periodSeconds从 10 提到 15。同时把initialDelaySeconds和startupProbe配好,避免冷启动阶段就被判死。

3.3 集群侧:节点扩容和临时资源的取舍

确认了是集群整体资源不够,就得扩。扩容有几个维度:

  • 横向加节点:往节点池里加机器。有弹性伸缩组件的会自动加,没配的手动加。
  • 纵向升配:把现有节点规格调大。这个操作通常需要重启节点,压测期间不推荐。
  • 临时节点池:专门起一个压测用的节点池,压完就释放,成本可控。

横向扩容最大的问题是延迟。从触发扩容到新节点 Ready,中间要经历实例创建(1 到 3 分钟)、系统初始化、容器运行时拉起、网络插件就绪、镜像拉取(如果镜像大,再等一两分钟)。整个过程三到五分钟很正常。如果你的压测爬坡曲线是「10 秒内从 0 打到满」,那自动扩容永远追不上,压测必然失败。

所以压测前我一般会做两件事:一是提前预热节点,把最小节点数调高,让节点处于已就绪状态;二是放缓爬坡曲线,比如每 30 秒加 100 并发,给扩容留出反应窗口。

还有一个特别容易忽略的点:云上 vCPU 配额。临时加机器的时候被配额挡住,报错信息还不在 k8s 层面,得去云控制台看。我建议压测前一天就把配额提上去,或者提前确认当前配额余量。

节点侧还有两个信号要盯着:MemoryPressure和DiskPressure。磁盘压力在压测时特别常见,因为日志量暴增。默认的evictionHard阈值是nodefs.available<10%,一旦触发,kubelet 开始驱逐 Pod,而且优先驱逐那些用了超过 request 的 Burstable Pod。表现就是压测跑着跑着副本数突然少几个。压测前把日志级别调低、把日志轮转配好,比事后救火划算得多。

4. 临时救火与长期治理的分界线

4.1 压测现场允许做的三件事

压测是有时间窗口的,很多时候来不及做完整分析,得先让数据跑出来。我给自己划的线是:现场只做三类操作,且必须记录下来,压完就还。

第一类,调整压测口径。如果确认是压测机自身瓶颈(CPU 打满、端口耗尽、连接池不够),改压测端配置是最快的。压测机端口耗尽的判断方法是看netstat里的 TIME_WAIT 数量,如果几万个,那就是本地端口范围不够。压测机的内核参数和业务机的内核参数一样重要,别只顾着调服务端。

第二类,临时放宽调度约束。去掉podAntiAffinity的硬约束,把topologySpreadConstraints的whenUnsatisfiable从DoNotSchedule改成ScheduleAnyway,放宽节点选择器的标签范围。这些改动不影响服务逻辑,只影响调度,压完恢复成本低。

第三类,临时提高资源上限。把 limit 往上调一档,把 HPA 的最小副本数拉高避免扩容抖动,把副本数手动定死避免自动扩缩容的滞后干扰压测。但注意,提 limit 只是让容器不被杀,不会让节点凭空多出资源。如果节点已经满载,提 limit 只会让节点更快进入压力状态,甚至触发驱逐。所以提 limit 之前必须先确认节点水位。

有一件事我在现场坚决不做:不改业务代码逻辑。压测中改代码,测出来的数据没有任何参考价值,而且会引入新的不确定性。

4.2 压测结束后必须还的债

这句话听起来像老生常谈,但我确实见过有人把压测时临时调的 limit 带到生产,结果单副本吃了 8 核,节点上只能放两个 Pod,资源利用率低得离谱。

压测结束后要还的债,我列了个清单:

  • 资源配额回归:把临时调高的 requests/limits 改回去,改回去之后重跑一次短时压测验证。
  • 调度约束恢复:亲和性、分布约束、污点容忍全部复原。
  • HPA 参数标定:压测得到的拐点数据正好用来定 HPA 的阈值。CPU 目标利用率一般设在 request 的 60% 到 70%,留出扩容窗口。同时把behavior配好,扩容可以激进(比如 30 秒窗口),缩容一定要保守(默认 300 秒是有道理的,防止抖动)。
  • 容量模型沉淀:把「单副本 X QPS 对应 Y 核」这个关系写进文档,后续扩容直接查表,不用重新压。
  • 瓶颈清单:这次压测暴露的瓶颈按优先级排个序,是代码问题、配置问题还是架构问题,分派到具体的改进项。

顺便说个 HPA 的坑。HPA 的 CPU 利用率是相对于requests算的,不是相对于 limit,也不是相对于节点容量。所以你把 request 从 2 核改成 1 核,HPA 看到的利用率会直接翻倍,然后疯狂扩容。压测期间调 request 的时候,记得同步检查 HPA 阈值,不然会出现「明明负载没变,副本数翻了三倍」的诡异现象。另外 HPA 的同步周期默认 15 秒,加上 metrics-server 的采样延迟,实际响应可能要 30 秒以上,压测爬坡太快它根本跟不上。

5. 那些年踩过的坑:问题速查与避坑清单

5.1 常见问题速查表

现象最可能的原因验证方式处理方向
Pod 一直 Pending节点 request 不足或碎片describe pod看 Events降 request、扩节点、拆碎片
Pod 反复重启,Exit Code 137内存超 limit 被 OOMKilleddescribe pod看 Last State、cgroup 峰值提内存 limit、查内存泄漏
服务不挂但 P99 抖动大CPU limit 导致 CFS 节流算 throttled_periods 比值提 CPU limit 或不设 limit
副本数忽上忽下节点压力驱逐 BestEffort Poddescribe node看 Conditions设 requests、升 QoS 等级
QPS 上不去但服务端很闲压测机自身瓶颈看压测机 CPU、TIME_WAIT 数量加压测机、调端口范围、加连接池
压测中突然大量超时conntrack 表满或 fd 耗尽conntrack -S、`ls /proc/ /fdwc -l`
新副本一直起不来镜像拉取慢或启动探针失败看 Pod Events、镜像大小预热镜像、调 startupProbe
扩容后 QPS 没涨流量没均匀打到新副本kubectl top pod看分布检查负载均衡、连接复用策略

5.2 几条不太成文但很有用的经验

先证伪压测端,再怀疑服务端。这条我放在第一位,因为它能省掉一半的无效排查。压测机的 CPU、内存、网络带宽、本地端口、文件描述符,任何一项打满都会表现为「服务端扛不住」。我的习惯是压测时同时开两个窗口,一个看服务端kubectl top,一个看压测机top和ss -s。两边同时到达瓶颈,说明测试环境是均衡的;只有一边到瓶颈,另一边就是瓶颈本身。

看 usage 别看 request。request 是你写的,usage 是真实的。很多资源不足的判断错误,都是拿 request 当实际用量来分析。kubectl top pod只能看当前值,要看历史趋势得上监控系统,重点看 P95 和峰值,别只看平均值。

并发不是越高越好,找到拐点比找到上限更有价值。上限告诉你「最多能扛多少」,拐点告诉你「从多少开始变差」。生产上真正该关心的是拐点,因为超过拐点之后,加再多并发也只是让排队变长、延迟变高,吞吐反而可能下降。

压测前把缓存预热跑一遍。冷启动状态下压测,第一波流量全部穿透到下游存储,压出来的数字完全不能用。预热的方式可以是用低并发跑几分钟,也可以是直接灌一批热点数据。

日志和监控的采样精度要匹配压测时长。跑 3 分钟的压测,用 1 分钟采样间隔的监控,你只能看到三个点,什么都分析不出来。压测期间把关键指标的采样间隔调到 10 秒甚至 5 秒,压完再改回去。

多个服务共用集群时,给压测单独划命名空间和节点池。一是避免影响其他业务,二是方便整套资源压完就释放。配合ResourceQuota限制命名空间总量,配合PriorityClass保证压测 Pod 在资源紧张时的优先级符合预期。

别忘了中间件。应用侧扩到 20 个副本,结果数据库连接池只有 100 个连接,多出来的副本全在等连接。压测前把下游的容量上限也摸一遍,别让应用层背锅。

最后分享一个我最近才想明白的事:压测中所谓的「资源不足」,很多时候不是资源真的不够,而是资源的形状不对。你有 100 核,但分散在 50 台 2 核的机器上,一个需要 4 核的 Pod 永远调度不上去;你有 500G 内存,但每个 Pod 的 limit 都卡得死死的,一有突发就被杀。所以与其盯着总量看,不如多花点时间看分布和形状。这个视角转换之后,很多之前想不通的「明明资源够却跑不动」的问题,答案就自己浮出来了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询