3.3 配套代码:集成基准与全链路压测
对应小节:3.3 集成基准与全链路 两层各一份代码:集成基准(单服务完整启动)与全链路 k6(多进程/多机)。
一、集成基准:单服务完整启动
// src/test/kotlin/bench/IntegrationBenchmark.kt
package bench
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.coroutines.*
import kotlin.system.measureNanoTime
/**
* 集成基准:把服务完整启动起来测。
*
* 与组件基准的区别:
* ✅ 覆盖 拦截器 → 鉴权 → 参数校验 → 事务 → 多 Repository → 序列化 的完整链路
* ✅ 能观察连接池在并发下的行为
* ❌ 不含网络与网关(那是全链路的事)
* ❌ 闭环模型会低估尾延迟(第 0 章 0.6 节)
*/
object IntegrationBenchmark {
@JvmStatic
fun main(args: Array<String>) = runBlocking {
// ① 完整启动服务(真实端口,不用 testApplication —— 后者会略过一些真实开销)
val server = embeddedServer(Netty, port = 8080) {
app.module() // 你的 Application.module()
}.start(wait = false)
val client = HttpClient(CIO) {
engine { maxConnectionsCount = 256 }
}
try {
// ② 独立预热阶段:JIT + 连接池 + 缓存(数据不计入统计)
println("预热中(60 秒,不计入统计)...")
runLoad(client, concurrency = 8, durationMs = 60_000, record = false)
// ③ 正式测量
println("\n正式测量:并发 8,持续 30 秒")
val latencies = runLoad(client, concurrency = 8, durationMs = 30_000, record = true)
report(latencies)
} finally {
client.close()
server.stop(1_000, 5_000)
}
}
/**
* 闭环并发压测(VU 模型)。
* ⚠️ 已知局限:服务端变慢时会自动降压,尾延迟被低估。
* 这里用它是因为「与组件基准做相对比较」足够,且实现简单。
* 要测真实尾延迟请用 k6 的开环模型(见下方)。
*/
private suspend fun runLoad(
client: HttpClient,
concurrency: Int,
durationMs: Long,
record: Boolean,
): LongArray = coroutineScope {
val collector = if (record) java.util.concurrent.ConcurrentLinkedQueue<Long>() else null
val jobs = (0 until concurrency).map {
launch(Dispatchers.IO) {
val deadline = System.currentTimeMillis() + durationMs
while (System.currentTimeMillis() < deadline) {
val ns = measureNanoTime {
client.get("http://127.0.0.1:8080/orders/1")
}
collector?.add(ns)
}
}
}
jobs.joinAll()
(collector?.toLongArray() ?: LongArray(0)).also { it.sort() }
}
private fun report(sorted: LongArray) {
if (sorted.isEmpty()) return
fun pct(q: Double) = sorted[((sorted.size - 1) * q).toInt()].toDouble() / 1000
println("样本数: ${sorted.size}")
println("P50 = %.1f us".format(pct(0.50)))
println("P95 = %.1f us".format(pct(0.95)))
println("P99 = %.1f us".format(pct(0.99)))
println("max = %.1f us".format(sorted.last() / 1000.0))
println()
println("⚠️ 这是闭环模型的结果,只用于「与组件基准比较」,不能用于验收 SLO。")
}
}
集成基准要观察的三件事:
| 观察 | 怎么看 |
|---|---|
| 与组件基准的差额 | 差额 = 框架开销(拦截器、事务、序列化) |
| 连接池行为 | 压测期间看 /metrics 的 db_pool_active / db_pool_pending |
| 启动预热曲线 | 从启动到「P99 稳定」用了多久(第 2 章 2.4 节) |
# 压测期间另开终端观察连接池
watch -n 1 'curl -s localhost:8080/metrics | grep -E "db_pool_(active|idle|pending|total)"'
二、全链路压测:k6 开环脚本
// loadtest/e2e-baseline.js
import http from 'k6/http';
import { check } from 'k6';
import { Trend, Rate } from 'k6/metrics';
const okRate = new Rate('ok_rate');
export const options = {
scenarios: {
steady: {
// ✅ 开环:按固定到达率发压,不受服务端响应速度影响(防协调遗漏)
executor: 'constant-arrival-rate',
rate: Number(__ENV.RATE || 500),
timeUnit: '1s',
duration: __ENV.DURATION || '10m',
preAllocatedVUs: 500, // ⚠️ 不足会导致实际到达率低于设定值
maxVUs: 5000,
gracefulStop: '30s',
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
'http_req_duration{expected_response:true}': ['p(95)<200', 'p(99)<500'],
ok_rate: ['rate>0.99'],
},
discardResponseBodies: true,
summaryTrendStats: ['avg', 'min', 'med', 'p(90)', 'p(95)', 'p(99)', 'p(99.9)', 'max'],
};
// 幂律取样的 ID(制造真实热点)
function zipf(max) {
const u = Math.random();
return Math.max(1, Math.floor(max * Math.pow(u, 3)) + 1);
}
export default function () {
const res = http.get(`${__ENV.BASE_URL}/orders/${zipf(1_000_000)}`, {
tags: { name: 'GET /orders/{id}' }, // ⚠️ 用路由模板,不要用具体 URL(防指标基数爆炸)
});
const ok = check(res, { 'status 200': (r) => r.status === 200 });
okRate.add(ok);
}
# 运行并把原始数据落盘
mkdir -p docs/experiments/E03-two-level-test/results
BASE_URL=http://app-host:8080 RATE=500 DURATION=10m \
k6 run --out json=docs/experiments/E03-two-level-test/results/k6-raw.json \
--summary-export=docs/experiments/E03-two-level-test/results/k6-summary.json \
loadtest/e2e-baseline.js
三、压测完成后必须做的第一件事
验证实际到达率是否等于设定值。 这是判断数据能不能用的第一步:
# 从 summary 里读出实际 QPS,与设定值对比
python3 - <<'PY'
import json, os
s = json.load(open("docs/experiments/E03-two-level-test/results/k6-summary.json"))
actual = s["metrics"]["http_reqs"]["rate"]
expected = float(os.environ.get("RATE", 500))
diff = (actual - expected) / expected
print(f"设定到达率 : {expected:.0f} req/s")
print(f"实际到达率 : {actual:.1f} req/s")
print(f"偏差 : {diff:+.1%}")
if abs(diff) > 0.05:
print("❌ 偏差超过 5% —— 压测机可能是瓶颈(preAllocatedVUs 不足 / CPU 打满)")
print(" 此时服务端的 QPS 与延迟数据【不可用】,请先修压测侧。")
else:
print("✅ 到达率符合预期,数据可用于分析")
PY
四、三层结果对比表(Lab 3 的核心产出)
| 指标 | 组件基准 | 集成基准 | 全链路 | 差额归因 |
| --- | --- | --- | --- | --- |
| P50 (ms) | | | | |
| P95 (ms) | | | | |
| P99 (ms) | | | | |
| 错误率 | — | | | |
| 测量点 | Repository 方法 | 服务端 handler | 压测客户端 | — |
| 并发模型 | 单线程循环 | 闭环 8 并发 | **开环 500 req/s** | — |
| 含网络 | ❌ | ❌ | ✅ | |
| 含网关 | ❌ | ❌ | ✅ | |
| 含排队 | ❌ | 部分 | ✅ | |
读表方法:
全链路 P99 − 集成基准 P99 = 网络往返 + 网关处理 + 客户端排队
集成基准 P99 − 组件基准 P99 = 框架开销(拦截器、事务、序列化)+ 服务内排队
如果这两个差额都很小(比如各占 5% 以内),说明你的服务没有明显的框架/网络开销——这是一个好结论。如果差额很大,差额本身就是优化方向。
五、动手改造
| 改动 | 观察什么 |
|---|---|
| 把集成基准的并发从 8 改成 64 | 连接池 pending 是否开始 > 0?延迟如何变化? |
把 k6 的 preAllocatedVUs 从 500 改成 50 |
实际到达率会明显低于设定值——这就是协调遗漏在工具层面的体现 |
把 k6 的 constant-arrival-rate 改成 constant-vus |
对比两种模型的 P99 差异(闭环会更乐观) |
| 在集成基准里去掉预热阶段 | 前几秒的数据会明显偏高 |
把 tag: { name: ... } 去掉,直接用带 ID 的 URL |
观察 Prometheus 指标数量爆炸(/metrics 输出体积) |
六、这段代码的局限
- 集成基准用闭环模型:会低估尾延迟。它的定位是「与组件基准做相对比较」,不能用于验收 SLO。真正的尾延迟要用 k6 的开环模型。
- 单机压测:集成基准的客户端与服务在同一台机器,会互相抢资源。全链路压测也应该用独立机器——这是最低要求。
- k6 脚本里的
zipf()是近似的齐夫分布,与真实热点分布未必一致。更真实的做法是从生产访问日志里提取真实的 ID 分布。 - 没有覆盖多副本:真实部署下请求会被负载均衡分发到多个实例,单实例压测测不出跨实例的争用(比如共享数据库连接数)。