欢迎光临
我们一直在努力

百分考在线考试压测实录:从独立考生到集中交卷的 k6 全链路实践

在线考试压测不能只把某个接口连续调用几万次。可信的测试需要让每名虚拟考生拥有独立身份和独立答卷,完整经历入场、取题、持续保存、集中交卷和结果完成,并同时核验接口、业务结果、最终状态与资源水位。

本文记录百分考在单企业百万题库背景下的完整考试压测:通过免登录考试入口登记并创建独立考生身份,完成条件随机抽题、持续保存、集中交卷和结果处理,并分别记录固定卷、随机卷在 1000、3000、5000 名考生档位的五机分离实测结果。本轮测量单个应用实例的完整链路能力,多实例规模由同系列专项测试展开。

本文的 5000 用户档,是指 5000 名独立考生在 20 秒窗口内入场,即平均每秒启动 250 名考生,并在同一轮完成每人 50 次答案保存和集中交卷。这是一轮完整考试的考生总量,不等于 5000 个请求同时执行,也不是 5000 RPS。

在“百分考在线考试技术实践”专栏中,试卷快照确定内容依据,答卷一致性确定保存、提交和阅卷的状态规则。本文将这些规则转化为考试业务 HTTP 链路的负载与验收指标;这里的实测规模不代表同时开启主摄、屏幕和手机副摄的音视频监考容量。

测试环境

本轮在五台同内网主机上完成,负载机与单个应用实例各为 8 vCPU、16 GiB,数据库为 8 vCPU、16 GiB,缓存与消息队列各为 4 vCPU、8 GiB,从而把被测应用、压测机和数据服务各自的资源消耗区分开。环境版本如下:

角色规格软件版本
负载机 8 vCPU / 16 GiB k6 v1.2.3
应用实例 8 vCPU / 16 GiB Java 21 / Spring Boot 3.4.1
数据库 8 vCPU / 16 GiB MySQL 8.4.11
缓存 4 vCPU / 8 GiB Redis 8.2.9
消息队列 4 vCPU / 8 GiB RabbitMQ 4.2.0

测试数据与考试配置

本轮在同一个企业租户下提前生成 100 万道模拟试题,分布到多个题库,并设置题型和难度。试题构造属于压测前的数据准备,百万题库作为各档位考试运行的数据基础。

随机卷按题库、难度和题型组合设置抽题规则,在考生开始考试时生成个人卷面。每条规则从符合条件的候选题中抽取;100 万道是企业试题总量,并非每条规则的候选数量。固定卷与随机卷分别运行,让预先确定题目与入场时按规则抽题两类流程都有对应结果。

两类卷面的版本绑定差异,见《百分考在线考试随机组卷与试卷快照:如何保留每名考生的实际试卷》。本篇据此区分负载场景,记录开始考试、保存、提交与结果完成的表现。

数据层次本轮处理方式在测试中的作用
试题数据 单企业提前生成 100 万道模拟试题,分布于多个题库,设置题型和难度 提供大规模题库与组合条件抽题的数据背景
考生数据 虚拟考生从免登录入口登记,创建独立身份后进入考试 将身份创建纳入本轮入场负载
考试配置 提前准备固定卷与随机卷;随机规则按题库、难度和题型组合 分别验证两类卷面的创建与读取
考试运行数据 入场创建独立答卷,随后执行每人 50 次答案保存、交卷和结果处理 覆盖业务写入、状态迁移与最终结果核验

“免登录”描述的是考生无需提前持有账号登录的参与方式。进入考试后,登记形成的身份、个人答卷和后续结果保持对应。本轮考生身份在入场链路中创建,不以预先导入账号替代这部分请求。

这套数据安排同时考查两个方面:百万题库背景下能否按业务条件生成卷面,以及不断产生的考生、答卷和作答数据能否顺利处理。随机卷“开始考试”指标反映其入场处理表现,不能直接当作抽题 SQL 的独立耗时。

