目录
现象
我这边是在页面上执行云数据库修改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()等待子进程结束。