一次线上导出功能频发 OOM 与超时问题的全链路性能调优实战

在企业级后端业务系统中,Excel 与 CSV 格式的数据导出是订单、财务、采购等核心业务模块的高频基础能力。但该类功能在研发阶段通常基于小数据量场景完成验证,未充分考虑数据规模增长后的性能表现,线上运行中易出现接口超时、JVM 堆内存溢出(OOM)、Tomcat 线程池耗尽、网关连接资源占满等故障。

本次线上性能故障排查围绕系统内三个核心导出场景展开,三类场景均存在共性性能缺陷,覆盖后端全量数据加载、内存全量计算、同步长耗时阻塞,以及前端批量请求设计不合理、浏览器渲染性能瓶颈等多层问题。本文将完整复盘故障根因定位过程,并呈现从临时应急优化、架构重构到基础设施兜底的全层级调优方案。

一、问题概述

近期线上持续收到用户反馈,销售订单汇总、回款概览、订单采购总控台三大模块的导出功能在大数据量场景下频繁出现前端请求超时、导出失败、页面无响应等问题,严重时会触发服务内存溢出、网关连接耗尽,对整体业务可用性造成影响。

经初步排查定位,上述问题并非由单一代码缺陷引发,而是数据加载策略失当、内存计算方式低效、同步执行模型适配不足、前端请求逻辑存在设计缺陷共同导致的系统性性能问题。下文将针对三类场景的共性与个性化问题展开精准根因分析。

二、核心问题根因全量分析

三类导出场景所属业务领域存在差异,但核心性能瓶颈具备高度一致性:均采用全量数据加载、全量内存拼装计算的实现方式,基于同步 HTTP 线程执行全流程,未设计流式分批处理机制;部分场景还存在前端请求架构缺陷,进一步放大了性能问题的影响范围。

1. 销售订单汇总导出

该接口的核心问题为一次性全量加载数据、内存全量执行单元格合并、全流程同步长耗时执行,未采用分页与流式处理机制,对 JVM 内存与服务器线程资源造成极大消耗。

数据加载阶段存在根本性设计缺陷。代码通过pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE)关闭分页查询,等同于将数据库中所有符合条件的订单 DO 对象一次性全量加载至 JVM 堆内存。紧随其后的buildSaleOrderVOPageResult方法,会基于全量订单主键批量关联查询订单明细、汇率、产品、客户、业务员等多张数据表,在应用内存中完成多表全量关联操作,而非依托数据库索引以分页方式增量查询。仅该环节即可导致大数据量场景下堆内存占用量急剧上升。

Excel 文件构建逻辑进一步加剧了内存消耗。业务逻辑通过exportSaleOrderWithMerge方法手动调用 POI 的addMergedRegion实现单元格合并,具体实现为先按照「订单级固定字段 + 明细多行数据」的结构构建全量List<Object>行数据,同时将所有单元格合并区间预存入内存集合,待全部数据、样式与合并规则组装完成后,再一次性刷新至输出流。该实现方式会导致整本 Excel 的 Workbook 对象长期驻留堆内存,无法进行垃圾回收。

最为核心的架构问题在于,整条业务链路全部在 HTTP 请求线程内同步执行。大数据量场景下,JVM 堆内存被大量业务对象占用,单元格合并、全量数据拼装属于 CPU 与内存双重密集型操作,会长时间占用 Tomcat 工作线程。前端呈现的请求超时仅为表层现象,其本质是服务器线程被长时间阻塞,请求持续堆积后进一步耗尽线程池资源,极端情况下会引发服务雪崩。

2. 回款概览导出

回款概览导出是线上内存溢出与接口超时问题的高发模块,核心痛点为多 Sheet 全量内存构建、多数据源重复全量查询、内存预计算合并规则,其内存占用规模为三类场景中最高。

在数据查询层面,exportPaymentSummary方法分别对报销、付款申请、付款单三类数据源执行全表selectList查询,随后再通过主键批量查询全部明细数据,两轮全量查询直接导致内存中的数据规模翻倍。除此之外,导出执行前还会调用用户接口批量获取创建人昵称、调用供应商 Mapper 查询全部供应商名称,所有关联数据均一次性加载至内存,未设置分批加载与懒加载机制。

在 Excel 文件构建层面,该功能支持多 Sheet 导出,业务逻辑会预先通过内存中的computeMergeRanges方法计算每张 Sheet 的单元格合并区间,待三张 Sheet 的全部数据、合并规则、样式信息在内存中组装完成后,再通过ExcelUtils.writeMultiSheet方法一次性完成序列化写入。

最终形成三重内存压力叠加效应:单 Sheet 数据体量庞大、三张 Sheet 数据同时驻留内存、合并规则预计算占用额外内存空间。当数据量达到阈值后,必然触发内存溢出;同时同步执行模式会长时间阻塞线程,引发接口超时。

3. 订单采购总控台导出

与前两类以后端性能瓶颈为主的场景不同,该功能的性能问题主要集中于前端,后端接口仅存在少量不合理调用,核心故障由前端请求逻辑与浏览器渲染机制的限制共同引发。

