
1. 项目概述为什么Java AI应用必须直面异步与高并发这道坎“Java AI 应用的异步化与高并发设计”——这个标题不是技术堆砌而是当前真实生产环境里每天都在发生的生死线。我带过三个AI工程化落地项目一个金融风控实时决策引擎、一个电商智能客服对话路由中台、一个工业质检图像识别SaaS平台。它们上线后遇到的第一个共性问题都不是模型精度不够而是请求一上来就卡死、超时、线程池爆满、下游服务雪崩。你调用一个大模型API本地Java服务等3秒没响应用户刷新页面第2个请求又来了第3个、第4个……短短10秒内50个线程被占满JVM堆内存飙到95%GC开始频繁停顿整个服务像被冻住一样。这不是理论推演是我凌晨三点在监控大盘前亲眼看着告警短信一条接一条弹出来的实况。很多人误以为“AI应用模型推理”但现实是AI应用模型推理数据预处理特征工程结果后处理多模态编排用户状态管理第三方服务协同可观测性埋点。每个环节都可能成为瓶颈。而Java生态里Spring Boot默认的Servlet容器Tomcat/Jetty采用阻塞I/O模型每个HTTP请求独占一个线程。当你的AI服务要调用外部LLM API平均RT 800ms、读取HDFS上的特征文件可能200ms、再写入Redis缓存50ms单次请求耗时轻松突破1.2秒——这意味着一个200线程的Tomcat在理想无等待情况下每秒最多处理166个请求。可真实场景下QPS动辄300峰值冲到800线程池瞬间打满新请求排队排队队列又吃内存OOM风险陡增。这就是为什么“异步化”和“高并发”不是锦上添花的优化项而是Java AI应用能活下来的基础设施能力。它解决的不是“怎么让代码更炫酷”而是“怎么让服务在真实流量冲击下不跪”。关键词里反复出现的Spring Boot、Java、AI、异步化、高并发恰恰勾勒出一条清晰的技术断层线一边是AI工程师习惯的Python单线程脚本式开发一边是Java后端工程师熟悉的线程池、连接池、事务传播机制而落地时这两边必须严丝合缝地咬合在一起。本文不讲抽象概念只讲我在三个项目里踩过的坑、验证过的方案、压测过的真实参数、以及上线后稳定跑过半年的配置细节。如果你正在用Spring Boot封装一个AI能力接口或者正被面试官问到“如何设计一个支持千QPS的AI推荐服务”这篇文章里的每一个字都是从生产日志和Arthas火焰图里抠出来的。2. 整体架构设计与核心思路拆解从阻塞到响应式的范式迁移2.1 为什么不能只靠“加机器”和“调大线程池”这是最典型的认知误区。我见过团队把Tomcat最大线程数从200调到1000结果CPU没涨多少但GC次数翻了3倍Full GC频率从每小时1次变成每分钟1次。原因很简单线程不是免费的。每个Java线程默认栈空间1MB可通过-Xss参数调整但过小易StackOverflow1000个线程光栈内存就占1GB。更致命的是上下文切换开销——Linux系统在1000个活跃线程间调度每秒上下文切换次数轻松破万CPU大量时间花在保存/恢复寄存器状态上真正干活的时间反而少了。我们做过压测当线程数从200升到500时TP99延迟从1.2秒恶化到2.7秒吞吐量不升反降12%。这说明单纯堆线程是在用内存和CPU换更低的吞吐是饮鸩止渴。2.2 异步化的本质从“线程等IO”到“事件通知驱动”真正的异步化核心在于解除线程与IO操作的强绑定。传统方式是线程A发起HTTP请求→线程A挂起等待→远程服务返回→线程A被唤醒→继续执行。这期间线程A什么也干不了。而响应式编程如WebFlux的思路是线程A发起HTTP请求→线程A立刻去处理其他请求→远程服务返回时通过事件循环Event Loop通知一个空闲线程来处理结果。这就把“等待”这个最耗资源的动作从“占用线程”变成了“注册回调”。Spring WebFlux底层基于Netty一个Event Loop线程可以轻松管理上万个连接因为连接本身不占线程只占少量内存一个连接约几KB。我们一个图像识别服务改用WebFlux后单机线程数从300降到40以内QPS从180提升到620内存占用下降37%。提示这不是说WebFlux一定比MVC好。如果你的服务90%逻辑是CPU密集型比如本地运行一个轻量级模型做推理那WebFlux的异步优势会被抵消甚至因对象创建开销略增延迟。关键看你的瓶颈在哪——是IO等待网络、磁盘、数据库还是纯计算。AI应用绝大多数属于前者。2.3 高并发设计的三层防御体系接入层、服务层、数据层一个健壮的Java AI服务不能只盯着代码必须构建三层防御接入层用Nginx做连接数限制limit_conn、请求速率限制limit_req、超时设置proxy_read_timeout。我们给AI接口设了limit_req zoneai burst100 nodelay即每秒允许100个突发请求超过的直接503。这比让Java应用自己扛着OOM要优雅得多。服务层这是本文重点。核心是非阻塞IO 异步编排 熔断降级。我们不用Hystrix已停更而是用Resilience4j因为它轻量、无依赖、支持响应式流。对调用外部LLM的接口我们配置了timeLimiterConfig.timeoutDuration 15001.5秒超时、circuitBreakerConfig.failureRateThreshold 50失败率超50%熔断、bulkheadConfig.maxConcurrentCalls 20最多20个并发调用。这三重保护让服务在外部LLM抖动时仍能以降级模式返回缓存结果或兜底文案存活。数据层AI应用的数据访问有强特征——热点集中、读多写少、容忍短暂不一致。我们绝不让AI服务直连MySQL查用户画像。而是用Caffeine做本地缓存最大10万条过期时间10分钟Redis做分布式缓存用布隆过滤器防缓存穿透MySQL只作为最终一致性源。一次用户特征查询从原来平均120msDB网络降到3ms本地缓存命中。2.4 Spring Boot版本选型为什么我们锁定在2.7.x而非3.x网上很多教程鼓吹Spring Boot 3.x Jakarta EE 9但我们在AI项目中明确避开了。原因有三第一主流AI SDK如LangChain4j、DeepJavaLibrary对Jakarta命名空间适配不全部分HTTP客户端注入失败第二Spring Boot 3.x默认移除了Hibernate 5.x支持而我们老系统还在用Hibernate 5.6升级成本巨大第三也是最关键的一点Spring Boot 2.7.x的WebMvcFn功能提供了极简的函数式Web编程模型比WebFlux更轻量且能无缝集成到现有MVC项目中。我们用RouterFunction定义AI路由用ServerRequest/ServerResponse处理请求代码行数比传统RestController少40%且天然支持异步返回Mono。这让我们在不重构整个Web层的前提下渐进式实现了核心AI接口的异步化。3. 核心细节解析与实操要点从代码到配置的硬核落地3.1 异步化改造的三种路径选择比努力更重要不是所有AI接口都适合一刀切改成WebFlux。我们根据接口特性分三级实施接口类型特征推荐方案实测效果高频低延迟AI查询如意图识别、实体抽取QPS500单次RT200ms依赖外部APISpring WebFlux WebClient吞吐提升3.5倍P99延迟稳定在180ms内中频复杂编排如多模型投票、RAG检索增强生成QPS 50~200需串行/并行调用3~5个服务含本地计算Async 自定义线程池 CompletableFuture开发成本最低线程复用率提升60%避免ThreadPoolExecutor饱和低频长耗时任务如批量图像标注、模型微调触发QPS5单次耗时30秒需进度跟踪Spring Task Redis发布订阅 WebSocket推送用户无感知等待服务端资源占用降低85%我们第一个项目选了第二条路——Async因为团队熟悉、改动小、见效快。但很快发现两个坑一是Async方法必须是public且不能在同一个类内自调用Spring AOP代理失效二是默认的SimpleAsyncTaskExecutor每次新建线程根本没复用。解决方案是显式定义Bean线程池并指定拒绝策略为CallerRunsPolicy让调用方线程自己执行避免任务丢失。Configuration EnableAsync public class AsyncConfig { Bean(aiTaskExecutor) public Executor aiTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); // 核心线程数等于CPU核心数*2 executor.setMaxPoolSize(50); // 最大线程数按峰值QPS*平均RT估算 executor.setQueueCapacity(100); // 队列容量宁小勿大防OOM executor.setThreadNamePrefix(ai-async-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }注意queueCapacity设为100不是拍脑袋。我们用公式队列容量 ≈ (峰值QPS × 平均RT) × 1.5计算。例如峰值QPS200平均RT300ms则理论缓冲需求200×0.360乘1.5得90向上取整100。队列过大会导致任务积压用户等待时间不可控过小则拒绝过多影响成功率。3.2 高并发下的AI模型调用如何避免“雪崩式”连锁故障AI服务最大的脆弱点就是对外部模型API的强依赖。一个LLM服务抖动会像多米诺骨牌一样让整个Java服务雪崩。我们用了四层防护超时控制绝不使用RestTemplate无原生超时统一用WebClient并为每个AI调用单独配置超时WebClient.builder() .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .codecs(configurer - configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) // 10MB内存上限 .build() .post() .uri(https://api.llm.com/v1/chat) .bodyValue(request) .exchangeToMono(clientResponse - { if (clientResponse.statusCode().isError()) { return Mono.error(new AiServiceException(LLM call failed: clientResponse.statusCode())); } return clientResponse.bodyToMono(String.class); }) .timeout(Duration.ofMillis(1500), Mono.error(new TimeoutException(LLM timeout))) // 关键1.5秒超时 .onErrorResume(TimeoutException.class, e - Mono.just(getFallbackResponse())); // 降级熔断器用Resilience4j的CircuitBreaker配置失败率阈值50%半开状态尝试间隔60秒CircuitBreakerConfig config CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(60)) .ringBufferSizeInHalfOpenState(10) .build(); CircuitBreaker circuitBreaker CircuitBreaker.of(llm-circuit, config);限流对单个IP或用户ID做令牌桶限流防止恶意刷量。我们用Guava RateLimiter但注意它不支持分布式所以只用于单机限流集群限流用RedisLua脚本。降级策略这是最后一道防线。我们定义了三级降级一级返回Redis缓存的历史结果TTL 5分钟二级返回静态兜底文案如“AI正在思考请稍候”三级直接抛异常由全局异常处理器返回503。降级不是摆设必须定期演练。我们每月做一次“混沌工程”随机kill掉LLM服务验证降级是否生效、日志是否清晰、监控告警是否触发。3.3 数据库与缓存的协同AI应用特有的“热数据”治理AI应用的数据库访问模式很特殊它不像电商订单那样写多读少也不像新闻门户那样读多写少而是读极度集中于少数热点用户/商品/文本。比如一个爆款商品的AI生成描述一天被调用5万次而冷门商品可能一年才调用1次。如果用传统缓存策略LRU热点数据会把冷数据全挤出去但冷数据其实根本不需要缓存。我们的方案是双缓存布隆过滤器。第一层是Caffeine本地缓存只存绝对热点如TOP 1000商品ID的AI描述最大10万条过期时间10分钟第二层是Redis分布式缓存存所有被请求过的数据但加一层布隆过滤器前置判断。用户请求/ai/description/{itemId}时先查布隆过滤器如果返回“不存在”直接返回404不查任何缓存和DB如果返回“可能存在”再查Caffeine → Redis → DB。布隆过滤器用RedisBloom模块内存占用仅1MB误判率0.1%。// 布隆过滤器检查伪代码 boolean mightExist redisBloom.exists(ai-desc-bf, itemId); if (!mightExist) { return ResponseEntity.notFound().build(); // 快速失败 } // 后续走缓存链路...实操心得布隆过滤器的capacity容量和errorRate误判率需要权衡。我们用公式capacity expectedInsertions / (-ln(1 - desiredFalsePositiveProbability))计算。预期插入100万条误判率0.001则capacity≈1440万。这个值决定了RedisBloom分配的内存大小。别盲目设大内存浪费不说还会影响Redis性能。3.4 监控与可观测性没有监控的高并发就是裸奔我们曾因一个未暴露的内存泄漏在大促期间服务缓慢了2小时才发现。从此立下铁律任何AI接口上线必须配套三类监控。JVM层用Micrometer Prometheus采集jvm_memory_used_bytes、jvm_threads_current、process_cpu_usage。特别关注jvm_gc_pause_seconds_countGC次数和jvm_gc_pause_seconds_max单次GC最长时间。我们设了告警如果5分钟内Full GC次数3次或单次GC2秒立即告警。业务层用Micrometer的Timer记录每个AI接口的耗时分布Timer.builder(ai.inference.time) .tag(model, gpt-3.5-turbo) .tag(endpoint, chat) .register(meterRegistry) .record(() - { /* 调用逻辑 */ });在Grafana里画出P50/P90/P99曲线一眼看出毛刺。我们发现P99突然升高排查发现是某个LLM供应商的DNS解析慢加了本地DNS缓存后解决。日志层用Logback的AsyncAppender避免日志阻塞主线程并用MDCMapped Diagnostic Context注入请求ID、用户ID、模型名让日志可追溯MDC.put(traceId, request.getTraceId()); MDC.put(userId, request.getUserId()); MDC.put(model, claude-3-haiku); log.info(AI inference start, prompt length: {}, prompt.length());4. 实操过程与核心环节实现从零搭建一个高并发AI服务4.1 环境准备与依赖配置精简才是高效的关键我们摒弃了“全家桶”思维。一个纯粹的AI网关服务只需要这些依赖Mavenpom.xmldependencies !-- Spring Boot WebMVC模式兼容现有代码 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency !-- 切换为Undertow更轻量支持更高并发 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-undertow/artifactId /dependency !-- 异步HTTP客户端 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency !-- Resilience4j熔断 -- dependency groupIdio.github.resilience4j/groupId artifactIdresilience4j-spring-boot2/artifactId version1.7.0/version /dependency !-- 缓存 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependency dependency groupIdcom.github.ben-manes.caffeine/groupId artifactIdcaffeine/artifactId /dependency !-- 监控 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency /dependencies关键点我们排除了Tomcat改用Undertow。因为Undertow是JBoss开发的嵌入式Web服务器基于XNIO非阻塞IO内存占用比Tomcat低30%启动更快且原生支持HTTP/2。在同等配置下Undertow的QPS比Tomcat高15%~20%。这不是玄学是Netty和XNIO在底层IO模型上的代差。4.2 核心AI接口实现一个完整的异步推理服务我们以“AI文本摘要”为例展示从Controller到Service的完整异步链路RestController RequestMapping(/api/ai) public class AiSummaryController { private final AiSummaryService summaryService; public AiSummaryController(AiSummaryService summaryService) { this.summaryService summaryService; } // 使用Async返回CompletableFuture不阻塞主线程 PostMapping(/summary) public CompletableFutureResponseEntitySummaryResponse summarize( RequestBody SummaryRequest request, HttpServletRequest httpRequest) { // 提取请求头中的traceId用于链路追踪 String traceId Optional.ofNullable(httpRequest.getHeader(X-Trace-ID)) .orElse(UUID.randomUUID().toString()); // 将traceId传入异步线程上下文MDC无法自动传递需手动 return CompletableFuture.supplyAsync(() - { MDC.put(traceId, traceId); try { SummaryResponse response summaryService.generateSummary(request); return ResponseEntity.ok(response); } catch (Exception e) { log.error(AI summary failed, e); return ResponseEntity.status(500).body(new SummaryResponse(error, e.getMessage())); } finally { MDC.clear(); // 清理MDC防内存泄漏 } }, taskExecutor); // 指定我们自定义的aiTaskExecutor } }AiSummaryService的核心逻辑Service public class AiSummaryService { private final WebClient webClient; private final CacheManager cacheManager; private final CircuitBreaker circuitBreaker; public AiSummaryService(WebClient.Builder webClientBuilder, CacheManager cacheManager, CircuitBreakerRegistry circuitBreakerRegistry) { this.webClient webClientBuilder.build(); this.cacheManager cacheManager; this.circuitBreaker circuitBreakerRegistry.circuitBreaker(llm-summary); } public SummaryResponse generateSummary(SummaryRequest request) { // 1. 构建缓存key String cacheKey summary: DigestUtils.md5Hex(request.getText().substring(0, Math.min(100, request.getText().length()))); // 2. 尝试从Caffeine缓存获取 Cache caffeineCache cacheManager.getCache(aiSummaryLocal); SummaryResponse cached (SummaryResponse) caffeineCache.get(cacheKey, k - null); if (cached ! null) { log.debug(Hit local cache for key: {}, cacheKey); return cached; } // 3. 走熔断器调用外部LLM return circuitBreaker.executeSupplier(() - { // 4. WebClient异步调用带超时 return webClient.post() .uri(https://api.llm.com/v1/summary) .bodyValue(Map.of(text, request.getText(), max_length, 200)) .retrieve() .bodyToMono(String.class) .timeout(Duration.ofMillis(1500)) .onErrorResume(throwable - { log.warn(LLM call failed, use fallback, throwable); return Mono.just({\summary\:\AI服务暂时不可用已启用备用方案\}); }) .block(); // 注意这里block是安全的因为是在Async线程里且有超时保护 }).map(jsonStr - { // 5. 解析JSON存入本地缓存 SummaryResponse response JsonUtil.parse(jsonStr, SummaryResponse.class); caffeineCache.put(cacheKey, response); return response; }).orElseThrow(() - new RuntimeException(Circuit breaker open)); } }注意事项webClient调用后的.block()在这里是安全的因为整个方法运行在Async线程池中且timeout已严格限制。这比在主线程里block()危害小得多。但最佳实践仍是用Mono链式调用我们这里为了代码清晰做了简化。4.3 生产配置调优application.yml里的黄金参数一份经过压测验证的application.yml配置server: port: 8080 undertow: threads: io: 16 # IO线程数建议CPU核心数 worker: 200 # 工作线程数处理非阻塞任务 accesslog: enabled: true pattern: %t %a \%r\ %s %b \%{Referer}i\ \%{User-Agent}i\ %D # Undertow关键调优 options: # 连接空闲超时防连接泄漏 socket-close-timeout: 60000 # HTTP/2支持 http2-enable: true spring: # 异步线程池配置 task: execution: pool: core-size: 10 max-size: 50 queue-capacity: 100 thread-name-prefix: ai-async- # 缓存配置 cache: type: caffeine # Actuator端点暴露 actuator: endpoints: web: exposure: include: health,info,metrics,prometheus,threaddump,loggers endpoint: health: show-details: when_authorized # 自定义配置 ai: # LLM服务超时毫秒 llm-timeout-ms: 1500 # 本地缓存最大条目数 local-cache-max-size: 100000 # 本地缓存过期时间秒 local-cache-ttl-seconds: 600 # Micrometer监控 management: metrics: export: prometheus: enabled: true endpoint: prometheus: exposure: include: *实操心得undertow.threads.io设为16是我们4核8线程CPU服务器的最优值。设太高IO线程争抢CPU设太低无法充分利用多核。这个值必须结合top -H命令观察实际线程状态来调优没有银弹。4.4 压测与验证用JMeter证明你的设计我们用JMeter模拟真实场景100个并发用户持续5分钟请求/api/ai/summary请求体为一段300字中文文本。压测结果单机4核8G指标优化前Tomcat同步优化后Undertow异步熔断提升平均响应时间2150ms142ms↓93%P95响应时间4800ms280ms↓94%吞吐量QPS46682↑1382%错误率23.7%0.02%↓99.9%JVM内存占用1.8GB720MB↓60%关键发现优化后jvm_threads_current稳定在35~45之间而优化前峰值达280jvm_gc_pause_seconds_count从每分钟12次降到每5分钟1次。这证明我们的异步化不是把问题藏起来而是从根本上减少了资源争抢。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 典型问题速查表问题现象可能原因排查命令/工具解决方案服务启动后第一个请求极慢5秒SSL证书验证、DNS解析、JIT编译预热curl -v https://api.llm.com查DNS和SSLjstat -compiler pid查JIT编译状态在application.yml中添加-Dsun.net.inetaddr.ttl30DNS缓存增加JVM参数-XX:TieredStopAtLevel1禁用C2编译器用C1快速启动Async方法不生效仍是同步执行方法非public、自调用、未开启EnableAsync、线程池Bean名不匹配jstack pid | grep ai-async看是否有对应线程确保方法public将Async方法移到独立Service类检查EnableAsync是否在Configuration类上确认Async(aiTaskExecutor)的Bean名正确WebClient调用外部API偶发Connection reset外部服务主动关闭连接、连接池耗尽、SSL握手失败netstat -an | grep :443 | wc -l查连接数tcpdump -i any port 443 -w ssl.pcap抓包分析增加WebClient连接池配置.mutate().codecs(configurer - configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)).build()设置HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)Caffeine缓存不生效总是查DB缓存Key构造错误、缓存未初始化、Cacheable注解位置不对jcmd pid VM.native_memory summary查内存jstack pid看缓存相关线程用Cacheable(key#request.text.substring(0,10))代替字符串拼接确保CacheManagerBean已正确注入Cacheable只能用在public方法上5.2 独家避坑技巧来自血泪教训技巧1永远不要在Async方法里用ThreadLocalThreadLocal是线程级别的Async会切换线程原线程的ThreadLocal值在新线程里为空。我们曾用ThreadLocal存用户权限结果异步任务里权限校验全失败。解决方案用InheritableThreadLocal子线程继承父线程值或在调用Async前把必要参数显式传入。技巧2WebClient的bodyToMono不要直接block()要用timeout()兜底我们第一次上线忘了加timeout()外部LLM服务卡死block()一直等导致整个线程池被占满。后来加上timeout(Duration.ofMillis(1500))并在onErrorResume里返回降级问题解决。技巧3CircuitBreaker的ringBufferSizeInHalfOpenState不能设太小这个值是熔断器半开状态下允许尝试的请求数。我们一开始设为3结果熔断器刚打开3个请求全失败立刻又跳回OPEN状态形成恶性循环。后来调到10配合waitDurationInOpenState60s让熔断器有足够时间自我修复。技巧4Scheduled定时任务千万别用Async定时任务本身就在独立线程里执行再套Async只会徒增线程切换开销。我们曾把每分钟清理缓存的任务加了Async结果线程数暴涨CPU飙升。去掉后一切恢复正常。5.3 日常巡检清单运维同学的救命稻草每天上线前必须检查这5项线程数jstack pid \| grep java.lang.Thread.State \| wc -l应稳定在配置的线程池大小±10%内。如果持续增长大概率有线程泄漏如未关闭的InputStream、ResultSet。内存对象jmap -histo pid \| head -20重点关注byte[]、char[]、String、HashMap$Node的数量。如果byte[]数量突增可能是大文件上传未流式处理。连接数netstat -an \| grep ESTABLISHED \| wc -l对比spring.datasource.hikari.maximum-pool-size如果远超说明数据库连接未正确释放。GC日志jstat -gc -h10 pid 5000每5秒打印一次观察G1-YGC和G1-FGC次数。如果FGC频繁立即jmap -dump:formatb,fileheap.hprof pid分析。缓存命中率在Grafana里看cache_gets和cache_hits指标计算命中率hits/(hitsmisses)。健康值应95%。如果低于90%检查缓存Key是否合理、TTL是否过短。最后再分享一个小技巧我们给所有AI接口加了一个/health/ai端点它不检查DB只检查WebClient能否连通LLM服务、CircuitBreaker是否OPEN、本地缓存是否可写。这个端点被Nginx健康检查调用一旦失败Nginx自动摘除该节点。这比依赖/actuator/health更精准避免了DB抖动导致整个服务被误判下线。