跳到主要内容
版本:3.1

K8s 资源评估与压测定值方案

服务从裸机/IIS 迁到 Kubernetes,第一个要回答的问题就是:request/limit 开多少、副本数算几个。这份文档给出一套可执行的完整流程——从本地压测拿到单 pod 容量曲线,到类生产环境校准,到定值上线,再到上线后用真实流量收敛。配套的观测体系一并给出:哪些指标现成、哪些要补、用什么判读。

适用对象:基于 Aegis(3.1.x,net8.0)构建的 WebAPI 与后台服务。

三条原则​

后面所有公式和流程都从这三条推出来:

  1. request 锚稳态,limit 锚峰值。CPU request 取目标流量下稳态用量的 P95 向上取档;limit 取压测拐点档的用量留余量。内存 request 取含 L1 缓存满载的稳态 RSS,limit 再乘 1.3。
  2. TPS 必须绑 SLO 才是容量。"单 pod 能扛 2000 TPS" 在 P99 = 2s 时没有意义。容量定义为满足延迟 SLO(如 P99 ≤ 300ms)前提下的最大到达速率。另外注意 Broker 熔断的 SlowCallMs 默认 3000ms,压测时 P99 逼近这个值会先触发熔断——这本身就是现成的拐点探测器。
  3. 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 万也就几十 MBBrokerCircuitRegistry.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~500MiLibreOfficeOptions.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'],
},
};

状态仿真,混合场景的真实性主要取决于三项:

  1. key 热度分布:真实流量是长尾型(少数热 key 扛大部分读),不是均匀随机。按生产 popularity 分布生成请求,L1 命中率才对——命中率差一倍,容量差一倍。
  2. DB 数据量:测试库灌到生产量级,或至少确认执行计划同构。
  3. 认证状态:token 新签发与复用的成本完全不同,按生产行为来。

背景负载:Jobs 按生产真实注册开着测(1s 扫描 + 8 并行是常驻地板);补一档"整点风暴"——cron 集中在整点触发的任务洪峰,是经典的隐藏拐点。

故障混合:掺 1% 下游超时的流量。Broker 重试默认 3 次指数退避,下游降速时在途请求被重试放大,内存与连接行为与 happy path 完全不同——容量曲线的"抗退化能力"从这档读。

成本账本:单接口压测的新角色。 每个场景类单独低速率(约 20 rps)跑 5 分钟,算"每百请求的 CPU 增量 + 分配速率"。用途有二:

  • 预测容量 ≈ CPU 预算 ÷ 加权平均每请求成本,与混合实测拐点对照,偏差应在 ±15% 内;超出即存在干扰项(连接池竞争、GC 交叉污染、锁竞争),差值本身就是诊断线索。
  • 流量结构变化或新增接口时,先用账本估新容量,结构大变才重跑混合曲线。

阶梯与判读​

  1. 预热 10 分钟,约 30% 预估负载:让 JIT、Tiered PGO、连接池、L1 缓存全部热起来。
  2. 按预估容量的 25% → 50% → 75% → 100% → 125% 阶梯加压,每档稳 5~8 分钟。5 分钟的下限来自 L1 缓存 TTL 300s——每档不持稳过一个 TTL 周期,看到的内存就是不完整的缓存足迹。
  3. 每档记录四组数:
组指标来源
流量达成速率、错误率、P50/P95/P99k6 summary(--summary-export)
CPU容器 CPU 用量、throttle 次数docker stats / cgroup 文件
内存RSS(working set)docker stats
运行时GC 堆、Gen2 次数、ThreadPool 队列长度、分配速率dotnet-counters monitor -n <进程名> --counters System.Runtime
  1. 拐点判据,以下先到为准:P99 穿 SLO;达成率 < 95%;CPU 顶到 limit 且出现 throttling;% Time in GC 持续 > 15~20% 或 Gen2 频繁触发;错误率抬升。拐点对应速率即该资源配置下的单 pod 容量。

  2. 资源读法:读目标工况档,不读拐点档。拐点和日常流量之间要留余量。比如拐点 400 TPS、日常期望 250 TPS:读 250 TPS 档的稳态 CPU(如 550m)和稳态 RSS(如 380Mi)来定 request;limit 维持测拐点时的档位(1 核)。

  3. 主曲线后加一档突发:目标速率 × 2 持续 60~90 秒再回落,观察恢复速度、Gen2 是否被冲出来、RSS 是否回落。这一档对应生产毛刺,也是检验余量的试金石——2 倍冲击就 OOM 或 P99 十几秒不回落,说明收得太紧。

  4. key 分布要仿真 L1 命中:全重复 key(全命中)和全随机 key(全 miss)的 TPS 与内存差好几倍。用贴近生产的 key 分布,或者两种都测取区间。

  5. 压测配置对齐生产:日志级别与生产一致(日志是真实的 CPU 消耗者,关日志 TPS 虚高);下游连真 Redis/DB 或等延迟仿真;连接复用方式(keep-alive)与生产网关一致。

