news 2026/10/8 3:58:57

AIGC赋能在线编程评测系统:架构设计与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AIGC赋能在线编程评测系统:架构设计与实践

简介:基于人工智能生成内容(AIGC)技术的在线编程题目评测系统完整工程源码,面向编程教育平台开发者、计算机专业学生及在线评测系统研究人员。系统整合了题目自动生成、代码智能评测、实时反馈、个性化学习路径推荐、用户认证授权、数据缓存与异步消息处理等核心模块,可支撑从基础语法到复杂算法的题目练习与评测场景。资源共61个文件,压缩包约81KB,以46个Java源文件为主体,辅以XML、YML及properties等配置文件,并包含SQL脚本、Maven配置、README说明与附赠素材,整体覆盖后端业务实现、测试用例、数据库初始化及项目构建配置。当前已有53人学习下载,适合希望快速理解AIGC教学平台整体架构、参考工程化实现细节的读者;借助代码结构与文档说明,可梳理出题目生成、评测反馈、路径推荐的完整业务流程。

1. 在线编程评测系统:为什么这次把 AIGC 放进题库生产线

校园 OJ 里最磨人的不是写题,而是提交之后那十几秒的等待,以及题库里翻来覆去那几百道老题。基于 AIGC 的在线编程题目评测系统,就是把“出题”和“判题”这两件最重的事都自动化:大模型按知识点生成题目与测试用例,评测服务在沙箱里跑代码、比对输出、回传实时反馈,再叠加学习路径推荐、用户认证授权、数据缓存和异步消息处理,构成一个能直接部署的在线编程练习闭环。它适合正在做在线教育、OJ 平台或编程训练营产品的团队,也适合想把题库运营成本降下来的教学管理者。

2. 整体架构与技术选型:把六个模块串成一条数据流水线

2.1 一条完整的请求链路:登录、出题、提交、评测、回传

先别急着选框架,把这条链路走一遍,你就知道哪些地方必须用异步、哪些地方必须做缓存。

第一个环节是用户认证授权。用户在 Web 端登录,后端签出 JWT,前端把 token 存下来,后续每次请求头里带上。这里就一个要求:认证服务必须无状态,不然分布式部署的时候 session 同步会变成噩梦。角色上一般分学生、教师、管理员三种,教师能审核题目、看统计数据,管理员管用户和沙箱节点。RBAC(基于角色的访问控制)在这个规模下够用,不需要上图谱权限那样重的模型。

第二个环节是 AIGC 出题。学生或教师发起“生成一道题”的请求,先把生成任务扔给消息队列,立刻返回“生成中”。大模型推理耗时可能十几秒甚至半分钟,如果 HTTP 请求同步等,前端早就超时了。生成服务消费消息,调用大模型 API,拿到题目描述、输入输出格式、测试用例,然后做校验,校验通过才进题库。

第三个环节是提交与评测。这是整个系统最重的流量入口。学生提交代码,后端不做任何判题动作,只把提交记录落库、状态置为 PENDING,然后往评测队列里丢一条消息,立即返回“已提交”。评测节点从队列里取消息,拉起沙箱,编译、运行、比输出,再把结果写回,通过 WebSocket 推给前端。用户看到的是“评测中 → Accepted/WA”,背后的异步链路已经把瞬时流量摊平了。

第四个环节是学习路径推荐。评测结果产生后,系统更新用户的知识点掌握度,推荐下一个该练的题。这个模块不要求实时,晚一分钟都无所谓,所以可以用异步消费者慢慢算。

整条链路走完你会发现一个规律:凡是耗时不确定的操作(生成题目、评测代码、更新推荐)全部放异步;凡是读多写少的数据(题目详情、用户信息、排行榜)全部走缓存。这就是后面技术选型的依据。

2.2 技术选型优先级:隔离、削峰、读缓存为什么排在最前面

在线编程评测和普通业务系统有个本质区别:你要执行一段来自用户的、未知的、可能存在恶意行为的代码。因此选型优先级里,沙箱隔离比数据库选型更关键。其次是并发削峰,因为评测计算耗时,瞬间大量提交会直接把评测节点打挂。最后才是常规的缓存和 ORM 选择。

下面这套组合是社区里跑得最多、踩坑资料也最多的方案。不是唯一解,但照着搭不会走偏。