先看结论

  • 在单企业百万题库背景下,5000 用户单轮完成免登录登记入场与独立答卷创建,产生 285000 个不含结果轮询的核心 HTTP 请求,其中包含 250000 次有效答案保存和 5000 次交卷;5000 份结果完成是业务状态计数;
  • 固定卷 5000 用户档的答案保存 P95 为 38.4ms,交卷受理 P95 为 612.6ms,完成观测等待 P95 为 1240ms;
  • 单个 8 vCPU、16 GiB 应用实例的整机 CPU 峰值为 82.5%,数据库、缓存和消息队列主机分别为 52.3%、15.6% 和 11.4%;这些数值反映了 5000 人档对各层级实际施加的压力;
  • 百分考支持多应用实例横向扩展,Nginx 负载均衡负责请求分发,多个应用实例共享独立部署的数据库、缓存和消息队列。

一名虚拟考生为什么必须对应一份独立答卷

如果多个虚拟用户共用账号、答卷或题目上下文,测到的只是接口重复调用能力,无法覆盖真实考试中的身份、答卷绑定、答案写入和交卷状态迁移。本次测试的每名用户通过免登录入口登记建立独立身份,只读写自己的答卷;随机卷还会按本场题库、难度和题型规则确定实际题目。

#mermaid-svg-ruEt16hLFVGrSiSo{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ruEt16hLFVGrSiSo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ruEt16hLFVGrSiSo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ruEt16hLFVGrSiSo .error-icon{fill:#552222;}#mermaid-svg-ruEt16hLFVGrSiSo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ruEt16hLFVGrSiSo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ruEt16hLFVGrSiSo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ruEt16hLFVGrSiSo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ruEt16hLFVGrSiSo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ruEt16hLFVGrSiSo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ruEt16hLFVGrSiSo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ruEt16hLFVGrSiSo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ruEt16hLFVGrSiSo .marker.cross{stroke:#333333;}#mermaid-svg-ruEt16hLFVGrSiSo svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ruEt16hLFVGrSiSo p{margin:0;}#mermaid-svg-ruEt16hLFVGrSiSo .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ruEt16hLFVGrSiSo .cluster-label text{fill:#333;}#mermaid-svg-ruEt16hLFVGrSiSo .cluster-label span{color:#333;}#mermaid-svg-ruEt16hLFVGrSiSo .cluster-label span p{background-color:transparent;}#mermaid-svg-ruEt16hLFVGrSiSo .label text,#mermaid-svg-ruEt16hLFVGrSiSo span{fill:#333;color:#333;}#mermaid-svg-ruEt16hLFVGrSiSo .node rect,#mermaid-svg-ruEt16hLFVGrSiSo .node circle,#mermaid-svg-ruEt16hLFVGrSiSo .node ellipse,#mermaid-svg-ruEt16hLFVGrSiSo .node polygon,#mermaid-svg-ruEt16hLFVGrSiSo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ruEt16hLFVGrSiSo .rough-node .label text,#mermaid-svg-ruEt16hLFVGrSiSo .node .label text,#mermaid-svg-ruEt16hLFVGrSiSo .image-shape .label,#mermaid-svg-ruEt16hLFVGrSiSo .icon-shape .label{text-anchor:middle;}#mermaid-svg-ruEt16hLFVGrSiSo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ruEt16hLFVGrSiSo .rough-node .label,#mermaid-svg-ruEt16hLFVGrSiSo .node .label,#mermaid-svg-ruEt16hLFVGrSiSo .image-shape .label,#mermaid-svg-ruEt16hLFVGrSiSo .icon-shape .label{text-align:center;}#mermaid-svg-ruEt16hLFVGrSiSo .node.clickable{cursor:pointer;}#mermaid-svg-ruEt16hLFVGrSiSo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ruEt16hLFVGrSiSo .arrowheadPath{fill:#333333;}#mermaid-svg-ruEt16hLFVGrSiSo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ruEt16hLFVGrSiSo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ruEt16hLFVGrSiSo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ruEt16hLFVGrSiSo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ruEt16hLFVGrSiSo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ruEt16hLFVGrSiSo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ruEt16hLFVGrSiSo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ruEt16hLFVGrSiSo .cluster text{fill:#333;}#mermaid-svg-ruEt16hLFVGrSiSo .cluster span{color:#333;}#mermaid-svg-ruEt16hLFVGrSiSo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ruEt16hLFVGrSiSo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ruEt16hLFVGrSiSo rect.text{fill:none;stroke-width:0;}#mermaid-svg-ruEt16hLFVGrSiSo .icon-shape,#mermaid-svg-ruEt16hLFVGrSiSo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ruEt16hLFVGrSiSo .icon-shape p,#mermaid-svg-ruEt16hLFVGrSiSo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ruEt16hLFVGrSiSo .icon-shape .label rect,#mermaid-svg-ruEt16hLFVGrSiSo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ruEt16hLFVGrSiSo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ruEt16hLFVGrSiSo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ruEt16hLFVGrSiSo :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

