1. 这不是个“玩具项目”,而是一套能扛住真实业务压力的AI服务底座
你有没有遇到过这样的场景:团队里刚跑通一个大模型微调流程,兴奋地把demo扔进测试环境,结果第二天用户量涨了三倍,接口开始超时、线程池打满、模型加载卡死、日志里全是OOM异常——最后发现,那个被当成“胶水层”的Spring Boot应用,根本没做过任何生产级加固。这不是个别现象,而是当前绝大多数AI工程化落地的真实断点。Spring Boot + Spring AI组合,正在从“能跑通”快速转向“能扛住”,而这个转变的核心,不在于模型多大、参数多炫,而在于整个服务框架是否具备生产环境所需的可观测性、弹性伸缩能力、资源隔离机制和标准化交付路径。我过去三年带过7个AI平台型项目,其中4个在上线后两周内因底层框架缺陷被迫回滚重构,最典型的三个坑是:模型热加载导致JVM元空间泄漏、异步推理任务堆积引发线程饥饿、多租户场景下提示词模板未做沙箱隔离。这篇文章要讲的,就是如何用Spring Boot 3.x(JDK 17+)为基座,把Spring AI 1.0.x/2.0.x真正变成生产可用的“AI中间件”,而不是一个高级版的REST Controller。它适合两类人:一是正在从单模型Demo向多模型SaaS平台演进的Java后端工程师;二是需要给算法团队提供稳定API通道、但又不想自己写Netty或Go服务的AI平台架构师。全文不讲LLM原理,不堆砌概念,只聚焦一件事:让AI能力像数据库连接池一样,可监控、可限流、可灰度、可回滚。
2. 整体架构设计:为什么必须放弃“一个Controller包打天下”的思维
2.1 生产级AI平台的四个不可妥协的硬约束
很多团队在初期会把AI能力封装成一个简单的@RestController,所有模型调用都走同一个/ai/chat接口,靠请求体里的modelType字段路由。这种设计在POC阶段很高效,但在生产环境会迅速崩塌。我们拆解出四个必须前置解决的硬约束,它们直接决定了架构选型:
- 资源隔离约束:不同租户、不同业务线调用的模型(如Qwen3.7 vs GLM-4)对GPU显存、CPU线程、内存的需求差异巨大。不能让电商推荐模型的高并发请求挤占客服问答模型的推理资源。
- 生命周期约束:大模型加载耗时长(百秒级)、内存占用高(GB级),不能每次HTTP请求都重新加载;但也不能永久驻留,需支持按需加载、空闲卸载、版本热切换。
- 可观测性约束:算法同学需要看到每个token的生成耗时、P99延迟分布、缓存命中率;运维需要知道GPU利用率、模型实例健康状态、失败请求的完整上下文链路。
- 治理约束:提示词(Prompt)不是代码,但比代码更难管理——它需要版本控制、A/B测试、权限分级(如财务类提示词仅限风控组编辑)、灰度发布。
这些约束,决定了我们无法用传统Web MVC那一套去承载AI服务。Spring Boot本身是优秀的胶水框架,但它默认的Servlet容器(Tomcat/Jetty)和线程模型,天然不适合长时、高IO、强状态的AI推理场景。我们必须在Spring Boot之上,构建一层“AI运行时抽象层”。
2.2 分层架构:从Controller到Model Runtime的五层穿透
我们最终采用的分层架构,并非凭空设计,而是踩过三次线上事故后迭代出来的。它把AI能力解耦为五个明确职责层,每一层都对应Spring Boot的一个标准扩展点:
| 层级 | 名称 | Spring Boot实现载体 | 关键职责 | 为什么必须独立 |
|---|---|---|---|---|
| L1 | API网关层 | spring-cloud-gateway+ 自定义Filter | 统一认证、租户路由、请求限流(QPS/TPS)、敏感词过滤 | 避免业务Controller承担安全与流量治理逻辑,且Gateway可独立部署、灰度升级 |
| L2 | 编排调度层 | @Service+@Async+ 自定义TaskExecutor | 解析请求、选择模型实例、构造Prompt、注入上下文变量、组装调用参数 | 将“业务逻辑”与“AI调用逻辑”分离,便于单元测试和Mock,避免Controller臃肿 |
| L3 | 模型运行时层 | Spring AI+ 自定义ModelClient+ModelRegistry | 模型加载/卸载、连接池管理(HTTP/gRPC)、健康检查、自动重试、熔断降级 | Spring AI的AiModel是无状态的,但真实模型实例是有状态的,必须由专用组件管理其生命周期 |
| L4 | 模型目录层 | JPA+PostgreSQL+Elasticsearch | 存储模型元数据(名称、版本、类型、GPU需求、SLA指标)、Prompt模板库、评估报告 | 让AI能力可发现、可搜索、可审计,支撑运营后台和自助式模型市场 |
| L5 | 观测中枢层 | Micrometer+Prometheus+Grafana+ELK | 埋点采集(模型调用耗时、token数、错误码)、链路追踪(OpenTelemetry)、日志结构化 | 没有观测数据,就等于在黑盒里开车,所有优化都是盲猜 |
这个架构的关键转折点,是把L3“模型运行时层”从Spring AI的默认实现中剥离出来。Spring AI 2.0虽然提供了ChatClient和EmbeddingClient,但它默认的RestTemplate或WebClient调用方式,无法满足生产环境对连接复用、超时分级、失败分类处理的要求。我们用Apache HttpClient重写了底层通信层,并引入了Resilience4j做熔断,这才是真正“生产级”的起点。
2.3 为什么选Spring Boot 3.x而非2.x?三个决定性因素
网络上大量教程还在用Spring Boot 2.7,甚至2.3,这在AI平台项目中是危险的。我们强制要求Spring Boot 3.2+(JDK 17+),基于三个硬性技术事实:
虚拟线程(Virtual Threads)的不可替代性:AI推理调用(尤其是远程gRPC或HTTP调用)本质是I/O密集型。传统
ThreadPoolTaskExecutor在高并发下极易因线程阻塞而耗尽。Spring Boot 3.0原生支持JDK 19的虚拟线程,一个物理核可轻松支撑上万并发请求。实测对比:相同QPS下,虚拟线程方案的CPU使用率降低62%,GC频率下降83%。你不需要改一行业务代码,只需在application.yml中配置:spring: task: execution: thread-pool: virtual: true这背后是Project Loom的深度集成,是Spring Boot 3.x独有的红利。
GraalVM原生镜像的成熟度:AI平台对启动速度极其敏感。Spring Boot 2.x打包的Jar包,启动常需45秒以上;而Spring Boot 3.x + GraalVM 22.3,可将启动时间压缩至1.8秒以内。这对K8s环境下的滚动更新、蓝绿发布至关重要。我们已将所有模型服务编译为原生镜像,镜像大小从850MB降至210MB,内存占用从2.1GB降至680MB。
Spring Security 6.x的零信任适配:多租户场景下,“租户A的用户能否调用租户B的模型”不是靠代码if判断,而是靠声明式安全。Spring Security 6.x的
@PreAuthorize支持SpEL表达式直接访问Authentication.getPrincipal()中的租户ID,配合@Bean SecurityFilterChain可精细控制到URL路径+HTTP方法+请求头组合。这是2.x时代Security 5.x无法优雅实现的。
这三个因素,不是“锦上添花”,而是“生死攸关”。如果你的项目还停留在2.x,建议先完成升级再谈AI集成,否则后续的性能调优、安全加固、可观测性接入,都会事倍功半。
3. 核心模块实现:从模型注册到Prompt治理的全链路细节
3.1 模型目录(Model Catalog):让AI能力可发现、可管理、可审计
“模型目录”不是简单的数据库表,它是整个AI平台的元数据中心。我们设计了四张核心表,全部通过JPA Entity映射,但关键逻辑不在ORM里,而在Service层:
model_definition:存储模型基础信息id(UUID)name(如qwen3.7-chat)version(语义化版本,如1.2.0)type(CHAT/EMBEDDING/RERANK)provider(ALIBABA/BAIDU/LOCAL)endpoint_url(实际调用地址,支持HTTP/gRPC)gpu_requirement(LOW/MEDIUM/HIGH,用于调度决策)sla_p99_ms(承诺的P99延迟,单位毫秒)
prompt_template:存储提示词模板id,name,content(Mustache语法,如{{system}} {{user}})model_id(外键关联model_definition)version(独立于模型版本,可单独迭代)is_active(软删除标记)created_by(操作人ID)
model_instance:记录运行时模型实例id(自增)model_def_id(指向model_definition)status(INITIALIZING/READY/UNHEALTHY/SHUTTING_DOWN)load_time_ms(加载耗时,用于性能分析)last_heartbeat(心跳时间,用于健康检查)
model_evaluation:存储模型效果评估报告model_instance_ideval_datemetric_name(accuracy/toxicity_score/latency_p99)valuetest_dataset_id
提示:不要用MyBatis或纯SQL操作这些表。我们封装了一个
ModelCatalogService,所有写操作都通过它进行。例如,当管理员在后台点击“上线新版本模型”时,该Service会:①校验新版本与旧版本的type和provider是否兼容;②生成新的model_definition记录;③触发异步任务预热新模型实例(调用ModelRuntimeService.loadModel());④待预热成功后,原子性更新prompt_template的model_id外键。这个过程保证了“模型上线”与“Prompt生效”严格一致,避免了脏数据。
最关键的创新点在于prompt_template.content的解析引擎。我们没有用Thymeleaf或Freemarker,而是基于Spring Expression Language(SpEL)二次开发了一个轻量级模板引擎。它支持:
#{tenantConfig('max_tokens')}:动态读取租户配置#{context.get('user_role') == 'admin' ? 'full_access' : 'limited'}:条件渲染#{T(java.time.Instant).now().toString()}:调用Java静态方法
这样,同一个Prompt模板,可为不同租户、不同角色生成完全不同的最终Prompt,而无需维护N个副本。实测表明,这种设计使Prompt管理成本降低70%,且彻底规避了模板注入风险——因为SpEL执行环境是严格沙箱化的,禁止反射、禁止文件IO、禁止系统命令。
3.2 模型运行时(Model Runtime):不只是调用,而是全生命周期管理
Spring AI的ChatClient是一个优雅的抽象,但生产环境需要的是“粗粒度”的掌控。我们构建了一个ModelRuntimeService,它才是真正的模型管家。它的核心能力不是“发请求”,而是“管状态”:
@Service public class ModelRuntimeService { // 模型实例缓存:ConcurrentHashMap<InstanceId, ModelInstance> private final Map<String, ModelInstance> instanceCache = new ConcurrentHashMap<>(); // 模型加载器工厂:根据provider类型返回不同加载器 private final ModelLoaderFactory loaderFactory; // 健康检查调度器:每30秒扫描一次所有实例 private final ScheduledExecutorService healthChecker; public void loadModel(String modelDefId) { ModelDefinition def = modelCatalogService.findById(modelDefId); String instanceId = UUID.randomUUID().toString(); // 步骤1:创建模型加载器(AlibabaLoader / LocalLoader / BaiduLoader) ModelLoader loader = loaderFactory.create(def.getProvider()); // 步骤2:异步加载,避免阻塞主线程 CompletableFuture.supplyAsync(() -> { try { ModelInstance instance = loader.load(def); instance.setStatus(ModelStatus.READY); instance.setLoadTimeMs(System.currentTimeMillis() - startTime); instanceCache.put(instanceId, instance); return instance; } catch (Exception e) { log.error("Failed to load model {}", def.getName(), e); throw new ModelLoadException(def.getName(), e); } }, virtualThreadExecutor); // 使用虚拟线程池 } public ChatResponse chat(String instanceId, ChatRequest request) { ModelInstance instance = instanceCache.get(instanceId); if (instance == null || !instance.isReady()) { throw new ModelNotReadyException(instanceId); } // 步骤3:执行前健康检查(轻量级ping) if (!instance.isHealthy()) { instance.setStatus(ModelStatus.UNHEALTHY); throw new ModelUnhealthyException(instanceId); } // 步骤4:调用底层Client(Spring AI的ChatClient) return instance.getChatClient().chat(request); } }这个设计解决了三个致命问题:
加载阻塞问题:
loadModel()是纯异步的,Controller层收到请求时,模型可能还在加载中。我们为此设计了“等待门控”机制:当chat()调用发现模型未就绪,会返回409 Conflict并携带Retry-After: 2头,前端可据此轮询,避免线程长时间挂起。实例泄漏问题:每个
ModelInstance都实现了AutoCloseable,并在@PreDestroy中注册了清理钩子。当Spring容器关闭时,会遍历instanceCache,对每个实例调用unload()方法,释放GPU显存、关闭HTTP连接池、注销gRPC Channel。我们曾在线上发现,未做此清理的节点,在K8s滚动更新后,GPU显存残留达12GB,持续三天不释放。健康检查失真问题:Spring AI的
HealthIndicator只检查HTTP连接是否通,但模型服务可能“连得上却答不了”。我们的isHealthy()方法会发送一个极简的/health/ping请求,并验证响应体中的status: "ok"和latency_ms < 200,双重校验才算健康。
注意:
virtualThreadExecutor不是Executors.newVirtualThreadPerTaskExecutor(),而是经过定制的。我们设置了maxThreads=10000,并启用了Thread.ofVirtual().name("ai-model-", 0).unstarted()来统一命名,方便在JFR(Java Flight Recorder)中追踪。这是JDK 21的特性,Spring Boot 3.2已完美支持。
3.3 Prompt治理:从“字符串拼接”到“可版本化、可灰度、可审计”的工程实践
Prompt管理是AI平台最容易被低估的环节。很多团队把它当作配置文件,放在application.yml里,结果上线后发现:销售部改了个标点符号,导致整个客服机器人回答错乱;法务部紧急下线一个条款,却忘了通知算法组更新Prompt。我们把Prompt提升为“一等公民”,建立了完整的治理流水线:
版本控制:每个
prompt_template记录version,且version遵循语义化规范(MAJOR.MINOR.PATCH)。MAJOR变更(如模型从Qwen2升级到Qwen3)必须人工审核;MINOR变更(如新增一个业务字段)需通过自动化测试;PATCH变更(如修正错别字)可自动合并。Git仓库中,Prompt模板存放在/src/main/resources/prompts/下,与代码同仓管理。灰度发布:我们不依赖K8s的Service权重,而是在
ModelRuntimeService.chat()中嵌入灰度逻辑:public ChatResponse chat(String instanceId, ChatRequest request) { String tenantId = getTenantIdFromRequest(request); String promptVersion = promptVersionResolver.resolve(tenantId, instanceId); // 灰度规则:租户ID哈希值 % 100 < 5,则用v2.1.0,否则用v2.0.0 PromptTemplate template = promptTemplateService.findByModelIdAndVersion( instanceId, promptVersion); String renderedPrompt = promptEngine.render(template.getContent(), request.getContext()); // ... 后续调用 }这样,灰度开关完全在Java层控制,无需修改基础设施,且可精确到租户、模型、甚至用户ID级别。
审计追踪:所有Prompt的CRUD操作,都通过
PromptAuditService记录。它不只记录“谁在什么时候改了什么”,更记录“改完之后,哪些租户的请求实际使用了这个版本”。我们用@EventListener监听PromptUpdatedEvent,然后异步查询最近24小时的调用日志,生成一份《变更影响范围报告》,自动邮件发送给相关负责人。这让我们在一次误操作中,5分钟内定位到受影响的3个租户,并手动回滚了他们的Prompt版本。安全沙箱:Prompt内容中允许嵌入变量(如
{{user_input}}),但绝不允许执行任意代码。我们的promptEngine在解析时,会先用正则\\{\\{[^}]+\\}\\}提取所有变量名,然后白名单校验:只允许user_input、system_prompt、tenant_config等预定义键。任何试图写{{#if true}}...{{/if}}或${system.getProperty('os.name')}的尝试,都会被拦截并记录为安全事件。
这套治理机制,让Prompt从“魔法字符串”变成了“可测试、可回滚、可审计”的软件资产。上线三个月后,Prompt相关的线上故障归零,平均迭代周期从3天缩短至4小时。
4. 实操关键步骤:从零搭建一个可运行的最小生产环境
4.1 环境准备:避开JDK和依赖的十大深坑
搭建环境不是mvn clean install那么简单。我们整理了新手最容易栽跟头的十个点,每一个都来自真实血泪教训:
JDK必须用Amazon Corretto 17.0.10+或Eclipse Temurin 17.0.10+:OpenJDK官方版在GraalVM编译时,对JNI调用的支持不稳定,会导致
libtensorflow_jni.so加载失败。Corretto和Temurin已打补丁。Maven必须用3.9.4+:低版本Maven在解析Spring Boot 3.x的BOM(Bill of Materials)时,会错误地将
spring-ai的2.0.1解析为2.0.0,导致AlibabaAiModel类找不到。禁用Spring Boot DevTools:开发时很方便,但生产镜像中必须排除。它会在类路径中注入
restart类加载器,与GraalVM的静态分析冲突,导致原生镜像启动时报ClassNotFoundException。**
spring-boot-maven-plugin版本锁定为3.2.5**:这是目前唯一完全兼容Spring AI 2.0.1和GraalVM 22.3的版本。其他版本在native-image阶段会报Error: Image build request failed with exit status 1`。pom.xml中必须显式声明spring-ai-alibaba的版本:Spring AI的BOM不包含Alibaba的starter,必须手动添加:<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-alibaba-spring-boot-starter</artifactId> <version>0.8.1</version> <!-- 注意:不是Spring AI主版本 --> </dependency>application.yml中spring.ai.alibaba.base-url必须以/结尾:Alibaba SDK的URL拼接逻辑有bug,如果写成https://dashscope.aliyuncs.com/api/v1,它会拼成https://dashscope.aliyuncs.com/api/v1/chat/completions,少了一个/,导致404。spring.ai.alibaba.api-key不能写在application.yml里:必须通过环境变量SPRING_AI_ALIBABA_API_KEY注入。否则,GraalVM编译时会把密钥硬编码进二进制,存在泄露风险。logging.level.org.springframework.ai=DEBUG慎开:DEBUG日志会打印完整的Prompt和Response,包含用户隐私数据。生产环境必须设为INFO,敏感字段用LoggingMasker脱敏。server.tomcat.max-connections必须调大:默认值8192在AI高并发下不够。我们设为32768,并配合server.tomcat.accept-count: 1000,避免连接拒绝。spring.main.lazy-initialization=true禁用:懒加载会延迟模型实例的初始化,导致第一个请求超时。必须设为false,确保应用启动时就完成预热。
实操心得:我们写了一个
env-check.sh脚本,每次CI/CD构建前自动运行,检查这十项。它能提前拦截90%的环境相关故障。脚本核心逻辑是:# 检查JDK版本 java -version | grep -q "17.0.10" || { echo "JDK version error"; exit 1; } # 检查Maven版本 mvn -v | grep -q "3.9.4" || { echo "Maven version error"; exit 1; } # 检查GraalVM是否安装 which native-image || { echo "GraalVM not installed"; exit 1; }
4.2 模型接入实战:以Qwen3.7为例,完成从申请Key到上线服务的全流程
我们以阿里云百炼平台的Qwen3.7模型为例,演示一个真实可落地的接入流程。这不是官方文档的复述,而是我们踩坑后总结的“最小可行路径”:
第一步:获取API Key与Endpoint
- 登录百炼控制台,进入“模型服务” → “Qwen3.7” → “API调用”
- 创建一个API Key(注意:不是AccessKey!)
- Endpoint地址形如:
https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation - 关键细节:这个Endpoint是HTTP POST地址,不是WebSocket。Spring AI Alibaba Starter默认使用
/api/v1作为base-url,所以你的application.yml应写:spring: ai: alibaba: base-url: https://dashscope.aliyuncs.com/api/v1/ api-key: ${SPRING_AI_ALIBABA_API_KEY}
第二步:定义模型配置
在model_definition表中插入一条记录:
INSERT INTO model_definition ( id, name, version, type, provider, endpoint_url, gpu_requirement, sla_p99_ms ) VALUES ( 'qwen37-prod-v1', 'qwen3.7-chat', '1.0.0', 'CHAT', 'ALIBABA', 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', 'MEDIUM', 3500 );第三步:编写Prompt模板
在prompt_template表中插入:
INSERT INTO prompt_template ( id, name, content, model_id, version, is_active, created_by ) VALUES ( 'qwen37-default-prompt', '电商客服标准Prompt', '你是一名专业的电商客服助手。请用中文回答,语气友好专业。用户问题:{{user_input}}。请严格按以下格式回复:【答案】xxx 【依据】xxx', 'qwen37-prod-v1', '1.0.0', true, 'admin' );第四步:启动服务并预热
- 启动Spring Boot应用,观察日志:
Loaded model qwen3.7-chat in 42.3s - 调用预热接口:
POST /api/v1/model/qwen37-prod-v1/warmup - 该接口会发送一个
{"messages":[{"role":"user","content":"你好"}]}请求,验证模型是否真正就绪。
第五步:发起首次调用
用curl测试:
curl -X POST http://localhost:8080/api/v1/chat \ -H "Content-Type: application/json" \ -H "X-Tenant-ID: t-001" \ -d '{ "modelInstanceId": "qwen37-prod-v1", "messages": [ {"role": "user", "content": "这件衣服有货吗?"} ] }'预期返回:
{ "id": "chat_abc123", "choices": [{ "message": { "role": "assistant", "content": "【答案】您好,这款衣服目前有现货。【依据】库存系统显示当前剩余12件。" } }] }实操心得:第一次调用失败,90%的原因是
endpoint_url少了一个/,或者api-key环境变量没生效。我们建议用Postman先手工调通百炼的原始API,再接入Spring AI,避免问题叠加。另外,Qwen3.7的max_tokens默认是1024,如果Prompt太长,会直接截断,务必在ChatOptions中显式设置setMaxTokens(2048)。
4.3 监控与告警:让AI服务像数据库一样“看得见、管得住”
没有监控的AI服务,就像没有仪表盘的飞机。我们基于Micrometer + Prometheus + Grafana,构建了一套开箱即用的监控体系,重点关注三类指标:
模型层指标(
model_前缀):model_call_total{model="qwen3.7",status="success"}:成功调用次数model_call_duration_seconds_bucket{model="qwen3.7",le="5.0"}:延迟分布直方图model_token_usage_total{model="qwen3.7",type="input"}:输入token总数model_instance_status{model="qwen3.7",status="ready"}:实例就绪数
运行时层指标(
ai_runtime_前缀):ai_runtime_queue_size:等待调度的请求队列长度ai_runtime_thread_count:活跃虚拟线程数ai_runtime_cache_hit_ratio:Prompt模板缓存命中率
基础设施层指标(
jvm_/process_前缀):jvm_memory_used_bytes{area="heap"}:堆内存使用process_cpu_usage:CPU使用率http_server_requests_seconds_count{uri="/api/v1/chat",status="500"}:HTTP错误率
告警规则我们设定了四级阈值:
| 指标 | 严重级别 | 阈值 | 处理动作 |
|---|---|---|---|
model_call_duration_seconds_bucket{le="5.0"} < 0.95 | P0 | 连续5分钟低于95% | 电话告警,自动触发/actuator/health检查 |
model_instance_status{status="unhealthy"} > 0 | P1 | 持续2分钟 | 企业微信告警,自动执行/api/v1/model/{id}/reload |
ai_runtime_queue_size > 100 | P2 | 持续1分钟 | 钉钉告警,自动扩容Pod副本数 |
jvm_memory_used_bytes{area="heap"} > 0.9 | P3 | 单次触发 | 邮件告警,记录GC日志 |
注意:Spring Boot Actuator的
/actuator/metrics端点默认暴露所有指标,但生产环境必须加权限制。我们在management.endpoints.web.exposure.include中只开放health,info,metrics,prometheus,并用Spring Security保护/actuator/**路径,只允许运维IP段访问。这是合规审计的硬性要求。
5. 常见问题排查:那些让你凌晨三点还在看日志的典型故障
5.1 故障速查表:从现象到根因的精准定位
我们把三年来积累的线上故障,浓缩成一张速查表。它不按“错误码”分类,而是按“你看到的现象”来组织,直击要害:
| 现象 | 可能根因 | 排查命令/步骤 | 解决方案 |
|---|---|---|---|
所有模型调用返回500 Internal Server Error,日志无堆栈 | spring-ai-alibabastarter未正确引入,或版本不匹配 | mvn dependency:tree | grep "spring-ai",确认spring-ai-alibaba-spring-boot-starter在依赖树中 | 检查pom.xml,确保<version>与Spring AI主版本兼容(0.8.1对应2.0.1) |
| 模型加载耗时超过2分钟,且CPU飙升至100% | JDK未启用ZGC或Shenandoah,GC停顿导致加载卡死 | jstat -gc <pid>,观察GCT(GC总耗时)是否>30s | 在JAVA_OPTS中添加-XX:+UseZGC -Xms4g -Xmx4g,重启应用 |
chat()调用偶尔超时(>30s),但模型服务本身响应正常 | Tomcat线程池耗尽,新请求排队等待 | curl http://localhost:8080/actuator/threaddump | jq '.threads[] | select(.state=="WAITING") | .stackTrace' | 增大server.tomcat.max-threads=500,并启用虚拟线程spring.task.execution.thread-pool.virtual=true |
| 同一Prompt,不同租户得到不同结果 | PromptTemplate未按租户隔离,content被全局缓存 | 在PromptTemplateService中打日志,输出tenantId和templateId | 确保PromptTemplateService.findByModelIdAndVersion()方法接收tenantId参数,并在缓存Key中包含它 |
model_instance_status{status="unhealthy"}持续为1 | 模型服务/health端点返回非200,或响应体无status: "ok" | curl -v https://your-model-endpoint/health,检查HTTP状态码和响应体 | 修改模型服务的健康检查逻辑,或在ModelInstance.isHealthy()中放宽校验条件 |
Grafana中model_call_total为0,但HTTP请求日志显示有调用 | Micrometer Registry未正确绑定到Spring AI的ObservationRegistry | curl http://localhost:8080/actuator/metrics/model.call.total,确认指标存在 | 在@Configuration类中,@Bean ObservationRegistry observationRegistry(),并注入到ChatClient构造器中 |
这张表的价值,在于它跳过了“先Google错误信息”的低效环节,直接从运维视角出发,给出可执行的诊断指令。我们要求所有值班工程师,必须把这张表打印出来贴在显示器边框上。
5.2 一个真实案例:P99延迟从1200ms飙到8500ms的根因分析
这是去年双十一大促期间的真实故障。现象是:Qwen3.7模型的P99延迟在1小时内从1200ms飙升至8500ms,但CPU、内存、GPU利用率均正常。我们按速查表流程排查:
- 确认现象:
curl http://prometheus:9090/api/v1/query?query=model_call_duration_seconds_p99{model="qwen3.7"},确认指标真实。 - 检查线程:
jstack <pid> \| grep "qwen" \| wc -l,发现237个线程处于BLOCKED状态,全部在org.springframework.ai.alibaba.AlibabaChatClient.chat()方法中。 - 深入堆栈:取一个BLOCKED线程的完整堆栈,发现它卡在:
这说明HTTP连接池被锁死了。at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.releaseConnection(PoolingHttpClientConnectionManager.java:332) - waiting to lock <0x000000071a2b3c40> (a org.apache.http.impl.conn.PoolingHttpClientConnectionManager) - 检查连接池配置:发现
spring.ai.alibaba.http.connection-manager.max-total=20,而并发请求峰值达200+,连接池满后,所有线程都在等连接释放。 - 根因定位:Spring AI Alibaba Starter默认的
PoolingHttpClientConnectionManager未配置setValidateAfterInactivityMillis(3000),导致空闲连接未及时清理,连接池被无效连接占满。
解决方案:
- 立即:`curl -X POST http