生产环境异常记录—— java.io.IOException: Broken pipe 的深入分析

项目场景:

客户生产环境的业务系统能够正常访问,但是业务无法正常进行,排查日志发现大量的java.io.IOException: Broken pipe 的异常,处理问题后以此文章作为记录。


问题描述

业务系统的前端页面接收后端传来的图片显示在前端页面进行业务操作,但是现在前端页面的图片无法正常显示,业务系统可正常访问,其他功能均正常。


原因分析:

 我们需要从以下几个角度来分析

1 . 首先排查日志根据异常的抛出点判断问题发生原因

排查问题发生时间的日志发现抛出了一个io异常  java.io.IOException: Broken pipe  断开的管道

在查看最底部的堆栈信息看看是哪里抛出的异常

观察最底部的堆栈信息发现异常是在 OutputBuffer 类的 realWriteBytes 方法中抛出的。

2 . 从源码角度进一步分析

首先我们要清楚realWriteBytes 方法是 Tomcat 中 OutputBuffer 类的一部分,它用于将数据从缓冲区写入到目标输出流(通常是网络套接字)。这个方法是 OutputBuffer 类的核心方法之一,用于处理数据写入的底层细节。

在源码中可以看到realWriteBytes 方法调用 doWrite(buf) 方法,实际的数据写入由 coyoteResponse 的 doWrite 方法完成,该方法将数据从缓冲区写入到网络套接字。如果在写入过程中抛出了 CloseNowException,则关闭输出流并重新抛出异常,如果发生 IOException,则会调用 setErrorException(e) 来记录错误,然后抛出 ClientAbortException。

3 . 网络套接字 (Socket)和通信管道 (Pipe)定义

在了解Broken Pipe异常的含义前首先让我们了解一下网络通信过程中网络套接字(Socket)和通信管道(Pipe)的使用

网络套接字 (Socket)

定义:网络套接字是一个通信端点,它用于在网络上建立和管理连接。一个套接字代表通信的一个端点,它是由IP地址和端口号的组合所标识的。套接字允许在不同的计算机之间发送和接收数据。网络通信的大多数协议(如TCP、UDP)都依赖于套接字来实现数据传输。

工作方式

TCP连接:在TCP协议中,套接字提供了一种面向连接的、可靠的通信方式。客户端和服务器通过套接字建立连接,然后在这个连接上进行数据交换。

UDP连接:在UDP协议中,套接字提供了无连接的、尽力而为的通信方式。数据通过UDP套接字发送到目标地址,而不保证可靠的传输。

通信管道 (Pipe)

定义:在计算机科学中,通信管道是一种用于在同一台计算机上的进程之间进行数据传输的机制。管道提供了一个双向或单向的数据流通道,进程可以通过管道发送和接收数据。

工作方式

匿名管道:通常用于在父子进程之间传输数据,数据从一端写入,从另一端读取。

命名管道 (FIFO):可以用于不同进程之间的数据传输,甚至可以在不同用户之间共享。

Pipe 在网络通信中的抽象意义

现在本文中我们讨论的网络通信的上下文的术语“pipe”并不是指进程间通信的物理管道,而是指客户端和服务器之间的通过网络套接字建立的数据传输路径。在实际的网络通信中,客户端与服务器之间通过套接字建立连接,这个连接就是通信的“管道”。数据通过这个“管道”进行传输。可以将它看作是一条“虚拟通道”,通过这个通道,客户端和服务器之间可以进行双向数据传输。

由于套接字(Socket)并不是严格意义上发生在单一层的概念,而是跨越了传输层和应用层。套接字编程发生在应用层。开发人员使用编程语言的套接字API来创建套接字、连接到远程主机、发送和接收数据。传输层协议(如TCP或UDP)则在传输层实现实际的网络通信,而套接字在传输层的角色是管理端到端的通信连接,并确保数据可靠传输或快速发送。

套接字作为一种抽象概念,连接了应用层与传输层。它为应用层提供了一个接口,使应用程序能够利用传输层协议实现网络通信。

在本文中为了直观的展示套接字在网络通信中的使用将应用层和传输层抽象为同一层网络模型,这一层网络模型通过网络套接字 (Socket)建立的通信管道 (Pipe)进行数据传输

4 . Broken Pipe 异常的含义

