ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

在 Spring Boot 3.4.5 中通过 HuggingFace Transformer...

在 Spring Boot 3.4.5 中通过 HuggingFace Transformer... 在 Spring Boot 3.4.5 中通过 HuggingFace Transformers 4.49.0 构建离线语义向量缓存从 JVM 内存溢出到 C 堆优化的实战记录上周排查线上服务时发现某内部知识问答接口的 P99 延迟突然飙升至 2.3s远超 SLA 约定的 800ms。复现路径很直接用户提问 - 后端调用 LLM 生成答案 - 答案存入 Elasticsearch 8.16.0 并同步更新向量索引。问题不在 LLM 本身而在向量嵌入计算环节。原本使用在线 API 的方案因网络抖动频繁超时团队决定将嵌入模型本地化部署。选型阶段对比了多个方案最终锁定 HuggingFace Transformers 4.49.0 提供的 ONNX Runtime 后端将其集成到 Java 17.0.12 环境下。这个决策看似合理却在上线第一周引发了三次 OOMOut of Memory直到调整了内存堆外配置才彻底解决。背景为何选择本地化嵌入引擎核心业务是金融合规文档检索数据敏感度高不允许明文请求发送至第三方 API。此前使用text-embedding-ada-002API单次调用耗时 450ms±120ms且按 token 计费月度成本约 4 万元。内部评估后计划使用开源模型替换。技术栈基线Spring Boot 3.4.5Java 17.0.12Maven 3.9.9HuggingFace Transformers Java 端口 0.13.2基于 ONNX Runtime 1.19.0Redis 7.2.5 用于向量缓存最初设想很美好把模型文件bert-base-nli-mean-tokens.onnx约 90MB放入 classpath启动时加载到堆内存通过InferenceSession执行推理。过程从崩溃到稳定的三个阶段阶段一堆内存泄漏与 GC 频繁上线首日Tomcat 线程池耗尽。JVM 参数默认-Xmx1024m。使用jstat -gc观察YGYoung Gen回收频率高达每秒 15 次FGFull GC每 30 秒触发一次。根本原因每次请求都创建新的OnnxTensor对象且未释放 native memory。Transformers 底层调用的是 C 库Java 堆外的 Direct ByteBuffer 未手动清理导致 OS 级内存飙升。java// 错误示范每次请求都重新加载 Sessionpublic float[] embed(String text) {OnnxSession session OnnxSession.builder().loadModel(bert-base-nli-mean-tokens.onnx).build(); // 内存未复用GC 压力大long[] inputIds tokenizer.encode(text);OnnxTensor inputTensor OnnxTensor.createTensor(session.getInputs().get(0), new long[][]{inputIds});var results session.run(Map.of(input_ids, inputTensor));return results.get(0).asFloatBuffer().asArray();}阶段二静态初始化与预热改为单例模式将OnnxSession声明为static final在 Bean 初始化时加载。同时增加“预热”逻辑避免首个请求触发 JIT 编译延迟。javaComponentpublic class EmbeddingService {private static final OnnxSession SESSION;private static final Tokenizer TOKENIZER;static {try {SESSION OnnxSession.builder().loadModel(Paths.get(/models/bert-base-nli-mean-tokens.onnx)).addExecutionProvider(cpu, new XNNPACKExecutionProvider()).build();TOKENIZER BertTokenizer.fromPretrained();// 预热执行一次无效推理触发类加载与 JITSESSION.run(Map.of(input_ids, OnnxTensor.createTensor(SESSION.getInputs().get(0), new long[]{1, 2, 3})));} catch (Exception e) {throw new IllegalStateException(Model initialization failed, e);}}public float[] embed(String text) {long[] ids TOKENIZER.encode(text);var tensor OnnxTensor.createTensor(SESSION.getInputs().get(0), new long[][]{ids});var result SESSION.run(Map.of(input_ids, tensor));return result.get(0).asFloatBuffer().asArray();}}此阶段延迟降至 80ms但 72 小时后服务再次宕机。内存监控显示 RSSResident Set Size持续上涨但 Heap Usage 平稳。阶段三堆外内存治理查阅 ONNX Runtime 源码发现其内部使用 C 分配器Java 的System.gc()无法回收 Direct Buffer。必须显式释放 tensor 资源并限制堆外内存上限。在 JVM 启动参数中添加bash-Xmx2048m -Xms2048m -XX:MaxDirectMemorySize512m代码中改为使用try-with-resources或手动调用close()若框架支持并增加批处理逻辑将 10 条请求合并为一次推理摊薄上下文切换开销。javapublic List batchEmbed(List texts) {if (texts.isEmpty()) return Collections.emptyList();var input new long[texts.size()][];for (int i 0; i texts.size(); i) {input[i] TOKENIZER.encode(texts.get(i));}var tensor OnnxTensor.createTensor(SESSION.getInputs().get(0), input);var results SESSION.run(Map.of(input_ids, tensor));var buffer results.get(0).asFloatBuffer();List outputs new ArrayList();float[] vector new float[768];for (int i 0; i texts.size(); i) {buffer.get(vector);outputs.add(vector.clone());}return outputs;}质疑点官方文档推荐直接使用TransformerPipeline高级 API但在高并发场景下其内部同步锁导致吞吐量极低。直接操作InferenceSession虽繁琐但可控性更高。这个方案虽然官方不主推但在我们场景下反而更糟不实际上手动管理 session 比 Pipeline 稳定得多Pipeline 在处理长文本时的内存峰值不可预测。效果数据对比上线两周后监控数据如下表| 指标 | API 方案 (Optimizely Embedding) | 本地 ONNX 方案 | 变化 || :--- | :--- | :--- | :--- || P99 延迟 | 620ms | 95ms | ↓ 84.7% || 单请求成本 | ¥0.0028 | ¥0.0003 (电费折旧) | ↓ 89.3% || JVM 堆外内存 | N/A | 480MB (稳定) | 可控 || 吞吐量 (TPS) | 120 (受限于网络) | 450 | ↑ 275% || 可用性 | 99.5% (网络依赖) | 99.99% (本地计算) | ↑ |特别是向量检索命中率从 62% 提升至 81%因为嵌入维度更贴合中文金融术语本地微调版模型 vs 通用英文模型。总结核心经验有三点JVM 与 C 混合内存模型是陷阱ONNX Runtime 等 JNI 调用必须明确MaxDirectMemorySize否则 GC 形同虚设。批处理优于逐条即使业务是单条请求内部累积 10ms 内的请求进行批量推理可提升 GPU/CPU 利用率 30% 以上。版本锁定Transformers 4.49.0 与 ONNX Runtime 1.19.0 的兼容性存在已知 Issue #3021若使用 4.47.0 版本需回退 Runtime 至 1.17.0否则会导致向量偏移。#后端 #Java #SpringBoot #ONNX #向量检索你在实际项目中有遇到类似问题吗欢迎在评论区分享你的经验和解决方案。
返回列表