记一次使用tika解析文件文本导致的内存溢出问题
背景
笔者曾供职于某信息安全公司,接到过一个需求,提取文档中的文本以供后续分析。tika是apache开源的解析文档内容的组件,应用十分广泛。tika几乎支持你能想到的所有文档格式,docx , pptx , xlsx , pdf, zip , rar , tar 等。
tika本身只是一个门面,不提供文档解析实现,这有点类似与sl4j。例如tika使用pdfbox解析pdf文件,使用poi解析 office文档。然而文档种类繁多,有压缩的未压缩的,有加密的有未加密的,有大文件有小文件。甚至还有一些恶意文件,例如 压缩炸弹,xml炸弹。这些恶意文件可能会导致整个应用挂掉。
案发
这里先简单介绍一下:使用tika解析文本之后将文本存入ElasticSearch数据库中是一个非常经典的使用场景。一些图书馆,档案馆,将文档内容提取出来存入ES做索引,这样便可以通过内容检索到对应的文档了。
某天,外场报告说ElasticSearch客户端报告了Request cannot be executed; I/O reactor status: STOPPED 错误,导致所有的ES插入报错。
报错堆栈如下:
Caused by: java.lang.IllegalStateException: Request cannot be executed; I/O reactor status: STOPPED
at org.apache.http.util.Asserts.check(Asserts.java:46)
at org.apache.http.impl.nio.client.CloseableHttpAsyncClientBase.ensureRunning(CloseableHttpAsyncClientBase.java:90)
at org.apache.http.impl.nio.client.InternalHttpAsyncClient.execute(InternalHttpAsyncClient.java:123)
at org.elasticsearch.client.RestClient.performRequestAsync(RestClient.java:529)
at org.elasticsearch.client.RestClient.performRequestAsyncNoCatch(RestClient.java:514)
at org.elasticsearch.client.RestClient.performRequest(RestClient.java:226)
at org.elasticsearch.client.RestHighLevelClient.performRequest(RestHighLevelClient.java:1256)
at org.elasticsearch.client.RestHighLevelClient.performRequestAndParseEntity(RestHighLevelClient.java:1231)
at org.elasticsearch.client.RestHighLevelClient.search(RestHighLevelClient.java:730)
看起来是不是挺无厘头的?一般来说越是底层的报错越难排查。在网上可以大量搜到ES这个 I/O reactor status: STOPPED错误,我尝试很多方法,甚至对ES进行了长时间的压力测试,没有复现该问题。
就这样过去了很长一段时间,我在一次偶然的压力测试中发现,日志中赫然出现了OutOfMemoryError错误,并生成了堆转储文件。伴随着这个内存溢出错误,I/O reactor status: STOPPED这个错误也复现了。这强烈提示OutOfMemoryError导致了ES客户端的I/O reactor status: STOPPED错误!后续的多次测试也证明了这一点。
原因
后续就简单了,使用Eclipse MAT工具分析堆转储文件,发现pdfbox相关的对象占用空间特别大。这个pdfbox就是tika解析pdf文件所引入的依赖。
tika使用了pdfbox解析了一个大概40M的pdf文件,直接吃掉了2.1G左右的堆内存!文件的大小和占用的堆内存并没有直接的关联。笔者在之后还遇到了只有40Kb左右但单压缩比极高的docx文件(测试构造的极端文件)。这个40Kb左右的文件直接吃掉了1G以上的堆内存。
接下来就很清晰了,tika解析特定的文件占用大量内存,ES客户端的底层I/O线程申请不到足够的内存,抛出OutOfMemoryError错误,I/O线程意外退出,最终导致了 I/O reactor status: STOPPED
令很多人意外的是,出现内存溢出后,jvm不会退出,如果线程中没有捕获Error(区别于Exception),抛出错误的线程就会退出。如果是关键线程退出(例如定时任务),整个系统就会处于不稳定的状态,带来的问题是难以排查的。我这里的例子就是这样,出现了内存溢出的错误,jvm还在,整个服务仿佛还是正常,但底层的I/O线程挂了,导致所有ES的操作全部失败。当然,如果你的运气比较好,挂掉的线程仅仅是普通的线程(例如线程池中的线程),那么线程池还会拉起一个线程,只是丢失了这个线程执行的任务而已。
解决
tika解析pdf文件可能会占用大量内存,并最终导致内存溢出,在这里我们可以限制pdf文件的内存占用
public String parse() throws Exception { ContentHandler contentHandler = new BodyContentHandler(); //设置pdf文件占用最大内存50M
PDFParserConfig pdfParserConfig = new PDFParserConfig();
pdfParserConfig.setMaxMainMemoryBytes(50 * 1024 * 1024L);
Metadata metadata = new Metadata(); //填入pdf参数
ParseContext parseContext = new ParseContext();
parseContext.set(PDFParserConfig.class, pdfParserConfig); Parser parser = new AutoDetectParser();
try (InputStream in = new FileInputStream(file)) {
parser.parse(in, contentHandler, metadata, parseContext);
return contentHandler.toString();
}
}jvm参数添加 -XX:+CrashOnOutOfMemoryError
出现内存溢出后立刻退出,等待守护进程拉起(需要有守护进程),避免应用处于不稳定状态
除了内存溢出外,tika社区还报告另外两个问题,内存泄漏(memory leak)和 死循环(infinite loop)。上面我们勉强通过参数限制了pdf文件的内存占用,但是文件会有极端文件破坏我们服务的稳定。内存泄漏最总会导致内存溢出,添加虚拟机参数溢出后重启即可解决,可死循环会导致线程永久卡死,直接导致服务不可用。
对于死循环问题,可以通过设定超时时间勉强解决,超时后,进程自动退出,等待守护进程拉起
终极解决方式
上述的解决方式本质上都是通过重启服务的方式实现的。据tika社区介绍,新版本的tika已默认使用fork的方式解析文件。所谓的fork的方式,就是每处理一个文件就用一个单独的进程(当然进程可以复用),这种进程隔离的方式将错误局限在了子进程中而父进程不受影响,实现了隔离和保护。详见:https://dist.apache.org/repos/dist/release/tika/2.1.0/CHANGES-2.1.0.txt
所谓的终极解决方式就是实现进程隔离,将错误限制在子进程中。例如浏览器就是典型的多进程,每个页面都是单独的进程,避免个别页面的崩溃带崩整个应用。实现多进程的核心是进程间的通信,关于这一主题的讨论将放在我的下一篇博客中。
总结
- 使用tika提取文件内容这个行为充满着不确定性,可能会导致内存溢出,内存泄漏,死循环等严重问题
- 不要在你的应用中直接集成tika,应当将内容提取独立成服务
- 内存溢出进程不会退出,只是出现内存溢出错误的线程退出,这让整个服务处于不稳定状态
最新文章
- ✡ leetcode 170. Two Sum III - Data structure design 设计two sum模式 --------- java
- 【转】Java出现No enclosing instance of type E is accessible. Must qualify the allocation with an enclosing
- java 24 - 1 GUI之GUI的概述和基本代码
- nginx实现ssl反向代理实战
- MSSQL 分组后取每组第一条(group by order by)
- MonoRail学习:可重复组件ViewComponents的使用
- 【CentOS】samba服务器安装与配置
- hdu 4800 Josephina and RPG
- Qt对话框QDialog
- CF 578A A Problem about Polyline
- JAVA Metrics 度量工具使用介绍
- Selenium_WebDriver_控制浏览器
- Jenkins配置备份恢复插件ThinBackup
- Oracle数据库字段数据拆分成多行(REGEXP_SUBSTR函数)
- 【转】Intellij IDEA调试功能
- mysql存储过程中使用游标
- JDK1.5新特性,基础类库篇,XML增强
- C# 中的await
- Java设计模式(3)建造者模式(Builder模式)
- 7 -- Spring的基本用法 -- 8... 抽象Bean与子Bean;Bean继承与Java继承的区别;容器中的工厂Bean;获得Bean本身的id;强制初始化Bean
热门文章
- nm命令解释
- Linux禁止摄像头自动曝光(手动调节曝光)
- 关于CSDN博客上传图片的接口研究
- Java框架--SSM&;Oracle&;Maven高级
- 利用云服务提供商的免费证书,在服务器上发布https前端应用和WebAPI的应用
- pickle兼容问题
- VsCode搭建C语言运行环境以及终端乱码问题解决
- 使用Springboot+redis+Vue实现秒杀的一个Demo
- 分享一个自己项目中用到的.net中正则替换工具处理类(支持先用特征匹配内容整体模板,同时模板内对相关字内容进行替换)
- [OpenCV实战]12 使用深度学习和OpenCV进行手部关键点检测