模块常见选型选型理由
Web 框架Spring Boot / FastAPI生态成熟、连接池/线程池管理完善,方便对接 Redis 和 MQ
业务数据库PostgreSQL / MySQL题目、提交记录、用户表都是强事务数据
缓存Redis题目详情、排行榜、用户会话这类读多写少的数据
消息队列RabbitMQ / Kafka提交评测任务、生成题目任务的削峰与解耦
评测沙箱Docker + 资源限制隔离用户代码,API 成熟,社区案例多
实时推送WebSocket评测状态变化实时推到浏览器
认证JWT + RBAC无状态,便于水平扩展

选型逻辑再强调一下:框架层面用你团队最熟的那个就行,真正影响命运的选型是消息队列和沙箱。消息队列决定你能扛多大瞬时提交量,沙箱决定恶意代码能不能搞挂宿主机。缓存更像是提速器,等并发真的上来了再调也不迟,但架构上要留好位置。

部署上,我一般会用一个 docker-compose 先把基础设施串起来。下面这个编排文件是常见的最小集合,后端服务和评测节点先用镜像占位:

version: "3.8" services: postgres: image: postgres:15 environment: POSTGRES_DB: oj_platform POSTGRES_USER: oj_user POSTGRES_PASSWORD: oj_password ports: - "5432:5432" volumes: - pg_data:/var/lib/postgresql/data redis: image: redis:7 ports: - "6379:6379" command: ["redis-server", "--appendonly", "yes"] rabbitmq: image: rabbitmq:3-management ports: - "5672:5672" - "15672:15672" environment: RABBITMQ_DEFAULT_USER: oj_mq RABBITMQ_DEFAULT_PASS: oj_mq_password judge-node: image: judge-image:latest depends_on: - rabbitmq - redis environment: RABBITMQ_HOST: rabbitmq REDIS_HOST: redis deploy: replicas: 2 volumes: pg_data:

参数说明:postgres 挂载了命名卷,数据不会因为容器重启丢失;redis 开了 appendonly 持久化,防止缓存节点重启后热数据全丢;rabbitmq 暴露了 15672 管理端口,方便看队列积压;judge-node 的 replicas 设成 2,这是最朴素的评测节点扩容方式。生产环境不要把端口映射暴露到公网,内网访问即可。

2.3 核心表结构与任务状态机:不建模清楚,后面全是隐患

评测系统的核心表不多,但状态机必须提前定义清楚,否则后面排查超时、断线、重复消费都会很痛苦。提交记录表是核心中的核心,我常这样设计:

CREATE TABLE submission ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, problem_id BIGINT NOT NULL, code TEXT NOT NULL, language VARCHAR(20) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'PENDING', judge_score INT, used_time_ms INT, used_memory_kb INT, error_message TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() ); CREATE INDEX idx_submission_user_problem ON submission(user_id, problem_id); CREATE INDEX idx_submission_status ON submission(status) WHERE status IN ('PENDING', 'JUDGING');

status 字段的流转是评测系统里最容易翻车的地方:PENDING 表示消息已投递、还没被评测节点消费;JUDGING 表示沙箱正在跑;AC、WA、TLE、MLE、RE、CE 是终态;还有一个特殊状态 JUDGE_ERR,表示评测节点本身出问题了,不是用户代码的问题。所有中间状态都要带 updated_at 时间戳,超时排查就靠它。

为什么要在状态机上花这么多精力?因为生产环境里你会遇到消息重复消费、评测节点宕机、消费者重启等意外。状态机清晰,至少你能回答三个问题:这条提交卡在哪一步?卡了多久?是用户代码问题还是基础设施问题。就算是 PENDING 超过一分钟的积压,也能通过查队列长度快速定位。

3. 代码自动评测引擎:沙箱隔离与判定逻辑的落地细节

3.1 沙箱选型:为什么不能直接 subprocess 跑用户代码

见过不少内部试验项目,一开始图省事,直接在服务端 subprocess 调用系统编译器跑用户代码,结果上线没几天就出事故。最轻的是用户写个 while True 把 CPU 打满拖垮整个服务,最重的是遇到会读环境变量、扫描内网端口、把宿主文件删了的代码。评测系统的沙箱不是功能需求,是底线需求。