定义:Broken Pipe 异常发生在服务器端尝试向已经关闭的套接字写入数据时。通常情况下,这是由于客户端在服务器完成数据发送之前已经关闭了连接,导致服务器无法继续发送数据。

在网络通信中,套接字是通信的基础,它为客户端和服务器之间的数据交换提供了通道。当客户端或服务器中断连接时,就会出现 Broken Pipe 异常。这意味着数据流的“管道”已经被破坏,数据无法传输。

这种情况可能发生在以下几种场景中:

1. 客户端意外断开连接:

• 客户端可能由于网络故障、应用程序崩溃或用户主动关闭应用程序而断开连接。

2. 长时间未响应:

• 如果客户端长时间没有响应,服务器端可能继续尝试向客户端发送数据,这时会导致 Broken pipe 异常。

3. 缓冲区未及时 flush:

• 如果缓冲区中的数据未及时写入到套接字,当对端关闭连接时,再次尝试写入可能导致异常。

5 . 判断问题来源 

服务端问题的可能性:

处理时间过长: 服务器处理请求时可能因为资源不足、锁定资源、复杂的计算任务等原因导致响应延迟。如果超过了浏览器或服务器的超时时间,就会中断连接,服务器就可能会报告 Broken pipe 。

资源限制: 服务器可能因为负载过高,无法在合理时间内响应请求。这在高并发场景下尤其常见。

配置问题: 服务器配置错误或不当(例如超时时间过短)也可能导致连接被意外关闭。

客户端问题的可能性:

浏览器等待时间短: 如果请求需要很长时间来完成,而浏览器的等待时间(例如90秒)不足以等待响应,就可能会出现超时错误。但通常浏览器的默认超时时间是相对宽松的,除非请求需要极长的处理时间。

网络连接不稳定: 客户端(浏览器)的网络连接不稳定,可能导致连接中断。

中间代理或防火墙: 有时中间的代理服务器、防火墙或负载均衡器可能会因各种原因中断连接。


解决方案:

针对上面提到的服务端问题和客户端问题的可能性分别提出方案进行解决

1.模拟事故代码

首先我们模拟事故代码,使测试环境能够抛出同样的异常

package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