各档位独立虚拟考生

在 20 秒窗口内启动免登录登记入场

登记信息并创建独立考生身份

开始考试并创建独立答卷

读取本人试题

循环保存 50 道题的有效答案

等待共同交卷时点

每人提交自己的答卷

轮询本人的结果状态

核验保存、提交与完成数量

汇总接口延迟、业务结果与资源水位

完整考试负载编排:免登录登记建立独立身份,开始考试时绑定卷面并创建答卷;随后持续保存,在共同交卷时点形成集中提交压力。

完整链路中每名用户发起 57 个核心 HTTP 请求:分享入口解析、免登录考试信息读取、登记注册、开始考试共 4 个,答卷摘要 2 个,答案保存 50 个,正式交卷 1 个。登记注册已经包含在这 4 个入场请求中,不在原有请求量之外另加一次。5000 用户档因此产生 285000 个核心 HTTP 请求,其中包含 250000 次有效答案保存。保存次数描述请求工作量,不直接等同于新增数据库行数。交卷后的结果查询次数取决于每份答卷完成的时间,总 HTTP 请求数按下式计算:

总 HTTP 请求数 = 285000 + 所有考生的实际结果轮询次数

下面是删去业务地址和鉴权细节后的 k6 结构示意,指标按业务阶段分别记录。

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter, Rate, Trend } from 'k6/metrics';

const beginDuration = new Trend('begin_exam_duration');
const saveDuration = new Trend('answer_save_duration');
const submitDuration = new Trend('submit_exam_duration');
const finishWait = new Trend('finish_wait_duration');
const savedAnswers = new Counter('saved_answers');
const submittedUsers = new Counter('submitted_users');
const finishedUsers = new Counter('finished_users');
const businessFailures = new Rate('business_failures');

export default function () {
// 包含分享入口解析、考试信息读取及免登录登记注册。
const candidate = registerViaAnonymousExamEntry();
const paper = beginExam(candidate, beginDuration);

for (const question of paper.questions) {
const response = saveAnswer(candidate, question, saveDuration);
const ok = check(response, { '答案保存成功': isBusinessSuccess });
businessFailures.add(!ok);
if (ok) savedAnswers.add(1);
sleep(randomThinkTime());
}

waitUntilSharedSubmitTime();
const submitted = submitExam(candidate, submitDuration);
const submitOk = check(submitted, { '交卷受理成功': isBusinessSuccess });
businessFailures.add(!submitOk);
if (!submitOk) return;
submittedUsers.add(1);

if (pollUntilFinished(candidate, finishWait)) {
finishedUsers.add(1);
}
}

为什么把负载机、应用和中间件分开

本轮不是“单台 8 核 16 GiB 服务器承载整套系统”,而是五台主机走同一内网:负载机和单个应用实例各为 8 vCPU、16 GiB;数据库为 8 vCPU、16 GiB;缓存和消息队列各为 4 vCPU、8 GiB。这样可以区分被测应用、压测机和数据服务各自的资源消耗。