常见的隔离方案按强度分三档。第一档是纯软件的语言虚拟机限制,比如 Python 的 resource 模块限制内存和 CPU 时间,只能挡住初学者,挡不住恶意代码;第二档是 nsjail 这类基于系统调用的隔离沙箱,性能损耗低,但配置比较复杂,对多语言支持需要自己适配;第三档是 Docker 容器加资源配额,这也是最主流的做法——每个评测任务启动一个一次性容器,限制 CPU、内存、进程数、网络权限,跑完销毁。

三者的对比可以这样看:

方案隔离强度性能损耗配置复杂度适用场景
resource 模块低极低极低原型验证
nsjail中高低高对性能极敏感的大规模 OJ
Docker 容器高中中绝大多数生产平台

我的建议是:新项目直接上 Docker,别在 nsjail 上折腾。等你的评测节点需要追求极致吞吐、Docker 启动开销成为瓶颈时,再考虑用 nsjail。Docker 不是没有坑,它的坑在资源限制参数需要显式声明,不声明就默认共享宿主机资源,这部分下面避坑章节细说。

3.2 判定逻辑:编译、运行、限时、比对一条龙(附评测核心代码)

评测节点是消费者,从队列里拿到一条任务后,处理逻辑固定五步:拉代码、准备沙箱、编译、跑测试用例、比对输出。下面这段 Python 伪代码是核心流程,去掉业务包装后就是这个骨架:

"""评测节点核心流程:消费消息 -> 沙箱执行 -> 写回结果""" import docker import redis import pika import uuid MAX_CPU_TIME = 2 # 单测试用例最大CPU时间(秒) MAX_MEMORY = 256 * 1024 # 单测试用例最大内存(KB) IMAGE_NAME = "oj-python-runner:latest" def judge_task(ch, method, properties, body): task = json.loads(body) submission_id = task["submission_id"] code_path = task["code_path"] language = task["language"] test_cases = task["test_cases"] # [{input, expected}, ...] client = docker.from_env() container_name = f"judge-{submission_id}-{uuid.uuid4().hex[:8]}" try: # 1. 编译阶段:把用户代码挂载进容器内,编译失败的直接判 CE result = client.containers.run( IMAGE_NAME, command=f"python3 -m py_compile /workspace/main.py", volumes={code_path: {"bind": "/workspace/main.py", "mode": "ro"}}, mem_limit=f"{MAX_MEMORY}k", nano_cpus=1_000_000_000, # 1 个 CPU 核心 network_disabled=True, pids_limit=64, detach=True, remove=True, ) compile_result = result.wait(timeout=10) if compile_result["StatusCode"] != 0: write_result(submission_id, "CE", 0, 0, "compile error") return # 2. 跑每个测试用例,逐个限制时间和内存 for i, case in enumerate(test_cases): run_result = client.containers.run( IMAGE_NAME, command=f"python3 /workspace/main.py", stdin_open=True, volumes={code_path: {"bind": "/workspace/main.py", "mode": "ro"}}, mem_limit=f"{MAX_MEMORY}k", nano_cpus=1_000_000_000, network_disabled=True, pids_limit=64, detach=True, remove=True, ) # 写入输入,并等待结果 run_result.exec_run(cmd="sh -c 'cat > /workspace/input.txt'", stdin=True, data=case["input"]) wait_res = run_result.wait(timeout=MAX_CPU_TIME + 1) logs = run_result.logs(stdout=True, stderr=True).decode() if run_result.attrs["State"]["OOMKilled"]: write_result(submission_id, "MLE", 0, MAX_MEMORY, "memory limit exceeded") return if wait_res["StatusCode"] != 0: write_result(submission_id, "RE", 0, 0, logs[-500:]) return # 3. 输出比对:先归一化再比较 actual = normalize_output(logs) expected = normalize_output(case["expected"]) if actual != expected: write_result(submission_id, "WA", 0, 0, f"case {i} failed") return write_result(submission_id, "AC", used_time_ms, used_memory_kb, "") except docker.errors.ContainerError as e: write_result(submission_id, "JUDGE_ERR", 0, 0, str(e)) finally: try: client.containers.get(container_name).remove(force=True) except docker.errors.NotFound: pass

参数说明:mem_limit 用 256MB 上限,nano_cpus 是 1 个核心,network_disabled=True 断网,pids_limit 限制进程数防止 fork 炸弹。这里的逻辑说明一下:编译和运行分开,编译失败直接 CE,运行阶段每个测试用例独立计时,超出 wait(timeout) 会抛异常,映射成 TLE 更合理。输出比对先走 normalize_output 去掉行尾空白和统一换行符,避免学生代码和标准输出之间因为肉眼不可见的差异误判。每个用例失败立即返回 WA,不需要跑完所有用例。

