
t3code 是我去年从零开始做的一个在线编程练习平台名字很直白T3 指 Web 开发圈里常见的 TypeScript、tRPC、Tailwind 组合“code”就是代码练习。整个项目把这些技术拼到一起做成一个能注册登录、能刷算法题、能在线提交代码、并实时返回判题结果的全栈应用。这项目要解决的痛点很具体一些人准备技术面试时需要同时开三四个网站——一个看题目、一个写代码、一个搜题解本地环境还经常被 JDK 版本、Node 版本、Python 路径折腾得够呛。t3code 想把这些环节收敛到一个窗口里题目列表在左边代码编辑器在中间右侧实时显示判题状态所有提交记录和错误输出都沉淀在个人档案里方便回看。它适合三类人正在刷题准备面试的求职者、想做全栈练手项目但缺乏完整闭环经验的开发者以及希望用低成本方式建立内部刷题题库的技术团队。文章后面我会把整个平台的定位、核心模块、判题链路和踩坑实录全部展开代码细节会直接给出来方便想自建类似工具的读者直接参考。1. 核心定位与需求拆解1.1 一句话说清 t3code 是什么t3code 是一个面向算法训练与编程面试准备的在线判题平台交互形态类似 LeetCode、牛客这类在线刷题系统但技术上完全自研题目库也可以按团队需求自由维护。它本质上是一个“提交-执行-反馈”闭环一个用户看到题目编辑代码点击提交后端把代码放进受限的执行环境去跑跑完把运行结果、运行时间、内存占用、判题结论返回给前端。这个过程看起来简单真正落地时涉及权限隔离、进程管理、超时控制、结果判定、数据结构设计等多个环节每个环节都藏着不少细节。我选择把它做成全栈应用而不是纯前端玩具是因为只有真正跑在服务端才能设置一个所有人都一样的标准执行环境。纯浏览器端跑用户代码有很多限制不同浏览器对 WebAssembly、worker 的支持不同而且安全边界很难做所以即便从零搭建的成本不低服务端判题仍然是最合理的路线。1.2 解决的三个核心问题第一个问题是环境一致性。大家在本地跑算法题换一台电脑结果可能完全不同有人用 Python 3.11有人还用 3.6有人机器内存 32G有人只有 8G。t3code 在服务端统一了运行时版本限定执行资源上限让判题结果只跟代码本身相关。第二个问题是反馈链路太长。传统刷题流程是写完代码、在本地反复测样例、再手动对比输出慢而且容易出错。t3code 把测试用例自动注入执行流程点击提交后立刻看到结果包括每一组测试用例是否通过、耗时、预期输出和实际输出省掉所有手工步骤。第三个问题是学习过程缺乏沉淀。很多人刷题刷了几百道回头看时却记不清自己当时卡在哪里、哪些边界条件反复踩坑。t3code 把每次提交的代码、报错信息、运行时表现全部归档成历史记录能让用户直观看到自己对一个知识点的掌握过程。1.3 功能分级与优先级设计平台并非一口气把所有功能做全而是按“能跑通 → 好用 → 有沉淀”的节奏逐步迭代。一张表就能看清整体的功能分级优先级功能模块具体内容状态P0用户系统注册、登录、个人信息、会话保持完成P0题目模块题目列表、题目详情、标签筛选、搜索完成P0在线编辑编辑器、语言选择、代码保存完成P0判题执行代码提交、测试用例执行、结果返回完成P1提交历史历次提交记录、代码回看、结果对比完成P1排行榜按通过题数、提交活跃度排序完成P2评论与题解用户题解、讨论区、点赞规划中P2比赛模式限时竞赛、实时排名、虚拟参赛规划中优先级的设计原则是“先打通主链路再装饰边缘体验”。新手常犯的错误是先把 UI 做得很漂亮、加了一堆布局特效但核心的提交执行链路还在跑 mock 数据。t3code 的第一步只做一件事提交代码之后能看到真实的运行结果哪怕界面丑一点也无所谓。2. 整体设计思路与技术选型解析2.1 为什么选择 T3 技术栈组合“T3”这个后缀不是巧合而是整个技术选型的结果。T3 Stack 不算是某个固定框架更像一套以 TypeScript 为中心的全栈解决方案Next.js 负责前后端一体化的页面与应用路由tRPC 负责前后端之间的类型安全通信Tailwind 负责样式体系Prisma 负责数据库操作。这套组合的最大优势是把类型安全贯穿到数据链路的最末端。写代码的人都有过这种体验后端接口改了一个字段名前端因为强类型检查直接报错毫无商量的余地。t3code 使用 tRPC 之后前端调用后端方法就像调用本地函数不用手工维护一堆 API 文档也不用在多个文件之间来回同步接口类型定义改一个 schema 全链路同步更新这对个人开发的效率提升非常明显。因为只有我一个人在维护前后端省掉的沟通成本可能感受不出来但如果团队扩容这种类型安全的价值会被进一步放大。前端编辑器选择了 CodeMirror 6 而不是 Monaco Editor主要原因是体积。Monaco 是 VS Code 的编辑器内核功能极其强大但打包出来体积很大首屏加载需要多花不少时间。CodeMirror 6 的模块化做得极好支持按需加载语言包对 t3code 这种只需要基础代码编辑、语法高亮、括号匹配、自动缩进的场景完全够用。算法刷题场景不需要复杂的智能提示把目标依赖写小一点比堆功能更重要。2.2 判题沙箱为什么必须隔离执行环境判题系统最核心、也最危险的部分是要在服务器上运行用户提交的代码。直接在主进程里跑一段来自不明用户的代码等于把整台服务器敞开给人进来这是绝对不可接受的。用户代码可能是无害的“两数之和”也可能是恶意脚本想读服务器上的环境变量、遍历文件系统、甚至发起网络请求。所以隔离不是加分项而是生死线。t3code 采用的隔离方案是进程级隔离配合资源限制而不是更重的虚拟机隔离。理由很简单算法题的执行时间通常是毫秒到秒级用 Docker 容器隔离已经很充分不需要再叠加一层完整虚拟机的开销。执行单元的流程是后端收到提交请求将用户代码写入临时目录在容器内启动一个受限的 runner 进程由 runner 加载测试用例、编译、执行、采集结果整个过程中容器不挂载宿主目录、不开放网络、限制 CPU 和内存执行完毕后销毁临时文件。实现里的一个关键细节是允许的语言运行时内置在镜像里而不是从用户代码中动态决定。例如 Python 提交会被提交到 python-runner 容器依赖和系统包版本全部固定避免代码在本地能跑、在判题环境跑不了的尴尬情况。2.3 数据模型设计题目、提交、用例怎么建模数据建模是判题平台最容易被低估的部分。表面上只要建几张表就够了但字段设计不合理后面写统计查询时会痛苦不堪。t3code 的核心模型按职责划分成三类内容数据题目、标签、行为数据提交记录、用户答题记录、执行数据测试用例、判题结果。题目表的核心字段包括题目编号、标题、难度、描述、输入输出说明、标签关联、可见性、创建时间。这里特别强调一个字段题目的原始描述和渲染后的 HTML 描述要分开存。因为题目描述里通常要嵌入代码块、公式、表格后端用 Markdown 解析后生成 HTML 缓存起来用户每次访问都不用重复做解析渲染性能更好也方便后续支持 LaTeX 公式。提交记录表是所有统计报表的数据来源字段包括提交 ID、用户 ID、题目 ID、语言、代码快照、状态、判题结果 JSON、执行用时、内存占用、创建时间。判题结果 JSON 是核心它把每个测试用例的通过情况、程序输出、错误信息都序列化存在单条记录里前端拿到之后可以完整渲染判定详情。一张宽表存储 JSON 字段看起来有点违背传统数据库规范化直觉但在这类“写多读少、结果以展示为主”的场景下反而最实用。用户查看提交详情时不需要 join 多张表直接读一条记录就能画出完整页面。测试用例按题目标识关联每条用例包含输入内容、预期输出、是否可见样例用例、权重与特殊评分说明。可见与隐藏用例分开处理是刷题平台的一个共识样例用例在题目页面展示给用户帮助理解题意隐藏用例只跑在判题过程中防止用户针对样例做硬编码。判题器执行时先跑可见用例再跑隐藏用例两部分结果合并后一起返回。2.4 前端交互设计提交状态怎么同步用户点击提交以后后台判题流程可能耗时几秒甚至十几秒前端不能傻等也不能无限轮询。t3code 使用了 WebSocket 推送判题状态后端执行流程每到一个阶段队列中、编译中、测试用例运行中、完成都会实时推送状态到前端页面用户能看到进度条而不是白屏等待。如果团队没有什么复杂基础设施用一个更朴素的方案也可以前端提交后拿着 taskId每隔 1 秒请求一次结果接口。轮询的优点是实现简单不需要额外维护 WebSocket 连接缺点是请求量大、状态不是实时。t3code 之所以选 WebSocket是因为还要做排行榜和比赛模式这些功能天然依赖服务端主动推送与其为了一个功能引入一套库不如一步到位把长连接基础打好。轮询和推送方案可以并行观察核心判题结果用短轮询做兜底额外的状态变化走 WebSocket 推送。这样即使长连接断掉用户也能通过轮询拿到最终结果不会卡在中间状态。3. 核心环节实操从题目到判定结果的完整链路3.1 最小可运行的执行流程如果要复现 t3code 的核心链路不需要理解全部业务代码只需要把一条最小链路跑通题目定义 → 用户提交 → 创建执行任务 → 沙箱执行 → 结果判定 → 数据落库 → 前端展示。这个链路看似不复杂但每一环的输入输出都必须严格对得上。具体流程可以拆成七个步骤用户在题目详情页选中语言编辑器里写代码点击提交。前端把题目 ID、语言、代码内容发送到后端提交接口。后端校验用户登录状态、代码大小、语言合法性生成提交记录状态置为“排队中”。判题任务被交给一个工作队列由执行器节点消费。执行器把用户代码与测试用例写入沙箱执行编译运行收集输出、耗时、内存、错误信息。判题逻辑比对测试用例输出与预期输出生成结论通过/失败/超时/运行错误。后端更新提交记录状态通知前端结果。第 5 步是整个链路里最容易出问题的地方。以 Python 题目为例判题器拿到的是一段用户代码和一组测试用例它的任务是找到语言的入口、注入测试用例、收集输出而不是直接拼接字符串把测试用例塞进用户代码里。3.2 判题执行器的核心实现实际执行端并不需要非常复杂的框架一个核心的 runner 脚本就能承接“编译运行用户代码并收集结果”的职责。以 Python 判题为例执行器的关键逻辑大致长这样# runner.py 核心逻辑简化版 import resource import subprocess import json import sys import os def set_limits(): # 限制 CPU 时间为 2 秒 resource.setrlimit(resource.RLIMIT_CPU, (2, 2)) # 限制进程内存为 256MB超出直接杀死 resource.setrlimit(resource.RLIMIT_AS, (256 * 1024 * 1024, 256 * 1024 * 1024)) def run_user_code(user_code_path, test_case): # 将用户代码和测试用例合并为一次执行 combined_path /sandbox/main.py with open(combined_path, w, encodingutf-8) as f: f.write(user_code_path) f.write(\n) f.write(gen_test_harness(test_case)) try: proc subprocess.run( [python3, -I, combined_path], capture_outputTrue, textTrue, timeout5, preexec_fnset_limits, cwd/sandbox ) return { stdout: proc.stdout, stderr: proc.stderr, returncode: proc.returncode, } except subprocess.TimeoutExpired: return {error: timeout} except MemoryError: return {error: memory_limit}preexec_fn这个参数非常关键它直接在子进程启动前设置资源上限比父进程之后再想办法更强的控制方式可靠得多。CPU 时间和内存限制必须设置得比测试用例的理论需求略高留出一定缓冲否则一些正常代码会因为系统自身的运行开销被误判超时。判题平台的执行环境普遍不太喜欢交互式输入所有输入输出都通过标准输入输出。所以测试用例被包装成一个测试驱动函数读取用户定义的函数和预期结果然后运行并比较输出。比如题目要求实现一个two_sum函数测试框架会生成类似这样的代码import json def test(): cases [ ([2, 7, 11, 15], 9, [0, 1]), ([3, 2, 4], 6, [1, 2]), ([3, 3], 6, [0, 1]), ] for input_data, target, expected in cases: result two_sum(input_data, target) assert result expected, fcase failed: {input_data} print(ALL PASSED) test()这里的一个易错点是函数名冲突。测试框架和用户代码如果定义了同名函数会相互覆盖导致诡异的结果。实践中会把用户代码显式重写后再拼接测试代码并在测试代码中调用时加上命名空间限定避免冲突。3.3 判题结果判定规则的设计判定规则不能只盯着“输出是否相等”这一个维度否则遇到空白字符、浮点误差、额外输出时就容易误判。t3code 的判题结果划分为五种状态对应不同情况状态含义触发条件AC通过所有测试用例输出均与预期一致WA答案错误有测试用例输出内容不一致TLE超时CPU 执行时间超过限制MLE内存超限峰值内存超过限制RE运行错误代码崩溃、异常、非零退出码判定时先检查退出码和非零返回如果崩溃就直接标 RE不继续比较输出。之后再对比 stdout 内容为了减少由末尾换行造成的误杀比较时统一 strip 掉两侧的空白字符但保留内部的空行与空格。这样设计是因为算法题输出结果通常是纯文本空格有实际语义末尾换行则不具备。浮点数比较也是个隐藏坑。有些题目要求输出浮点数直接精确比较会因为精度差异判成 WA。t3code 专门设定了一个浮点数比较规则如果用例的预期结果类型是 float判定时绝对误差不超过 1e-6 即视为通过否则走字符串精确比较。这个 1e-6 的阈值在绝大多数算法题场景里是可接受的。3.4 沙箱安全与资源隔离的硬配置沙箱安全是整个平台必须反复强调的底线。t3code 的每个执行容器在启动时就做了几件关键的事情不开放任何网络端口、不使用宿主网络模式、挂载临时目录只读、工作目录使用内存文件系统、不允许读取宿主机环境变量、删除所有交互式接口。核心目的是把用户代码的“活动空间”压到最小。Docker 容器配置有一个容易被忽略的细节ulimits和pids-limit要一起设置。如果只限制内存和 CPU恶意代码可以疯狂创建子进程把系统进程表打爆。t3code 在创建执行容器时同步设置了--memory 512m --cpus 1 --pids-limit 64子进程数量一旦超过 64 个立即被系统拒绝。同时镜像本身内置了 seccomp 配置文件屏蔽了mount、reboot、ptrace等危险系统调用最大限度降低容器逃逸风险。执行完一次任务之后临时目录会被立即清理不能复用。有些人为了性能想保留容器热加载但对运行不可信代码的场景来说冷启动带来的性能损耗远低于安全风险带来的代价。安全隔离这种事情宁可损失一点吞吐量也不要在边界上留口子。4. 典型踩坑与调优实录4.1 排查问题速查表踩过的坑一次性说清整个平台从原型到能稳定跑中间遇到过的坑可以整理成一个速查表供做类似系统的人直接对照排查现象可能原因解决方案判题偶尔超时但本地很快执行容器系统开销占用了 CPU或语言启动时间未预留给执行器增加 0.5s 缓冲并把语言启动消耗计入超时池代码明明是对的但 WA输出比较时含无意义空白符使用 strip 后再比较内部空白不要动浮点输出结果不稳定不同语言浮点表示精度不一致统一使用误差阈值 1e-6 比较容器逃逸风险未禁用危险系统调用配置 seccomp禁止 mount、reboot、ptrace 等一次性大量提交导致排队积压执行器数量不足任务堆积引入队列增加 Worker 节点设置并发上限WebSocket 推送丢失长连接被代理断开前端改用“短轮询兜底 推送加速”双通道测试用例对用户可见过多用输入行数或特征匹配判断基于题目 ID 单独标记可见性而不是全局开关运行时高并发下频繁崩溃Python 的 GIL 限制了多线程计算执行器改为多进程 固定工作池代码快照存储过大用户反复提交的代码副本过多对历史提交做压缩或按用户维度的增量快照4.2 第一个大坑Python 判题器被环境变量坑了上线没多久有用户反馈“我判断环境变量后代码挂了”。定位问题后发现container 里装的是 Debian 基础镜像 Python 运行时而我的执行器代码在拼接用户代码时不小心把一段读取环境变量的 debug 代码也留在了镜像里。用户在字典里能读到DATABASE_URL、SECRET_KEY之类的敏感信息这绝对是安全危机。当时立刻把所有容器镜像做了一轮排查删掉了所有非必要环境变量并把env参数显式设置为空字典传入 Docker 创建接口。这里要提醒所有做判题系统的人容器里的环境变量默认会继承宿主的一部分不主动清空等于把自己家的钥匙放在门口人尽皆知。正确做法是在创建容器时env[]显式不传任何宿主变量判题脚本需要什么参数直接作为命令行参数传进去。4.3 第二个大坑并发峰值打爆执行队列排行榜和比赛功能上线后用户提交量一夜之间翻了十倍。当时的执行器是单机多线程模型每个提交直接调 Python 的subprocess.run请求一多消息队列里的任务积压越来越严重首个任务执行耗时从 1 秒涨到 20 多秒。排查时发现单个进程同时创建的子进程数量过多导致操作系统的进程调度出现了显著延迟反而把整体吞吐拖垮。这个问题的标准解法是做“执行器池”把并发上限锁死在一定数量之内而不是无限制地拉起新进程。t3code 把执行器从单机多线程改成多进程 Worker 池每个进程最多同时跑 3 个容器容器启动失败则回退到重试队列。同时引入 Redis 作为任务队列前端提交只写入队列不再直连执行器执行器的处理速度不会影响提交接口的响应时间提交体验立刻稳定下来。4.4 第三个大坑判题稳定性比功能新意更重要很多时候线上判题系统真正的考验不是功能丰富度而是在异常输入下的稳定性。比如用户提交一个进入死循环的代码CPU 限制设置不合理的话一个死循环就能把整台机器拖到卡死。t3code 早期的超时设置是 CPU time 1 秒执行器跑一个死循环会占满 CPU直到系统主动 kill这期间其他用户的提交全部被拖慢。后来把超时控制从“进程 CPU 时间”升级为“cgroup 资源上限”不会等到进程把机器吃干抹净才介入而是在进程超出配额的那一瞬间直接终止。容器级别再叠加一层timeout整体有一个硬性上限兜底就算 cgroup 配置出错也有最后防线。判题平台的正确设计理念是“宁可误杀不可拖垮”一个超时用例毁了整个判题集群的风险远比误判一次严重得多。4.5 前端轮询变成压力源的教训初期版本我用纯轮询获取判题结果每 2 秒轮询一次。用户量小的时候毫无感觉但比赛当天几千人同时提交前端发起的轮询请求把后端接口打成了热点数据库连接量不断往上爬接口响应时间飙升。最终把前端改成 WebSocket 主动监听状态变更判题完成后由后端单次推送结果前端只在连接断开时用轮询兜底。这个改造让接口调用量直接下降了一个数量级用户体验也好了不少。前端优化的另一个重点是代码编辑器。有一个版本用了 Monaco Editor首屏加载体积接近 3MB压缩后也超过 1MB普通网络环境要等接近两秒才能看到编辑器。换了 CodeMirror 6 并开启语言包懒加载之后编辑器体积降到 200KB 左右首屏加载提升明显。这类优化不需要动业务逻辑但实实在在影响着每天打开平台的用户感受。5. 单机到集群判题平台的演进路线5.1 架构演进的核心思路t3code 一开始是纯单机部署Next.js 应用、判题执行器、数据库全部跑在同一台服务器上。这个阶段适合验证核心链路但并发能力非常有限而且判题任务会占用大量 CPU 和内存很容易影响正常 API 请求的响应。随着题目量和用户量增长最值得先拆出来的是判题执行器。执行器本身是无状态的只需要从队列拿任务、执行、返回结果天然适合独立成服务。拆出来之后面对高峰期只需要增加执行器节点数量即可速度和转换时间都很短。数据库是第二个需要独立部署的部分不建议继续和业务服务混在一台机器上否则数据库一抖动整个判题和页面全部受影响。5.2 容器化部署的执行清单如果要做容器化部署任何一个环节漏了都有可能出问题。基于 t3code 的实践列一份关键检查清单执行器镜像固定语言版本使用多阶段构建镜像只保留运行所需文件。数据库连接使用独立账号不将管理员账号写入环境变量。判题任务的临时目录必须独立挂载执行结束立即卸载。任务队列设置死信队列执行失败超过 3 次后进入人工核查通道。监控指标至少包含提交量、队列积压数、容器启动耗时、超时率、崩溃率。日志采集要打上 traceId方便排查单次提交全链路。用 Docker Compose 在单机先把这套拓扑跑熟练之后再迁移到 K8s 或云原生平台就顺理成章了。对于大部分中小型刷题平台单机 Docker Compose 加几台执行器服务器已经足够应对几百人的并发判断不需要一上来就上 K8s。5.3 性能监控与数据驱动的取舍判题平台很多优化决策靠拍脑袋不如靠数据。我后来在 t3code 里加了一个提交耗时分布图表按语言、难度、测试用例数量拆分很快发现 C 判题耗时比 Python 快一个量级但用户提交最多的恰恰是 C 和 Python 中较慢的题目。这个数据直接影响了对执行器节点数量的决策给 Python 执行器多分配两个节点整体队列积压情况立刻缓解。这给做同样系统的朋友一个提醒判题平台的性能优化优先看队列积压时长和判题完成耗时而不是单纯看服务端 QPS。只要队列里积压的任务持续堆积用户端感知的“慢”就会越来越明显。优化策略应该围绕“减少任务在队列中的时间”来做提升单节点吞吐量只是手段之一合理扩容队列消费能力才是核心。6. 数据与内容运营平台的生命线写完代码平台只是走完了工程师的视角。一个刷题平台真正让用户留下的是内容质量和体验闭环。t3code 在数据维护上摸索出了几个实践方法。题目库不能只靠单一来源。t3code 上的题目一部分来自经典算法书籍的改编一部分来自真实面试场景的还原还有一部分来自用户自己的投稿。对所有题目都要求满足三个标准输入范围明确、边界条件完整、至少有一组隐藏用例能覆盖极端情况。题目会被打上多种标签包括数据结构类型、算法思想、难度梯度、适用面试阶段用户可以通过标签组合进行筛选比如“动态规划 中等难度 最近两周新增”。判题平台的内容运营有一个很多人忽略的指标题目被反复提交的次数和通过率。通过率过低的题目往往是描述有歧义或者测试用例有坑而不是题目本身有多难。我隔一段时间就会拉一次通过率列表把通过率低于 15% 的题目重新审一遍确认题目描述、样例、数据范围都足够清晰再判断是否要对测试用例进行调整。这个流程建立的机制保证平台不会被“故意刁难用户”的题毁掉口碑。另外平台里的编辑器相关配置也直接影响用户体验。比如 Python 提交时缩进被自动转成全角空格这种问题一旦出现会直接刷掉一批新手用户。代码保存时要确认 Tab 键行为、自动换行、行尾符设置全部统一避免一些没必要的困惑。7. 一点后续想做的事如果把 t3code 继续往下做我觉得最有价值的方向不是再堆更多题目而是把“复盘”做得更深。目前平台已经可以展示每次提交的运行时间和通过用例数但还缺少“这次提交相比上次改了哪些地方”这种对比能力。如果能把前后两次提交的代码 diff 展示出来再结合判题结果变化用户能很直观地看到哪一步调整让性能提升、哪一步导致了回归这种反馈对算法能力成长是非常有价值的。也想增加一个错题本功能根据用户提交失败的题目自动聚合出薄弱考点。比如一个人递归题反复超时、DP 题反复边界错误系统可以自动生成针对性的推荐题目。这一块的可玩性很高技术上不复杂核心是数据打标和推荐逻辑。我的个人体会是t3code 最大的价值不在于它用了多新的技术栈而在于把“写代码 → 跑代码 → 看结果 → 再修改”这个循环做闭环了。很多人刷题最大的阻力不是不会写而是得不到足够快的反馈反馈够快练习才有动力。每次看到有人在平台上一题提交十几次从 WA 一路试到 AC我就觉得这个项目最值得的地方不是代码本身而是它真的帮人把学习路径走通了。