#mermaid-svg-pwjH2p9ueTfTZzDv{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pwjH2p9ueTfTZzDv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pwjH2p9ueTfTZzDv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pwjH2p9ueTfTZzDv .error-icon{fill:#552222;}#mermaid-svg-pwjH2p9ueTfTZzDv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pwjH2p9ueTfTZzDv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pwjH2p9ueTfTZzDv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pwjH2p9ueTfTZzDv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pwjH2p9ueTfTZzDv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pwjH2p9ueTfTZzDv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pwjH2p9ueTfTZzDv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pwjH2p9ueTfTZzDv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pwjH2p9ueTfTZzDv .marker.cross{stroke:#333333;}#mermaid-svg-pwjH2p9ueTfTZzDv svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pwjH2p9ueTfTZzDv p{margin:0;}#mermaid-svg-pwjH2p9ueTfTZzDv .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-pwjH2p9ueTfTZzDv .cluster-label text{fill:#333;}#mermaid-svg-pwjH2p9ueTfTZzDv .cluster-label span{color:#333;}#mermaid-svg-pwjH2p9ueTfTZzDv .cluster-label span p{background-color:transparent;}#mermaid-svg-pwjH2p9ueTfTZzDv .label text,#mermaid-svg-pwjH2p9ueTfTZzDv span{fill:#333;color:#333;}#mermaid-svg-pwjH2p9ueTfTZzDv .node rect,#mermaid-svg-pwjH2p9ueTfTZzDv .node circle,#mermaid-svg-pwjH2p9ueTfTZzDv .node ellipse,#mermaid-svg-pwjH2p9ueTfTZzDv .node polygon,#mermaid-svg-pwjH2p9ueTfTZzDv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pwjH2p9ueTfTZzDv .rough-node .label text,#mermaid-svg-pwjH2p9ueTfTZzDv .node .label text,#mermaid-svg-pwjH2p9ueTfTZzDv .image-shape .label,#mermaid-svg-pwjH2p9ueTfTZzDv .icon-shape .label{text-anchor:middle;}#mermaid-svg-pwjH2p9ueTfTZzDv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pwjH2p9ueTfTZzDv .rough-node .label,#mermaid-svg-pwjH2p9ueTfTZzDv .node .label,#mermaid-svg-pwjH2p9ueTfTZzDv .image-shape .label,#mermaid-svg-pwjH2p9ueTfTZzDv .icon-shape .label{text-align:center;}#mermaid-svg-pwjH2p9ueTfTZzDv .node.clickable{cursor:pointer;}#mermaid-svg-pwjH2p9ueTfTZzDv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pwjH2p9ueTfTZzDv .arrowheadPath{fill:#333333;}#mermaid-svg-pwjH2p9ueTfTZzDv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pwjH2p9ueTfTZzDv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pwjH2p9ueTfTZzDv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pwjH2p9ueTfTZzDv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pwjH2p9ueTfTZzDv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pwjH2p9ueTfTZzDv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pwjH2p9ueTfTZzDv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pwjH2p9ueTfTZzDv .cluster text{fill:#333;}#mermaid-svg-pwjH2p9ueTfTZzDv .cluster span{color:#333;}#mermaid-svg-pwjH2p9ueTfTZzDv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pwjH2p9ueTfTZzDv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pwjH2p9ueTfTZzDv rect.text{fill:none;stroke-width:0;}#mermaid-svg-pwjH2p9ueTfTZzDv .icon-shape,#mermaid-svg-pwjH2p9ueTfTZzDv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pwjH2p9ueTfTZzDv .icon-shape p,#mermaid-svg-pwjH2p9ueTfTZzDv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pwjH2p9ueTfTZzDv .icon-shape .label rect,#mermaid-svg-pwjH2p9ueTfTZzDv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pwjH2p9ueTfTZzDv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pwjH2p9ueTfTZzDv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pwjH2p9ueTfTZzDv :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

HTTP 完整考试链路

答卷与状态持久化

会话与试卷缓存

交卷后异步消息

负载机k6 v1.2.38 vCPU / 16 GiB

单应用实例Java 21 / Spring Boot 3.4.18 vCPU / 16 GiB

数据库MySQL 8.4.118 vCPU / 16 GiB

缓存Redis 8.2.94 vCPU / 8 GiB

消息队列RabbitMQ 4.2.04 vCPU / 8 GiB

五机分离拓扑。具体组件名用于说明容量模型,不表示压测方法只能用于这些组件。

单实例基线与负载节奏

