欢迎光临
我们一直在努力

百分考如何承载 3 万人在线考试:Nginx 多实例架构与全链路压测实录

3万人在线考试不是把单机压测结果简单乘以应用实例数。考生集中入场时,Nginx 需要稳定分发请求;持续作答时,多个应用实例会同时访问数据库和缓存;集中交卷时,交易写入、状态迁移和异步消息又会在短时间内叠加。

本文记录百分考在单企业百万题库背景下,3 万名独立考生通过免登录登记进入考试的完整压测。5 个应用实例与 Nginx 负载均衡共同承接身份创建、卷面生成、答案保存、交卷和结果处理。本篇沿用 k6 全链路压测专题的负载方法与单实例基线,重点验证入口分发、共享服务和节点故障。

本文的 3 万人结论对应上述考试业务 HTTP 链路,多路音视频发布、实时观看与录播的容量需要单独验证,不能由本轮结果推导。

专栏定位:本文为“百分考在线考试技术实践”的多实例容量专题。

测试环境

本轮环境由三台负载机分片施压、双 Nginx 入口、5 个同规格无状态应用实例构成,数据库、Redis 和 RabbitMQ 作为共享服务独立部署,规格按多实例汇总负载重新核算:

处理角色数量单节点规格主要职责
负载机 3 8 vCPU / 16 GiB 分片创建 3 万个独立考生迭代
Nginx 2 4 vCPU / 8 GiB TLS 终止、连接复用和上游分发
应用实例 5 8 vCPU / 16 GiB 入场、取题、保存、交卷和结果查询
MySQL 1 套 16 vCPU / 32 GiB 答卷、答案和状态持久化
Redis 1 套 8 vCPU / 16 GiB 会话、试卷与运行状态缓存
RabbitMQ 1 套 4 vCPU / 8 GiB 交卷后异步处理与消费者分发

先看结论

  • 3 万名考生在 60 秒内入场,平均到达速度为 500 人/秒,不等于 30000 RPS;
  • 每人执行 57 个核心 HTTP 请求,单轮共产生 171 万个核心请求,其中包含 150 万次答案保存和 3 万次交卷;
  • Nginx 集群负责将请求分发到 5 个应用实例,按总业务量折算,平均每实例承担约 6000 名考生对应的请求工作量;
  • 3 万人档 HTTP 峰值约 15800 RPS,5 个应用实例各自峰值 RPS 的极差约占峰值均值的 5.3%;
  • 固定卷答案保存 P95 为 48.6ms,交卷受理 P95 为 728.4ms,完成观测等待 P95 为 1420ms;
  • 多实例压测的验收对象不只是应用 CPU,还包括 Nginx 分发偏差、数据库总连接数、Redis 热点、RabbitMQ 积压和节点故障时的请求恢复。

3万人在线考试的多实例拓扑

入口层通过统一入口将流量送入两个 Nginx 节点,应用层使用 5 个同规格无状态实例。数据库、Redis 和 RabbitMQ 作为共享服务独立部署,规格按多实例汇总负载重新核算。图中统一入口表示接入职责。

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

30000 名独立考生

统一入口

Nginx 1

Nginx 2

统一上游池

应用实例 1

应用实例 2

应用实例 3

应用实例 4

应用实例 5

MySQL

Redis

RabbitMQ

为什么5个实例不能直接按5倍换算

从 5000 人提升到 3 万人,考生总量增加了 6 倍,应用实例增加到 5 个,入场窗口也从 20 秒调整为 60 秒。6000 人是按业务总量折算的平均工作量,同一考生的不同请求仍可由不同实例处理。两轮同时调整了入口节奏和共享服务规格,因此这里比较的是两套完整容量方案的实际表现。

