SpringBoot接口重复调用问题排查

671 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

最近线上有个下载文件的接口,偶尔会抛出异常:org.apache.catalina.connector.ClientAbortException: java.io.IOException: Broken pipe

在经过排查后,发现抛出异常的接口是固定的几个传参,并且这几个参数会导致接口执行时间比较长,超过1min。而每次到了一分钟接口就会抛出上面的异常。

一般来说Broken pipe 只在写操作中出现。出现的原因就是连接已经不存在了,但是还在继续写数据。这里的下载操作是应用侧在往客户端响应字节数据,所以推测是客户端断开了连接   。

在确定后端逻辑没有问题的情况下,并且基于上面的异常出现的分析推测应该是因为前端页面请求在超过1min后断开导致后端下载接口抛出的异常。找前到端一起排除看看是否前端有参数控制请求时间。最终发现问题和nginx的代理功能的相关配置有关系。

下面是先来看看基于nginx相关配置如何解决对应的问题。

1-基础配置

我们可以先简单写一个测试接口,比如暂停个2分钟。

@GetMapping
public void test(){
	System.out.println("哈哈哈");
        //处理逻辑
	try {
		Thread.sleep(120000);
	} catch (InterruptedException interruptedException) {
		interruptedException.printStackTrace();
	}
	System.out.println("嘻嘻嘻");
}

这里是最简单的一个配置,代理到了后面的9087接口。

server {
    listen       80;
    server_name  localhost;
    
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        proxy_pass   http://192.168.0.104:9087;
    }

}

如果使用上面的配置请求我们的接口会发现nginx在1min中后悔返回504-timeout。

2-超时配置

server {
    listen       80;
    server_name  localhost;


    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        proxy_pass   http://192.168.0.104:9087;
        proxy_read_timeout 180s;
    }

}

新增配置proxy_read_timeout,默认配置是60s,官方说明

proxy_read_timeout 60s;
Defines a timeout for reading a response from the proxied server. The timeout is set only between two successive read operations, not for the transmission of the whole response. If the proxied server does not transmit anything within this time, the connection is closed.
大概意思是说从服务端等待响应的时间。

这里我们设置为180s,应该就可以解决504-timeout的问题。再次调用接口此时会发现接口可以正常响应200。

3-重试配置

如果proxy_read_timeout是60s,并且某个接口处理超过60s,有时候你会发现一次请求之后这个接口会被重复调用多次。那基本可以判断是nginx中配置了upstream:

upstream nginxretry {
    server 192.168.0.104:9087 weight=10;
    server 192.168.0.105:9087 weight=10;
    server 192.168.0.106:9087 weight=10;
}

server {
    listen       80;
    server_name  localhost;


    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        proxy_pass   http://nginxretry;
        proxy_send_timeout 180s;
        proxy_read_timeout 180s;        proxy_next_upstream http_502 http_504 error timeout invalid_header;
        proxy_next_upstream_tries 2;
    }

}

配置说明:

proxy_next_upstream:指定重试的异常情况,比如timeout,http_504。比如我们上面所说的接口处理处理时间超过proxy_read_timeout配置的时间 ,则会504。

此时就会根据proxy_next_upstream_tries配置的数量来进行重试。默认是0,会重试所有的upstream里面配置的server。这里配置2的情况下,如果192.168.0.104接口响应超时,则会继续请求192.168.0.105。

这种重试的情况需要特别注意。防止影响业务逻辑。

更多的nginx配置,可以参考最后的nginx文档说明。我也是第一次遇到这种问题,如果有不对的地方,欢迎大家指导下。

4-代码优化

虽然可以基于nginx的配置来增加超时的时间,但是对于这种耗时较长的接口我们还是需要进行处理。查看代码中下载文件的逻辑,是根据不同的枚举值导入到一个Excel的不同sheet中。实现逻辑是针对每个枚举值遍历,然后从DB中根据枚举值为条件分页查询最终写入到对应的sheet中。

于是做了两个调整:

一是调大了分页读取的fetchSize。减少次数。(需要测试一个合适的大小)。

二是针对不同的sheet,可以进行并发处理,(如果并发写一个sheet可能会有问题)。先创建所有的sheet,然后多线程对不同的sheet来进行数据的写入。写excel的方式有很多种,这里就不展开了,多线程的代码大致如下:

//整体逻辑,ExcelTask中完成具体每个枚举值的数据写入,从db中读取数据
List<ExcelTask> taskList = new ArrayList<>();
for (Value value : values) {
	excelTaskList.add(
		new ExcelTask(wb, value));
}

List<Future<Boolean>> futures = null;
try {
	futures =threadPool.invokeAll(taskList);} catch (InterruptedException e) {
	Thread.currentThread().interrupt();
}
if (futures != null) {
	for (Future<Boolean> future : futures) {
		try {
			future.get();
		} catch (ExecutionException e) {
			e.printStackTrace();
			throw new ServiceException(e);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
	}
}

总结

对于这个问题的解决也是花了一些时间,因为nginx相关的部署也有相应的人去维护。此时会发现还是需要多了解相关的知识才能快速定位问题。最后自己也是在本地安装了nginx并且复现了问题,当问题可以复现那么就已经有解决的方法了。

nginx官方配置参考
nginx.org/en/docs/htt…