java执行shell命令时ps.waitFor卡主问题

现象

我这边是在页面上执行云数据库修改ip的功能时,页面上卡了许久
在这里插入图片描述

排查过程

1、查看日志,确认问题

在这里插入图片描述

2、查看堆栈信息–ps.waitFor()卡主了

用arthas查看修改ip的子进程:
在这里插入图片描述
查看这6个子进程的堆栈信息:
在这里插入图片描述

卡在了ModifyIpServiceImpl的833行:
在这里插入图片描述

和CmdUtils的147行:
在这里插入图片描述
147行啥也没有。将CmdUtils类反编译出来看看:
在这里插入图片描述
147是执行ps.waitFor操作,等待子进程结束。

3、为什么ps流卡主了?

查看shell进程:
在这里插入图片描述

查看进程树:

在这里插入图片描述
可以看到卡在了子进程tee操作上,然后通过strace进一步查看tee操作死在哪个系统调用上:

在这里插入图片描述
write(1, 表示正在执行系统调用write,写入的文件描述符是1,这个描述符的含义可以通过lsof查看:

在这里插入图片描述

可以看到文件描述符1是在写pipe管道,时间是2024-03-06 19:01:12,写入的字节数是94。
卡住的原因是:子进程产生一些数据,他们会被buffer起来,当buffer满了,会写到子进程的标准输出和标准错误输出,这些东西通过管道发送给父进程。当管道满了之后,子进程就停止写入,于是就卡住了。

4、管道的大小是多少?

centos系统上执行man 7 pipe命令查看默认管道大小
结果为:

Pipe Capacity
       A  pipe  has  a limited capacity.  If the pipe is full, then a write(2) will block or fail, depending on whether the O_NONBLOCK flag is set (see below).  Different
       implementations have different limits for the pipe capacity.  Applications should not rely on a particular capacity: an application should be designed  so  that  a
       reading process consumes data as soon as it is available, so that a writing process does not remain blocked.

       In  Linux versions before 2.6.11, the capacity of a pipe was the same as the system page size (e.g., 4096 bytes on i386).  Since Linux 2.6.11, the pipe capacity is
       65536 bytes.

可以看到,Linux内核自从2.2.11版本之后,管道的默认大小就是64k。

5、shell命令产生了多少字节?

sh /xxx/Console-Web-clouddb updateIpInfoList /xxx/conf/modifyRelationIp.conf.1709722871362 > /tmp/test.log

在这里插入图片描述
可以看到,导致管道阻塞的shell命令产生了81k字节的输出流。

6、测试管道阻塞问题

import java.io.IOException;
  
public class TestPipeSize {
    public static void main(String[] args) {
        System.out.println("Test 64KB - 1");
        test(63 * 1024 - 1);
        // 64KB
        System.out.println("Test 64KB");
        test(64 * 1024);
        
        // 64KB + 1B
        System.out.println("Test 63KB + 1");
        test(64 * 1024 + 1);
    }

    public static void test(int size) {
        System.out.println("start");

        String cmd = "dd if=/dev/urandom bs=1 count=" + size + " 2>/dev/null";
        ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", cmd);
        processBuilder.redirectErrorStream(true);

        try {
            Process ps = processBuilder.start();
            ps.waitFor();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("end");
    }
}

测试代码中,分别使用dd命令产生64K - 1、64K、64K + 1的标准输出,然后调用ps.waitFor()等待,写法与代码中一样。
结果:
在这里插入图片描述

可以看到,64K - 1、64K的正常打印了,但是在64K + 1的输出流测试中,只打印了start,没有打印end,说明程序执行卡在了ps.waitFor()上,死锁了。

7、如何避免ps.waitFor()死锁

及时取走管道的输出就可以,也就是将标准输出和错误输出及时读取到内存中。

Process ps = executeNoWait(cmd);
BufferedReader br = null, ebr = null;
InputStreamReader is = null, es = null;
StringBuilder sbr = new StringBuilder();
StringBuilder seer = new StringBuilder();
try {
    is = new InputStreamReader(ps.getInputStream());
    br = new BufferedReader(is);
    es = new InputStreamReader(ps.getErrorStream());
    ebr = new BufferedReader(es);
    String line;
    while ((line = br.readLine()) != null) {
        sbr.append(line).append('\n');
    }
    while ((line = ebr.readLine()) != null) {
        seer.append(line).append('\n');
    }
} catch (Exception e) {
    log.error("开始执行命令:{},命令执行正常结果:{},命令执行异常结果:{},命令异常:{}",
            cmd, sbr, seer, e);
} finally {
    if (is != null) {
        is.close();
    }
    if (es != null) {
        es.close();
    }
    if (br != null) {
        br.close();
    }
    if (ebr != null) {
        ebr.close();
    }
}
int code = ps.waitFor();

将输出全部读取到内存中,然后调用ps.waitFor()等待子进程结束。

Java执行shell命令可以使用Runtime类或ProcessBuilder类。以下是使用Runtime类执行shell命令的示例代码: ```java import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class ShellCommand { public static void main(String[] args) { try { // 执行ls命令 Process process = Runtime.getRuntime().exec("ls"); // 读取命令输出结果 BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } // 等待命令执行完成 int exitCode = process.waitFor(); System.out.println("命令执行完成,退出码:" + exitCode); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } } ``` 使用ProcessBuilder类执行shell命令的示例代码如下: ```java import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; public class ShellCommand { public static void main(String[] args) { try { // 构建命令 List<String> command = new ArrayList<>(); command.add("ls"); command.add("-l"); // 执行命令 ProcessBuilder processBuilder = new ProcessBuilder(command); Process process = processBuilder.start(); // 读取命令输出结果 BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } // 等待命令执行完成 int exitCode = process.waitFor(); System.out.println("命令执行完成,退出码:" + exitCode); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } } ``` 以上代码都是在Linux或MacOS上执行的示例,若在Windows上需要将命令改为相应的Windows命令
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值