本轮先测量单个应用实例的完整考试链路能力。增加应用实例后,Nginx 分发和共享数据库、缓存、队列需要按汇总负载重新验证;具体拓扑、逐节点分布与故障复核由多实例专题展开。

本轮到达窗口为 20 秒,开启 gzip 与连接复用。固定卷和随机卷各覆盖 1000、3000、5000 用户,形成六个基础场景;两种卷型再分别完成一次 5000 用户复核。各轮 dropped iteration 均为 0,压测及观测进程退出码均为 0。

5000 名考生不等于 5000 个同时请求

这是解读本文数据的关键。在 k6 的到达率模型中,“每秒启动多少次迭代”、“需要预分配多少 VU”和“实际产生多少 RPS”是三个不同概念。Grafana k6 官方文档 也明确说明:到达率决定迭代启动速度,VU 数量则还受单次迭代持续时间影响。

因此,本文中的 5000 应解读为“本轮共有 5000 名独立考生”;20 秒入场窗口对应的是平均 250 名考生/秒的启动速度。一名考生内部还包含多个请求、思考时间和结果轮询,所以不能用 5000 直接推导 HTTP RPS、MySQL 连接数或 Redis 连接数。

5000 人档实测得到的吞吐量如下:

负载指标5000 人档实测值
20 秒入场窗口 250 名考生/秒
核心 HTTP 请求 285000
结果轮询请求 约 10000
含结果轮询的 HTTP 请求 约 295000
完整链路持续时间 约 128 秒
全程平均 HTTP 吞吐 约 2300 RPS
HTTP 峰值 约 5200 RPS
答案保存峰值 约 2600 次/秒
交卷请求峰值 约 3100 次/秒

这组数据避免了“285000 个请求全部发生在 20 秒内”的误读。20 秒只是考生入场窗口,50 次答案保存、思考时间、共同交卷和结果轮询会把完整链路拉长到约 2 分钟。入场窗口内 30000 个入场与摘要请求集中到达,叠加刚入场考生的首批保存,是 HTTP 峰值约 5200 RPS 的主要来源;答案保存散布在约 100 秒作答期内,峰值约 2600 次/秒;交卷集中在共同时点爆发,峰值约 3100 次/秒,高于保存峰值。结果查询采用延迟首查和未完成再查,平均每名考生约 2 次,不会在交卷后立即形成无限频率轮询。

1000、3000、5000 用户的完整链路结果

下表统一采用每人保存 50 次的完整链路。单位为 ms,数据为各阶段 P95 实测值;“完成观测等待”表示交卷后通过查询观察到处理完成的等待时间,包含轮询间隔,不是 RabbitMQ 自身的纯消息处理延迟。

用户数卷型开始考试 P95保存 P95交卷受理 P95完成观测等待 P95保存 / 交卷 / 完成
1000 固定卷 34.8 19.6 310.8 1120 50000 / 1000 / 1000
1000 随机卷 48.2 20.8 304.6 1160 50000 / 1000 / 1000
3000 固定卷 46.5 27.9 421.7 1180 150000 / 3000 / 3000
3000 随机卷 66.8 30.2 438.9 1210 150000 / 3000 / 3000
5000 固定卷 63.7 38.4 612.6 1240 250000 / 5000 / 5000
5000 随机卷 88.9 42.6 658.4 1290 250000 / 5000 / 5000

本轮 HTTP 失败率和业务失败率均为 0,保存、交卷和最终完成计数与计划值一致。固定卷 5000 用户档的开始考试 P99 为 92.4ms、保存 P99 为 66.8ms、交卷 P99 为 884.2ms;全局 HTTP 分位数不能套用到某个具体接口。

从 1000 升至 5000 用户时,固定卷开始考试 P95 从 34.8ms 增至 63.7ms,交卷受理 P95 从 310.8ms 增至 612.6ms;完成观测等待 P95 只从 1120ms 小幅变化到 1240ms。原因是前三项直接承受同步请求压力,而结果完成由固定并行度的消费者持续处理;在队列没有形成持续积压时,观察时间主要受消费耗时和轮询间隔影响,不随考生总量成倍增长。随机卷与固定卷的完成观察值也接近,因为两者都在交卷后进入同一套异步处理链路。

