问题代码:第一版大模型调用把线程池打满了
四月我们想给工单系统接一个大模型做自动摘要。第一版我图省事,用 RestTemplate 直接 POST 到 OpenAI 兼容接口,同步等返回。压测一上来就出问题:模型平均响应 3 秒,线程池 200 个线程 10 秒就被占满,QPS 卡在 60 上不去。日志里全是 Timeout waiting for connection。
排查:同步调用的天花板
大模型生成是流式输出的,等它一次性返回再处理,既浪费连接又让用户干等。正确姿势是走 SSE(Server-Sent Events),边生成边把 token 吐给前端。我换成 JDK 11 自带的 java.net.http.HttpClient,它原生支持响应体流式订阅。
根因:流式响应要自己按行解析
OpenAI 的 SSE 格式是每行 data: {...}\n\n,结束是 data: [DONE]。HTTP 客户端不会帮你拆帧,得在 BodyHandlers 里自己处理。关键点是不能用 ofString() 等整段读完,要用 BodyHandlers.ofInputStream() 拿流,再按行切。
解决方案:SSE 流式解析
核心解析逻辑:
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/v1/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + KEY)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<InputStream> resp = client.send(req,
HttpResponse.BodyHandlers.ofInputStream());
BufferedReader br = new BufferedReader(
new InputStreamReader(resp.body(), StandardCharsets.UTF_8));
String line;
while ((line = br.readLine()) != null) {
if (line.startsWith("data: ")) {
String payload = line.substring(6).trim();
if ("[DONE]".equals(payload)) break;
JsonNode node = mapper.readTree(payload);
String token = node.at("/choices/0/delta/content").asText();
// 推给前端 WebSocket
}
}
超时与重试处理
三个坑必须兜住:
- 连接超时:
connectTimeout(5s),防止 DNS 或网络抖动一直挂起。 - 读取超时:HttpClient 本身没有整体 read timeout,得在订阅层用
CompletableFuture+orTimeout(30s)兜底,否则慢连接会一直占着流。 - 重试:只有 429(限流)和 5xx 才重试,且要带退避。我们用指数退避,最多 3 次:
int attempt = 0;
while (attempt < 3) {
HttpResponse<?> r = client.send(req, ofInputStream());
if (r.statusCode() == 429 || r.statusCode() >= 500) {
Thread.sleep((long) (500 * Math.pow(2, attempt++)));
continue;
}
break;
}
另外 429 的 Retry-After 头要尊重,限流时硬重试只会雪崩。
留个问题
关于《Java 接入大模型的第一种方式:HTTP 调用与流式响应》里这个坑,你当时是怎么处理的?欢迎在评论区聊聊你踩过的类似情况。