最近项目里需要在 Java 中调用 Python 脚本,最初的实现其实很简单,果然欠下的债迟早要还的,也许哪一天stdin的债也要还吧。
Java 通过 ProcessBuilder 启动 Python,把参数通过 stdin 传过去,再从 stdout 中读取 Python 的输出结果。
刚开始觉得这个方案挺方便,写起来也不复杂,功能很快就跑通了。
不过随着功能越来越多,Python 脚本开始负责爬虫、AI 调用、文档解析等工作,输出的数据越来越大,问题也慢慢暴露出来了。
这次就记录一下整个优化过程。
最初的实现
整体流程大概是这样的:
Java
│
├── ProcessBuilder 启动 Python
│
├── stdin 写入参数(JSON)
│
├── Python 执行
│
└── stdout 输出结果
│
▼
Java 读取结果
Python 端约定输出:
print("python_result:" + result)
Java 再去解析:
python_result:xxxxx
这种方式最大的优点就是简单。
不需要 Socket;
不需要 HTTP;
也不用 MQ。
对于很多内部工具来说已经足够了。
第一个问题:日志越来越大
最开始为了方便排查问题,我直接打印了 Python 的输出:
logger.info("Python输出:{}", line);
后来 Python 开始返回 AI 生成的 Markdown、HTML、JSON。
有时候一份结果就是几百万字符。
这时候日志开始出现各种问题:
- IDEA 控制台越来越卡;
- Web 页面日志加载越来越慢;
- 日志文件迅速膨胀;
- 有时候看起来像数据被截断,其实只是日志显示不完整。
日志不是用来存放业务数据的,只有一天的时间,你不用stdin谁用呢
日志真正应该记录的是:
- 是否执行成功;
- 数据有多大;
- 出错位置;
- 必要的上下文。
于是把日志统一改成了:
logger.info(
"Python输出长度:{},前100字符:{},后100字符:{}",
line.length(),
line.substring(0, Math.min(100, line.length())),
line.substring(Math.max(0, line.length() - 100))
);
这样定位问题基本已经够用了。
第二个问题:Python 卡死
一开始我是这样写的:
process.waitFor();
看起来没什么问题。
但是后来 Selenium 偶尔会卡住。
Python 不退出。
Java 就一直等。
整个线程就挂在那里。
如果这是一个 Web 请求,那么用户页面就会一直转圈。
后来改成了带超时的等待:
boolean finished = process.waitFor(10, TimeUnit.MINUTES);
如果超过十分钟:
- 强制结束 Python;
- 返回失败;
- 释放资源。
这样至少不会把线程一直占着。
第三个问题:多人同时执行
刚开始这个系统只有自己用。
后来同事也开始使用。
这时候就出现了一个问题:
假设三个人同时点击执行。
后台就会同时启动三个 Python。
如果 Python 又启动 Selenium,那么后台可能瞬间出现:
Chrome
Chrome
Chrome
Chrome
Chrome
CPU 飙高。
内存一路上涨。
后来我加了一个很简单的控制:
private static final Semaphore PYTHON_SEMAPHORE =
new Semaphore(1);
执行之前:
PYTHON_SEMAPHORE.acquire();
执行结束:
PYTHON_SEMAPHORE.release();
这样就保证:
同一时刻只允许一个 Python 任务执行。
虽然吞吐量下降了一点,但系统稳定了很多。
如果以后服务器性能足够,只需要把:
new Semaphore(1)
改成:
new Semaphore(3)
就能支持三个任务并发。
第四个问题:AI 返回 Markdown
后来接入大模型以后。
很多时候返回的是:
```json
{
"name":"张三"
}
```
而不是纯 JSON。
直接解析:
JSON.parse(...)
自然就失败了。
后来统一做了一层处理:
去掉:
```json
以及最后的:
```
`
这样后面的代码就不用关心 AI 返回的是:
- JSON
- Markdown
- 还是代码块。
统一处理即可。
第五个问题:异常日志太大
以前异常直接这样写:
throw new RuntimeException(
"执行失败:" + output
);
`
如果 output 有几百万字符。
一次异常日志就是几 MB。
后来统一改成:
输出长度
输出前500字符
真正的大数据不要放进异常。
否则排查问题的时候,日志本身反而成了新的问题。
我最终保留了哪些设计
经过几轮修改之后,最终保留下来的东西其实并不多。
主要有下面几个。
1. stdin 传参数
所有参数统一 JSON。
不用命令行参数。
不用环境变量。
扩展最方便。
2. stdout 返回结果
Python 仍然通过:
print("python_result:" + result)
返回结果。
Java 负责提取。
这种方式简单直接。
3. Semaphore 控制并发
限制同一时间运行的 Python 数量。
避免把服务器资源打满。
4. 超时机制
任何 Python 最多执行固定时间。
超过时间直接结束。
避免线程一直等待。
5. 日志只记录摘要
日志不是数据库。
更不是缓存。
日志应该记录:
- 长度
- 耗时
- 状态
- 前后少量内容
而不是整个业务数据。
如果以后继续升级
目前这套方案已经可以满足日常使用。
但是如果以后:
- 用户越来越多;
- Python 任务越来越重;
- AI 输出越来越大;
我不会继续让 stdout 传输大文本。
更合理的方案应该是:
Java
│
├── 创建任务
│
├── 启动 Python
│
├── Python 写结果文件
│
├── Java 读取文件
│
└── 删除临时文件
也就是说:
stdout 负责状态。
文件负责数据。
这样整个链路会稳定得多。
总结
回头来看,这次优化其实没有引入什么复杂框架。
只是把几个很容易忽略的小问题处理好了:
- 增加并发控制;
- 增加超时机制;
- 控制日志大小;
- 统一 Python 输出协议;
- 兼容 AI 返回格式;
- 避免异常日志过大。
这些改动单独看都不大。
但组合起来之后,整个执行器的稳定性提升了不少。
很多时候,真正让系统变稳定的,并不是引入更多技术,而是把这些容易被忽略的细节一点一点做好。
完整代码在githup:https://github.com/Succos

评论
填写昵称与邮箱即可评论,无需登录。