
关键词:DeepSeek、Spring Boot 3、OkHttp 连接池、SSE 流式输出、ReadTimeoutException
最近公司在做内部 AI 助手项目,技术选型为 Java 21 + Spring Boot 3.2,大模型 API 使用 DeepSeek(兼容 OpenAI 接口协议)。
在开发环境联调时一切正常,但上线预发布环境后,只要并发用户数超过 5 个,系统就会频繁抛出以下异常:
2026-08-17 14:23:11.123 ERROR [http-nio-8080-exec-8] o.s.web.client.RestTemplate :
I/O error on POST request for "https://api.deepseek.com/v1/chat/completions":
Read timed out; nested exception is java.net.SocketTimeoutException: Read timed out更严重时,整个 Tomcat 线程池被阻塞,健康检查接口 /actuator/health 都无法响应,导致 K8s 频繁重启 Pod。
很多文章上来就贴代码,但从不解释为什么。这里我们先分析底层原理。
Spring Boot 自动注入的 RestTemplate 或 WebClient,如果不做特殊配置,底层默认使用 JDK 的 HttpURLConnection。它有两个致命问题:
connectTimeout 和 readTimeout 为无限(0),但实际网络环境不可能无限等待。DeepSeek 高峰期生成长文本可能需要 10s+,一旦网络抖动,线程将永久阻塞。我们使用了 spring-ai-openai-spring-boot-starter(版本 1.0.0-M6),虽然它封装了 ChatClient,但其内部默认的 RestClient 同样没有配置专用连接池,且默认的 readTimeout 为 60 秒,看似够长,却忽略了连接耗尽时的队列等待。
当并发请求超过底层连接池最大连接数时,新请求会进入排队等待可用连接。若排队等待时间 + 模型推理时间 > 60s,依然会抛出超时异常。
在 pom.xml 中,不仅要引入 Spring AI,还要强制引入 OkHttp 并排除默认的 Netty(如果用了 WebFlux)。
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<dependencies>
<!-- Spring AI 核心 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- 关键:替换默认 HTTP 客户端为 OkHttp,支持连接池 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- 可选:若使用 WebFlux 流式,需排除冲突 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>我们需要手动创建 OkHttpClient 的 Bean,并注入到 Spring AI 的 OpenAiChatModel 中。
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Configuration
public class DeepSeekConfig {
@Value("${spring.ai.openai.api-key}")
private String apiKey;
@Value("${spring.ai.openai.base-url}")
private String baseUrl;
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
// 1. 连接超时:握手阶段最多等 10s
.connectTimeout(Duration.ofSeconds(10))
// 2. 读取超时:等待大模型返回第一个 Token 的时间(TTFT)
// 注意:这里设为 120s,是因为长文本生成慢,但配合下文流式,我们会缩短这个值
.readTimeout(Duration.ofSeconds(120))
// 3. 写入超时:发送 Prompt 的时间(通常极短)
.writeTimeout(Duration.ofSeconds(30))
// 4. 【核心】连接池配置:MaxIdle=50,存活时间 5 分钟
.connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES))
// 5. 重试机制:网络抖动自动重试一次(但注意幂等性)
.retryOnConnectionFailure(true)
.build();
}
@Bean
public OpenAiChatModel deepSeekChatModel(OkHttpClient okHttpClient) {
// 使用 OkHttp 构建 Spring AI 的底层 Api
OpenAiApi openAiApi = new OpenAiApi(baseUrl, apiKey, okHttpClient);
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.options(OpenAiChatOptions.builder()
.model("deepseek-chat") // DeepSeek 模型名
.temperature(0.7)
.maxTokens(4096)
.build())
.build();
}
}同步调用会一直占用 Tomcat 线程等待 120s,高并发下线程池必然耗尽。必须使用 SSE(Server-Sent Events) 流式返回。
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class AIChatService {
private final OpenAiChatModel chatModel;
public AIChatService(OpenAiChatModel chatModel) {
this.chatModel = chatModel;
}
// 流式调用,返回 Flux
public Flux<String> chatStream(String userMessage) {
Prompt prompt = new Prompt(new UserMessage(userMessage));
// Spring AI 的 stream 方法底层基于 WebClient,此时配合 OkHttp 的 EventSource
return chatModel.stream(prompt)
.map(response -> response.getResult().getOutput().getContent())
.onErrorResume(e -> {
// 流式中断时的降级处理:返回友好提示
return Flux.just("【系统提示】AI 服务响应超时,请稍后重试。");
});
}
}import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class ChatController {
private final AIChatService chatService;
public ChatController(AIChatService chatService) {
this.chatService = chatService;
}
@GetMapping(value = "/ai/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestParam String msg) {
// 前端使用 EventSource 接收
return chatService.chatStream(msg);
}
}application.yml 细节spring:
ai:
openai:
api-key: ${DEEPSEEK_API_KEY} # 从环境变量读取,禁止硬编码
base-url: https://api.deepseek.com/v1 # DeepSeek 官方地址
# 调整 Tomcat 线程池,防止排队过长
server:
tomcat:
threads:
max: 200 # 最大线程数
min-spare: 10
accept-count: 50 # 等待队列长度
connection-timeout: 120000 # 连接超时 120s在优化前后,我们使用 JMeter 模拟 20 并发用户,持续压测 5 分钟,结果如下:
指标 | 优化前(默认 RestTemplate) | 优化后(OkHttp 连接池 + 流式) |
|---|---|---|
平均响应时间 | 15.2s(大量超时重试拉高平均) | 2.1s(TTFT 首字延迟) |
超时异常率 | 67% | 0.2%(偶发网络抖动) |
CPU 负载 | 85%(线程频繁切换上下文) | 45% |
活跃 TCP 连接数 | 瞬时峰值 150+(每次新建) | 稳定在 30~50(复用连接) |
如果业务传入的 Prompt 较长(例如 RAG 检索后拼接了大量文档),DeepSeek 网关会返回 413 Payload Too Large。
解决办法:在 OkHttpClient 配置中调整发送缓冲区大小,但更优雅的方式是在网关层前置拦截,计算 Token 数并截断。
// 简易拦截器:在 Service 层判断字符长度,粗略截断(精确截断需引入 Tokenizer)
if (userMessage.length() > 12000) {
userMessage = userMessage.substring(0, 12000) + "...(内容过长已截断)";
}RestTemplate 去调用大模型 API,必须配置专用 HTTP 连接池(OkHttp 或 Apache HttpClient)。spring.ai.openai.options.timeout 配合代码配置,而不是笼统设置。本次优化后,系统已在生产环境稳定运行 2 周,未再出现因大模型响应慢导致的 Pod 重启。希望这份踩坑记录能帮助到同样在 Java 生态中接入大模型的开发者。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。