Netty 实现文件上传的过程及内存泄露问题排查
Posted 毕小宝
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty 实现文件上传的过程及内存泄露问题排查相关的知识,希望对你有一定的参考价值。
背景
一个用 Netty 实现的 Web 应用,压力测试期间,其文件上传功能一直报io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16777216 byte(s) of direct memory (used: 939524103, max: 954728448)
异常,但是解析文件表单的地方已经加了资源释放代码。
通过和官方提供的示例代码比对,发现不同的表单解析方式,对资源释放有影响。本文将分析 Netty 实现文件上传功能的过程与内存泄露问题,以及文件上传需要释放内存的几个地方。
文件上传表单解析
项目对文件上传的需求是不能本地化,文件上传流程为:
- Handler 解析文件表单,读取文件的字节数据并存储到业务处理队列中;
- 业务处理线程消费队列数据,完成对上传文件的分析。
最初实现是参考网络代码 《实现Netty文件上传》,在 demo 验证时没问题,但是经过压力测试运行一两天后,就大量报内存泄露。
经过反复排查内存泄露的原始代码,加上必要的资源释放后,最终得到正确代码。MultipartRequest
类完成了对文件表单的解析,出参是一个 Map
对象,值是文件的二进制数组 byte[]
,支持多文件表单。
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.multipart.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class MultipartRequest {
private final static Logger logger = LoggerFactory.getLogger(MultipartRequest.class);
/**
* 解码器工厂类,公用
*/
private final static HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);
/**
* 文件表单名称及文件表单对象:直接存文件的二进制数据
*/
private Map<String, byte[]> fileDatas;
public Map<String, byte[]> getFileDatas() {
return fileDatas;
}
public void setFileDatas(Map<String, byte[]> fileDatas) {
this.fileDatas = fileDatas;
}
/**
* 解析表单请求,分离出文件字节数据,忽略普通表单
* @param request
* @return
*/
public static MultipartRequest createMultipartBody(FullHttpRequest request) {
HttpPostRequestDecoder httpDecoder = null;
try {
//使用HTTP POST解码器
httpDecoder = new HttpPostRequestDecoder(factory, request);
httpDecoder.setDiscardThreshold(0);
if (httpDecoder != null) {
//获取HTTP请求对象,并加入解码器
final HttpContent chunk = request;
httpDecoder.offer(chunk);
//自定义返回对象和相关属性
MultipartRequest multipartRequest = new MultipartRequest();
Map<String, byte[]> fileContents = new HashMap<>();
try{
// while 循环方式遍历表单内容
while (httpDecoder.hasNext()) {
InterfaceHttpData formData = httpDecoder.next();
if (formData == null) {
continue;
}
// 文件表单类型,收集文件内容为 byte[]
if (formData.getHttpDataType() == InterfaceHttpData.HttpDataType.FileUpload) {
FileUpload fileUpload = (FileUpload) formData;
if (fileUpload.isCompleted()) {
byte[] readData = fileUpload.get();
httpDecoder.removeHttpDataFromClean(fileUpload); //remove
fileContents.put(formData.getName(), readData);
// 1、第一个容易内存泄露的地方
fileUpload.release();
}
}
}
}catch (Exception e) {
// Ignore HttpPostRequestDecoder$EndOfDataDecoderException
}
//存放文件信息
multipartRequest.setFileDatas(fileContents);
return multipartRequest;
}
} catch (Exception e) {
logger.error("解析日志文件异常",e);
} finally {
// 资源释放
if (httpDecoder != null) {
try{
// 2、第二个容易磁盘空间泄露的地方
httpDecoder.cleanFiles();
// 3、第二个容易内存泄露的地方
httpDecoder.destroy();
httpDecoder = null;
}catch (Exception e) {
logger.error("释放资源异常",e.getMessage());
}
}
}
return null;
}
}
代码涉及到资源释放的三个地方,还有一个异常:
FileUpload
文件表单对象- 磁盘上存储的表单临时文件
HttpPostRequestDecoder
解码器- while 遍历时的
EndOfDataDecoderException
异常,可以忽略
内存泄露问题排查
第一步,添加检测参数。出现资源泄露后,先对应用添加 -Dio.netty.leakDetectionLevel=advanced
,定位到解码器需要释放:
第二步,对 httpDecoder
进行资源释放,相当曲折啊,最初遍历解码器数据用的下面这段代码:
List<InterfaceHttpData> InterfaceHttpDataList = httpDecoder.getBodyHttpDatas();
for (InterfaceHttpData data : InterfaceHttpDataList) {
//如果数据类型为文件类型,则保存到fileUploads对象中
if (data != null && InterfaceHttpData.HttpDataType.FileUpload.equals(data.getHttpDataType())) {
FileUpload fileUpload = (FileUpload) data;
fileUploads.put(data.getName(), fileUpload);
continue;
}
}
后面再调用 httpDecoder.destroy();
时一直报错,说引用计数器已经归零了,但是内存泄露日志又固执地提示这里没释放,这个怪圈。代码看起来没问题,但在压力测试和长时间运行的场景下堆外内存溢出问题始终存在。
意识到还是应该参考 Netty 官网的文件上传的例子,对比后发现,遍历方式不一样,改成一样的 while (httpDecoder.hasNext())
后,解码器资源就能正常释放了,终于见到了曙光。
第三步,继续观测,还是存在堆外内存溢出。溢出日志显示,在释放解码器资源的时候,文件表单 fileUpload.get()
还有相关引用:
至此,完善代码后,看不到内存泄露问题了。
启示录
这个内存泄露问题的诡异之处在于,用 for
方式遍历解码器,调用资源释放时,不知道哪里把引用计数器设置为 0 了,但实际上并没有真正释放,这就导致了内存泄露。
另外,关于泄露日志级别配置,advanced
级别,由于是采样 1%,不太容易观测到泄露。在解码器资源释放后,还以为问题解决了呢,改成 paranoid
后,刚运行没一个小时,就出现了,所以测试排查阶段调成最高等级,有利于快速定位问题。
最后一点,是关于使用文件表单的,既然不需要本地化,最好直接用 get()
获取 byte[]
,因为文件表单对象对应的 ByteBuf
同样也存在内存泄露风险,不好控制。
以上是关于Netty 实现文件上传的过程及内存泄露问题排查的主要内容,如果未能解决你的问题,请参考以下文章