实际生产里,test_cases 不是跟消息一起传的,而是题目表里存一份、评测节点从 Redis 或数据库取。上面代码为了展示流程做了简化。另外,Docker API 每次 containers.run 都要拉镜像或启动容器,开销不小,所以生产系统会做一个“预启动容器池”,需要时直接复用,这个在最后一章讲。

3.3 实时反馈:用 WebSocket 替代轮询,把评测状态推到浏览器

评测结果写回数据库后,前端怎么感知状态变化?老办法是轮询:每 2 秒发一次请求查状态,简单粗暴,但用户开着页面不动,也要持续消耗请求资源。评测场景下,一次评测通常 3 到 10 秒,轮询的浪费非常明显。

换 WebSocket 之后,评测节点每次更新状态就 push 一条消息到前端。链路是:评测节点写库 → Redis 发布订阅或消息队列广播 → WebSocket 服务端推给对应连接。按用户 ID 维护连接映射就行。这里的关键参数是心跳间隔和断线重连,心跳间隔建议 30 秒,前端检测到连接断开要自动重连,并补拉一次当前状态,防止断线期间状态更新丢失。

WebSocket 不是没坑:反向代理需要开启长连接支持,Nginx 默认的 proxy_read_timeout 60 秒会导致频繁断开;Docker 部署时要给 WebSocket 服务预留连接数上限。这些在避坑章节展开。能用 WebSocket 就尽量别用 Server-Sent Events,SSE 只能单向推,评测场景还需要客户端主动上报“停止评测”之类的指令,双向通道更省事。

4. AIGC 题目生成:Prompt 模板、质量校验与学习路径推荐

4.1 Prompt 模板:让大模型稳定生成“可运行”题目的关键参数

AIGC 在这个系统里承担的第一件事是出题。很多人以为出题就是“让 AI 写一道题”,实际跑起来会发现:生成出来的题目描述像模像样,但测试用例对不上,边界条件描述模糊,甚至样例输出都是错的。要让大模型稳定产出可用题目,关键在于把约束写进 Prompt,而不是靠它自由发挥。

下面是一个经过反复调校的 Prompt 模板,核心是强制结构化输出并让模型同时生成题解代码:

prompt = f""" 你是一名算法竞赛出题人。请按照以下要求生成一道编程题,并以JSON格式返回。 要求: 1. 知识点范围:{knowledge_point} 2. 难度等级:{difficulty}(easy / medium / hard) 3. 编程语言:不限,题解用Python 3编写 4. 题目描述需包含完整的输入输出格式说明和样例 5. 边界条件必须明确(数据范围、是否可能为空输入) 6. 生成3组测试用例,覆盖正常场景、边界场景、极端场景 7. 必须同时生成一份通过所有测试用例的题解代码 8. 输出格式严格如下: {{ "title": "题目名称", "description": "题目描述", "input_format": "输入格式", "output_format": "输出格式", "samples": [{{"input": "...", "output": "..."}}], "test_cases": [ {{"input": "...", "expected": "..."}} ], "solution_code": "通过测试用例的题解代码", "difficulty": "{difficulty}", "knowledge_points": ["{knowledge_point}"] }} 请只输出JSON,不要输出任何解释文字。 """

调用大模型 API 时的参数比 Prompt 本身还关键。temperature 设为 0.2,太低会变得机械,太高会开始编造输入输出格式;max_tokens 给 2000,某些模型默认值太短导致 JSON 被截断,截断了直接解析失败。如果平台支持 JSON mode,一定要开,输出结构稳定性和解析成功率是两个量级。响应拿到了先用 json.loads 解析,失败就丢弃重试一次,不要做字符串正则提取,那个路子维护成本太高。

4.2 质量校验四步:生成内容不能直接入库

大模型的输出直接入库是新手最容易踩的坑。生成内容在“看起来合理”和“真的能跑”之间隔着一条鸿沟。每一道生成的题目,在正式进入题库之前至少要过四道校验:格式校验、题解验证、测试用例验证、查重。