@RestController
public class HaHa {
    @GetMapping("/h")
    public void sendFile(HttpServletRequest req, HttpServletResponse resp) {
        FileInputStream fis = null;
        try {
            // 设置要发送的文件路径(使用一个大文件)
            File file = new File("/opt/SoundSource.zip");  // 替换为你的文件路径
            fis = new FileInputStream(file);
            // 设置响应的内容类型和文件名
            resp.setContentType("application/octet-stream");
            resp.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
            // 获取响应输出流
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                resp.getOutputStream().write(buffer, 0, bytesRead);
                // 模拟发送过程中的延迟,便于在中途断开连接
                Thread.sleep(100);
                
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上面的代码中我们提供了/h 这个路径进行访问,访问这个路径服务器端就会向客户端发送一个大文件,而我们只要在接收时断开连接即可触发同样的异常报错。

2 . 服务端问题的解决方案:

(1)优化处理时间

减少单次请求的处理时间:优化代码,减少单次请求的处理时间,使用异步处理或多线程处理,并及时调用flush()方法,将缓冲区中的数据强制写入到目标流中。在Spring Boot中,可以使用@Async注解来实现异步处理。

package com.example.demo.controller;

import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

@RestController
public class HaHa {
    @Async  //  使用异步处理请求
    @GetMapping("/h")
    public void sendFile(HttpServletRequest req, HttpServletResponse resp) {
        FileInputStream fis = null;
        try {
            // 设置要发送的文件路径(使用一个大文件)
            File file = new File("/opt/SoundSource.zip");  // 替换为你的文件路径
            fis = new FileInputStream(file);
            // 设置响应的内容类型和文件名
            resp.setContentType("application/octet-stream");
            resp.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
            // 获取响应输出流
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                resp.getOutputStream().write(buffer, 0, bytesRead);
                resp.getOutputStream().flush();  // 及时调用flush() 方法将缓冲区中的数据强制写入到目标流中
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

(2)增加超时时间

增加服务器端超时时间:可以通过配置Tomcat或其他应用服务器,增加连接的超时时间。这样即使处理时间较长,连接也不会被意外关闭。

• 对于Tomcat,可以通过修改server.xml中的connectionTimeout属性来增加超时时间。

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"  <!-- 设置超时时间为20秒 -->
           redirectPort="8443" />

• 在Spring Boot中,您可以通过修改application.properties文件来增加超时时间:

server.tomcat.connection-timeout=20000ms  # 设置Tomcat连接超时时间为20秒

通过这些设置,您可以有效增加服务器的超时时间,避免由于超时导致的连接中断。

(3) 资源配置调整

增加服务器资源:在高并发情况下,适当增加服务器的资源(如CPU、内存)或者扩展集群,减少服务器资源不足导致的响应延迟。

负载均衡:使用负载均衡器分发请求,减少单个服务器的压力。

(4) 重试机制

实现重试机制:在捕获到Broken pipe异常时,可以实现重试机制,以避免用户请求失败。例如,可以在捕获到异常后,重新发送响应。

package com.example.demo.controller;

import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

@RestController
public class HaHa {
    @Async  //  异步处理请求
    @GetMapping("/h")
    public void sendFile(HttpServletRequest req, HttpServletResponse resp) {
        FileInputStream fis = null;
        int maxRetries = 3;  // 设置最大重试次数
        int retryCount = 0;
        boolean success = false;
        try {
            // 设置要发送的文件路径(使用一个大文件)
            File file = new File("/opt/SoundSource.zip");  // 替换为你的文件路径
            fis = new FileInputStream(file);
            // 设置响应的内容类型和文件名
            resp.setContentType("application/octet-stream");
            resp.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
            // 获取响应输出流
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                resp.getOutputStream().write(buffer, 0, bytesRead);
                resp.getOutputStream().flush();  // 及时调用flush() 方法将缓冲区中的数据强制写入到目标流中
            }
        } catch (IOException | InterruptedException e) {
            retryCount++;
            System.err.println("发送过程中出现异常,重试次数:" + retryCount);
            e.printStackTrace();
            if (retryCount == maxRetries) {
                System.err.println("最大重试次数达到,停止重试");
                resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

(5)监控和报警

建立监控机制:对服务器的处理时间、连接数量、资源使用情况进行监控,及时发现问题并报警。

3 . 客户端问题的解决方案:

1. 增加浏览器等待时间

• 修改客户端代码,可以增加浏览器的等待时间(如通过JavaScript的XMLHttpRequest或fetch设置更长的超时时间)。

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 20000); // 20 秒超时
fetch('/yourApi', { signal: controller.signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Fetch error:', error));

PS:为什么这里不直接查看和调整浏览器的超时时间

Chrome 浏览器:默认连接超时时间是 300 秒(5 分钟)。这个时间只能通过命令行参数或网络代理配置来间接调整,浏览器本身不提供内置选项来更改这一时间。

Firefox 浏览器:默认超时时间是 90 秒。可以通过访问 about:config 来查看和调整一些网络相关的设置。 在地址栏中输入 about:config 并回车来查看配置。搜索 network.http.connection-timeout,可以修改该值来调整连接超时时间。其他相关设置如 network.http.keep-alive.timeout 可以调整 Keep-Alive 连接的超时时间。

Edge 和 Safari:这些浏览器的超时设置与 Chrome 类似,也不提供直接的用户可调选项。

2. 优化网络连接

• 如果是网络连接不稳定引起的问题,建议使用更稳定的网络环境,或者尽量使用有线连接,减少无线网络带来的不稳定性。

3. 避免大文件传输

• 尽量避免在单次请求中传输超大文件,如果必须传输,可以考虑将文件压缩后再传输,或者采用分片传输技术。

4. 中间代理配置

• 如果网络中存在代理、防火墙或者负载均衡器,可以在相关服务中配置网络超时时间,例如在Nginx 中的 client_body_timeout 和 client_header_timeout 设置了客户端请求正文和头部的超时时间。


问题总结

总结:Broken Pipe 异常是网络通信中常见的一种异常,通常在服务器尝试向已经关闭的客户端连接发送数据时发生。在设计和实现系统时,应该考虑到这种异常情况,进行相应的优化和配置。并确保客户端和服务器端的网络稳定,避免因网络波动导致连接中断。

本文内容仅作为参考和学习交流使用,如有错误请麻烦指正。

  • 13
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值