最高档位能否重复

5000 用户档按同样的 20 秒到达窗口、每人 50 次保存和共同交卷参数进行了复核。固定卷与随机卷的三个主要 P95 指标相对首轮偏差约为 1.55%~2.82%,均低于 4%。在相同负载条件下,两轮关键响应指标保持接近。

卷型轮次保存 P95(ms)交卷受理 P95(ms)完成观测等待 P95(ms)
固定卷 首轮 38.4 612.6 1240
固定卷 复核轮 39.2 625.8 1260
随机卷 首轮 42.6 658.4 1290
随机卷 复核轮 43.8 676.5 1310

复核轮沿用 250000 次保存、5000 次交卷和 5000 份结果完成的验收目标,计数完整且两轮波动处于可解释范围,说明顶档结果具备可重复性。

1000、3000、5000 三档资源和组件水位

下面的 CPU 和内存均按对应压力窗口的整机高水位统计,组件吞吐、连接和队列深度取相同窗口内的指标高水位。负载机与应用机负责产生和处理 HTTP 流量,放在一张表中;MySQL、Redis、RabbitMQ 的瓶颈信号不同,分别观测。

负载机与应用机:确认压力来源有效

两台机器均为 8 vCPU、16 GiB。负载机不能先于被测应用耗尽资源,应用机则要同时观察 CPU、内存、GC 和连接池。

资源指标1000 用户档3000 用户档5000 用户档
负载机 CPU 高水位 13.8% 31.7% 49.6%
负载机内存高水位 0.78 GiB 1.38 GiB 2.10 GiB
应用机 CPU 高水位 34.6% 57.3% 82.5%
应用机内存高水位 3.18 GiB 4.12 GiB 5.10 GiB

5000 人档下负载机 CPU 仍低于 50%,没有 dropped iteration,计划负载得到完整执行。应用机 CPU 峰值达到 82.5%,是本轮主要资源压力点;同时保存、交卷和结果完成计数全部达标。单应用实例在较高 CPU 水位下仍完成了完整考试链路,更大规模由后续多实例测试验证。

MySQL:CPU、内存、事务和连接一起看

MySQL 主机为 8 vCPU、16 GiB。数据库内存不会像 CPU 那样随人数线性变化,因为 InnoDB 缓冲池通常在启动时已经预留主要空间。

MySQL 指标1000 用户档3000 用户档5000 用户档
CPU 高水位 14.2% 31.8% 52.3%
整机内存高水位 8.14 GiB 8.36 GiB 8.60 GiB
峰值 QPS / TPS 3100 / 520 7900 / 1320 13200 / 2200
连接高水位 / max_connections 34 / 500 55 / 500 72 / 500
Threads_running 峰值 7 12 18
行锁等待超时增量 0 0 0
死锁增量 0 0 0

连接数由应用连接池约束,不会与考生人数等比例增长。在 5000 人完整链路负载下,MySQL CPU 峰值为 52.3%,连接高水位为 72/500,未出现行锁等待超时和死锁。数据库在完成本轮混合读写任务的同时,仍保有 CPU 和连接余量,集中交卷未引发这些拥塞信号。

Redis:区分整机内存和数据内存

Redis 主机为 4 vCPU、8 GiB。整机内存包含系统、Redis 进程和缓冲区开销,数据内存才对应实际缓存数据集,两者分别展示。

Redis 指标1000 用户档3000 用户档5000 用户档
CPU 高水位 4.2% 9.8% 15.6%
整机内存高水位 0.62 GiB 0.88 GiB 1.18 GiB
Redis 数据内存高水位 0.24 GiB 0.48 GiB 0.76 GiB
峰值 ops/s 1900 4900 7800
连接高水位 / maxclients 28 / 10000 39 / 10000 48 / 10000
命中率 96.8% 97.0% 97.1%
blocked clients 0 0 0
rejected connections 增量 0 0 0
evicted keys 增量 0 0 0

三个档位的命中率保持稳定,blocked、rejected 和 evicted 均为 0,说明吞吐增长没有依靠降低缓存命中率或逐出数据换取。

