问题背景
在一次线上JAVA服务运行中,发现有个任务长时间处于执行中状态,这显然不合理。通过监控排查,发现某个线程长时间处于 RUNNABLE状态,但实际并未执行任何有效代码。进一步分析后,发现该线程在执行外部命令时发生了阻塞,导致任务无法正常结束。
排查过程
1. 初步分析
- 线程转储分析: 通过jstack查看线程堆栈信息,发现线程状态为 RUNNABLE,但实际上阻塞在 BufferedInputStream.read() 方法中,等待读取子进程的标准输出流。
- 进程状态分析: 系统中有多个 状态的 ansible-playbook 进程,这些是僵尸进程。僵尸进程表示子进程已结束,但父进程尚未读取其退出状态。
2. 深入排查
- 检查子进程状态: 发现部分子进程已结束,但处于僵尸状态,只有一个子进程处于休眠状态,说明父进程未正确处理其退出状态,导致资源未被释放。
- 日志记录: 增加日志记录,发现子进程的标准错误流中有大量日志输出。
- 检查输入流读取: 发现父进程仅读取了子进程的标准输出流,未读取标准错误流。想到之前看到过,在Java中,如果只读取 stdout 而不读取 stderr 或者不处理子进程的输入,可能会导致子进程因为写入缓冲区满而阻塞。于是,我们尝试同时读取标准输出流和标准错误流,测试后发现任务恢复正常。
3. 根本原因
- 未读取标准错误流: 父进程未读取子进程的标准错误流,导致标准错误流的缓冲区被填满,子进程在写入操作时发生阻塞。具体来说,当Java执行外部命令时,会创建一个子进程,并打开三个管道:stdin、stdout和stderr。如果父进程只读取stdout,而子进程仍在向stderr写入数据,stderr的缓冲区可能会被填满,导致子进程阻塞,进而使父进程的线程也陷入阻塞状态。
解决方案
1. 同时读取标准输出和标准错误流
在父进程中,必须同时读取子进程的标准输出流 stdout 和标准错误流 stderr ,以避免缓冲区阻塞。 以下是改进后的代码示例:
Process process = Runtime.getRuntime().exec(command);
// 读取标准输出流
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
// 处理标准输出
}
} catch (IOException e) {
// 处理异常
}
}).start();
// 读取标准错误流
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
// 处理标准错误
}
} catch (IOException e) {
// 处理异常
}
}).start();
// 等待子进程结束
int exitCode = process.waitFor();
// 处理退出代码
2.设置超时机制
为了避免子进程长时间挂起,可以为子进程的执行设置超时机制。如果子进程在指定时间内未完成,则强制终止
if (!process.waitFor(30, TimeUnit.SECONDS)) {
process.destroy(); // 超时后终止子进程
throw new TimeoutException("子进程执行超时");
}
3.增加日志记录
记录子进程的标准输出和标准错误流的内容,便于排查问题。
总结
通过本次线上问题的排查,发现根本原因是父进程未读取子进程的标准错误流,导致子进程的缓冲区被填满,进而引发阻塞。为了解决这个问题,我们采取了以下措施:
- 同时读取子进程的标准输出流和标准错误流;
- 为子进程设置超时机制,避免长时间挂起;
- 增加日志记录,便于后续问题排查。