阶段一产出​

  • 容量表:每档(到达率、P99、CPU、RSS、GC 概况)
  • 拐点速率 + 目标工况档的资源稳态值
  • 资源初值:CPU request / limit,内存 request / limit

零平台时的本机预测试(无 OTel / Prometheus / Seq)​

阶段一不依赖任何观测平台——每层观测都有本机等价物,四个终端跑完:

观测需求本机等价物说明
运行时指标(GC/线程池/分配)dotnet-countersEventPipe 直连进程,不经任何管道
容器 CPU/内存水位docker stats内存口径即 working set,与 kubelet 的 OOM 判据一致
CPU 限流证据cgroup cpu.statnr_throttled 增长 = limit 太紧的直接证据
OOM 判定docker inspectState.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'],
},
};

三个边界要清楚:

  1. 开发机(尤其 Mac)结论的适用范围:M 系列 CPU 与生产 x86 节点不同构,绝对 TPS 不可迁移;曲线形态、内存足迹、线程池与 GC 行为的结论有效。定量 TPS 放到 Linux 开发环境跑——这就是"开发机预测试 + 开发环境定量"的分工。
  2. 没有 OTLP 采集端就把 tracing 关掉(OpenTelemetry:Enabled = false)。框架在无 endpoint 时退化为 console exporter(ServiceCollectionExtensions.cs),压测时刷屏且空耗 CPU,阶段一用不到 trace。
  3. 预测试的产出边界:资源初值 + 粗问题排查(L1 实际占用、内存泄漏、线程池饥饿)。最终定值依赖阶段二/四的真实监控,平台是那两个阶段的依赖,不是第一阶段的。

阶段二:类生产校准​

需要 DevOps 配合的就三件事,带着阶段一的数字去谈:

  1. 环境保真。节点规格一致(CPU 型号直接影响 TPS,共享超卖节点上的数字不可信);独占的 Redis/DB 实例(压测打共享下游会冲掉别人的测试环境,也污染自己的数字)。
  2. 监控查询权限。container_memory_working_set_bytes 等 cAdvisor 指标的 P95 曲线来自 Prometheus/Grafana,没有权限就退回 kubectl top 粗值,定值质量降一档。
  3. 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
低流量内部服务 / 任务宿主100250m / 500m1 核256Mi / 512Mi
典型业务 API(几十~百余 QPS)250~500m / 1 核384Mi / 768Mi~1Gi
数百 QPS、JSON + Broker 转发为主500m1 核 / 12 核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_totalP95/P99 对 request,收敛复核的主指标
container_memory_working_set_bytesOOM 判定依据就是它,不是 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 面板​

  1. 压测驾驶舱:同一时间轴上叠 k6 到达率/达成率/错误率/P99 × pod CPU%/throttle/RSS/Gen2 计数。拐点在这块板上一眼可见。
  2. 容量定值面板:CPU 用量 P95 ÷ request 利用率、working_set ÷ limit、throttle 比例,按 deployment 分组。这块板就是阶段四的复核工作台。
  3. 运行时健康面板:GC、线程池队列、分路由 P99、下游延迟(Broker 窗口)、健康检查状态。

告警清单​

告警条件(示例)含义与处置
CPU 限流rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m]) > 0.10 持续 10mlimit 太紧或 request 偏低;升 limit 或查线程池
内存贴限working_set > 0.9 × limit 持续 15m即将 OOM;查 L1 key 增长或升 limit
OOMKilledlast_terminated_reason{reason="OOMKilled"} 出现立即:limit 太低(对照 75% 堆硬限)或泄漏
HPA 顶格current_replicas == max_replicas 持续 30mrequest 锚错或容量真不足
P99 超 SLOhistogram_quantile(0.99, rate(http.server.request_duration_bucket[5m])) 阈值按 SLO先看 throttling 和下游延迟定位归属
熔断打开日志/Seq 关键字下游故障或自身限流传导,看熔断注册表状态

常见坑速查​

  1. 内存 limit 贴 RSS 设 → GC 堆硬限只有 limit 的 75%,线程栈 + native + 缓冲挤在 25% 里,OOMKill 或 GC 抖动。永远留 30%。
  2. CPU limit < 1 核 → Server GC 线程随核数走,被限流后 GC 停顿放大,延迟抖动没有业务原因。有流量的服务 limit ≥ 1 核。
  3. request-only 不设 DOTNET_GCHeapCount → 运行时按宿主机核数开 GC 堆,64 核节点 64 个堆,白吃几十 MB 还加剧上下文切换。
  4. request 低于稳态真实用量 → HPA 读数永远 200%+ 长期顶格(等于失效);节点超卖时按权重抢不到 CPU。
  5. 3 pod 实测不线性(< 2.2×) → 瓶颈在下游共享资源(DB 池/Redis/被调服务),加 pod 无解,先扩下游。
  6. 压测关日志、用随机 key → TPS 虚高、L1 内存失真,两头的定值都不准。
  7. 单 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% 调整