乐于分享
好东西不私藏

文件下载OOM

文件下载OOM

文件下载,一般都是公司的通用基础组件了,下载报表的时候居然也报了OOM,一个项目很多人用呢,领导让我来搞定,那我们就看一下问题吧。
发现问题,原来业务量少,报表数据才3M,现在业务量增大,月末一个报表300M了,-Xmx2g,一共才2g,再看看代码,直接蒙蔽
File file = new File(filePath);// 几百兆的文件,直接拉到内存?byte[] data = Files.readAllBytes(file.toPath());
多点几次下载,内存直接爆了。
那么我们用流式来优化,看看效果如何
try (InputStream in = new FileInputStream(file);OutputStream out = new BufferedOutputStream(response.getOuptputStream())) {  // 每次读1KB  byte[] buffer = new byte[1024];  int len;  while ((len = in.read(buffer)) != -1) {    out.write(buffer, 0 ,len);  }  out.flush(); } catch (IOExcepetion e) {   throw new RuntimeException("下载失败", e); }
1024,很多人都这么写对吧,每次读1KB,但是300M的文件,要操作30万次IO,read和write都涉及系统调用,上下文切换和IO操作开销很大,300M的文件要处理8分钟,如果文件更大呢,2G的文件,要操作多久?显然这里还有优化空间
NIO的魔法:零拷贝,数据无需经过用户空间,直接从内核空间发送到网络通道
try (FileInputStream fis = new FileInputStream(file);    FileChannel fileChannel = fis.getChannel()) {    long position = 0;    long size = fileChannel.size();    WritableByteChannel writableChannel = Channels.newChannel(response.getOutputStream());    // 每次传输最多8KB的倍数, 但 transferTo 会尽量利用 DMA一次传更多    while (position < size) {      long transfered = fileChannel.transferTo(position, sizd - position, writableChannel);      if (transfered <= 0break;      position += transfered;     }}
速度直接起飞,300M的文件30秒内传完,妈妈再也不用担心我的学习了
但是文件太大了,网络堵塞造成传输中断咋办,2G的文件下载到99%,再重新下载?我的天,要疯了,HTTP的Range请求可以处理
long fileLength = file.length();Sting fileName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8);String rangeHeader = request.getHeader("Range");long start = 0;long end = fileLength - 1;//解析 Range 头if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {  String rangeValue = rangeHeader.substring("bytes=".length());  String[] parts = rangeValue.split("-");  try {    start = Long.parseLong(parts[0]);    if (parts.length > 1 && !parts[1].isEmpty()) {      end = Long.parseLong(parts[1]);        }   } catch (NumberFormatException e) {       response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);       return;   }    if (start < 0 || start > end || end >= fileLength) {     response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);     return;   }}
这样,我们就完成了断点续传功能了,小伙伴们,你们学会了吗。
当然,还有限流+监控,不一一展开了,有兴趣的小伙伴可以留言