RabbitMQ:生产、消费和积压一起看

RabbitMQ 主机为 4 vCPU、8 GiB。CPU 和内存只说明主机水位,publish、deliver/ack、ready 与 unacked 才能说明消费者是否跟上集中交卷流量。

RabbitMQ 指标1000 用户档3000 用户档5000 用户档
CPU 高水位 3.0% 6.8% 11.4%
整机内存高水位 0.58 GiB 0.72 GiB 0.90 GiB
publish 峰值 880 msg/s 2280 msg/s 3650 msg/s
deliver/ack 峰值 840 msg/s 2180 msg/s 3480 msg/s
messages_ready 峰值 72 168 280
unacked 峰值 56 122 196
负载结束 ready / unacked 0 / 0 0 / 0 0 / 0

三个档位的 publish 与 deliver/ack 基本匹配,ready 和 unacked 只出现短时高水位,并在负载结束后归零。这些条件与完成计数同时成立,说明异步处理链路完成。

本次压测在五机拓扑、单应用实例和 5000 名考生的负载模型下,未出现负载机饱和、应用资源失控、数据库连接耗尽、Redis 拒绝或逐出、RabbitMQ 测试结束残留消息等明显异常。由于本轮没有将各中间件单独加压到饱和点,不应由此推导它们的极限 QPS、剩余容量百分比或多实例线性扩展倍数。

为什么接口成功之后还要核验业务结果

只看 HTTP 200 会遗漏业务错误,只看提交受理会遗漏异步处理未完成。本次测试设置 HTTP、业务、计数、状态、资源和清理六个核验环节,并在每轮结束后按测试边界清理,再复核测试产生的数据库记录和 Redis key 已删除,RabbitMQ 相关队列的 ready 与 unacked 均为 0。

“交卷受理”和“结果完成”为何必须分开,以及重复请求如何裁决,见《百分考在线考试答卷一致性设计:答案保存、重复交卷与异步阅卷》。本篇将这些状态规则作为验收依据,分别记录响应、计数和最终状态。

#mermaid-svg-spwS8PuUWGSxBCAd{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-spwS8PuUWGSxBCAd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-spwS8PuUWGSxBCAd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-spwS8PuUWGSxBCAd .error-icon{fill:#552222;}#mermaid-svg-spwS8PuUWGSxBCAd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-spwS8PuUWGSxBCAd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-spwS8PuUWGSxBCAd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-spwS8PuUWGSxBCAd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-spwS8PuUWGSxBCAd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-spwS8PuUWGSxBCAd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-spwS8PuUWGSxBCAd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-spwS8PuUWGSxBCAd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-spwS8PuUWGSxBCAd .marker.cross{stroke:#333333;}#mermaid-svg-spwS8PuUWGSxBCAd svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-spwS8PuUWGSxBCAd p{margin:0;}#mermaid-svg-spwS8PuUWGSxBCAd .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-spwS8PuUWGSxBCAd .cluster-label text{fill:#333;}#mermaid-svg-spwS8PuUWGSxBCAd .cluster-label span{color:#333;}#mermaid-svg-spwS8PuUWGSxBCAd .cluster-label span p{background-color:transparent;}#mermaid-svg-spwS8PuUWGSxBCAd .label text,#mermaid-svg-spwS8PuUWGSxBCAd span{fill:#333;color:#333;}#mermaid-svg-spwS8PuUWGSxBCAd .node rect,#mermaid-svg-spwS8PuUWGSxBCAd .node circle,#mermaid-svg-spwS8PuUWGSxBCAd .node ellipse,#mermaid-svg-spwS8PuUWGSxBCAd .node polygon,#mermaid-svg-spwS8PuUWGSxBCAd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-spwS8PuUWGSxBCAd .rough-node .label text,#mermaid-svg-spwS8PuUWGSxBCAd .node .label text,#mermaid-svg-spwS8PuUWGSxBCAd .image-shape .label,#mermaid-svg-spwS8PuUWGSxBCAd .icon-shape .label{text-anchor:middle;}#mermaid-svg-spwS8PuUWGSxBCAd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-spwS8PuUWGSxBCAd .rough-node .label,#mermaid-svg-spwS8PuUWGSxBCAd .node .label,#mermaid-svg-spwS8PuUWGSxBCAd .image-shape .label,#mermaid-svg-spwS8PuUWGSxBCAd .icon-shape .label{text-align:center;}#mermaid-svg-spwS8PuUWGSxBCAd .node.clickable{cursor:pointer;}#mermaid-svg-spwS8PuUWGSxBCAd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-spwS8PuUWGSxBCAd .arrowheadPath{fill:#333333;}#mermaid-svg-spwS8PuUWGSxBCAd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-spwS8PuUWGSxBCAd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-spwS8PuUWGSxBCAd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-spwS8PuUWGSxBCAd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-spwS8PuUWGSxBCAd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-spwS8PuUWGSxBCAd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-spwS8PuUWGSxBCAd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-spwS8PuUWGSxBCAd .cluster text{fill:#333;}#mermaid-svg-spwS8PuUWGSxBCAd .cluster span{color:#333;}#mermaid-svg-spwS8PuUWGSxBCAd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-spwS8PuUWGSxBCAd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-spwS8PuUWGSxBCAd rect.text{fill:none;stroke-width:0;}#mermaid-svg-spwS8PuUWGSxBCAd .icon-shape,#mermaid-svg-spwS8PuUWGSxBCAd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-spwS8PuUWGSxBCAd .icon-shape p,#mermaid-svg-spwS8PuUWGSxBCAd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-spwS8PuUWGSxBCAd .icon-shape .label rect,#mermaid-svg-spwS8PuUWGSxBCAd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-spwS8PuUWGSxBCAd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-spwS8PuUWGSxBCAd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-spwS8PuUWGSxBCAd :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

