文档目录

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 分布。
  • 没有覆盖多副本:真实部署下请求会被负载均衡分发到多个实例,单实例压测测不出跨实例的争用(比如共享数据库连接数)。