其一为严重的前端 N+1 RPC 请求问题。前端通过 while 循环分页拉取接口数据,单页容量为 100 条、最大上限为 5000 条;每加载完成一页订单数据,即对每个采购订单主键串行或并发调用一次详情接口getPurchaseOrder(id)。单次导出操作会产生大量冗余请求,总请求量级可达数十至上百次。

其二为浏览器端性能瓶颈。前端所有 CSV 文件拼接、Blob 对象构造操作均在浏览器内存中执行。5000 条仅为订单主单的分页上限,单条订单展开后的明细行数无上限约束,最终生成的 CSV 文件行数会成倍增长,大幅消耗浏览器内存与主线程资源,易造成页面无响应、解析失败等问题。

同时受限于浏览器 HTTP/1.1 协议同域名 6 个并发连接的限制,前端大量并发的分页请求与详情请求会抢占有限的连接资源,极易耗尽 Nginx 与网关的连接池,导致部分请求返回 502 或 504 状态码。且当前导出逻辑未设计失败重试与断点续跑机制,任意一个分页请求失败都会导致整次导出任务完全失败,系统容错能力不足。

三、分层调优方案(按落地成本从低到高)

针对上述三类模块的共性与个性化问题,本次调优采用阶梯式优化思路,分为零代码应急优化、短期业务改造、中期基础设施优化、通用架构重构四个层级,兼顾快速故障止血与长期问题根治,适配不同的项目迭代节奏。

A. 通用架构优化:异步任务导出

所有大数据量导出功能的核心共性问题为「同步 HTTP 模型承载长耗时任务」,因此最为彻底的优化方案是将同步导出模式改造为异步任务 + 任务中心模式,使导出流程完全脱离 Tomcat 请求线程,从架构层面根治接口超时、线程阻塞、故障扩散等问题,该方案适用于全部三类问题场景。

后端改造:搭建通用ExportTaskService任务服务,配套新建erp_export_task任务记录表,核心字段包含业务类型、请求参数 JSON、任务状态、文件下载地址、错误信息、创建时间、完成时间等。三类导出控制器新增/export-async异步接口,接口接收参数后立即返回唯一任务 ID,实际的数据查询、文件生成、对象存储上传全流程由独立的 Spring 异步线程池执行;任务执行成功则回填文件下载地址,失败则记录完整错误日志。

前端改造:用户触发导出操作后,页面不再进入阻塞等待状态,而是弹出导出任务抽屉面板,通过 10 秒间隔轮询或 SSE 服务推送实时获取任务执行状态,任务完成后展示文件下载链接,执行失败则展示具体错误原因。

核心收益:彻底切断「导出任务占用 Tomcat 工作线程」的传导链路,避免单一大数据量导出任务拖垮整体服务;即使导出任务发生内存溢出,也仅会影响独立的异步线程池,不会干扰核心业务接口运行,实现故障隔离。

B. 短期落地方案

针对暂不具备异步架构落地条件的场景,可通过局部代码改造与逻辑优化快速降低线上故障发生概率,所有改造均无需调整整体系统架构,迭代实施成本较低。

B1. 销售订单汇总导出优化

  1. 设置数据量硬上限,引导用户使用异步导出。查询订单数据总量后进行阈值判断,当总条数大于 5000 时,直接拦截同步导出请求,向前端返回「数据量较大,请使用异步导出功能」的提示,从源头规避大数据量同步导出带来的风险。

  2. 采用分批流式写入 Excel,消除全量内存存储问题。摒弃一次性组装全部行数据的实现逻辑,改为以 1000 条为单位分批写入。调用 EasyExcel 的ExcelWriter.write(Iterable, WriteSheet)方法向同一张 Sheet 增量写入数据,当单 Sheet 数据量超过阈值时自动拆分至新 Sheet,从根本上解决单 Sheet 内存溢出问题。

  3. 优化单元格合并实现逻辑。废弃手动预存全量合并区间、批量执行addMergedRegion的高内存消耗写法,复用框架内置的PredefinedMergeHandler处理器,按批次注册合并规则,无需提前构建全量合并地址列表,可大幅降低内存占用。

  4. 重构汇率查询逻辑。将「全量订单加载后批量匹配汇率」的逻辑调整为按「币种 + 月份」维度预缓存汇率表数据,减少运行时的远程调用与内存 HashMap 拼装开销。

  5. 关闭非必要功能,降低额外性能开销。设置autoCloseStream(false)关闭自动流关闭机制,移除导出场景下非必需的自定义样式处理器,减少反射调用与样式渲染带来的额外性能损耗。