HTTP 层状态码与接口分位数

业务层返回码与业务失败率

计数层保存、交卷、完成数量

状态层轮询观察答卷完成

资源层五机连续采样

清理复核测试记录与 key 删除队列无残留

压测证据链:接口受理、业务成功、最终状态与资源观测相互补充,任何单一指标都不能独立代表完整考试成功。

mq_finish_duration 记录的是从进入结果等待到轮询观察到完成的总时间,所以本文称它为“完成观测等待”,不将其误写为消息队列耗时。本轮使用固定轮询间隔;RabbitMQ 的 publish 与 deliver/ack 基本匹配、ready 没有持续增长,三个用户档的该指标保持在相近区间。真正判断 MQ 是否受压,应优先看 publish、deliver/ack、ready、unacked 和消费者利用率,而不是只看客户端何时轮询到最终状态。数据库内容核验也与“保存接口成功计数”分开:前者证明最终状态,后者证明请求阶段的业务响应。

5000 是单实例实测档位,多实例由专项压测验证

本次实测结果表明,单个 8 vCPU、16 GiB 应用实例在 5000 名独立考生的指定完整考试链路下,应用 CPU 约 82.5%、MySQL 约 1.32 万 QPS、Redis 约 7800 ops/s 和 RabbitMQ 约 3650 msg/s 是重点观测量级。这组数据给出单应用实例在指定链路下的基线;集群容量还需结合入口分发、共享服务和节点故障单独验证。

在单企业百万题库背景下,百分考通过一个 8 vCPU、16 GiB 应用实例,完成 5000 名考生的免登录登记入场、25 万次答案保存、集中交卷与结果处理。 固定卷与按题库、难度和题型规则生成的随机卷均完成全部业务计数;随机卷开始考试 P95 为 88.9ms、保存 P95 为 42.6ms、交卷受理 P95 为 658.4ms。大规模题库、动态考生身份与考试业务写入在同一轮链路中得到覆盖,低延迟与完整结果同时成立。顶档复核的主要 P95 偏差均低于 4%。

延伸阅读

  • 更大规模的入口分发、共享服务和节点故障验证,见《3 万人多实例压测》。

完整系列与推荐阅读顺序见《百分考在线考试架构设计》中的专栏目录。

赞(0)
未经允许不得转载:171主机测评 » 百分考在线考试压测实录:从独立考生到集中交卷的 k6 全链路实践
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址