Spring Boot中调用Python评分脚本如何实现并发支持
Spring Boot 调用自定义Python评分脚本并发改造方案
核心问题定位
当前并发缺陷的本质是所有评分进程共用全局固定路径的TEST_GRADING_REPORT结果文件,多请求并行时会出现文件覆盖、读写竞争,直接导致批改结果串号、文件读取异常。
落地改造方案
1. 单任务独立工作目录隔离(零侵入兼容存量脚本,最快上线)
这是改造成本最低的方案,完全不需要教师调整已有评分脚本写法:
- 每次调用
gradeHomework()时,基于请求UUID/作业ID生成全局唯一的临时工作目录,路径规则参考./grading-workspace/{taskUuid}/ - 启动Python进程前,通过
ProcessBuilder.directory()方法将进程的工作目录设置为该专属临时目录。原有脚本写相对路径TEST_GRADING_REPORT的逻辑不需要任何修改,文件会自动落到专属目录下,彻底避免文件读写竞争。该方案对教师侧完全透明,相对路径基于进程工作目录解析,用户完全感知不到底层调整 - 进程执行完成、结果反序列化成功后,立刻递归删除整个临时工作目录,避免磁盘空间泄漏
- 核心代码参考:
// 生成全局唯一任务ID String taskId = UUID.randomUUID().toString(); Path workDir = Files.createDirectories(Paths.get("./grading-workspace/" + taskId)); // 按需将评分脚本、学生作业文件拷贝到当前工作目录 Files.copy(teacherScriptPath, workDir.resolve("grade.py"), StandardCopyOption.REPLACE_EXISTING); Files.copy(studentHomeworkPath, workDir.resolve("student_submit.py"), StandardCopyOption.REPLACE_EXISTING); // 构建进程时指定专属工作目录 ProcessBuilder pb = new ProcessBuilder("python3", "grade.py", "student_submit.py"); pb.directory(workDir.toFile()); // 合并错误流方便排查问题 pb.redirectErrorStream(true); Process process = pb.start(); // 设置执行超时,防止死循环脚本占满资源 boolean execSuccess = process.waitFor(3, TimeUnit.SECONDS); if (!execSuccess) { process.destroyForcibly(); // 清理临时目录 FileUtils.deleteDirectory(workDir.toFile()); throw new RuntimeException("评分脚本执行超时"); } // 读取当前任务专属目录下的结果文件,无并发冲突 Path reportPath = workDir.resolve("TEST_GRADING_REPORT"); Grade grade = new ObjectMapper().readValue(reportPath.toFile(), Grade.class); // 执行完成清理临时文件 FileUtils.deleteDirectory(workDir.toFile());
2. 进程资源池管控(避免无限制创建进程拖垮服务)
单任务50ms耗时虽短,但无限制创建进程会快速耗尽CPU、内存资源,必须做并发管控:
- 自定义专用线程池执行评分任务,核心线程数设置为CPU核心数*2即可(Python单进程受GIL限制,CPU密集场景下过多线程不会提升效率),队列设置合理长度阈值,超出阈值直接返回系统繁忙提示,避免雪崩
- 禁止直接在Tomcat请求线程内阻塞等待进程执行,防止占满Web容器线程导致全服务不可用
- 所有进程生命周期必须和请求绑定,请求中断、超时场景下要主动调用
destroyForcibly()销毁对应进程,避免孤儿进程残留
3. 进阶性能优化:移除中间文件,通过标准输入输出传参
如果可以统一约束教师评分脚本的写法,可以彻底去掉磁盘文件读写开销,进一步降低单任务耗时:
- 要求Python脚本从标准输入(stdin)读取作业参数、学生提交内容,评分完成后将结果JSON直接打印到标准输出(stdout),不需要写任何本地文件
- Java端直接通过
Process.getInputStream()读取进程输出流,拿到JSON后直接反序列化,连临时目录都不需要创建,彻底消除文件IO开销和文件竞争风险 - 适配的Python脚本示例:
import sys, json # 从标准输入读取传入的作业参数 submit_info = json.loads(sys.stdin.read()) # 执行原有评分逻辑 final_score = 95 grade_detail = "边界用例未通过,扣5分" # 结果直接输出到标准输出 print(json.dumps({"score": final_score, "detail": grade_detail}))
- 注意事项:必须单独开线程消费进程的stdout、stderr流,避免缓冲区满导致进程阻塞挂死
4. 必做安全加固
由于是执行教师上传的任意Python脚本,必须做基础隔离防止恶意脚本破坏系统:
- 所有评分进程使用低权限系统用户启动,禁止使用root/管理员权限运行
- 临时目录设置读写权限控制,禁止脚本访问工作目录外的系统敏感文件
- 安全要求高的场景可以用Docker容器启动每个评分进程,单独限制CPU、内存、磁盘IO配额,彻底和宿主机隔离
选型建议
- 赶上线进度优先选方案1+方案2,无脚本适配成本,改完Java端逻辑即可直接支撑并发场景
- 长期迭代可以逐步推进方案3,去掉文件IO后单任务执行效率还能提升30%左右
- 只要涉及执行用户自定义脚本,方案4的安全加固必须补上,避免出现安全事故
内容的提问来源于stack exchange,提问作者JJJohn
相关产品推荐
相关产品推荐