格式校验最简单,就是检查 JSON 字段齐全、类型正确。题解验证是把模型生成的 solution_code 放进沙箱跑一遍——能通过编译且能运行就说明题目描述至少不会自相矛盾。测试用例验证更严格,用题解代码跑每一组 test_case,期望输出必须和实际输出一致;不一致说明测试用例本身有错,要重新生成。查重是防止模型“复述”题库里已有的题,逻辑上类比论文查重——新题和已有题目文本相似度超过阈值就丢弃。

下面是一个校验脚本的骨架,放在生成服务里作为一道题入库前的最后关卡:

"""题目生成后的自动化校验流程""" import json import subprocess import sys def validate_generated_problem(problem_json: dict) -> bool: # 1. 格式校验:必需字段是否齐全 required = ["title", "description", "input_format", "output_format", "samples", "test_cases", "solution_code"] if not all(k in problem_json for k in required): return False # 2. 题解验证:把模型生成的题解代码跑一遍,确保能运行 solution = problem_json["solution_code"] # 这里复用评测沙箱的接口,把solution当作提交代码执行 run_result = execute_in_sandbox(solution, input_data="") if run_result["status"] != "AC": return False # 3. 测试用例验证:用题解跑每组用例,输出必须一致 for case in problem_json["test_cases"]: case_result = execute_in_sandbox(solution, input_data=case["input"]) if case_result["stdout"].strip() != case["expected"].strip(): return False # 4. 查重:和题库里已有题目做文本相似度比较 if compute_similarity(problem_json["title"], problem_json["description"]) > 0.8: return False return True

这里有个细节值得注意:第 2 步的输入数据给空字符串是为了验证代码本身不会一运行就崩,真正的题解正确性要到第 3 步才验证。查重阈值 0.8 是经验值,太高会放过改头换面的重复题,太低会误杀正常新题。上面代码里的 execute_in_sandbox 和 compute_similarity 都是占位函数,实际接入的时候分别指向沙箱服务和向量库检索。

4.3 学习路径推荐:先用规则跑起来,再谈强化学习

题目生成之后,系统还要回答用户“接下来练什么”。这个模块经常被过度设计——团队一上来就想用强化学习做自适应学习路径,结果数据量不够,冷启动问题解决不了,推荐结果也没法解释。

最稳的做法是先把规则推荐跑起来。核心是三张数据:知识点库(含前置关系)、题目-知识点映射表、用户的提交历史。推荐逻辑基于两个简单原则:用户薄弱的知识点优先出题;前置知识点没掌握前,不推荐依赖它的进阶题。

下面是一个规则推荐的 SQL 思路,可以直接落地:

-- 找出用户最近20次提交中通过率最低的知识点 SELECT kp.id, kp.name, AVG(CASE WHEN s.status = 'AC' THEN 1.0 ELSE 0.0 END) AS pass_rate FROM submission s JOIN problem p ON s.problem_id = p.id JOIN problem_knowledge pk ON p.id = pk.problem_id JOIN knowledge_point kp ON pk.knowledge_id = kp.id WHERE s.user_id = :current_user AND s.created_at > now() - interval '30 days' GROUP BY kp.id, kp.name ORDER BY pass_rate ASC LIMIT 1;

拿到这个薄弱知识点后,再从题目表里挑选一道该知识点覆盖、通过率在 40% 到 70% 之间(太难打击信心,太简单没提升)、且用户最近没刷过的题作为推荐结果。这个 SQL 看起来简单,但它把“学什么”的决策权交给了历史数据,而不是人的拍脑袋。跑一段时间后,可以评估这个规则的点击率和完成率,用 AB 实验验证优化方向,等积累了足够行为数据再引入模型化推荐。

学习路径推荐的另一个方向是按“知识点图谱”做路径规划:先确定起点知识点,用图的广度优先搜索生成一条从基础到进阶的路径,推荐时按路径推进。这个方案比单纯看通过率更稳定,也不依赖大量训练数据,适合作为规则层的第二版升级。

5. 避坑名单:缓存穿透、消息堆积与 LLM 幻觉的典型翻车现场

5.1 生成与评测的坑:LLM 幻觉、容器失控、输出比对误判

坑1:大模型写错了测试用例的预期输出,用户代码被冤枉判 WA

现象:学生提交了一段手算和逻辑都正确的代码,评测结果却是 WA。多次反馈后发现,同一个样例输出模型给的期望值本身就少了一位小数。