B2. 回款概览导出优化

  1. 全量查询改造为游标分页流式拉取。将三类数据源的全表selectList查询全部改造为分页循环拉取模式,每次加载 1000 条数据后立即写入对应 Sheet,内存中始终仅保留当前批次数据,彻底解决三张 Sheet 全量数据同时驻留内存的问题。

  2. 消除重复数据库查询。梳理代码中的冗余逻辑,当前业务存在「入口查询一次收集主键、构建 Sheet 时再次查询一次」的重复调用问题,重构为单次查询所需数据、全局复用查询结果,杜绝无效数据库请求。

  3. 精简 VO 对象内存占用。移除明细 VO 的深拷贝逻辑,通过对象引用共享公共字段;同时将内存预计算合并规则的方式替换为 EasyExcel 原生合并能力,可使单条数据的内存占用降低约 50%。

  4. 替换多 Sheet 写入为流式实现版本。废弃原有的ExcelUtils.writeMultiSheet一次性全量写入方法,基于 EasyExcel 封装多 Sheet 流式写入能力,通过自定义迭代器逐批次提供数据,内存中仅保留单分页数据集。

B3. 订单采购总控台导出优化

核心优化思路:将前端所有复杂请求、数据拼装逻辑迁移至后端实现,从根源上解决前端 N+1 请求与浏览器性能瓶颈问题。

  1. 新增后端专属导出接口。新建/crm/sale-order/export-purchase-dashboard专用导出接口,复用现有分页查询参数,由后端统一完成分页流式查询、采购单批量关联、数据拼装处理,直接输出 Excel 或 CSV 文件流。前端仅负责参数收集、接口调用、接收下载文件,彻底移除前端循环分页、N+1 详情查询的逻辑。

  2. 前端临时兼容优化(保留 CSV 方案场景)。若短期内需保留前端 CSV 导出能力,实施三项优化:将分页单页容量从 100 条调整为 500 条,总数据上限从 5000 条下调至 2000 条,大幅减少请求次数;将前端逐条查询详情的 N+1 逻辑替换为后端批量主键查询接口,一次性返回全部关联数据;将 CSV 拼接、Blob 构造逻辑迁移至 Web Worker 异步执行,避免阻塞浏览器主线程。

C. 中期:基础设施统一优化

在业务侧代码优化的基础上,同步推进框架层与基础设施升级,将导出最佳实践沉淀为通用能力,避免各业务模块重复建设,从底层提升全系统的导出性能表现。

  1. 封装通用流式 Excel 工具 API。对ExcelUtils工具类进行扩展,新增流式写入方法,基于迭代器实现数据懒加载,调用方仅需传入数据供应迭代器,无需关注分批逻辑与内存控制。统一规范后,所有新增导出功能默认具备流式处理能力,从工具层面规避内存溢出风险。

  2. 框架层统一封装异步导出能力。通过 Spring 切面与注解结合的方式,对标注@AsyncExport的控制器方法自动包装为异步任务模式,业务开发人员仅需添加注解即可接入任务中心,无需重复开发任务管理逻辑。

  3. 数据库索引与查询逻辑优化。针对订单核心查询语句校验索引有效性,补充status + order_timesceneType + order_time复合索引;同时优化批量 IN 查询逻辑,当 IN 参数数量超过阈值时自动拆分为分批查询,避免单 SQL 参数过多导致执行计划劣化。

  4. 服务器参数兜底调优。同步调大 Spring MVC 异步请求超时时间、Nginx 读写超时时间,作为极端场景下的兜底保障。需明确的是,参数调优仅为容错手段,不能替代业务逻辑与架构层面的优化。

D. 立即可落地优化

该层级优化无需修改业务代码,仅通过前端页面配置与监控配置即可快速上线,从用户操作侧与运维监控侧提前规避性能风险。

  1. 前端增加数据量二次确认机制。优化三类模块导出按钮的弹窗提示,在原有确认弹窗基础上,实时展示当前筛选条件下的数据总量,当数据量超过阈值时高亮提示「数据量较大,建议缩小筛选范围或使用异步导出」,引导用户合理使用导出功能。

  2. 新增慢 SQL 监控告警机制。将无分页全量查询、批量明细查询等高频风险 SQL 纳入慢 SQL 监控体系,及时发现线上全量查询、低效查询问题,提前介入优化,避免批量故障发生。

四、调优总结与复盘

本次三类导出模块暴露的性能问题,本质是企业级系统发展过程中的典型问题:研发阶段仅基于小数据量场景完成功能验证,未针对数据规模增长做兼容设计,伴随业务数据持续积累,性能问题集中爆发。

本次调优工作遵循「先应急止血、再深度优化、后架构重构」的实施原则:零成本的监控配置与文案优化可快速降低故障发生概率,短期代码改造解决现有内存与查询性能瓶颈,中期基础设施统一优化规范全系统导出能力,最终通过异步任务架构彻底解决长耗时导出的各类问题。

针对所有导出类功能,后续研发工作需坚守三项开发规范:第一,严禁执行全量数据库查询与全量内存拼装,必须采用分页或流式处理机制;第二,大数据量导出场景严禁采用同步 HTTP 执行模式,统一接入异步任务体系;第三,复杂数据拼装、文件构建逻辑统一由后端实现,避免前端发起海量请求与执行重型计算任务。


一次线上导出功能频发 OOM 与超时问题的全链路性能调优实战
https://www.hellojustin.cn/archives/OOM%E8%B0%83%E4%BC%98
作者
Justin_Tang
发布于
2026年06月18日
许可协议