原文java
在容器打印日志到控制台阻塞的排障的时候看到一个观点:linux
把日志打印到控制台要比打印到文件慢,并且是很是慢。
log4j2和logback的两个issue官方也提到了这一点(见LOG4J2-2239、LOGBACK-1422)。git
那么为什么输出到控制台慢?有何办法加速呢?问题要从三个角度来分别回答:github
stdout
角度stdout
角度写到控制台其实就是写到stdout
,更严格的说应该是fd/1
。Linux操做系统将fd/0
、fd/1
和fd/2
分别对应stdin
、stdout
和stdout
。docker
那么问题就变成为什么写到stdout
慢,有何优化办法?apache
形成stdout
慢的缘由有两个:segmentfault
stdout
的输出效率stdout
的缓冲机制在SO的这个问题中:Why is printing to stdout so slow? Can it be sped up?,这回答提到打印到stdout慢是由于终端的关系,换一个快速的终端就能提高。这解释了第一个缘由。bash
stdout
自己的缓冲机制是怎样的?Stdout Buffering介绍了glibc对于stdout缓冲的作法:多线程
stdout
指向的是终端的时候,那么它的缓冲行为是line-buffered
,意思是若是缓冲满了或者遇到了newline字符,那么就flush。stdout
没有指向终端的时候,那么它的缓冲行为是fully-buffered
,意思是只有当缓冲满了的时候,才会flush。其中缓冲区大小是4k。下面是一个总结的表格“
GNU libc (glibc) uses the following rules for buffering”:post
Stream | Type | Behavior |
---|---|---|
stdin | input | line-buffered |
stdout (TTY) | output | line-buffered |
stdout (not a TTY) | output | fully-buffered |
stderr | output | unbuffered |
那也就是说当stdout
指向一个终端的时候,它采用的是line-buffered
策略,而终端的处理速度直接影响到了性能。
同时也给了咱们另外一个思路,不将stdout
指向终端,那么就可以用到fully-buffered
,比起line-buffered
可以带来更大提速效果(想一想极端状况下每行只有一个字符)。
我写了一段小代码来作测试(gist)。先试一下stdout
指向终端的状况:
$ javac ConsolePrint.java $ java ConsolePrint 100000 ... lines: 100,000 System.out.println: 1,270 ms file: 72 ms /dev/stdout: 1,153 ms
代码测试了三种用法:
System.out.println
指的是使用System.out.println
所花费的时间file
指的是用4k BufferedOutputStream 写到一个文件所花费的时间/dev/stdout
则是一样适用4k BufferedOutputStream 直接写到/dev/stdout
所花费的时间发现写到文件花费速度最快,用System.out.println
和写到/dev/stdout
所花时间在一个数量级上。
若是咱们将输出重定向到文件:
$ java ConsolePrint 100000 > a $ tail -n 5 a ... System.out.println: 920 ms file: 76 ms /dev/stdout: 31 ms
则会发现/dev/stdout
速度提高到file
一个档次,而System.out.println
并无提高多少。以前不是说stdout
不指向终端可以带来性能提高吗,为什么System.out.println
没有变化呢?这就要Java对于System.out
的实现提及了。
下面是System
的源码:
public final static PrintStream out = null; ... private static void initializeSystemClass() { FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out); setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding"))); } ... private static native void setOut0(PrintStream out); ... private static PrintStream newPrintStream(FileOutputStream fos, String enc) { ... return new PrintStream(new BufferedOutputStream(fos, 128), true); }
能够看到System.out
是PrintStream
类型,下面是PrintStream
的源码:
private void write(String s) { try { synchronized (this) { ensureOpen(); textOut.write(s); textOut.flushBuffer(); charOut.flushBuffer(); if (autoFlush && (s.indexOf('\n') >= 0)) out.flush(); } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } }
能够看到:
System.out
使用的缓冲大小仅为128字节。大部分状况下够用。System.out
开启了autoFlush,即每次write都会当即flush。这保证了输出的及时性。PrintStream
的全部方法加了同步块。这避免了多线程打印内容重叠的问题。PrintStream
若是遇到了newline符,也会当即flush(至关于line-buffered
)。一样保证了输出的及时性。这解释了为什么System.out
慢的缘由,同时也告诉了咱们就算把System.out
包到BufferedOutputStream里也不会有性能提高。
那么把测试代码放到Docker容器内运行会怎样呢?把gist里的Dockerfile和ConsolePrint.java放到同一个目录里而后这样运行:
$ docker build -t console-print . $ docker run -d --name console-print console-print 100000 $ docker logs --tail 5 console-print ... lines: 100,000 System.out.println: 2,563 ms file: 27 ms /dev/stdout: 2,685 ms
能够发现System.out.println
和/dev/stdout
的速度又变回同样慢了。所以能够怀疑stdout
使用的是line-buffered
模式。
为什么容器内的stdout
不使用fully-buffered
模式呢?下面是个人两个猜想:
docker run -t
分配tty
启动,仍是docker run -d
不非配tty启动,docker都会给容器内的stdout
分配一个tty
。tty
必须是line-buffered
。虽然System.out.println
很慢,可是其吞吐量也可以达到~40,000 lines/sec,对于大多数程序来讲这不会形成瓶颈。