原因:模型生成题目描述时,样例输出是靠模型“推算”出来的,复杂的数值计算模型会一本正经地给出错误答案。数据集里 LLM 幻觉导致的错题,比想象中普遍,甚至能到百分之几的比例。

解决:Prompt 里强制要求模型同时生成题解代码,并且在校验阶段先把题解代码跑一遍,用实际执行结果作为预期输出,而不是直接用模型生成的文本当标准答案。这道工序不能省——前两周图省事跳过它,课代表就提过一次某道 DP 题的用例答案是错的。

坑2:沙箱容器不设资源上限,一个死循环拖垮整个评测节点

现象:某天线上评测大面积超时,排查发现一台评测节点上有一个容器 CPU 占用持续 100%,这节点的其他评测任务全部变慢。

原因:containers.run 没有传 mem_limit 和 nano_cpus,容器默认共享宿主机所有资源。容器之间没有隔离边界,一个写 while True 的用户代码就能把同节点拖垮。

解决:所有评测容器的 CPU、内存、进程数限制在编排层强制默认,不仅是代码里加参数——在 Docker daemon 配置里设置默认资源配额,或者用 docker-compose 的 deploy.resources 限制每个评测节点可调度的总资源。任何评测参数都不能依赖调用方自觉传参。

坑3:输出比对“只看严格相等”,肉眼不可见的空格坑了所有学生

现象:学生代码输出换行符是 CRLF,标准答案是 LF,评测结果全是 WA。还有一道题判题标准是忽略空白,但代码里实现成了普通精确匹配,边界错误全漏了。

原因:评测比对策略分两种——精确匹配和归一化匹配。不同题型有不同需求:字符串匹配类题目要求严格,数学计算类题目通常容忍行尾空白和换行差异。一套策略套全部题目必然出问题。

解决:判题配置增加匹配模式字段,normalize 模式统一把换行归一化并去除行尾空白,exact 模式保持不变。默认用 normalize,字符串类题目手动开 exact。比对前先看匹配模式,别一把梭。

5.2 缓存与消息队的坑:穿透、积压、断线重连

坑4:Redis 缓存穿透,不存在的题目 id 被刷爆数据库

现象:题库里没有的题目 ID 被持续请求,Redis 全部未命中,请求直接打到 PostgreSQL,数据库连接数被打满,正常题目查询也变慢。

原因:缓存策略只对“存在”的数据做缓存,不存在的请求每次穿透。尤其是“生成题目”功能上线后,测试同学拿随机 ID 去刷接口,或者有人恶意遍历接口。

解决:对不存在的数据也做空值缓存(key 存在,value 为 null,TTL 设 60 秒),并在缓存前加一层布隆过滤器,用题目 ID 集合过滤大概率不存在的 key。这两层加上后,穿透请求到不了数据库。另一个附加手段是查询接口做简单的限流,按 IP 和用户 ID 双维度。

坑5:消息队列消息积压,评测结果延迟到分钟级

现象:集中刷题活动一开始,用户提交后状态栏卡在“评测中”超过一分钟。看 RabbitMQ 管理后台,一条队列堆积了几万条消息。

原因:消息生产速度远大于消费速度,评测节点数量不足;单条评测任务要跑多个测试用例,执行时间长;消费者没有设置合理的 prefetch,消息分配不均匀,部分节点空转。

解决:评测队列按题目难度或语言拆分为多个队列,消费者动态扩容;设置 basic_qos 的 prefetch_count 为 1,让节点处理完一条再取下一条;另外加上死信队列和重试机制,消费失败的消息不丢失。消息堆积的监控阈值要提前配好,积压超过 500 条就报警——凡是等用户反馈才发现积压的,都已经晚了。

坑6:WebSocket 连接被代理断开,前端一直转“评测中”

现象:代码提交后前端收不到结果推送,页面一直转圈;刷新页面后状态立刻变成 AC。

原因:反向代理的 proxy_read_timeout 默认 60 秒,评测任务超过 60 秒或者长时间没有心跳包,代理主动断开连接。评测节点推送状态时找不到这条连接,状态更新丢失。

解决:WebSocket 服务端每 30 秒发一次心跳;反向代理的 proxy_read_timeout 拉到 3600 秒;前端连接断开后自动重连,重连成功后主动拉一次当前评测状态做校准,不能只依赖推送。这三层缺一不可。

