在线考试压测不能只把某个接口连续调用几万次。可信的测试需要让每名虚拟考生拥有独立身份和独立答卷,完整经历入场、取题、持续保存、集中交卷和结果完成,并同时核验接口、业务结果、最终状态与资源水位。
本文记录百分考在单企业百万题库背景下的完整考试压测:通过免登录考试入口登记并创建独立考生身份,完成条件随机抽题、持续保存、集中交卷和结果处理,并分别记录固定卷、随机卷在 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 人档实测得到的吞吐量如下:
| 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 自身的纯消息处理延迟。
| 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%。在相同负载条件下,两轮关键响应指标保持接近。
| 固定卷 | 首轮 | 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 和连接池。
| 负载机 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 缓冲池通常在启动时已经预留主要空间。
| 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 进程和缓冲区开销,数据内存才对应实际缓存数据集,两者分别展示。
| 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 才能说明消费者是否跟上集中交卷流量。
| 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 万人多实例压测》。
完整系列与推荐阅读顺序见《百分考在线考试架构设计》中的专栏目录。

