Skip to content
掌握度

RAG 项目深挖 · 面试故事,托底简历技能栏「可独立定位线上 OOM、CPU 飙高等故障」。设计事实全部来自文件上传模块,事故框架请按真实经历微调,见文末「口径提醒」。CPU 飙高版故事的种子在 CodeAgent 的 ripgrep检索性能:Java 正则回溯打满 CPU → rg 主路径 + 兜底超时,需要时可以同款扩写。

什么时候讲 ​

面试官问「讲一次你定位内存 / OOM 问题的经历」时用。本质是把上传模块里「合并为什么用 composeObject」这个真实设计决策,倒叙成一个 90 秒的定位故事:现象 → 定位 → 修复 → 沉淀原则。

口述版(约 90 秒) ​

“这个经历出在 RAG 知识库项目的文件上传模块,当时要支持几百兆到 1GB 的大文档上传。第一版合并是我在服务端写的:把全部分片从对象存储读回来,在 Java 里按顺序拼成完整文件,再写回对象存储。自测传大文件时发现,堆占用跟着合并直线拉升,Full GC 变得频繁,文件再大一点直接 OOM;而且上传期间带宽占用也翻倍。

定位按「先看现象、再看堆里有什么」来。jstat 看 GC 曲线,确认不是正常波动;jmap 拉直方图,最大的一类是 byte 数组;再用 MAT 看支配树,引用链直接指到合并方法里那个拼接缓冲——所有分片同时躺在堆里,等于把整个文件装进了内存。到这里原因就清楚了:这不是泄漏,分片拼完是能回收的,问题是单次峰值等于整个文件。

修复分两步。根治是把合并下沉到对象存储:MinIO 的 composeObject 走服务端分片拷贝,应用只发一条指令,数据不再过 JVM。顺手把同类隐患一起清了——文档解析原来是整篇读入,改成流式解析、攒够一个父块就切片释放。改完传 1GB 文件,堆曲线平稳、Full GC 消失,带宽占用减半:以前分片进一次、完整文件出一次,全是双倍流量。

从这事沉淀下来的判断是:大文件场景,数据面不该过应用内存——要么流式、要么下沉到存储层。后来 PDF 解析放到子进程、失败重试只带元数据不带文件内容,都是同一个原则。”

追问参考答 ​

Q1:怎么区分内存溢出和内存泄漏? 泄漏是「该回收的回收不掉」——多个时间点 dump 对比,同一路径的对象持续增长不回落;溢出是「单次需求超过容量」——峰值过后能正常回收。本例是后者:byte 数组合并完是可回收的,坏在峰值等于整个文件。所以解法是压峰值(数据不过内存),而不是查谁没释放。

Q2:直接 dump 不会影响服务吗? 会,dump 有停顿,大堆文件几百 MB 起。所以顺序是先用轻量手段——jstat 看曲线、jmap 直方图看分布,确认嫌疑后再拿完整堆 dump 做支配树。生产上更稳的做法是提前配 HeapDumpOnOutOfMemoryError,让 OOM 自己留现场;这个项目是自测环境,直接 dump 没有顾虑。

Q3:为什么用 composeObject,不用 S3 原生的 Multipart Upload 接口? 效果等价,都是让存储端完成合并、数据不过应用。选它是因为分片上传协议是我们自己定义的:MD5 指纹做身份、位图做断点、三方校验做幂等,合并入口还要做属主和状态复核。composeObject 让「复核 + 合并 + 清理」收在同一个服务端语义里,不用额外管理一层 Multipart 的 uploadId 状态。代价是绑定了 MinIO 的 SDK,但对象存储本来就是可替换的基础设施,这个绑定我能接受。

Q4:堆外内存的 OOM 怎么排查?(延伸题) 现象不同:堆是干净的,进程被 OOM killer 杀或者报 Native 内存不足。手段是 NativeMemoryTracking 和 pmap 看趋势,常见来源是直接内存、线程数过多、压缩解压流。本项目没踩到,我答到工具层;真要防,容器内存 limit 要比 -Xmx 留出堆外余量。

口径提醒 ​

  • 设计事实都是真的:composeObject 决策、解析流式化、PDF 子进程、5MB 分片、三方校验,全部写在文件上传模块。变的只是叙事顺序——把设计期的权衡倒叙成一次排查。
  • 事故烈度按真实经历讲:如果当时没真的 OOM 过,就降级成「自测时 Full GC 频繁、堆曲线异常」,定位和修复完全不变。面试可以讲小事故,不能讲编的事故——这个故事追问两层就到 composeObject,那里你是无敌的;前面编的数字反而会被记住。
  • 讲之前填数:文件多大、Full GC 频繁到什么程度、改完峰值多少,按你实际测过的填,不现场编。

本站仅供个人学习使用,请勿外传