跳到正文
hello world
文章封面

Java执行Python节点的胶水代码

最近项目里需要在 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

评论

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

推荐阅读