资讯详情

资讯详情

Spring AI 生产可观测性实战:通过 Spring Boot Actuator 暴露大模型核心调用指标

双 11 倒计时还剩不到二十天压测大屏上的 QPS 刚拉到五千告警群就炸了。运维兄弟直接把电话打到我工位上“李工你们接入的大模型服务耗时一直在往上飘平均响应时间直接飙到了八秒多下游到底在经历什么是网络超时、Provider 限流还是生成 Token 太慢现在的监控大盘全是一片灰色的调用超时根本看不出来是哪块在卡脖子。”这就是大模型落地的现实问题。写个 Demo 的时候大家只要看到屏幕上逐字吐出回答就欢天喜地但真要把大模型推到生产环境尤其是面临大促峰值流量时如果你手里没有一套颗粒度细到毫秒级、Token 级的可观测性体系大模型服务对你而言就是一个不可预测的黑盒。今天我们聊聊怎么把 Spring AI 接入 Spring Boot Actuator 与 Micrometer 观测体系把 TTFT首字延迟、端到端延迟、Token 消耗、限流拦截等核心指标彻底透明化。为什么传统的 HTTP 监控治不好大模型传统微服务架构下我们在 Spring Boot Actuator 里看http.server.requests指标就能把下游状态摸得七七八八。状态码 200 占比多少、P99 耗时多少、错误率多少一目了然。但在大模型调用场景下传统 HTTP 监控会直接抓瞎耗时分布极度诡异一个常规查询接口 P99 超过 500ms 就算性能隐患但大模型推理受限于 Prompt 长度和输出长度生成 500 个 Token 耗时三五秒是家常便饭。如果混为一谈监控大盘上的 P99 指标会被直接拉平失效。缺乏首字延迟TTFT维度在流式生成SSE场景下用户端感知最关键的不是整体什么时候结束而是第一个字什么时候蹦出来。如果整体耗时三秒但首字 300ms 出来用户体验极其流畅如果整屏卡死 2.8 秒最后瞬间弹出体验就是灾难。传统 HTTP 请求指标只能记录连接关闭的总耗时根本无法捕获流式响应的首字时间戳。计量维度从“请求数”变成“Token 数”下游供应商是按 Prompt Tokens 和 Completion Tokens 计费的大促期间每分钟的 Token 消耗速率直接挂钩真金白银。没有 Token 吞吐指标预算被打穿了你都不知道哪个业务方在狂刷。Spring AI 1.0 的观测基石Micrometer ObservationSpring AI 从底层就全面拥抱了 Spring 6 引入的Micrometer Observation抽象。只要引入了spring-boot-starter-actuatorSpring AI 在执行ChatModel.call()或ChatModel.stream()时默认会触发名为spring.ai.chat.client的 Observation 生命周期。但默认的 Observation 配置相对保守生产环境下我们需要通过自定义ChatModelObservationConvention来扩充业务标签同时补齐流式场景下的首字耗时统计。首先看 Maven 依赖依赖配置dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency /dependencies扩展大模型调用的 Observation 规约为了让暴露出来的 Prometheus 指标包含模型名称、业务场景以及租户标识我们实现自定义的ChatModelObservationConvention。这里有一条生死攸关的铁律千万不要把高基数High Cardinality数据打入指标标签。像用户 ID、订单号、甚至 Prompt 文本哈希值如果被塞进 Meter 的 Tag 里面Prometheus 会瞬间产生数以百万计的时间序列Time Series轻则 Grafana 渲染卡死重则直接导致 Prometheus 存储节点 OOM 崩盘。package com.yali.monitor.observation; import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; import org.springframework.ai.chat.observation.ChatModelObservationContext; import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.stereotype.Component; Component public class CustomChatModelObservationConvention implements ChatModelObservationConvention { private static final KeyValue STATUS_UNKNOWN KeyValue.of(call_status, UNKNOWN); private static final KeyValue STATUS_SUCCESS KeyValue.of(call_status, SUCCESS); private static final KeyValue STATUS_ERROR KeyValue.of(call_status, ERROR); Override public String getName() { // 自定义指标前缀 return spring.ai.chat.custom; } Override public KeyValues getLowCardinalityKeyValues(ChatModelObservationContext context) { // 提取低基数标签业务场景、模型类型、调用状态 String scene (String) context.computeIfAbsent(business_scene, key - default_chat); String modelName context.getRequest() ! null context.getRequest().getModel() ! null ? context.getRequest().getModel() : unknown_model; KeyValue statusValue STATUS_SUCCESS; if (context.getError() ! null) { statusValue STATUS_ERROR; } return KeyValues.of( KeyValue.of(scene, scene), KeyValue.of(model, modelName), statusValue ); } Override public KeyValues getHighCardinalityKeyValues(ChatModelObservationContext context) { // 高基数数据只进入 Tracing如 Zipkin/Tempo/SkyWalking严禁进入 Prometheus Meter if (context.getRequest() ! null) { return KeyValues.of( KeyValue.of(prompt.hash, String.valueOf(context.getRequest().getInstructions().hashCode())) ); } return KeyValues.empty(); } }精准捕获流式 TTFT 与 Token 消耗流式调用场景下我们通常希望捕获三个核心指标ai.chat.stream.ttft从发起请求到第一个 Chunk 返回的耗时Timer。ai.chat.stream.duration整个流式生成的端到端总耗时Timer。ai.chat.tokens.consumed消耗的 Token 计数器带typeprompt|completion标签。我们可以基于 AOP 或包装ChatClient实现一个拦截装饰器package com.yali.monitor.metrics; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; Component public class StreamMetricsCollector { private final MeterRegistry meterRegistry; public StreamMetricsCollector(MeterRegistry meterRegistry) { this.meterRegistry meterRegistry; } public FluxChatResponse wrapStream(FluxChatResponse rawStream, String scene, String model) { long startTime System.nanoTime(); AtomicBoolean firstTokenRecorded new AtomicBoolean(false); return rawStream .doOnNext(response - { // 记录首字延迟 TTFT if (firstTokenRecorded.compareAndSet(false, true)) { long ttftDuration System.nanoTime() - startTime; meterRegistry.timer(ai.chat.stream.ttft, scene, scene, model, model) .record(ttftDuration, TimeUnit.NANOSECONDS); } // 累加 Token 计数 if (response.getMetadata() ! null response.getMetadata().getUsage() ! null) { Long promptTokens response.getMetadata().getUsage().getPromptTokens(); Long compTokens response.getMetadata().getUsage().getGenerationTokens(); if (promptTokens ! null promptTokens 0) { meterRegistry.counter(ai.chat.tokens.consumed, scene, scene, type, prompt) .increment(promptTokens); } if (compTokens ! null compTokens 0) { meterRegistry.counter(ai.chat.tokens.consumed, scene, scene, type, completion) .increment(compTokens); } } }) .doFinally(signalType - { long totalDuration System.nanoTime() - startTime; meterRegistry.timer(ai.chat.stream.duration, scene, scene, model, model, signal, signalType.name()) .record(totalDuration, TimeUnit.NANOSECONDS); }); } }Actuator 端点与 Prometheus 抓取配置在application.yml中我们需要将指标暴露给 Prometheus 抓取器并对 Timer 配置合理的直方图桶Histogram Buckets。因为大模型的调用耗时分布从数百毫秒到几十秒不等Spring Boot 默认生成的直方图切片在超过 1 秒后基本就没有区分度了。为了在 Grafana 里绘制出漂亮的 TTFT 和端到端延迟热力图必须自定义桶间距management: endpoints: web: exposure: include: health,info,prometheus,metrics endpoint: prometheus: enabled: true health: show-details: always metrics: tags: application: ${spring.application.name} distribution: percentiles-histogram: ai.chat.stream.ttft: true ai.chat.stream.duration: true sla: ai.chat.stream.ttft: 200ms, 500ms, 1s, 2s, 5s ai.chat.stream.duration: 1s, 3s, 5s, 10s, 20s, 30s启动应用后直接访问本地的 Actuator 端点GET http://localhost:8080/actuator/prometheus可以看到类似如下的标准 Prometheus 文本格式# HELP ai_chat_stream_ttft_seconds Time To First Token for LLM stream # TYPE ai_chat_stream_ttft_seconds histogram ai_chat_stream_ttft_seconds_bucket{modelgpt-4o-mini,sceneorder_query,le0.2} 124.0 ai_chat_stream_ttft_seconds_bucket{modelgpt-4o-mini,sceneorder_query,le0.5} 3120.0 ai_chat_stream_ttft_seconds_bucket{modelgpt-4o-mini,sceneorder_query,le1.0} 4890.0 ai_chat_stream_ttft_seconds_count{modelgpt-4o-mini,sceneorder_query} 5000.0 ai_chat_stream_ttft_seconds_sum{modelgpt-4o-mini,sceneorder_query} 1982.35架构师的实战复盘与血泪告诫很多团队在双 11 前夕搞大模型监控往往把指标系统当成简单的打点工具。在过去几个大促项目的实测中我总结了两条必须严防死守的红线第一抓取超时的隐形雪崩。Prometheus 默认抓取超时往往是 10 秒。有些人在自定义 MeterBinder 里面试图去实时 ping 一下远程 LLM 服务的健康检查接口。大促期间一旦下游模型供应商延迟升高Actuator 的抓取线程直接阻塞导致 Prometheus 判定整个微服务节点挂死Kubernetes 的探针紧接着重启 Pod最终引发滚动崩溃。监控收集逻辑必须是内存级纯计算绝对禁止夹带远程 I/O。第二Token 突增与熔断联动。单靠静态告警规则很容易漏掉“慢刀子割肉”。我们在双 11 期间把rate(ai_chat_tokens_consumed_total[1m])接入了 Sentinel 动态规则。一旦发现某个非核心业务场景的每分钟 Token 消耗速率超过基线 3 倍立即无情降级到离线意图识别缓存把大模型算力资源让给核心的下单与售前咨询。没有度量就没有掌控。把大模型的每一次呼吸都变成时序数据库里的清晰轨迹大促之夜你才能坐在指挥室里安稳地喝上一杯温水。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →