K8s 资源评估与压测定值方案
服务从裸机/IIS 迁到 Kubernetes,第一个要回答的问题就是:request/limit 开多少、副本数算几个。这份文档给出一套可执行的完整流程——从本地压测拿到单 pod 容量曲线,到类生产环境校准,到定值上线,再到上线后用真实流量收敛。配套的观测体系一并给出:哪些指标现成、哪些要补、用什么判读。
适用对象:基于 Aegis(3.1.x,net8.0)构建的 WebAPI 与后台服务。
三条原则
后面所有公式和流程都从这三条推出来:
- request 锚稳态,limit 锚峰值。CPU request 取目标流量下稳态用量的 P95 向上取档;limit 取压测拐点档的用量留余量。内存 request 取含 L1 缓存满载的稳态 RSS,limit 再乘 1.3。
- TPS 必须绑 SLO 才是容量。"单 pod 能扛 2000 TPS" 在 P99 = 2s 时没有意义。容量定义为满足延迟 SLO(如 P99 ≤ 300ms)前提下的最大到达速率。另外注意 Broker 熔断的
SlowCallMs默认 3000ms,压测时 P99 逼近这个值会先触发熔断——这本身就是现成的拐点探测器。 - request 偏高浪费容量,偏低损害稳定性,代价不对称。request 偏低的三种后果:节点超卖时按 CFS 权重抢不到 CPU(延迟劣化,且会打穿熔断);HPA 利用率读数失真(实际用量 / request 永远 200%+,HPA 长期顶格);JIT 预热和 GC 突发借不到空闲 CPU。宁可偏高。
框架资源画像:什么在吃资源
先说结论:框架全链路没有设置任何 ServerGC、GCHeapHardLimit、Kestrel 限制、线程池参数(已核对全部 csproj/props/源码),运行时行为完全走 .NET 8 默认值。容量由部署侧决定,框架不会兜底。
会实质性影响资源画像的组件及其默认值:
| 组件 | 默认值 | 增长行为 | 代码位置 |
|---|---|---|---|
| Redis L1 本地缓存 | 默认开启、无容量上限,TTL 300s + 30s 抖动 | 最大的变量内存:等于 5 分钟窗口内唯一 key 数 × value 序列化大小 | src/Caching/Aegis.Caching.Redis/RedisOptions.cs |
| Broker HttpClientPool | 上限 128 个 client | 随 distinct baseUrl 增长,硬上限;MaxConnectionsPerServer 未设(默认无限) | src/Net/Aegis.Net.Broker/Implementation/HttpClientPool.cs |
| SOAP ChannelFactory 缓存 | 无上限 | 随 baseUrl × 契约类型增长,ChannelFactory 较重,但受端点数约束 | src/Net/Aegis.Net.Broker/BrokerClient.cs |
| Broker 窗口延迟/注册表 | 每条约 1.2KB 固定 | 随用到的 contract/method 数增长,不驱逐,量级可忽略 | WindowLatencyStore.cs 等 |
| 熔断采样队列 | 30s 滑窗 | 约 QPS × 30 × 小结构体,QPS 10 万也就几十 MB | BrokerCircuitRegistry.cs |
| EventBus 本地 Channel | 每分区容量 1024,满了 Wait | 有界,堵生产者不堵内存 | src/EventBus/Aegis.EventBus/Internal/ConsumerQueue.cs |
| Jobs.Postgres | 每 1s 扫描 + 单实例并行 8 | 固定 CPU 地板(1 查询/秒/实例),与业务量无关 | PostgresDistributedOptions.cs |
| Razor 文档渲染 | 并发 100 | 用到时是 CPU 大头 | src/Documents/Aegis.Documents.Rendering.Razor/RazorOptions.cs |
| LibreOffice 转换 | 并发 5,外部进程 | 内存按独立进程另算,每进程 200~500Mi | LibreOfficeOptions.cs |
| 定时器地板 | 健康巡检 15s、心跳 10s 等 | 空载 CPU 约 1~5% | 各模块 |
内存评估时把 L1 缓存单独算:L1 增量 ≈ 5 分钟窗口唯一 key 数 × 平均 value 序列化大小 × 1.5(1.5 是 IMemoryCache 的对象开销系数)。如果业务的 key 无界(按用户/订单号缓存),要么算清峰值基数,要么给 IMemoryCache 加 SizeLimit——框架默认不设,这是目前唯一没有兜底的内存增长点。
总体流程:四个阶段
| 阶段 | 做什么 | 产出 | 谁来干 |
|---|---|---|---|
| 一、本地容量曲线 | Docker 单容器压测,扫出"到达率 × 延迟 × 资源"曲线 | 单 pod 容量曲线 + 资源初值 | 开发,无需集群权限 |
| 二、类生产校准 | 类生产 pod 复测 + 3-pod 线性度验证 | 校准后的单 pod 容量 | 开发主导,DevOps 三项配合 |
| 三、定值与上线 | 套公式定 request/limit/副本数/HPA,接线探针 | 生产部署配置 | 开发定值,DevOps 落 values |
| 四、观测收敛 | 真实流量 P95 复核 request,持续调整 | 稳态配置 | 开发,用现成监控 |
阶段一:本地单 pod 容量曲线
这一步不需要 k8s。Docker 的 cgroup 限制与 pod 的 limit 行为一致,--cpus=1 --memory=1g 起服务,宿主机起压测端,dotnet-counters 挂上看运行时,就能产出第一版曲线和定值。差异主要在网络延迟和邻居干扰,放到阶段二校准。
压测模型:用固定到达率,不用固定并发
固定并发(closed model)下 TPS = 并发数 ÷ 平均延迟,三个量互相咬死,测出的每组 (并发, TPS, P99) 只是曲线上一个点,且过载时自动降速、掩盖拐点。容量测试用固定到达率(open model):直接控制每秒发出多少请求,与响应快慢解耦。超载信号极其清晰——实际达成速率 < 目标速率、错误率抬升,不用从延迟曲线里猜。
k6 配置示例:
export const options = {
scenarios: {
ramp: {
executor: 'constant-arrival-rate',
rate: 250, // 每秒到达 250 个请求(被控变量)
timeUnit: '1s',
duration: '8m',
preAllocatedVUs: 100,
maxVUs: 400, // VU 只是实现细节,给足余量
},
},
};
并发数不用选,它是观测量:实际并发 = 到达率 × 平均延迟(Little 定律)。反过来这个定律还有两个用法:
- 从业务反推并发预算:目标 250 TPS、SLO 200ms,稳态并发约 250 × 0.2 = 50 在途请求。这个数用来估 k6 的
maxVUs(3~5 倍余量),也用来核对网关/连接池配置。 - 判断瓶颈归属:Broker 转发型服务是异步 IO,在途并发很便宜(占内存不占线程);看到"加压 TPS 不涨但延迟涨"时,先分清是下游饱和还是自身 CPU 打满——看 Broker 窗口延迟(诊断页)里分端点的下游 P99,与进程内耗时对比。两者对策完全不同:前者加 CPU 无用,后者才有效。
业务流量建模:从单接口到混合场景
单接口压测回答"这条代码路径多贵",容量问题问的是"真实流量混合时 pod 什么时候倒"——两者是不同的测量。正确结构是两层:单接口压测降级为"成本账本"的输入,容量曲线由混合场景产出。
第一层:流量画像。 从生产数据建(网关 access log / Seq 请求日志 / OTel trace 聚合;无生产数据时按业务模型推算:用户旅程 × 用户量 × 操作频率),产出"接口 × 流量占比 × 载荷分布 × 下游依赖"表,做帕累托截断:top 接口覆盖 85% 流量,长尾按成本特征归并成等价类,不逐个建模。
第二层:按成本特征聚成 4~6 个场景类。 聚类依据是"吃资源的方式",不是业务功能:
| 场景类 | 成本特征 | 验证目标 |
|---|---|---|
| L1 命中读 | 纯内存 | Kestrel/序列化上限 |
| 穿透读 | L1 miss → Redis/DB | 下游链路 |
| 写事务 | DB 事务 + 事件发布 | 连接池与锁 |
| Broker 转发 | 延迟由下游决定,CPU 低 | 下游容量与熔断 |
| 重型操作 | Razor 渲染/文件转换 | 不进混合场景,单独测单独定容量 |
k6 实现:每个场景类一个 constant-arrival-rate,rate = 总到达率 × 流量占比,阶梯加压时同比例放大。k6 自动打 scenario 标签,summary 同时出分场景 P99 与整体 P99——SLO 按业务接口分别约束:
export const options = {
scenarios: {
cachedRead: { executor: 'constant-arrival-rate', rate: 112, timeUnit: '1s', duration: '8m', preAllocatedVUs: 60, maxVUs: 200, exec: 'cachedRead' },
dbRead: { executor: 'constant-arrival-rate', rate: 75, timeUnit: '1s', duration: '8m', preAllocatedVUs: 60, maxVUs: 200, exec: 'dbRead' },
write: { executor: 'constant-arrival-rate', rate: 24, timeUnit: '1s', duration: '8m', preAllocatedVUs: 40, maxVUs: 150, exec: 'writeTxn' },
brokerFwd: { executor: 'constant-arrival-rate', rate: 30, timeUnit: '1s', duration: '8m', preAllocatedVUs: 40, maxVUs: 150, exec: 'brokerFwd' },
},
thresholds: {
'http_req_duration': ['p(99)<300'], // 整体
'http_req_duration{scenario:write}': ['p(99)<800'], // 关键业务单独约束
'http_req_failed': ['rate<0.01'],
},
};
状态仿真,混合场景的真实性主要取决于三项:
- key 热度分布:真实流量是长尾型(少数热 key 扛大部分读),不是均匀随机。按生产 popularity 分布生成请求,L1 命中率才对——命中率差一倍,容量差一倍。
- DB 数据量:测试库灌到生产量级,或至少确认执行计划同构。
- 认证状态:token 新签发与复用的成本完全不同,按生产行为来。
背景负载:Jobs 按生产真实注册开着测(1s 扫描 + 8 并行是常驻地板);补一档"整点风暴"——cron 集中在整点触发的任务洪峰,是经典的隐藏拐点。
故障混合:掺 1% 下游超时的流量。Broker 重试默认 3 次指数退避,下游降速时在途请求被重试放大,内存与连接行为与 happy path 完全不同——容量曲线的"抗退化能力"从这档读。
成本账本:单接口压测的新角色。 每个场景类单独低速率(约 20 rps)跑 5 分钟,算"每百请求的 CPU 增量 + 分配速率"。用途有二:
- 预测容量 ≈ CPU 预算 ÷ 加权平均每请求成本,与混合实测拐点对照,偏差应在 ±15% 内;超出即存在干扰项(连接池竞争、GC 交叉污染、锁竞争),差值本身就是诊断线索。
- 流量结构变化或新增接口时,先用账本估新容量,结构大变才重跑混合曲线。
阶梯与判读
- 预热 10 分钟,约 30% 预估负载:让 JIT、Tiered PGO、连接池、L1 缓存全部热起来。
- 按预估容量的 25% → 50% → 75% → 100% → 125% 阶梯加压,每档稳 5~8 分钟。5 分钟的下限来自 L1 缓存 TTL 300s——每档不持稳过一个 TTL 周期,看到的内存就是不完整的缓存足迹。
- 每档记录四组数:
| 组 | 指标 | 来源 |
|---|---|---|
| 流量 | 达成速率、错误率、P50/P95/P99 | k6 summary(--summary-export) |
| CPU | 容器 CPU 用量、throttle 次数 | docker stats / cgroup 文件 |
| 内存 | RSS(working set) | docker stats |
| 运行时 | GC 堆、Gen2 次数、ThreadPool 队列长度、分配速率 | dotnet-counters monitor -n <进程名> --counters System.Runtime |
-
拐点判据,以下先到为准:P99 穿 SLO;达成率 < 95%;CPU 顶到 limit 且出现 throttling;% Time in GC 持续 > 15~20% 或 Gen2 频繁触发;错误率抬升。拐点对应速率即该资源配置下的单 pod 容量。
-
资源读法:读目标工况档,不读拐点档。拐点和日常流量之间要留余量。比如拐点 400 TPS、日常期望 250 TPS:读 250 TPS 档的稳态 CPU(如 550m)和稳态 RSS(如 380Mi)来定 request;limit 维持测拐点时的档位(1 核)。
-
主曲线后加一档突发:目标速率 × 2 持续 60~90 秒再回落,观察恢复速度、Gen2 是否被冲出来、RSS 是否回落。这一档对应生产毛刺,也是检验余量的试金石——2 倍冲击就 OOM 或 P99 十几秒不回落,说明收得太紧。
-
key 分布要仿真 L1 命中:全重复 key(全命中)和全随机 key(全 miss)的 TPS 与内存差好几倍。用贴近生产的 key 分布,或者两种都测取区间。
-
压测配置对齐生产:日志级别与生产一致(日志是真实的 CPU 消耗者,关日志 TPS 虚高);下游连真 Redis/DB 或等延迟仿真;连接复用方式(keep-alive)与生产网关一致。
阶段一产出
- 容量表:每档(到达率、P99、CPU、RSS、GC 概况)
- 拐点速率 + 目标工况档的资源稳态值
- 资源初值:CPU request / limit,内存 request / limit
零平台时的本机预测试(无 OTel / Prometheus / Seq)
阶段一不依赖任何观测平台——每层观测都有本机等价物,四个终端跑完:
| 观测需求 | 本机等价物 | 说明 |
|---|---|---|
| 运行时指标(GC/线程池/分配) | dotnet-counters | EventPipe 直连进程,不经任何管道 |
| 容器 CPU/内存水位 | docker stats | 内存口径即 working set,与 kubelet 的 OOM 判据一致 |
| CPU 限流证据 | cgroup cpu.stat | nr_throttled 增长 = limit 太紧的直接证据 |
| OOM 判定 | docker inspect | State.OOMKilled |
| 流量分位数 | k6 summary | --summary-export 导 JSON,thresholds 断言 p99 |
| 框架层数据 | 诊断页 | 由进程自己伺服,浏览器打开即有窗口延迟/池水位/健康快照,零外部设施 |
| 日志 | NLog 文件目标 | 本地 grep 够用;要图形化可本机 docker run datalust/seq,非必需 |
| 堆组成分析 | dotnet-gcdump | 拍稳态堆快照,L1 缓存实际占用一目了然 |
# 终端1:服务,cgroup 限制与 pod limit 行为一致
docker run --rm --name svc -p 8080:8080 --cpus=1 --memory=1g myservice
# 终端2:运行时指标全程落 CSV(镜像里预装 dotnet-counters 后 exec 进去)
docker exec svc dotnet-counters collect \
--counters System.Runtime,Microsoft.AspNetCore.Hosting \
--format csv --refresh-interval 5 --output /tmp/counters
# 终端3:容器水位 5 秒一采样
while true; do docker stats --no-stream --format \
"$(date +%T),{{.CPUPerc}},{{.MemUsage}}" >> stats.csv; sleep 5; done
# 终端4:阶梯压测,每档一次运行、summary 落 JSON
k6 run --summary-export=step250.json loadtest.js
# 限流与 OOM 证据(cgroup v2;v1 在 cpu/ 子目录下)
docker exec svc cat /sys/fs/cgroup/cpu.stat # nr_throttled / throttled_usec
docker exec svc cat /sys/fs/cgroup/memory.events # oom 峰值计数
docker inspect -f '{{.State.OOMKilled}}' svc # 压后检查
k6 脚本里把拐点判据写成 thresholds,每档过/不过自动判定:
export const options = {
thresholds: {
http_req_duration: ['p(99)<300'], // 按业务 SLO
http_req_failed: ['rate<0.01'],
},
};
三个边界要清楚:
- 开发机(尤其 Mac)结论的适用范围:M 系列 CPU 与生产 x86 节点不同构,绝对 TPS 不可迁移;曲线形态、内存足迹、线程池与 GC 行为的结论有效。定量 TPS 放到 Linux 开发环境跑——这就是"开发机预测试 + 开发环境定量"的分工。
- 没有 OTLP 采集端就把 tracing 关掉(
OpenTelemetry:Enabled = false)。框架在无 endpoint 时退化为 console exporter(ServiceCollectionExtensions.cs),压测时刷屏且空耗 CPU,阶段一用不到 trace。 - 预测试的产出边界:资源初值 + 粗问题排查(L1 实际占用、内存泄漏、线程池饥饿)。最终定值依赖阶段二/四的真实监控,平台是那两个阶段的依赖,不是第一阶段的。
阶段二:类生产校准
需要 DevOps 配合的就三件事,带着阶段一的数字去谈:
- 环境保真。节点规格一致(CPU 型号直接影响 TPS,共享超卖节点上的数字不可信);独占的 Redis/DB 实例(压测打共享下游会冲掉别人的测试环境,也污染自己的数字)。
- 监控查询权限。
container_memory_working_set_bytes等 cAdvisor 指标的 P95 曲线来自 Prometheus/Grafana,没有权限就退回 kubectl top 粗值,定值质量降一档。 - 3-pod 线性度验证。单 pod 外推不等于线性扩展——这个框架重 IO(Broker 转发、两级缓存),瓶颈常在下游共享资源。用 3 个 pod 复压,实测总容量应 ≥ 单 pod 的 2.2~2.5 倍。不达标说明瓶颈在 DB 连接池、Redis 或被调服务,加 pod 之前先解决下游。
阶段三:定值与上线
定值公式
- CPU request = 目标工况稳态 P95 用量,向上取整到档位(250m/500m/750m/1 核)
- CPU limit = 拐点档 CPU 用量 × 1.3 余量;有真实流量的服务不低于 1 核(原因见"常见坑"第 2 条)
- 内存 request ≈ 全档位最高稳态 RSS(含 L1 满载档)
- 内存 limit = 该 RSS × 1.3。容器里 GC 堆硬限自动取 limit 的 75%,剩下 25% 要装线程栈、native 内存、网络缓冲——limit 贴着 RSS 设必出 OOM
- 副本数
N = ceil(期望TPS × 1.3~1.5 / 单pod可持续TPS)。系数覆盖压测与生产的差异(TLS、日志量、邻居竞争);再验证滚动发布(surge 期间少一个 pod)和单节点故障时剩余副本能扛住流量
压测前的起点分档
没来得及压测时的初值,上线后按阶段四收敛:
| 场景 | CPU request / limit | 内存 request / limit |
|---|---|---|
| 低流量内部服务 / 任务宿主 | 100 | 256Mi / 512Mi |
| 典型业务 API(几十~百余 QPS) | 250~500m / 1 核 | 384Mi / 768Mi~1Gi |
| 数百 QPS、JSON + Broker 转发为主 | 500m | 512Mi / 1~1.5Gi |
| Jobs.Postgres 宿主 | 上一档 CPU request +50m | 同上 |
| Razor 渲染 / LibreOffice 转换 | 1 核 / 2 核起 | 1Gi / 2Gi 起,LibreOffice 每进程另加 200~500Mi |
单 pod 不建议超过 2 核:副本多意味着 GC 堆小、滚动发布炸面小、单实例故障分流少。
HPA 与 QoS
minReplicas= 日常所需副本数,maxReplicas= 峰值期望 × 1.5,target CPU utilization 70%。- 延迟敏感(走 Broker 同步调用)的服务建议 requests = limits(Guaranteed QoS),避免 CPU throttling 打进下游超时。
- 走 request-only(不设 CPU limit)路线时有个 .NET 特有的坑:cgroup 无限量时运行时按宿主机核数开 Server GC 堆,64 核节点就是 64 个堆。对策:设
DOTNET_GCHeapCount,或用DOTNET_CPU_LIMIT告知运行时虚拟限额(.NET 8+)。
探针接线:现成的,不用另写
Aegis.Diagnostics 已经按 k8s 探词语义实现了薄路由,直接接线即可(src/Diagnostics/Aegis.Diagnostics/DiagnosticsComponentRegister.cs):
- liveness →
GET <诊断路径>?probe=live:恒 200,不触碰任何依赖(进程能响应即活) - readiness →
GET <诊断路径>?probe=ready:只读巡检快照与组件台账,O(1) 无现场探测;存在阻断流量的不健康检查项或组件加载失败时返回 503
livenessProbe:
httpGet: { path: "/diagnostics?probe=live", port: 8080 }
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet: { path: "/diagnostics?probe=ready", port: 8080 }
periodSeconds: 15
failureThreshold: 3
(路径以 Aegis:Diagnostics:Path 配置为准。)
阶段四:观测收敛闭环
上线后头两周用真实流量复核,因为真实流量分布永远和压测有偏差:
- CPU 利用率 =
P95(rate(container_cpu_usage_seconds_total{...}[5m])) ÷ request,健康区间 50%~70%;偏差超 40% 就调整(低于 30% 降 request,长期高于 80% 升 request 或加副本)。 - 内存看
container_memory_working_set_bytes是否有爬升趋势(L1 key 集合比预估大时最常见)。 - 有 VPA 的集群可以用
recommendation模式的建议作参考,但不要开 auto 模式——VPA 和 HPA 基于同一指标(CPU)时会互相打架。
观测体系
现状盘点
| 层 | 现状 | 结论 |
|---|---|---|
| 链路 | Aegis.Trace.OpenTelemetry 输出 OTLP trace,含 Broker 契约方法级 span、AspNetCore/HttpClient 打标 | 现成 |
| 日志 | Seq 平台(见"Seq 部署和维护") | 现成 |
| 诊断页 | 健康快照、组件台账、窗口延迟等 section,live/ready 探针路由 | 现成 |
| 指标 | 整条管道缺失——但接线点已预留 | 需一步补齐 |
指标管道这件事比想象中小:src/Trace/Aegis.Trace.OpenTelemetry/ServiceCollectionExtensions.cs 里有一段现成但被注释掉的 WithMetrics 接线(第 92~109 行),连需要的 NuGet 包和版本都注释好了。解开注释、装上 OpenTelemetry.Exporter.Prometheus.AspNetCore 和 OpenTelemetry.Instrumentation.Runtime,服务就有了 /metrics 端点,net8.0 的标准指标全部出来。注意 OTel 的 AspNetCore filter 已经排除了 /metrics 和 /health 路径,不会自己打自己。
四层指标清单
层 1:运行时(服务 /metrics,OTel Runtime + AspNetCore instrumentation)
| 指标 | 判读 |
|---|---|
process.cpu.time / 容器 CPU 用量 | 稳态 P95 定 request 的主数据 |
dotnet.gc.heap.size | 堆趋势;稳态不回落但也不爬升即正常 |
dotnet.gc.collections{generation=2} 增速 | Gen2 频繁 = 内存压力拐点信号 |
dotnet.threadpool.queue.length | 持续 > 0 且增长 = 线程池饥饿(IO 同步阻塞或 CPU 不足) |
http.server.request.duration(直方图,按 http.route 维度) | P99 对 SLO,分接口定位慢点 |
http.server.current_requests | 在途并发,对照 Little 定律预算 |
压测时的临时观测不必走 Prometheus,dotnet-counters monitor -n <进程名> 直接看实时面板。
层 2:框架层
窗口延迟、分端点延迟、DB 连池水位(FreeSql/Npgsql 池统计)、健康快照都从诊断页看(<诊断路径>,浏览器直接打开,含组件台账与健康巡检结果)。L1 命中率、熔断状态、EventBus channel 水位目前不在指标管道上,走日志/诊断页判读;要上面板需各补一个 Meter,列为增强项,不阻塞本方案。
层 3:容器层(cAdvisor + kube-state-metrics)——定值的裁判
| 指标 | 用途 |
|---|---|
container_cpu_usage_seconds_total | P95/P99 对 request,收敛复核的主指标 |
container_memory_working_set_bytes | OOM 判定依据就是它,不是 RSS |
container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total | 比值 > 10% = limit 太紧或 request 偏低的直接证据 |
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} | OOM 复盘 |
kube_horizontalpodautoscaler_status_current_replicas vs spec_max_replicas | 长期贴顶 = request 锚错或容量不足 |
层 4:压测层
k6 结果 remote-write 进 Prometheus(k6 run --out prometheus-rw,旧版用 experimental-prometheus-rw),压测流量与 pod 指标同一条时间轴。
三块 Grafana 面板
- 压测驾驶舱:同一时间轴上叠 k6 到达率/达成率/错误率/P99 × pod CPU%/throttle/RSS/Gen2 计数。拐点在这块板上一眼可见。
- 容量定值面板:
CPU 用量 P95 ÷ request利用率、working_set ÷ limit、throttle 比例,按 deployment 分组。这块板就是阶段四的复核工作台。 - 运行时健康面板:GC、线程池队列、分路由 P99、下游延迟(Broker 窗口)、健康检查状态。
告警清单
| 告警 | 条件(示例) | 含义与处置 |
|---|---|---|
| CPU 限流 | rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m]) > 0.10 持续 10m | limit 太紧或 request 偏低;升 limit 或查线程池 |
| 内存贴限 | working_set > 0.9 × limit 持续 15m | 即将 OOM;查 L1 key 增长或升 limit |
| OOMKilled | last_terminated_reason{reason="OOMKilled"} 出现 | 立即:limit 太低(对照 75% 堆硬限)或泄漏 |
| HPA 顶格 | current_replicas == max_replicas 持续 30m | request 锚错或容量真不足 |
| P99 超 SLO | histogram_quantile(0.99, rate(http.server.request_duration_bucket[5m])) 阈值按 SLO | 先看 throttling 和下游延迟定位归属 |
| 熔断打开 | 日志/Seq 关键字 | 下游故障或自身限流传导,看熔断注册表状态 |
常见坑速查
- 内存 limit 贴 RSS 设 → GC 堆硬限只有 limit 的 75%,线程栈 + native + 缓冲挤在 25% 里,OOMKill 或 GC 抖动。永远留 30%。
- CPU limit < 1 核 → Server GC 线程随核数走,被限流后 GC 停顿放大,延迟抖动没有业务原因。有流量的服务 limit ≥ 1 核。
- request-only 不设
DOTNET_GCHeapCount→ 运行时按宿主机核数开 GC 堆,64 核节点 64 个堆,白吃几十 MB 还加剧上下文切换。 - request 低于稳态真实用量 → HPA 读数永远 200%+ 长期顶格(等于失效);节点超卖时按权重抢不到 CPU。
- 3 pod 实测不线性(< 2.2×) → 瓶颈在下游共享资源(DB 池/Redis/被调服务),加 pod 无解,先扩下游。
- 压测关日志、用随机 key → TPS 虚高、L1 内存失真,两头的定值都不准。
- 单 pod 越做越大 → 1~2 核之后每核边际收益递减;2 个小 pod 优于 1 个大 pod(GC 堆小、发布炸面小、故障分流)。
附:一页速查
- 容量 = 满足 SLO 的最大到达速率(不是最大 TPS,不是并发数)
- request = 目标工况稳态 P95;limit = 拐点用量 × 1.3;内存 limit ≥ RSS × 1.3
- 副本数 = ceil(期望 TPS × 1.3~1.5 ÷ 单 pod 可持续 TPS),滚动发布/单节点故障再验一遍
- 拐点判据(先到为准):P99 穿 SLO / 达成率 < 95% / CPU throttle / GC 恶化 / 错误率抬升
- 每档持稳 ≥ 5 分钟(L1 TTL 300s);预热 10 分钟;主曲线后补 2× 突发档
- 探针直接用
<诊断路径>?probe=live/?probe=ready - 收敛闭环:CPU 利用率(P95 ÷ request)健康区间 50%~70%,偏差 > 40% 调整