更重要的是,应用实例可以横向增加,共享的 MySQL、Redis 和 RabbitMQ 却会同时承接 5 个应用节点的汇总请求。多实例方案围绕三个问题展开核验:

  • Nginx 向 5 个应用实例分发请求;
  • 各应用实例共同完成 3 万名考生的请求工作量;
  • 共享中间件承接住汇总后的连接、查询、缓存命令和消息流量。
  • 3万人完整考试负载如何拆解

    本轮沿用单企业 100 万道模拟试题的数据规模,试题分布到多个题库。随机卷按题库、难度和题型组合抽取,固定卷与随机卷分别测试。百万题库提前准备,考生身份与答卷则在压测入场过程中创建,后续持续写入作答并推进提交和阅卷状态。

    每名考生通过免登录入口完成分享解析、考试信息读取、登记注册与开始考试,共 4 次入场请求;随后执行 2 次答卷摘要请求、50 次有效答案保存和 1 次正式交卷,再轮询本人结果。登记注册已经计入每人 57 个核心请求。150 万次答案保存是请求工作量,不直接等同于新增 150 万条数据库记录。

    负载项计算方式3万人档
    入场与摘要请求 30000 × 6 180000
    答案保存 30000 × 50 1500000
    正式交卷 30000 × 1 30000
    核心 HTTP 请求 30000 × 57 1710000
    结果轮询 延迟首查,未完成再查 约 60000
    含轮询 HTTP 请求 1710000 + 60000 约 1770000

    实测得到的调度与吞吐数据如下:

    指标实测值
    考生到达窗口 60 秒
    考生平均到达速度 500 人/秒
    完整链路持续时间 约 165 秒
    全程平均 HTTP 吞吐 约 10700 RPS
    HTTP 峰值 约 15800 RPS
    答案保存峰值 约 12400 次/秒
    交卷请求峰值 约 4800 次/秒

    60 秒只是考生入场窗口,不是整轮考试请求全部完成的时间。50 次保存、思考时间、集中交卷和结果轮询会将完整链路拉长到约 2.5~3 分钟。

    Nginx 验证的不只是能否转发

    Nginx 在多实例考试中的主要任务是稳定分发,而不是让某个应用节点因长连接或慢请求过早成为热点。本轮配置使用 least_conn、上游 keepalive 和被动健康检查:

    upstream exam_backend {
    least_conn;
    server 10.0.1.11:7000 max_fails=3 fail_timeout=10s;
    server 10.0.1.12:7000 max_fails=3 fail_timeout=10s;
    server 10.0.1.13:7000 max_fails=3 fail_timeout=10s;
    server 10.0.1.14:7000 max_fails=3 fail_timeout=10s;
    server 10.0.1.15:7000 max_fails=3 fail_timeout=10s;
    keepalive 512;
    }

    location /api/ {
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_connect_timeout 3s;
    proxy_read_timeout 30s;
    proxy_pass http://exam_backend;
    }

    答卷和提交状态由数据库持久化,共享缓存承接可重建的热点数据与运行信息,不依赖某个应用实例的本地内存。这样考生的相邻两次请求即使落到不同实例,也能围绕同一份答卷继续处理,不需要依赖 IP hash 或粘滞会话。上面的代码是关键配置节选,节点地址为示例地址。

    负载机、入口和5个应用实例的流量分布

    多实例压测先确认三台负载机没有成为瓶颈,再分别统计两个入口节点和每个应用节点的请求数、CPU、内存、GC 和连接池。只看集群总 RPS,会掩盖负载机饱和或单个服务节点过热。

    节点峰值 RPSCPU 峰值内存峰值
    负载机 1 5320 43.8% 2.18 GiB
    负载机 2 5260 42.9% 2.14 GiB
    负载机 3 5220 42.4% 2.11 GiB
    Nginx 1 8060 26.4% 1.12 GiB
    Nginx 2 7740 25.7% 1.09 GiB
    实例 1 3140 61.8% 5.26 GiB
    实例 2 3220 63.2% 5.39 GiB
    实例 3 3270 64.1% 5.48 GiB
    实例 4 3100 60.9% 5.21 GiB
    实例 5 3170 62.5% 5.34 GiB

    三台负载机的 CPU 峰值均低于 45%,dropped iteration 为 0,计划负载得到完整执行。两个 Nginx 的峰值差相对较大值约为 4.0%;五个应用实例的峰值均值为 3180 RPS,极差为 170 RPS,极差占均值约 5.3%。这说明各节点达到的峰值量级接近;同一时刻的分发均衡需要按同窗流量观察,各节点独立峰值不能相加作为集群峰值。

    五个应用实例的 CPU 峰值为 60.9%~64.1%,低于单实例测试的 82.5%。在本轮入口节奏和共享服务配置下,集群完成了更大的考试总量,同时将单节点 CPU 压力维持在较从容的区间。

    3万人档的完整链路结果

    下表采用每人 50 次有效答案保存,数据为各阶段 P95 实测值。“完成观测等待”包含结果轮询间隔,不是 RabbitMQ 的纯消息延迟。

    卷型开始考试 P95保存 P95交卷受理 P95完成观测等待 P95保存 / 交卷 / 完成
    固定卷 86.2ms 48.6ms 728.4ms 1420ms 1500000 / 30000 / 30000
    随机卷 124.8ms 54.3ms 796.5ms 1490ms 1500000 / 30000 / 30000

    在 3 万人档,两种卷型的答案保存 P95 均低于 60ms,交卷受理 P95 均低于 800ms,结果完成观测等待 P95 均低于 1.5 秒。完成观察值相较单实例 5000 人档小幅增加;结合下文的消费速率、队列水位和结束后归零结果,本轮异步处理跟上了考试流量。完成观测等待包含轮询间隔,与 MQ 指标分别解读。

    共享数据库、缓存和队列的观测结果

    应用实例增加后,中间件的容量不能继续使用单实例规格。数据库连接池也按“单实例连接池 × 应用实例数”重新核算,不能只看单个节点的活跃连接。

    三类组件不能合并成一行判断:MySQL 重点看事务、连接和运行线程,Redis 重点看命令吞吐、数据内存与逐出,RabbitMQ 重点看生产消费速率和队列深度。下表中的 CPU 和整机内存均取完整压力窗口内的主机高水位;QPS、ops/s、消息速率和队列深度取对应组件指标高水位。

    先用少量指标对照相同业务模型下的单实例基线,再展开本轮共享服务的完整结果。两轮入场窗口与机器规格不同,以下对照用于理解汇总负载变化,不能单独归因于应用实例数量;单实例完整资源表由全链路压测专题持有。

    对照指标5000 用户单实例档3 万用户多实例档
    核心 HTTP 请求总量 285000 1710000
    MySQL 峰值 QPS 13200 48600
    MySQL 连接高水位 / 上限 72 / 500 310 / 1000
    Redis 峰值 ops/s 7800 28600
    RabbitMQ publish 峰值 3650 msg/s 4700 msg/s

    MySQL:同时看 CPU、内存、事务和连接

    MySQL 指标3 万用户多实例档3 万人档判断
    测试规格 16 vCPU / 32 GiB 按多实例汇总负载提高数据库规格
    CPU 高水位 62.7% 承接汇总读写后仍保有 CPU 余量
    整机内存高水位 14.2 GiB 与 CPU、连接和锁等待一起判断数据库水位
    峰值 QPS / TPS 48600 / 8100 观察共享数据库承接的汇总读写
    连接高水位 / max_connections 310 / 1000 由 5 个应用连接池汇总,但仍低于连接上限
    Threads_running 峰值 28 与连接高水位分别观察实际运行线程
    行锁等待超时增量 0 集中交卷窗口无锁等待超时
    死锁增量 0 本轮没有出现死锁

    MySQL 的 310 个连接不是 3 万名考生直接连接数据库,而是 5 个应用实例连接池的汇总高水位。判断数据库是否健康,不能只看 QPS,还要同时确认 Threads_running、慢查询、锁等待和死锁没有在交卷窗口持续增长。

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

    Redis 指标3 万用户多实例档3 万人档判断
    测试规格 8 vCPU / 16 GiB 为多应用实例提供共享缓存和运行状态
    CPU 高水位 24.6% 命令量增长后仍有计算余量
    整机内存高水位 3.6 GiB 包含 Redis 进程、系统和缓冲区开销
    Redis 数据内存高水位 3.1 GiB 与整机内存分开统计,未触发逐出
    峰值 ops/s 28600 命令处理量随业务峰值增长
    连接高水位 / maxclients 210 / 10000 连接主要来自应用连接池,距离上限较远
    命中率 97.3% 结合吞吐和逐出指标判断缓存效果
    blocked clients 0 无阻塞客户端
    rejected connections 增量 0 无连接拒绝
    evicted keys 增量 0 无缓存数据逐出

    Redis 的整机内存和数据内存分开写。前者用于判断主机水位,后者才对应 Redis 实际数据集;如果只写“内存 3.6 GiB”,读者无法判断是缓存数据增长,还是操作系统与进程自身开销。

    RabbitMQ:同时看生产、消费和积压

    RabbitMQ 指标3 万用户多实例档3 万人档判断
    测试规格 4 vCPU / 8 GiB 按实际消息峰值和积压情况判断容量
    CPU 高水位 15.2% 消息峰值提升后仍有计算余量
    整机内存高水位 1.20 GiB 包含队列、连接、Erlang VM 和系统开销
    publish 峰值 4700 msg/s 与交卷请求峰值处于一致量级
    deliver/ack 峰值 4620 msg/s 与 publish 基本匹配,消费者没有明显掉速
    messages_ready 峰值 420 允许短时高水位,但下一观测周期应明显回落
    unacked 峰值 290 与消费者并行处理规模匹配,不持续增长
    负载结束 ready / unacked 0 / 0 压测结束后没有残留消息

    RabbitMQ 的 CPU 较低不能单独证明消费正常;只有 publish 与 deliver/ack 基本匹配、ready 和 unacked 不持续上升、负载结束后队列归零,才能说明消费者跟上了集中交卷流量。这也解释了为什么“完成观测等待”在不同人数档位只应小幅变化,而不应随考生数成倍增长。

    本轮核心业务请求总量是单实例 5000 人档的 6 倍,MySQL QPS 和 Redis ops/s 峰值约为其 3.6~3.7 倍,RabbitMQ publish 峰值约为其 1.3 倍。总工作量与单位时间峰值分别反映容量方案的两个侧面;这些比例是两轮观测值的对照,不直接归因于入场窗口。结合 3 万份结果全部完成、队列结束后归零,可以看出共享服务完成了本轮汇总负载,异步处理没有形成持续积压。

    多实例压测必须包含节点故障

    只验证五个节点全部正常时的吞吐量,不能证明集群具备故障恢复能力。本轮增加了以下复核:

  • 在持续保存阶段停止一个应用实例;
  • 验证 Nginx 在被动失败检测将节点标记不可用后,暂时停止向该节点分发新请求;
  • 验证其余四个实例的 CPU、延迟和连接池没有失控;
  • 核对保存成功数、交卷数和最终完成数,未因节点下线出现重复交卷或答卷丢失;
  • 恢复节点后观察流量平滑回切,未出现刚上线节点突然承接过多长连接。
  • 对于答案保存和正式交卷这类写请求,应用层通过答卷归属、业务唯一键和重复提交裁决保证幂等,而不是仅依赖 Nginx 盲目重试。

    写请求切换节点后为什么仍能围绕同一份答卷处理,以及重复交卷如何裁决,见《百分考在线考试答卷一致性设计:答案保存、重复交卷与异步阅卷》。本篇的故障复核检验这些规则在节点退出后的业务结果。

    3万人多实例验证结果

    多实例验收不只看“Nginx 返回 200”。本轮完整验证同时满足:

    • 3 万名考生均创建独立答卷;
    • 150 万次有效答案保存全部受理;
    • 3 万次交卷全部形成唯一最终状态;
    • 3 万份结果全部在规定等待窗内完成;
    • 正常容量轮的 HTTP 失败、业务失败和 dropped iteration 均为 0;
    • 5 个应用实例各自峰值 RPS 的极差占均值约 5.3%,各节点达到的峰值量级接近;
    • MySQL 没有连接耗尽、死锁或锁等待超时;
    • Redis 没有拒绝、阻塞或数据逐出;
    • RabbitMQ 在交卷尖峰后恢复,压测结束时队列无残留;
    • 节点故障复核中,已受理答案未丢失、交卷未重复,其余节点继续处理请求。故障恢复结论按最终业务结果核验,与正常容量轮的请求失败率分开表述。

    总结

    3万人在线考试的关键不是“在 Nginx 后面多放几台服务器”,而是让入口分发、应用实例、数据库连接、缓存热点和异步消费能力保持匹配。

    在本轮压测中,3 万名独立考生在 60 秒内入场,通过两个 Nginx 入口和 5 个 8 vCPU、16 GiB 应用实例承载;系统处理 171 万个核心 HTTP 请求、150 万次答案保存和 3 万次集中交卷。固定卷与随机卷的保存 P95 均低于 60ms,交卷受理 P95 均低于 800ms。共享服务未表现出资源饱和,消息队列出现短时高水位后恢复,测试结束时 ready 与 unacked 均归零。

    这轮测试展示了百分考在百万题库与 3 万人考试规模下的完整业务承载能力:从免登录登记创建身份,到按业务条件生成随机卷面,再到答案保存、交卷和结果完成,关键接口保持较低延迟。 节点故障复核也保持了已受理答案和提交状态的一致性。对考试组织者而言,容量价值体现在大题库与持续业务写入共同存在时,考试仍能从入场运行到结果完成。

    延伸阅读

    • 独立考生负载编排与单实例基线,见《k6 全链路压测》。
    • 共享服务中的企业隔离与数据保护,见《在线考试数据安全设计》。

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

    赞(0)
    未经允许不得转载:171主机测评 » 百分考如何承载 3 万人在线考试:Nginx 多实例架构与全链路压测实录
    分享到: 更多 (0)

    评论 抢沙发

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