摘要:四层负载均衡不会引起超时。动态修改包的目标地址,并转发数据包使其到达不同的机器上来实现负载均衡的目的,因此节点不会引起超时。七层负载均衡等待上游响应超时。例如使用多线程并发减少远程查询的总体时间如需数据有序,可以使用方案。
B端业务经常要提供下载报表的功能,一般的方法是先查询出所有数据,然后在内存中组装成报表(如XLS/XLSX格式)后统一输出。但是如果生成报表需要查询的数据量很大,远程服务的调用时间之和远远超过了链路上某节点(比如代理服务器Nginx、浏览器Chrome)的等待时间,因此该次Http连接就会被强制关闭,导致下载失败。
下面的示例代码调用了Thread.sleep,将处理线程挂起3分钟,模拟耗时的数据查询操作。
@GetMapping("/trade/income/excel") public HttpEntity常见超时原因和优化思路downloadTradeIncome() { ServletOutputStream stream = response.getOutputStream(); response.setContentType("application/octet-stream;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment;fileName=test.csv"); stream.write("start".getBytes(Charsets.UTF_8)); response.flushBuffer(); Thread.sleep((long) (3 * 64 * 1000));//chrome 2min超时会主动断开连接 stream.write("finish".getBytes(Charsets.UTF_8)); }
大型的Web应用一般都不是单纯的Client/Server模型。一次Http请求会在网络链路上经过多于2个的节点。
Chrome(用户端的浏览器) <=> 四层负载均衡(工作在传输层,如LVS和MGW) <=> 七层负载均衡(工作在应用层,如反向代理用的Nginx) <=> Tomcat(后端应用服务器)。
链路上的每个节点都有可能会产生超时,因此具体的超时原因也可以分为:
Chrome发起请求后,等待响应超时。该值为120秒,且用户不可更改,超时后页面上会提示EmptyResponse。
四层负载均衡不会引起超时。LVS动态修改TCP包的目标IP地址,并转发数据包使其到达不同的机器上来实现负载均衡的目的,因此LVS节点不会引起超时。个人理解,不一定准确。
七层负载均衡等待上游响应超时。Nginx代理了客户端浏览器对后端服务器的Http请求,作为反向代理服务器需要“同时”维护与浏览器和后端服务器的Http连接,因此也会产生相应的超时,例如Nginx等待上游响应超时就会产生504 Gateway Timeout。
Tomcat/Servlet处理超时。这层对应本地环境产生的超时,如Socket超时、InputStream/OutputStream超时。
对应的超时优化有3种思路。
1. 缩短后端查询数据的时间。例如使用多线程并发减少远程查询的总体时间(如需数据有序,可以使用Fork/Join方案)。
该方案的优点是减少了对外的整体查询的时间。缺点是多线程增加了开发和维护的难度;高并发压力转移到内部的查询服务上,对其QPS响应提出了更高的要求。
2. 将数据查询和下载的流程异步化。浏览器请求下载后,服务端立即返回报表的唯一标识Key同时开始远程查询数据,客户端可以凭借该Key查询报表的生成进度,报表完成后就可以下载;或者使用另一种方案,服务器在报表生成完成后通过一些渠道(如Long-Polling、WebSocket、即时通信软件、邮件等)通知客户端下载。
该方案的优点是并发能力强,不会阻塞服务器的Web连接池。缺点是需要开发Key的CRUD操作和相应的UI;需要公有文件云的支持用于存储生成的报表文件。
3. 服务端边生成报表,浏览器边下载报表。就像下载大文件一样,浏览器不断开和服务器的Http连接,同时服务器不断向浏览器追加Http体数据直到报表生成结束。
该方案的优点是开发难度低、速度快。缺点是数据查询是单线程的,速度较慢;而且文件下载会一直占用服务器的Web连接池,如果并发下载量较大可能会阻塞其他的Http请求。
因为在实际的业务开发中,前2种思路做的比较多,所以后文不再赘述。
方案3的具体实现该方案的关键在于业务方法返回后SpringMvc/Servlet不能主动关闭Http连接,而是要像平常下载文件一样保持Http的长连接(注意Http长连接要和Http 1.1协议默认采用的Tcp长连接相区分),唯一不同的是这次浏览器无法提前知道文件的大小。因此对于技术方案我考虑有几种选择:
服务器边查询数据并生成,浏览器边下载,像平常下载文件一样。
分多次查询/推送数据,浏览器最后把数据组装为报表。
轮询(Polling)。客户端轮询服务器,每次查询报表数据的一部分,查询结束后再组装成报表文件。
长轮询(Long-Polling)。客户端轮询服务器,服务器在收到请求后Hold住Http连接,等待另一部分的数据查询完成才释放连接并返回Response。
WebSocket。支持Html5特性的浏览器和服务器之间建立Socket管道,可以双向传递任意类型的消息。
第一种方案的优点是不需要前端参与开发,缺点是无法支持二进制格式的报表文件(如XLS/XLSX),只能用文本格式(如CSV/TSV),这会带来格式的损失,比如CSV格式里位数超过10位的数字会被Excel自动显示成科学记数法。第二种方案正好相反,需要前端开发人力,但是可以支持组装二进制格式的报表。
PS:除了经典的Apache POI库,据说Java世界还有流式生成XLS/XLSX的库,这点有待确认。
因为搞不到前端人力,实际上还是用方案1实现。下面的代码模拟了用SpringMvc实现异步下载报表的功能。handle7()结束后会立即返回Http头,告诉浏览器将返回一个长度未知且格式未知1的二进制文件,并推荐执行文件下载操作。
private ExecutorService pool = Executors.newFixedThreadPool(5); @GetMapping("events7") public ResponseEntityhandle7() throws IOException { ResponseBodyEmitter emitter = new ResponseBodyEmitter(); emitter.send("start,"); pool.execute(() -> { try { Thread.sleep((long) (3 * 64 * 1000)); emitter.send("finish "); emitter.complete(); } catch (IOException | InterruptedException e) {} }); HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/octet-stream;charset=UTF-8"); headers.set("Transfer-Encoding", "chunked"); headers.setContentDispositionFormData("attachment", "test.csv", Charsets.UTF_8); return new ResponseEntity<>(emitter, headers, HttpStatus.OK); }
SpringMvc的ResponseBodyEmitter实际上利用了Servlet3+的异步特性,耗时较长的请求无需一直占用Web请求处理的线程池,大大提高了服务器的并发能力。启动Tomcat后访问http://localhost:8080/events7即可查看效果。
但实际上上面的代码无法在Webkit核心下的Chrome/Safari浏览器上得到预期的结果。测试中Chrome无法自动开始下载,而是会阻塞在Loading阶段,直到超过了2分钟的最大等待时间后告诉用户发生了EmptyResponse。
在Inspector界面上不显示Response的Http头和部分Http体数据(即"start"字符串)。但是通过Charles抓包发现,Response的Http头和"start"字符串已经发出,这是一个奇怪的地方。
几次尝试后发现,问题出现在MIME(即Content-Type)上,Chrome对application/octet-stream类型似乎采取了接受到完整的Http包才开始下载文件的逻辑,换成application/csv后Chrome顺利的开始自动下载,下方状态栏出现Loading圆圈,文案提示即将开始下载,然后文件大小开始逐渐增长,最终完成下载过程。
两个未解之谜 1. MIME对Chrome下载行为的影响我尝试了几种Chrome会立刻触发下载的MIME。
text/csv
text/css
text/markdown
text/event-stream
text/html
application/csv
application/pdf
application/json
application/xhtml+xml
application/x-www-form-urlencoded
application/atom+xml
multipart/form-data
还有一些Chrome不会自动触发下载并最终导致超时的MIME。
application/octet-stream
application/xml
text/xml
text/plain
要解释这个问题可能需要查看Webkit源码,但是我没有找到相关逻辑,也有可能我找错了方向,希望熟悉这块的朋友不吝赐教。
2. Nginx引起的502问题解决了上面的问题后,代码在Beta环境出现了新的问题。Nginx代理提示502 Bad Gateway The proxy server received an invalid response from an upstream server。查看Nginx日志,具体的错误信息如下。应该是Transfer-Encoding设置为chunked,导致Nginx认为该Http头非法。这个问题也是令人摸不到头脑,希望熟悉Http1.1规范和分块传输编码的朋友不吝赐教。
Reference2017/10/19 15:14:17 [error] 30016#0: *409143 upstream sent invalid chunked response while reading upstream, client: 10.72.227.11, server: www.dianping.com, request: "GET /s/ajax/shop/finance/trade/income/excel?shopIdList[]=&startDate=2017-09-20%2000:00&endDate=2017-10-19%2023:59 HTTP/1.1", upstream: "http://127.0.0.1:8080/s/ajax/shop/finance/trade/income/excel?shopIdList[]=&startDate=2017-09-20%2000:00&endDate=2017-10-19%2023:59", host: "dev.orderdish.ecom.web.meituan.com"
MIME (Multipurpose Internet Mail Extensions) Part One: Mechanisms for Specifying and Describing the Format of Internet Message Bodies
Returning Values from Forms: multipart/form-data
Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types
Content-Disposition
webkit-2.18.0
Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)
Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
Returning Values from Forms: multipart/form-data
深入剖析 WebKit
HTTP 协议中的 Transfer-Encoding
分块传输编码
Webkit学习 ----网页资源的构建加载流程
WebKit内核源代码分析(四)
Nginx中502和504错误详解
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/39687.html
摘要:项目介绍本项目基本开发实现,并同时使用框架来进行开发实现,主要实现一个仓库管理系统。本系统的用户角色分为四个角色分别为客服角色,仓库人员,仓库管理员,系统管理员,不同的用户登陆系统可以进行不同的模块操作。 项目介绍: 本项目基本Springboot开发实现,并同时使用Springmvc+my...
摘要:为了能够处理中文的请求,再配置一个,以避免请求中文出现乱码情况至此,配置完毕。一般为一些基本的,用于进行相应的页面显示,用于处理网站的请求。现在,需要配置来运行该项目。 摘要讲解如何配置SpringMVC框架xml,以及如何在Tomcat中运行转载请注明出处:Gaussic(一个致力于AI研究却不得不兼顾项目的研究生)。 注:此文承接上一文:使用IntelliJ IDEA开发Sprin...
摘要:进阶多线程开发关键技术后端掘金原创文章,转载请务必将下面这段话置于文章开头处保留超链接。关于中间件入门教程后端掘金前言中间件 Java 开发人员最常犯的 10 个错误 - 后端 - 掘金一 、把数组转成ArrayList 为了将数组转换为ArrayList,开发者经常... Java 9 中的 9 个新特性 - 后端 - 掘金Java 8 发布三年多之后,即将快到2017年7月下一个版...
阅读 3713·2021-11-17 09:33
阅读 2723·2021-09-22 15:12
阅读 3343·2021-08-12 13:24
阅读 2438·2019-08-30 11:14
阅读 1732·2019-08-29 14:09
阅读 1324·2019-08-26 14:01
阅读 3060·2019-08-26 13:49
阅读 1774·2019-08-26 12:16