6. 压测与缓存预热:从 100 并发到 1000 并发的调参记录

系统功能完整以后,最后一道工序是压测。在线编程评测系统的瓶颈不像普通 Web 应用在数据库,而在排队链路和沙箱执行。我习惯直接用提交接口做压测,而不是只压一个健康检查接口。

# 用 hey 对提交接口做 20 秒、300 并发压测 hey -z 20s -c 300 -m POST \ -H "Authorization: Bearer $TOKEN" \ -d '{"problem_id": 101, "language": "python3", "code": "print(1)"}' \ https://api.example.com/api/submission

压测结果怎么看:先看两个指标——接口本身的 P95 响应时间(应低于 500ms),以及评测队列积压数量(压测结束后是否归零)。如果 P95 飙升到几秒,说明提交链路里存在同步阻塞点,常见的是 SQL 写库太慢或 Redis 操作没走连接池。如果积压数持续增长不归零,说明评测节点消费速度跟不上,先加节点,同时检查沙箱启动耗时——频繁创建容器是最常见的消费瓶颈。解决方式是用容器预启动池:评测节点启动时预先拉起 5 个空闲沙箱容器,来了任务直接复用,跑完清理重置,比每次 containers.run 快一个数量级。缓存预热也在这个阶段做:把题库高频题目详情、热门排行榜提前刷进 Redis,避免压测流量直接打到数据库。

这个压测流程我每次发版前都跑一遍,跑完顺带把评测节点的 CPU 和内存监控也开上。有了这轮压测数据,遇到线上反馈再判断“是代码问题还是资源问题”就有依据了。在线编程评测系统这个方向值得做,但它的工作量七成在评测链路的稳定性和边界处理上,题目生成反而是最省力的部分。把资源限制和消息链路的参数调教好,系统就扛得住真实用户了。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 3:58:57

523节全手写AI课:从零实现大模型,真正理解反向传播与Transformer

1. 523节课背后的野心:为什么"全手写"才是这个开源AI课最狠的地方第一次看到"523节全手写实现"这个说法,我的反应是:这要么是个噱头,要么是个疯子干的事。原因很简单——现在市面上讲AI的课程,绝大…

作者头像 李华
网站建设 2026/10/8 3:58:04

Linux下JDK安装与环境变量配置全指南:从版本选型到多版本共存

上周帮同事排查一台新到的云服务器,装了一下午的JDK。不是下载慢,就是版本搞错,好不容易装完环境变量又配不上,java -version慢悠悠报出一个陌生的版本号。这场景估计很多Linux新手都经历过。JDK安装本身不难,网上教程…

作者头像 李华
网站建设 2026/10/8 3:58:01

CPU与DDR3模块6层高速PCB布局布线完整实操指南

各位做硬件和 PCB Layout 的朋友,大家好。之前在设计一块基于 CPU 和 DDR3 的核心板时,最头疼的环节就是 CPU 与 DDR3 模块的布局和布线。DDR3 信号速率高、时序要求严,布局不合理会导致信号完整性问题,上板后数据读取不稳定&…

作者头像 李华
网站建设 2026/10/8 3:57:42

香港保健品OEM代加工值不值?十年从业者拆解四大优势与避坑指南

做保健品代工这行快十年了,从内地到香港的工厂合作过不少。这几年来找我咨询的客户,问得最多的一个问题就是:香港保健品OEM代加工到底值不值得选?大家普遍有这样一个模糊的印象——香港工厂品质好、管理规范,但又担心成…

作者头像 李华
网站建设 2026/10/8 3:57:41

程序员五年亲测书单:底层原理、工程方法与职业发展

这次没有用视频或者网课来凑数,而是实打实把过去五年读过、重读过、真正对工作起过作用的书整理了一遍。这份书单不是那种“程序员必读100本”的收藏夹,列完就吃灰;也不是软文推荐,每本都是我自己掏钱买过实体书(部分还…

作者头像 李华
网站建设 2026/10/8 3:57:08

Python控制硬件:Arduino串口通信与树莓派GPIO实操指南

1. 为什么人人都说“用Python玩硬件”,到底怎么玩先把这个事情说清楚。很多人一看到“Python控制Arduino或树莓派”这个标题,脑子里是懵的:Arduino本身用C/C写程序,树莓派装的是Linux系统,Python跟这俩到底什么关系&am…

作者头像 李华