背景

笔者曾供职于某信息安全公司,接到过一个需求,提取文档中的文本以供后续分析。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,应当将内容提取独立成服务
  • 内存溢出进程不会退出,只是出现内存溢出错误的线程退出,这让整个服务处于不稳定状态

最新文章

  1. ✡ leetcode 170. Two Sum III - Data structure design 设计two sum模式 --------- java
  2. 【转】Java出现No enclosing instance of type E is accessible. Must qualify the allocation with an enclosing
  3. java 24 - 1 GUI之GUI的概述和基本代码
  4. nginx实现ssl反向代理实战
  5. MSSQL 分组后取每组第一条(group by order by)
  6. MonoRail学习:可重复组件ViewComponents的使用
  7. 【CentOS】samba服务器安装与配置
  8. hdu 4800 Josephina and RPG
  9. Qt对话框QDialog
  10. CF 578A A Problem about Polyline
  11. JAVA Metrics 度量工具使用介绍
  12. Selenium_WebDriver_控制浏览器
  13. Jenkins配置备份恢复插件ThinBackup
  14. Oracle数据库字段数据拆分成多行(REGEXP_SUBSTR函数)
  15. 【转】Intellij IDEA调试功能
  16. mysql存储过程中使用游标
  17. JDK1.5新特性,基础类库篇,XML增强
  18. C# 中的await
  19. Java设计模式(3)建造者模式(Builder模式)
  20. 7 -- Spring的基本用法 -- 8... 抽象Bean与子Bean;Bean继承与Java继承的区别;容器中的工厂Bean;获得Bean本身的id;强制初始化Bean

热门文章

  1. nm命令解释
  2. Linux禁止摄像头自动曝光(手动调节曝光)
  3. 关于CSDN博客上传图片的接口研究
  4. Java框架--SSM&Oracle&Maven高级
  5. 利用云服务提供商的免费证书,在服务器上发布https前端应用和WebAPI的应用
  6. pickle兼容问题
  7. VsCode搭建C语言运行环境以及终端乱码问题解决
  8. 使用Springboot+redis+Vue实现秒杀的一个Demo
  9. 分享一个自己项目中用到的.net中正则替换工具处理类(支持先用特征匹配内容整体模板,同时模板内对相关字内容进行替换)
  10. [OpenCV实战]12 使用深度学习和OpenCV进行手部关键点检测