今天我们聊一个异常:java.io.IOException: Broken pipe,为什么会报这个异常,这个异常要怎么解决?
问题描述
最近遇到的一个小问题,希望对你有帮助。前几日在排查一个问题的时候,看到有几台服务器上日志偶发会打印这个错误 Caused by: java.io.IOException: Broken pipe
。
下面贴了详细堆栈信息,信息内容较多做了一点删减,可以滑动查看:
org.apache.catalina.connector.ClientAbortException: java.io.IOException: Broken pipe at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:356) at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:825) at org.apache.catalina.connector.OutputBuffer.append(OutputBuffer.java:730) at org.apache.catalina.connector.OutputBuffer.writeBytes(OutputBuffer.java:391) at org.apache.catalina.connector.OutputBuffer.write(OutputBuffer.java:369) at at java.lang.Thread.run(Thread.java:748) Caused by: java.io.IOException: Broken pipe at sun.nio.ch.FileDispatcherImpl.write0(Native Method) at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:47) at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93) at sun.nio.ch.IOUtil.write(IOUtil.java:65) at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:471) at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:134) at org.apache.tomcat.util.net.NioBlockingSelector.write(NioBlockingSelector.java:101) at org.apache.tomcat.util.net.NioSelectorPool.write(NioSelectorPool.java:157) at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1276) at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:670)
看堆栈信息是在写数据的时候,抛出了 IOException 异常,然后被 tomcat 中捕获抛出 ClientAbortException: java.io.IOException: Broken pipe。
跟踪了整个调用,发现业务处理是正常的,服务端也打印了响应需求返回的数据,但就是报了这个错误。还是怕这个异常对业务产生影响,于是检查了一下抛出这个异常产生的前后日志,发现只有一个接口会有这个问题。但这个也是偶现的,并不是每次请求就会抛这个异常。这更加让人迷惑。
于是去查了查相关资料,简单说一下Broken pipe。服务端向客户端的socket连接管道写返回数据时,链接(pipe)却断了!
出现这个异常的可能原因:
1、客户端请求服务器数据,服务器突然挂了; 2、Tomcat的connector在执行写操作的时候发生的,客户端读取超时关闭了连接,这时候服务器端再向客户端已经断开的连接写数据时就发生了broken pipe异常!
服务端肯定是没有挂的,可以排除第一点。那就是第二点了,大概率应该客户端自己的问题嘛,你断开了连接导致的我服务端没法写回数据了,然后就异常了,这是不是可以甩锅了~
模拟验证
秉着“科学严谨”的甩锅思路,对比了这个接口正常和异常情况下,入参和出参,以及响应时间。发现正常情况下响应时间10s,异常情况下响应时间高达40s,并且响应的数据量巨大。因为这个接口是第三方调用的,简单推测,应该是第三方调用此接口的时候设置了超时时间,超时后关闭了连接,导致服务端没法写出数据,异常了。
既然是这样那我自己验证一把,看是不是这个问题,找出出现异常的调用入参,使用Postman模拟第三方调用,看一看会发现什么。
软件环境: Spring Boot 2.0.3 Tomcat8 JDK8 Nginx代理
场景1:Postman不设置超时时间,发送请求。 结果: Postman响应正常,服务端也没有打印出异常信息
场景1:Postman设置超时时间,发送请求。
设置请求应等待多长时间]超时前的响应。默认0,永不超生,这里我设置了5s
结果:postman没有收到响应,5s后直接异常,Response time out。
会服务端处理完后,服务端打印出broken pipe
异常信息。
场景3:Postman设置超时时间,发送请求后,立马关闭Postman进程。 结果:查看日志,发现也报了 Broken pipe 问题。
上面这个场景模拟下来,服务端业务都完成处理成功,就是在响应的时候出现了异常。那直接看一下异常那块的代码吧。org.apache.catalina.connector.OutputBuffer#realWriteBytes
/** * Sends the buffer data to the client output, checking the * state of Response and calling the right interceptors. * 将缓冲区数据发送到客户机输出,检查Response的状态并调用正确的拦截器。 * @param buf the ByteBuffer to be written to the response * buf -要写入响应的ByteBuffer * @throws IOException An underlying IOException occurred * IOException -底层IOException发生 */ public void realWriteBytes(ByteBuffer buf) throws IOException { if (closed) { return; } if (coyoteResponse == null) { return; } // If we really have something to write if (buf.remaining() > 0) { // real write to the adapter try { coyoteResponse.doWrite(buf); } catch (IOException e) { // An IOException on a write is almost always due to // the remote client aborting the request. Wrap this // so that it can be handled better by the error dispatcher. throw new ClientAbortException(e); } } }
当执行到 coyoteResponse.doWrite(buf);
这里的时候,调用JDK方法去执行sun.nio.ch.FileDispatcherImpl#write0
, write0
是一个native的方法了,看看发生异常时候的注释内容:
// An IOException on a write is almost always due to // the remote client aborting the request. Wrap this // so that it can be handled better by the error dispatcher.
大致的意思是:写时的IOException几乎总是由于远程客户端中止请求而导致的。将其包装起来,方便程序可以更好地处理它。
这里就不继续深入卷了,感兴趣的伙伴可以在看看网络Socket,或者经典书籍《UNIX网络编程卷1》。
解决方案
这个异常,对于服务端来说,并没有多少影响。到这里大致的处理方案就知道了
- 1、延长客户端超时时间
- 2、合理优化服务端接口代码,提升,降低响应时间和返回数据量
最后闲聊
在排查这个问题的时候,找到国外小哥的一篇文章,写的很不错,值得学习。一开篇先介绍了他遇到了Broken Pipe 异常,给出异常出现的软件环境和版本,然后讲了Broken Pipe 是什么,这个异常是怎么发生的,这个异常是不是很严重,以及后面如何修复这个异常,总结以及参考文章。整个文章一气呵成,其中的英文也不难理解,看起来真丝滑。
感兴趣的伙伴可以在这里查看原文: How I fixed java.io.IOException: Broken Pipe in Java (Wildfly 10.1):bishwa.hashnode.dev/fix-broken-…
下面我大致翻译和加一下自己的解读。 文章的标题:我如何修复Java中 java.io.IOException:Broken Pipe(Wildfly 10.1)
我经常接触的Tomcat,WildFly没用到,大致说明下:WildFly,原名 JBoss AS(JBoss Application Server) 或者 JBoss,是一套应用程序服务器,属于开源的企业级 Java 中间件软件,用于实现基于 SOA 架构的 Web 应用和服务。
开头预览就介绍了他第一次遇到 Broken Pipe 问题时候对应的软件环境和相关版本:
- JavaEE 7 with JAX-RS
- Java 8
- Wildfly 10.1 as application server
- Nginx was used as reverse proxy and load balancer.
后面贴了异常的详细信息。 有趣的是在分析完 Broken Pipe 是如何发生的,里面写到
Not every Broken Pipe exception is developer's fault 并非每个 Broken Pipe 异常都是开发人员的错 他分析了导致此异常的可能因素:
- 最终用户数
由于 Broken Pipe 的主要原因之一是用户的行为(在服务器完成发送响应之前意外关闭了活动的浏览器会话),最终用户数量的增加增加了 Broken Pipe 的机会。
- 重响应负载(返回数据量大)
来自服务器的大量响应需要大量时间才能传输到客户端,而这个巨大的时间跨度可能就是 Broken Pipe 的情况。
- 服务器超时(有代理的情况)
如果 Web 服务器在等于服务器设置的超时值的特定时间内无法从服务获得响应,它会关闭与客户端的连接,返回 503: Gateway Timeout,从而导致 Broken Pipe。