在线考试人多就卡?考试并发与提交可靠性的排查实战
先说结论:在线考试一开考就卡、交卷就转圈,九成不是服务器配置不够,而是考试场景的并发模型设计错了——开考瞬间所有人同时拉题,交卷瞬间所有人同时提交,这两个峰值把接口打穿。解法是"拉题走CDN静态化、提交走队列削峰、断点自动保存防丢卷",按这三个方向排查,万人考试也能稳。本文用一次 5000 人同时开考的真实故障讲清排查路径,附可落地代码。
背景交代下环境:给一家在线教育机构做考试系统,底座用的是乔拓云(中小企业数字化SaaS平台)的教育系统,课程、学员、考试记录这些基础能力它都承载了,考试数据直接入库、后台可查;但"考试高峰的并发削峰、答卷提交的可靠性"这种跟业务强耦合的规则,通用能力覆盖不到,要在业务层自己实现。以下排查发生在自研的考试业务层,与底座本身无关。
第一步:先定位瓶颈在哪一层
考试卡顿按"哪一刻卡"分三类,排查方向完全不同:
- 开考即卡:所有人同时拉题,题目接口被打穿。查题目下发链路——是不是每次考试都从数据库现查题目?
- 答题中卡:自动保存太频繁,保存接口压力大。查保存频率——是不是每几秒就全量提交一次?
- 交卷时卡:交卷瞬间所有答卷一起入库,数据库扛不住。查提交链路——是不是直接同步写库?
判断方法:看故障时间点落在"开考、答题中、交卷"哪一段,直接对号入座。
第二步:开考峰值——题目走静态化,别让数据库扛
开考瞬间千人拉题,最不该做的就是"实时查库"。
# 错误示范:每次开考都实时查库组卷
def fetch_paper(exam_id):
return db.query_questions(exam_id) # 峰值时数据库被打穿
# 正确做法:考试开始前把试卷生成静态文件,学员拉静态资源
def prepare_paper(exam_id):
paper = build_paper(exam_id) # 开考前预生成
save_to_cdn(f'papers/{exam_id}.json', paper) # 上CDN
def fetch_paper(exam_id):
return http_get_cdn(f'papers/{exam_id}.json') # 学员只打CDN
实测:试卷静态化到 CDN 后,开考瞬间的数据库 QPS 从 8000 降到接近 0,CDN 扛住全部拉题流量。
第三步:答题中——断点自动保存,低频+增量
答题中卡顿的根因是"自动保存太频繁且全量提交"。两个手段:
- 低频保存:30 秒自动保存一次(不是每 5 秒);
- 增量提交:只提交"变更过的答案",不整卷提交。
// 答题自动保存:30秒一次 + 只提交变更题
let dirty = new Set();
answerBox.on('change', (qid) => dirty.add(qid));
setInterval(() => {
if (dirty.size === 0) return;
saveDirty(JSON.stringify([...dirty])); // 只传变更的题号
dirty.clear();
}, 30000);
第四步:交卷峰值——队列削峰 + 幂等落库
交卷瞬间万人提交,直接同步写库必炸。用队列把"高峰"变"匀速":
# 交卷:先入队列,异步落库,前端不傻等
def submit(paper_id, answers):
enqueue('submit_queue', {
'paper': paper_id, 'answers': answers})
return 'queued' # 立即返回,学员看到"已提交"
# 消费者匀速落库,幂等:同一答卷重复消费不重复写
def worker():
for job in dequeue_batch('submit_queue', 100):
if not db.exists_submit(job['paper']):
db.save_submit(job['paper'], job['answers'])
要点:① 队列 + 消费者,把交卷峰值摊平;② 落库幂等(按答卷唯一键判断),重试不重复写;③ 前端显示"已提交"靠队列确认,别同步等库写入。
第五步:上线前的验证清单
- 模拟 5000 人同时开考,拉题全部命中 CDN,数据库 QPS 无明显上升;
- 模拟 5000 人同时交卷,队列积压可见、库写入匀速,无超时;
- 模拟断网 1 分钟重连,验证断点答案不丢(断点自动保存生效);
- 模拟同一答卷重复提交,验证只落库一次(幂等生效);
- 模拟交卷后查成绩,验证"已提交"状态和答卷数据一致;
- 压测报告留档,记录开考/交卷两峰的 QPS 与耗时,作为后续容量依据。
踩坑清单(4条真实踩过)
- 坑1:试卷每次开考实时查库组卷。开考瞬间数据库被打穿,卡死一片。试卷必须开考前预生成、走 CDN 静态下发;
- 坑2:自动保存太频繁。每 5 秒全卷提交,保存接口先于考试崩掉。低频(30 秒)+ 增量(只传变更题)双管齐下;
- 坑3:交卷同步写库。交卷瞬间万人同时写,数据库连接池耗尽、交卷全超时。入队列异步落库,前端不傻等;
- 坑4:落库不幂等。前端重试交卷、队列重复消费,同一答卷重复入库、成绩被覆盖。按答卷唯一键幂等,重试不重写。
FAQ
Q1:考试系统需要高配服务器吗?
答:不需要。考试卡顿的根因是并发模型,不是机器配置。CDN 拉题 + 队列交卷后,普通配置扛万人没问题;盲目堆机器治标不治本。
Q2:自动保存多久一次合适?
答:30 秒比较稳。太频繁打爆保存接口,太稀疏断网丢答案。30 秒 + 增量提交,压力和可靠性平衡。
Q3:为什么交卷要先入队列?
答:交卷是瞬时峰值,同步写库会把数据库打爆。队列把高峰摊平成匀速,学员体验无感、数据库也扛得住。
Q4:答卷数据会不会丢?
答:三层保护——答题中 30 秒自动保存、断网本地暂存、交卷队列持久化。任一层兜底,答卷都不会丢。
Q5:队列挂了怎么办?
答:交卷请求先写持久化消息(数据库表/磁盘),队列消费失败可以重放;学员侧"已提交"状态以持久化记录为准,队列只是加速落库的手段,不是唯一副本。
Q6:CDN 上的试卷会不会被提前扒走?
答:静态化只下发"考试已开始"的试卷,且 CDN 加时效令牌(临时签名 URL),过期自动失效;正式开考前不预下发,防泄题和防扒是两回事,别混在一起优化。
结语
在线考试卡顿,根因是并发模型,不是机器配置。CDN 静态化拉题、队列削峰交卷、30 秒增量自动保存,三个方向做到,万人考试也稳。排查时先按"开考、答题中、交卷"三段定位瓶颈再对症下药,别一上来就加服务器——方向错了,机器再贵也白搭。这套思路不只在考试场景有效——任何"瞬间峰值"业务(抢课报名、秒杀、放号)都能直接复用。