Broken Pipe发生的原因
当某个进程试图往一个已收到RST的SOCKET连接写数据,就会出现Broken Pipe。
(由于TCP协议层已经处于RST状态了,因此不会将数据发出,而是发一个SIGPIPE信号给应用层,SIGPIPE信号的缺省处理动作是终止程序。)
那么确定什么时候TCP会发送RST报文段,就可以确定Broken Pipe发生的具体原因。
之前已经分析了TCP RST报文产生的几种情景了。
原因分析
broken pipe出现的前提条件是进程试图往一个已经在RST状态的TCP连接写入数据。
那么这个写入数据,到底应该怎么理解呢?到底是进程试图往本地SendQ发送缓存区写入数据还是TCP协议试图将SendQ的数据发送到对端的RecvQ呢?按照字面意思应该是前者。
之前我们已经分析了几种会出现RST报文的情况。
结合我们出现该异常的接口分析。我们发现我们出错的接口,返回的数据,最小的8K多,最大的超过128K。查阅了几天的异常日志,都没有发现一个报出broken pipe异常错误的接口的返回数据小于8K。
根据RST报文产生的情况,我们可以做出如下推断,当Client端与我们的服务器建立了TCP链接之后。当TCP协议将服务端SendQ队列里的内容发送到对端(Client)的ReceQ队列中后,Client关闭了进程,此时ReceQ 读取缓存区还有数据未被读取(不管ReceQ的数据Client端有没有读取过,也不管TCP将多少服务端的数据发到了ReceQ,总之,就是在关闭的时候,还有数据存在于读取缓存区中),这时候关闭socket,会导致端Client端产生一个RST重置报文。
这时候服务端的数据还没有写完,会继续写入,当再次写入的时候,TCP协议已经是RST状态的了,这个时候,就会发生broken pipe。
上面的推论能够解释broken pipe发生的一整个流程。
那我们来查看下linux服务器的默认SendQ大小与我们接口返回的数据大小的对比,就能够是否确实是这些接口的数据写入都需要多次写入缓存区。
那么如何查看linux默认的SendQ缓冲区大小呢。linux给我们提供了相应的命令
ubuntu@VM-104-50-ubuntu:/data/iyourcar$ cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
最小 默认 最大
我们发现,默认写缓存区大小是16k,可是我们的接口返回的数据最小的是8k多呀,这个接口返回的数据是可以一次写入缓存区的呀。咋回事呀,怎么这样也会broken呢。如果服务端一次性写入16k数据到写缓存区,那么是不可能出现broken pipe的呀。那只能证明我们的程序并不是一次性写入16k的数据给缓存区,这个大小肯定是要比8k多要小的。那我们就来求证一下,用一个会报出异常的接口在出现异常的地方进行debug,主要debug写入数据的流程。
使用的内置容器是Tomcat
OutputBuffer#appendByteArray
public static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
public OutputBuffer() {
this(DEFAULT_BUFFER_SIZE);
}
private void appendByteArray(byte src[], int off, int len) throws IOException {
if (len == 0) {
return;
}
int limit = bb.capacity();
//我们发现,每一次写入的字节数bb.capacity()大小,而默认的capacity大小就是8*1024,也就是8k
while (len >= limit) {
realWriteBytes(ByteBuffer.wrap(src, off, limit));
len = len - limit;
off = off + limit;
}
if (len > 0) {
transfer(src, off, len, bb);
}
}
我们发现,实际上,tomcat帮我们向socket写入数据的时候,是每8k写入一次SendQ(但是真正TCP发送数据,可能是分很多块去发送到对端的ReceQ的)
后来我们将内置web容器换成了undertow,发现依旧会发生这种情况,应该默认也是一次写入8k吧,具体还没有debug。
所以当使用的容器是tomcat的时候,只要接口返回数据的大小大于8k,就可能会出现broken pipe。