如何使用PDFBox高效合并S3存储桶中的1000个PDF文件
问题1:同时打开1000个S3输入流的风险
- 内存层面不会直接触发溢出:S3的输入流默认是按需拉取数据,不会将整个PDF文件加载到堆内存,1000个流本身的结构占用的内存非常低,不会直接引发OOM。
- 但大概率会触发两类运行异常:
- 操作系统文件句柄限制:Linux系统默认普通进程的最大打开文件/网络连接句柄数为1024,1000个S3输入流加上JVM本身占用的其他句柄,很容易触发
Too many open files错误,直接导致进程崩溃。 - AWS SDK连接池限制:AWS SDK默认的HTTP连接池大小通常为50~100,一次性发起1000个S3读请求,大部分请求会阻塞等待连接,甚至触发超时错误,反而拖慢整体执行效率。
- 操作系统文件句柄限制:Linux系统默认普通进程的最大打开文件/网络连接句柄数为1024,1000个S3输入流加上JVM本身占用的其他句柄,很容易触发
问题2:更优的实现方案
方案1:串行逐份合并(无本地暂存、改动最小、资源占用最低)
PDFBox的合并工具支持逐份追加文档,不需要一次性持有所有输入流,修改流程如下:
- 初始化PDF合并工具类,指定最终输出流(可以是本地文件流,也可以是S3直传的输出流)
- 遍历所有待合并的S3文件Key,每次仅拉取1个文件的输入流
- 将当前PDF追加到合并结果中,追加完成后立即关闭当前S3输入流,再处理下一个文件
- 所有文件处理完成后生成最终合并PDF
该方案全程仅持有1个S3输入流,完全规避句柄、连接池超限问题,代码示例如下:
// 初始化合并工具 PDFMergerUtility merger = new PDFMergerUtility(); merger.setDestinationStream(finalOutputStream); MemoryUsageSetting memSetting = MemoryUsageSetting.setupTempFileOnly(); // 逐份处理S3文件 for (String pdfKey : allPdfKeyList) { // try-with-resources自动关闭S3对象和流 try (S3Object s3Object = s3Client.getObject(bucketName, pdfKey); InputStream pdfStream = s3Object.getObjectContent(); PDDocument currentDoc = PDDocument.load(pdfStream, memSetting)) { merger.appendDocument(merger.getDestinationDocument(), currentDoc); } catch (IOException e) { // 自定义单个文件异常处理逻辑 } } // 完成合并 merger.mergeDocuments(memSetting);
方案2:批量预下载本地(适合PDF需要二次复用的场景)
如果业务后续还需要使用单个PDF文件,可以先控制并发数(比如设置并发为10),并行将所有PDF下载到本地临时目录,再逐个读取本地文件合并,合并完成后删除临时文件即可。该方案比纯串行合并速度更快,同时不会触发连接池、句柄限制。
额外优化提示
合并大体积PDF时,必须指定MemoryUsageSetting优先用本地临时文件缓存,避免超大PDF加载直接占满堆内存。如果最终合并文件需要上传到S3,可以直接将合并输出流对接S3上传流,无需先存本地再上传,减少IO开销。
内容的提问来源于stack exchange,提问作者user1502377
相关产品推荐
相关产品推荐

