1. 在线编程笔试平台:为什么值得从零搭一个
上周有个朋友跟我聊天,说他们公司招聘工程师时还在用在线文档加人工阅卷的方式组织笔试题,候选人写完代码贴回来,面试官肉眼核对输出结果,一份卷子批改下来至少半小时,遇到边界条件还得自己跑一遍验证。我当时就说,这种情况非常适合用一套轻量的在线编程笔试平台来改善。后来我抽空把之前做过的一个基于NodeJS+Vue+MySQL的在线编程笔试系统整理了一遍,发现这个方向的选题确实很典型,适合用来分享一整套从架构到落地的实践经验。
这套系统能做的事情其实非常聚焦:管理员发布题目,候选人通过浏览器进入考试环境,在线编写代码并提交运行,系统自动判定结果并打分,面试官在后台实时查看进度和成绩。相比传统笔试流程,它把“发卷、答题、收卷、判卷、统计”全链路压缩到一个Web应用里,组织一场技术笔试从按天计算变成了按小时计算。对正在学习全栈开发的同学来说,这个项目也很有代表性——它同时覆盖了权限体系、在线编辑、代码执行沙箱、消息推送、判题引擎、数据统计这些复杂场景,几乎把前端、后端、数据库、运维的知识点全部串起来了。
我当时选型时没有用Python后端,也没用React技术栈,最终定下NodeJS+Vue+MySQL这个组合。这一套选型不是随便拍的,每一个环节都有明确的考量。下文我会把这套方案的整体架构设计、判题引擎的核心原理、多个实战环节的落地细节、以及我踩过的坑和排查经验完整拆开讲一遍,希望对准备做类似系统或者正在学习全栈开发的朋友有参考价值。
顺便说一句,做这类系统,最让人头疼的并不是写代码本身,而是“让一份代码在服务器上安全地运行并得到正确结果”这一件事。这个环节坑很深,我在第3章会重点展开讲。如果你只是想了解前端界面怎么做,那市面上教程一大把;但如果你想让平台真正可用、敢发出去给候选人用,那判题引擎和安全隔离这两关是绕不过去的。
2. 整体方案设计:三端架构与核心模块拆解
2.1 为什么选NodeJS做后端,而不是Python或Java
很多人的第一反应是:判题类系统用Python写起来更顺手,因为判断结果、解析输出这些操作用脚本语言非常高效。但我实际对比过之后还是选了NodeJS,原因主要有三条。
NodeJS的异步I/O模型特别适合这类“连接密集型”场景。在线编程笔试过程中,浏览器和服务器之间会维持大量长连接:候选人打开题目页面要记录进入时间,编辑代码要自动保存草稿,运行结果要通过WebSocket实时推送,面试官后台要看到所有候选人的在线状态和答题进展。这些操作大多是短小的消息交互,NodeJS的单线程事件循环配合非阻塞I/O,在这种场景下资源消耗很低,一个Node进程撑几百个并发连接毫无压力。如果用Java写,同样的效果要引入更多线程配置和框架选型,开发成本会高不少。
前后端语言统一,心智负担小。Vue前端和Node后端都用JavaScript/TypeScript,意味着数据结构、命名风格、校验规则可以前后端复用。我在这个项目里把题目的判题配置、测试用例的期望输出、用户的权限标识这几个核心模型定义成了共享的JSON结构,后端和前端引用的是同一份类型定义,联调时少了很多“字段名对不上”的扯皮。
生态成熟,做原型和做生产级系统都有现成方案。Express/Koa处理路由,Sequelize/Prisma操作MySQL,jsonwebtoken做登录态,Socket.IO做实时通信,这些库我都实际用过,稳定可靠。另外NodeJS的包管理机制对部署非常友好,一条npm install就能把整个后端服务拉起来。
当然NodeJS也不是没有缺点。判题任务如果涉及大量CPU密集型运算,Node的事件循环会被阻塞,好在我们这个场景里判题逻辑是由单独的Worker进程承担的,主服务只负责接收请求和返回结果,这个会在后文展开。
2.2 Vue+MySQL在系统里的定位与选型理由
前端这一层,我选择Vue 3而不是Vue 2,因为Composition API在处理考试界面这种“状态多变、逻辑复用要求高”的场景非常顺手。候选人答题页面同时要管理当前题目索引、编辑器内容、剩余时间、运行结果、草稿自动保存状态,如果还用Options API,这些逻辑会散落在data、methods、watch里,代码很快就会失控。用Composition API可以按功能切分逻辑块,比如useTimer管理倒计时,useEditor管理IDE状态,useSubmission管理提交动作,每个文件内聚一个职责,维护起来非常舒服。
UI组件库用的是Element Plus。这套组件库对Vue 3支持好,表格、表单、对话框、标签页这些管理后台常用的组件开箱即用。候选人考试页面的代码编辑器我选的是Monaco Editor,就是VS Code同款的编辑器内核,通过npm包引入,支持语法高亮、自动补全、错误提示。用Monaco而不是简单的textarea,是为了给候选人接近真实IDE的体验——我实际调研过候选人反馈,他们普遍认为编辑器的自动缩进和括号配对功能对答题效率有明显帮助。
MySQL在这套系统里的角色就是最基础、最可靠的数据持久层。选它不选MongoDB,是因为笔试平台的数据关系非常明确:用户有角色,题目有关联的测试用例,提交记录属于某个用户和某道题目,这些关系用MySQL的外键和联合查询处理非常顺。而且MySQL对事务支持完善,比如发布一场考试时需要同时插入考试记录、关联题目列表、初始化所有候考人的答题状态,这一步必须用事务保证原子性,否则会出现“考试建好了但部分考生没有绑定上”这种脏数据。
MySQL的版本我建议直接用8.x。8.0以上的窗口函数、公用表表达式、JSON字段类型都是实打实的生产力提升。比如统计每道题目的提交次数和通过率,一个窗口函数就能搞定,换成5.7你得写一堆子查询。
数据库连接池我用的是连接池组件而不是默认的mysql库连接方式。生产环境下一旦有几十个候选人同时交卷,数据库连接数会瞬间飙升,没有连接池很容易把MySQL的连接数打满。配置上我一般把最大连接数设为20,等待超时设为10秒,这个参数在中等并发下表现很稳定。
2.3 系统模块全景拆解:从前端页面到后端服务
这个平台从功能上可以分成三个端,我用一张表先把核心模块盘清楚。
| 端 | 核心模块 | 关键功能 |
|---|---|---|
| 候选人端 | 登录/注册、考试列表、答题页 | 身份验证、倒计时、代码编辑、运行与提交、实时结果反馈 |
| 管理员端 | 题库管理、考试管理、用户管理、成绩统计 | 题目CRUD、测试用例维护、考试成绩导出、通过率分析 |
| 服务端 | 认证服务、题目服务、判题服务、实时通信服务、文件服务 | JWT签发、用例管理、沙箱执行、Socket推送、附件存储 |
开发顺序我建议是:先做管理员端的题库管理和考试管理,因为这是平台的地基;再做候选人端的答题流程,这是核心体验;最后做实时监控和成绩统计这类锦上添花的能力。这样每个阶段都能得到一个可演示的版本,不会到了最后一天才发现题目和考试连不起来。
做这个项目时,我有一个体会很深的点:不要一开始就想着做“完美的判题系统”。第一版能支持最常见的几种语言、能跑通“运行用例、比对输出、给出通过或失败”这条主链路就足够了。等主流程稳定后再慢慢扩展语言支持、增加内存和CPU限制、做相似度检测这些高级功能。我见过很多开发者把大量时间花在优化判题精度上,结果基础流程还没跑通,这个顺序需要注意。
3. 判题引擎的设计与实现:安全、性能与正确性三重考验
3.1 判题流程与沙箱隔离方案,怎么做到既安全又高效
判题引擎是整个平台技术含量最高的一块,因为它直接面临三个互相牵扯的问题:让候选人的代码能跑起来、让运行结果可信、同时不能让候选人的代码把服务器搞挂或泄露数据。
先讲这个引擎的架构分层。候选人提交代码后,主服务不会直接执行这段代码,而是把它写成一个任务,放进一个先进先出的消息队列。独立的判题Worker进程从队列里取任务,在隔离环境里执行代码,把执行结果(包括运行输出、错误信息、耗时、内存峰值)写回数据库,再通过WebSocket通知前端刷新结果。这样主服务和判题服务彻底解耦,即使某一段代码导致判题进程重启,主服务也不会受影响,候选人的其他请求依然可以正常处理。
沙箱方案我在这里先说结论:不要自己实现,也不要裸用child_process,直接采用成熟的隔离容器方案。选型时我在纯Docker方案和受限的本地沙箱方案之间犹豫过。纯Docker隔离性最强,但镜像制备和容器启动开销比较大,每判一道题都要创建和销毁容器,性能损耗明显。本地沙箱方案速度快,但隔离性不够彻底,一个系统调用就可能逃逸。最终我用的是折中方案:外层用容器做进程隔离,内层用资源限制机制限制CPU和内存,同时配合系统级的只读文件系统挂载,既保证了隔离强度,又把单次判题的启动耗时控制在了几十毫秒级别。
这里有一个细节值得提:为了提速,我没有每次判题都重新创建容器,而是采用“预创建+复用”的策略。系统启动时预先创建一批处于待命状态的容器池,收到判题任务后从池里取出一个可用容器执行,用完销毁。这就像餐厅提前摆好餐具,客人来了直接上菜,不用临时洗碗。实测下来,判题吞吐量提升了将近一倍。
3.2 判题判定规则与测试用例设计技巧
判定逻辑表面上很简单,就是跑一遍候选人的代码,把输出和期望输出逐字对比。但实际做起来,坑非常多。
第一个坑是输出格式的宽松匹配。真实场景里,候选人代码输出的空格、换行、大小写经常和标准答案不完全一致。比如题目要求输出“Hello World”,候选人写了“Hello World”多了一个空格,按逐字节比较肯定是错,但从语义上又是对的。我采用了两级匹配策略:第一级做严格匹配,不行再降级为忽略首尾空白和连续空格的模糊匹配,再不行就看数字类型的浮点误差是否在容差范围内。这个策略能有效减少争议,对候选人更公平。
第二个坑是测试用例的覆盖设计。一道题光有一两个示例用例远远不够,判题系统要验证的是代码的逻辑正确性,而不仅是示例能跑通。我在设计测试用例时遵循三条原则:先覆盖题目描述里的示例;再覆盖边界条件,比如数组的极值、空输入、单元素输入;最后是隐藏的随机化大数据量用例,用来验证算法的时间复杂度是否符合要求。最后一类用例我会在生成时记录随机种子,保证判题结果可复现,避免“这次过了下次没过”的情况。
第三个坑是超时和资源超限的处理。每个判题任务我设置了三重限制:CPU时间限制(普通题2秒,重计算题5秒)、内存限制(各家语言256MB到512MB之间)、输出体积限制(通常限制在10MB以内)。超限的任务直接判为失败,错误信息会明确告诉候选人是“运行超时”“内存超限”还是“输出过多”。这样候选人可以有针对性地优化自己的代码。
3.3 多语言编译执行方案:从Java到Python的差异化处理
支持多语言是这类平台的基本盘,但每种语言在判题环境里的处理方式差别很大,下面我逐个说。
C/C++是最干净的场景,编译和运行分离。编译阶段用g++把源码编译成可执行文件,编译参数统一为-O2 -std=c++14,关掉所有警告输出以免干扰信息,再把可执行文件放入隔离环境运行。运行时内存限制要略微放宽,因为程序本身加载就要占用一定空间,但CPU时间限制必须卡紧,防止死循环把判题Worker拖垮。
Java比较特殊,因为Java程序启动时JVM本身就要吃一部分CPU和内存。我设置了独立的规则:CPU时间限制比C++放宽50%,内存上限直接给到512MB。编译时用javac,运行阶段用java -cp加上类路径参数。这里还有一个小坑,Java的默认安全机制会限制程序访问某些系统属性,比如读取环境变量和系统信息时可能抛出异常。我在判题环境里预设了安全的系统属性白名单,避免候选人的代码因访问系统信息而意外失败。
Python是动态语言,没有编译环节,直接解释执行。但它有两个问题:一是解释器启动本身耗时,二是裸解释器很容易被恶意代码利用。我的处理方式是用受限模式启动Python解释器,并在代码执行前注入一个资源监控线程,每秒检查当前内存占用,超过阈值就主动终止进程。这样即使候选人写了个无限增长列表的程序,也不会把服务器内存吃掉。
Go语言这个场景用得相对少,但我也支持了。它的编译速度和C++接近,运行时没有虚拟机,资源消耗低,判题体验很好。唯一要注意的是Go编译出的二进制体积较大,需要限制编译产物的输出目录,避免一次性写满磁盘。
3.4 判题结果判定:不只是通过和不通过
判题结果不能只返回一个布尔值,候选人需要知道自己的代码到底错在哪。我的结果结构包含五个字段:通过状态(通过、失败、超时、内存超限、编译错误、运行错误、系统错误)、执行的测试用例数量、通过的数量、失败时前几个用例的期望输出与实际输出、以及耗时和内存峰值。候选人端拿到这份结果后,界面会展示运行时间、内存消耗和具体的错误输出对比,帮助候选人定位问题。
这个设计看似简单,但实际上非常影响用户体验。我第一版只返回了“通过/不通过”,候选人反馈根本不知道怎么改。加上了错误输出对比后,答题体验明显提升,很多候选人能根据提示快速修正代码,不再需要反复盲目试错。
从面试官视角,判题结果还有一个隐藏用途:观察候选人的调试过程。一次提交就通过、多次提交后才通过、提交中途转型换思路,这些行为模式从提交历史里都能看出来,对技术判断很有参考价值。所以我会在提交记录表里完整保存每次提交的代码快照、判题结果和时间戳,这个数据积累下来对招聘决策很有帮助。
4. 系统核心实现:权限、实时通信与数据库设计的实战细节
4.1 基于JWT的角色权限体系,前后端如何协同守卫
在线笔试平台天然有角色区分:管理员、面试官、候选人。三种角色看到的页面和能执行的操做完全不一样,所以权限体系必须从第一行代码就开始做。
我用的是JWT方案。用户登录成功后,后端用密钥签发一个JWT,里面携带用户ID和角色标识,默认有效期设置为2小时。前端拿到JWT后存储在当前会话存储里,每次发送请求都在Authorization头带上这个Token。后端中间件会统一解析Token、校验签名、检查过期时间,把用户信息挂到请求上下文里。这样每个接口只需要声明自己允许的角色,由中间层统一拦截。
但JWT方案有一个坑:它天生无状态,服务端不记录“当前登录用户”是谁,所以无法在服务端主动踢人下线。如果某个候选人在考试中被管理员判定了作弊,他的Token在过期之前依然有效。我的解决方案是维护一个用户状态缓存:登录和操作时更新用户在缓存中的状态标记,认证中间件在鉴权时会先检查这个标记,如果发现被禁用就直接拒绝。简单说就是用一层内存态缓存给无状态的JWT补上了“状态”能力。
前端也不能只靠隐藏按钮来实现权限控制。正确的做法是用路由守卫:在Vue Router的beforeEach钩子中读取当前用户角色,逐个比对目标路由的meta.roles配置,不匹配就跳转到一个无权限提示页。注意,这只是为了体验流畅,真正的安全边界一定在后端接口,前端路由守卫只能算锦上添花。
4.2 WebSocket实时通信在考试场景里的妙用
在线笔试平台里有几个场景天然需要实时通信:候选人提交代码后,判题结果要尽快推送回页面;面试官后台要实时看到每个候选人的当前状态(在线、答题中、已交卷);管理员发布考试变更公告时,所有候选人页面要即时收到弹窗提示。
这些场景里,HTTP轮询不是不能做,但会造成大量无效请求。我采用Socket.IO来实现双向通信,主要原因是它对断线重连、心跳检测、房间广播这些能力都有现成支持,不需要自己从零实现一套WebSocket协议细节。
关于通信的设计,有一个关键点是“按房间隔离消息”。候选人进入某场考试时,前端会调用socket.join('exam-考试ID')加入对应的房间。判题完成后,后端向exam-考试ID房间广播结果,只有这场考试里的人能收到。面试官后台则join一个管理员的全局房间,这样任何候选人交卷,后台都能第一时间收到通知。这种房间机制在Socket.IO里十分成熟,实现起来也优雅,比自己在每条消息里维护接收人列表要省心得多。
4.3 核心数据表设计与事务实践
数据库方面,核心数据表至少要有五张:用户表、题目表、测试用例表、考试表、提交记录表。加上辅助的关系表,总共十几张。
我把关键表的结构大致说一下。用户表包含账号、密码哈希、角色、姓名、所属组织、创建时间。密码用加盐哈希存储,不要用明文,这是底线。题目表包含题目标题、描述、难度等级、语言类型、时间限制、内存限制、模板代码、是否启用。测试用例表关联题目ID,存输入数据、期望输出、是否公开可见。考试表包含考试名称、开始时间、结束时间、时长、创建人、状态。提交记录表是数据量最大的表,关联用户、题目、考试,存提交代码、判题结果、耗时、内存、状态、时间戳,这个表后面还要建索引。
面条字段用TEXT类型,测试用例的输入输出数据也可能很长,用MEDIUMTEXT存储。需要核心查询条件的字段都建索引,比如提交记录表的考试ID、用户ID、状态复合索引,这直接决定了后台统计页面的响应速度。
事务我举个例子。管理员创建一场新考试时,后端要做一连串操作:插入考试记录、关联题目列表、初始化本场考试所有候选人的答题状态记录、创建统计摘要记录。任何一步失败,整个流程都必须回滚,否则会出现考试创建了一半的脏数据。我把这四个步骤包在同一个数据库事务里,任何异常都统一回滚,这是最基本的可靠性保障。
5. 实操过程实录:从零搭建到跑通一次完整答题链路
5.1 前后端项目初始化与关键依赖选配
我实际搭建的流程是:先建一个项目根目录,里面分三个子目录:client(Vue前端)、server(Node后端)、judger(判题服务)。
前端用Vite工具链初始化Vue 3项目,然后装插件依赖:路由插件、状态管理插件、UI组件库、请求库、编辑器组件。初始化时要注意,Vite默认端口是5173,后端API服务我用的是3000端口,跨域问题通过后端的跨域中间件统一处理。
后端用Express框架初始化,目录结构我习惯按业务模块划分:路由层、控制器层、服务层、数据访问层,外加中间件目录和工具目录。数据库访问我用的是ORM框架,配置好MySQL连接信息后,通过模型定义同步建表会更方便。ORM的迁移功能在开发阶段很实用,改模型后执行一次迁移就能更新表结构,省去了手工敲SQL的麻烦。
安装依赖时有一个实用经验:锁定版本。不要装“最新版”,因为新版可能引入破坏性变更。项目的依赖我全部写在package.json里并提交到版本管理,同事拉下来后npm install能复现完全一致的环境。这种可复现性在部署时太重要了。
5.2 候选人答题页的完整流程实现
答题页是候选人端体验的核心。我的实现流程是这样的:页面加载时先请求考试信息和第一道题目,同时启动一个前端定时器显示剩余时间。候选人选择题目后在左侧展示题面,右侧加载Monaco编辑器,并把题目自带的模板代码填入编辑器。编辑器内容变化时做防抖自动保存,每隔30秒自动把草稿同步到后端,避免候选人因为误关页面而丢失代码。
候选人点击“运行”按钮时,前端把当前编辑器内容连同语言信息发送到后端,进入判题流程。这里有一个状态处理的小细节:运行期间按钮要置灰并显示进度状态,防止候选人重复提交,同时用加载动画减少等待焦虑。判题结束时,WebSocket推送结果,前端更新界面,把通过/失败状态和测试用例对比清晰地展示出来。
最后是“交卷”环节。交卷前前端会做两次确认弹窗,第一次提示“确定要交卷吗,交卷后不能继续修改”,第二次要求输入“确认”关键字后才能真正提交。这个双确认机制非常有效,我统计过因为误触导致提前交卷的投诉几乎为零。
交卷动作的后端处理是一个事务:更新考试状态为已交卷、标记交卷时间、把当前所有未提交的草稿做一次最终提交、计算基础得分。这套逻辑确保交卷后数据是完整且一致的。
5.3 管理员后台的核心页面逻辑
管理员后台我做得比较克制,没有堆砌花哨的图表,而是把精力放在数据准确和操作高效上。
题库管理页面用表格展示题目列表,支持按难度、语言、状态筛选。新增或编辑题目时,用抽屉式表单,题面支持Markdown格式,测试用例用表格录入,可以动态增减行。这个页面最核心的是“保存前校验”:所有测试用例的输入和期望输出必须有值,时间限制和内存限制必须在允许范围内,否则不让保存。
考试管理页面用于创建和编辑考试。创建考试时选择题目并配置场次信息,系统会自动计算本场考试的总分和时长。创建完成后可以生成一个考试邀请码,候选人凭邀请码加入考试。
成绩统计页面展示每场考试的名单、每人的总分、每题得分、提交次数、最快通过时间。表格支持按分数排序,支持导出CSV。导出功能是用后端生成的,前端只负责下载,因为候选人和分数数据量大了以后,前端做导出容易卡死浏览器。
5.4 从本地到云服务器:完整部署过程记录
部署是我在这个项目里花时间比较多的一块。本地开发环境和生产环境的差异之大,超出很多新手的想象。
我用的部署方案是:一台Linux云服务器,安装Docker和Docker Compose。用一个编排文件把四个服务管理起来:MySQL数据库容器、Node后端容器、判题服务容器、Nginx静态文件服务器容器。前端构建后的产物通过构建镜像直接跑在Nginx里,后端API和判题服务分别映射到内部端口。
部署前需要准备的几个文件:后端的环境变量配置文件,里面配置数据库地址、JWT密钥、判题服务地址;Nginx的站点配置,把根路径指向前端静态目录,把/api路径反向代理到后端端口,再配置好WebSocket升级的转发头。WebSocket代理如果不加升级相关配置头,前端会一直连不上Socket,这是我第一次部署时排查最久的问题。
数据库方面,我用了一份初始化SQL脚本,包括建库、建表、插入默认管理员账号和示例题目。部署时执行一次初始化脚本,然后启动容器编排。这里有一个经验:不要在生产环境用ORM自动同步表结构,很容易出现意外。固定的初始化脚本可控得多,改表结构时走迁移更安全。
6. 稳定性与安全加固:这个平台不能只“能跑”
6.1 接口安全与防作弊的落地手段
在线笔试场景的防作弊跟线下监考完全不同,技术手段再强也只能降低作弊概率,不能彻底消除。我能做的是把常见的作弊路径尽量堵住。
接口层面我做了一套风控设计。每一项答题操作都记录了时间戳和动作类型,后端通过简单规则识别异常模式。比如,同一IP在几秒内提交了来自不同账号的几乎相同代码,或者某账号在极短时间内提交大量不同语言的代码,这些异常会被标记并在后台高亮展示给面试官。这套规则不追求精确判定,只负责把人可疑的行为暴露出来,最终判断交给人工。
代码相似度检测我也做了。因为判题服务里存了所有提交的代码副本,每次交卷时都会执行一次代码相似度哈希比较,超过阈值就把相关提交全部标记为疑似雷同,面试官可以在后台看到雷同代码片段的具体对比。这个功能用的是局部敏感哈希算法,性能和准确率之间的平衡还可以。
IP和登录设备指纹的采集是另一个维度。候选人登录、答题、交卷三个阶段都会上报IP、UA、屏幕分辨率等环境信息,后台可以横向对比,发现设备跳变等异常情况。这些数据平时完全不打扰候选人答题过程,只在需要回溯时作为参考。
6.2 高并发下的API限流与性能优化
笔试平台的流量通常集中在考试开始和交卷这两个瞬间,所以限流策略要针对这两个入口重点设计。
我给后端API做了一层全局限流中间件,接口维度按IP和用户ID分别限流。普通查询接口限制每用户每分钟60次,提交代码接口每用户每分钟5次,交卷接口每用户每分钟2次。超限后直接返回429状态,前端会友好提示“操作过于频繁,请稍后重试”。这层保护非常关键,某些候选人会用脚本循环点击“运行”按钮压测接口,没有限流的话判题队列会被瞬间打满。
数据库层面还有一个优化我特意做了:提交记录表按考试ID做了分区,因为每次考试的提交数据量会急剧增长。分区后按考试ID查询成绩时只扫描对应分区,速度提升非常明显。同时定时任务会在每天凌晨清理三个月前的过期草稿缓存,避免数据库无限膨胀。
6.3 日志、监控与数据备份
判题系统出问题时的定位速度直接取决于日志的质量。我的日志体系分三层:访问日志记录所有HTTP请求的方法、路径、状态码、耗时;业务日志记录关键业务动作,比如谁创建了考试、谁提交了判题、谁修改了题目;判题日志记录判题全过程的输出、错误、耗时和资源消耗。每一条日志都带上请求ID和时间戳,排查问题时可以按一个请求ID把所有链路日志串起来。
监控方面我没有上重型监控系统,就用了轻量方案:进程管理工具守护Node进程,进程崩溃后自动拉起;后端每30秒上报一次健康指标到管理接口,包括事件循环延迟、内存占用、数据库连接数;对判题失败率、接口错误率设置简单阈值,超过后触发webhook通知到钉钉群。这套轻量监控对于中小规模平台够用了,成本也低。
备份是很多人忽视但一旦出事就后悔到极点的环节。我设置了每天凌晨两点自动对MySQL执行全量备份,同时开启binlog保留最近七天的增量记录。恢复方案也测试过:在新环境执行全量恢复,再应用增量日志,数据能恢复到最近一次备份前五分钟的状态。这个能力在真正遇到误删数据时值千金。
7. 常见问题排查:面向真实踩坑现场
7.1 判题结果不正确、超时、内存超限的排查路线
先说说判题结果不对的情况。如果候选人的输出和期望输出不匹配,我会先看是全部用例都不对还是部分用例不对。全部不对,大概率是编译或运行环境出问题,比如依赖库没安装、类路径配错;部分不对,大概率是判定规则太严格,比如浮点精度问题没处理。排查时把判题Worker记录的原始输出全部打印出来,直接对比就能定位。
超时问题我一般查三个地方:代码本身是否有死循环,这是最常见的;判题容器是否被其他任务拖慢,因为单机容器资源竞争会放大耗时波动;CPU时间限制是否设置得过紧。排查方法是给慢查询加日志,看一次判题的完整生命周期时间分布,编译耗多少、运行耗多少、输出采集耗多少,一清二楚。
内存超限的方向不一样,正常代码跑完就释放了,如果被判定超限,往往意味着测试用例数据量太大或代码有内存泄漏。有一次我排查一个C++判题超限的案例,最后发现是代码里的全局向量没有清理,所有测试用例在同一个进程里跑导致内存累积。解决方案是每个用例用独立子进程执行,虽然慢一些但是结果更靠谱。
7.2 数据库连接数被打满与数据一致性异常
这类系统上线后第一个容易爆的问题就是数据库连接数。早期我配置的连接池太小,考试开始瞬间几十个人同时查询题目,连接池被打满,后面的请求全部排队卡死。排查时查后端日志,会发现大量“获取连接等待超时”的错误。解决方案有两层:调大连接池到合适的值,同时对高频读接口加了一层本地缓存,题目内容这种很少变化的数据不需要每次都查库。
数据一致性异常常见于并发交卷场景。两个候选人同时交卷,后端都在处理“更新考试状态、写入提交记录”,如果没有事务隔离,可能出现状态错乱。我遇到过的问题是:同一场考试的两个候选人同时交卷,结果一个人的交卷时间写到了另一个人的记录上,原因是更新语句的条件字段写错了。排查这类问题时,我习惯把关键操作的SQL打出来仔细核对条件,避免只盯着代码逻辑而忽略了具体查询条件。
7.3 前端页面白屏、Socket连不上、本地与线上表现不一致
前端页面白屏,第一反应查浏览器控制台报错。常见的原因有:接口跨域配置不对、打包后的资源路径不对、静态资源没有正确映射到Nginx。印象最深的一次是前端构建后图片路径变成了绝对路径,导致所有图片404,排查半天才确认是路径配置问题。
Socket连不上,大概率是Nginx代理没有正确转发WebSocket升级请求。必须显式配置升级相关的请求头,否则浏览器和服务器之间只能建立普通HTTP连接。这个坑我踩过一次,需要在Nginx的location块里加上升级相关的头配置才能解决,加完立刻连上,印象非常深刻。
本地正常线上异常,最常见的原因是环境变量差异。本地开发时数据库地址是localhost,线上是容器内网络别名;本地没开启严格跨域,线上开启后部分浏览器拦截了跨域请求。我的建议是,环境相关配置全部走环境变量注入,不要写死在代码里,而且要有一个文档把每个环境变量都写清楚是干什么用的。
8. 个人体会与后续扩展建议
真把这个平台从零做完一遍后,我对全栈开发的体感完全不同了。以前做CRUD管理系统只需要关心“数据能不能存进去、能不能查出来”,做这种在线编程平台才知道,真正难的是那些夹在页面和数据库之间的隐性工程问题:怎么在代码里执行代码、怎么隔离恶意输入、怎么保证并发场景的一致性、怎么让运维故障处理得像开卷考试一样有据可查。这些能力不是看教程能学来的,必须亲手踩过坑才能长在身上。
如果这个系统后续要继续扩展,我会优先做三件事:第一是引入更多的语言运行时支持,比如补上Rust和JavaScript,让平台覆盖更广的岗位类型;第二是给判题引擎加上K8s集群调度能力,让判题任务可以在多台机器上横向扩展,应对大规模校招这种并发非常高的场景;第三是完善成绩的统计分析报表,从平均分、分位数、每题的区分度这些维度做更深入的分析,让面试官能更快地识别出值得进入下一轮面试的候选人。
最后分享一个做这类系统最值得牢记的体会:平台的可靠性远比功能丰富度重要。一场考试进行到一半服务器挂了,哪怕你功能再全,对使用者的信任打击也是毁灭性的。所以耗费精力做好监控、限流、备份,不是浪费时间,而是这类产品能够被你放心推荐给别人的基础。