记一个写日志阻塞的问题

背景

我们有多家客户,每个客户有自己的服务器(Windows Server),现在需要在客户的服务器上部署程序并自动更新,考虑到后期还会有其他的程序,所以我们首先做了一管理程序,管理程序从云端获取应用列表, 如果版本更新或者本地没有,就自动下载应用并安装启动,启动方式是使用

Runtime.getRuntime().exec("java -jar xxx.jar")

问题

子应用运行起来后,调用其接口,若干次后就阻塞了, 任何接口都不返回数据了。

追查

让程序处于阻塞状态,登录客户服务器下载了jdk, 使用jstack查看线程状态, 发现是几个写日志的线程被阻塞了,处理wait状态, 根据锁信息找到有一个持有锁的线程,处于RUNNABLE状态,但一直没有释放锁:

"http-nio-10001-exec-5" #33 daemon prio=5 os_prio=0 tid=0x000000001d323800 nid=0x2a60 runnable [0x0000000024b4d000]
   java.lang.Thread.State: RUNNABLE
	at java.io.FileOutputStream.writeBytes(Native Method)
	at java.io.FileOutputStream.write(Unknown Source)
	at java.io.BufferedOutputStream.write(Unknown Source)
	- locked <0x00000000e0320808> (a java.io.BufferedOutputStream)
	at java.io.PrintStream.write(Unknown Source)
	- locked <0x00000000e03207e8> (a java.io.PrintStream)
	at java.io.FilterOutputStream.write(Unknown Source)
	at ch.qos.logback.core.joran.spi.ConsoleTarget$1.write(ConsoleTarget.java:37)
	at ch.qos.logback.core.OutputStreamAppender.writeBytes(OutputStreamAppender.java:199)
	at ch.qos.logback.core.OutputStreamAppender.subAppend(OutputStreamAppender.java:231)

可以得知线程是阻塞在java.io.FileOutputStream.writeBytes, 但很奇怪, 这个函数怎么会阻塞呢?
没有什么头绪,在linux下面跑的时候又没有问题, 初步怀疑是log4j在windows服务器有问题(最开始用的是log4j2来写日志), 然后把文件日志关了,依然报同样的错,搜索也没有发现log4j有相关的bug。
然后换成默认的日志库,依然出错,百思不得其解。
继续搜索java.io.FileOutputStream.writeBytes阻塞相关的问题,找到一篇blog: https://my.oschina.net/u/1030459/blog/908007(感谢作者), 和我的情况很相似, 他是由于点击了console导致console日志输出暂停了,然后服务无法写入日志, 所以接口阻塞了。
我看到了成功的希望,我这边很可能也是因为日志无法写入导致的问题, java.io.FileOutputStream.writeBytes不一定是在写文件,很可能是在写console(从上面关闭文件日志依然报错可以得知)。
为什么console无法写入呢, 我好像没有使用console日志。
我直接本地cmd启动子应用, 打印出console日志,这次居然没有报错了, emm…问题真正指向Runtime.getRuntime().exec。
想到要获取exec执行的日志时,通常是读取它的outputstream流, 那么很可能是这个输出流缓冲区大小有限制,并用它写满后不会自动覆盖,而是阻塞在那里。
分析Runtime.getRuntime().exec的源码:

    public Process exec(String[] cmdarray, String[] envp, File dir)
        throws IOException {
        return new ProcessBuilder(cmdarray)
            .environment(envp)
            .directory(dir)
            .start();
    }

调用了ProcessBuilder的start方法:

    // Only for use by ProcessBuilder.start()
	public Process start() throws IOException {
    		//......
            return ProcessImpl.start(cmdarray,
                                     environment,
                                     dir,
                                     redirects,
                                     redirectErrorStream);
           // ...
	}

ProcessImpl是操作系统相关的类,在windows平台下它的start方法如下:

    static Process start(String[] cmdarray, //待执行命令
                         java.util.Map<String,String> environment, //环境信息
                         String dir, //工作目录
                         ProcessBuilder.Redirect[] redirects,
                         boolean redirectErrorStream){
                   return new ProcessImpl(cmdarray, envblock, dir,
                                   stdHandles, redirectErrorStream);
                         }

ProcessImpl.start方法的入参ProcessBuilder.Redirect[] redirects决定的输入输出,这个参数默认类型是:

    private Redirect[] redirects() {
        if (redirects == null)
            redirects = new Redirect[] {
                Redirect.PIPE, Redirect.PIPE, Redirect.PIPE
            };
        return redirects;
    }

也就是说,默认情况下,将使用管道来进行输入输出,接着往下看,发现调用了native方法:

     * @param stdHandles array of windows HANDLEs.  Indexes 0, 1, and
     *        2 correspond to standard input, standard output and
     *        standard error, respectively.  On input, a value of -1
     *        means to create a pipe to connect child and parent
     *        processes.  On output, a value which is not -1 is the
     *        parent pipe handle corresponding to the pipe which has
     *        been created.  An element of this array is -1 on input
     *        if and only if it is <em>not</em> -1 on output.
     * @param redirectErrorStream redirectErrorStream attribute
     * @return the native subprocess HANDLE returned by CreateProcess
     */
    private static synchronized native long create(String cmdstr,
                                      String envblock,
                                      String dir,
                                      long[] stdHandles,
                                      boolean redirectErrorStream)

继续进入native层,找到创建pipe的地方

#define PIPE_SIZE (4096+24)
//......
     if (!CreatePipe(
            &pHolder->pipe[OFFSET_READ],
            &pHolder->pipe[OFFSET_WRITE],
            NULL, /* we would like to inherit
                     default process access,
                     instead of 'Everybody' access */
            PIPE_SIZE))

CreatePipe是window操作系统的一个api:

Creates an anonymous pipe, and returns handles to the read and write ends of the pipe.
When a process uses WriteFile to write to an anonymous pipe, the write operation is not completed until all bytes are written. If the pipe buffer is full before all bytes are written, WriteFile does not return until another process or thread uses ReadFile to make more buffer space available.

If the pipe buffer is full before all bytes are written, WriteFile does not return
如果管道被写满(4096+24)字节,那么写会被阻塞,直到有空间为止。
所以,打印日志的线程都被阻塞了,因为我根本没有调用读取pipe的方法。

解决

  1. 在应用管理程序中,启动子应用后,开启线程不断去读取pipe的输出;
  2. 使用其他输入输出,不使用管道。
    这里,我选用第二种方法,来看一下怎么设置不同的输入输出,设置输入输出关键在于Rediect,除了管道之外,Rediect还有几个常用的类型:
       public enum Type {
            PIPE, //连接父进程的管道
            INHERIT, // 继承父进程的输入输出
            READ, //从某个文件读取,只用于输入
            WRITE,//写入到某个文件,只用于输出
            APPEND //追加到某个文件,只用于输出
        };

所以,我们可以使用文件来做输出和错误流(PS:输入不管)。
RunTime类没有提供相关接口,只能手动调用ProcessBuilder中的方法,大概代码如下:

    public void execute(String appName,String cmd) throws IOException {
        log.info("executeCmdAsync: {}", cmd);
        StringTokenizer st = new StringTokenizer(cmd);
        String[] cmdarray = new String[st.countTokens()];
        for (int i = 0; st.hasMoreTokens(); i++) {
            cmdarray[i] = st.nextToken();
        }
        new ProcessBuilder(cmdarray)
                .redirectOutput(new File(appName+"-console.log"))
                .redirectError(new File(appName+"-console-error.log"))
                .start();
    }

修改后,运行没有出现问题了。

你可能感兴趣的:(后台开发)