Java教程

Java与AI:用 RestTemplate 或同步 HTTP 客户端调大模型,Tomcat 工作线程全部阻塞在模型调用上,改成异步之后线程池反而堆积得更快——AI 场景的“异步”到底该怎么和普通 Web 请求的异步区分开?

👤 为我痴狂 👁 56 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-10-23
首页› 理学› Java教程› 正文
DEEP DIVE / ASYNC BOUNDARY
Java 与 AI:当“异步”成为新的阻塞源——大模型调用场景下 Web 异步与推理异步的边界重构
摘要:一个典型的线上现象是:把同步 RestTemplate 调用大模型改成 CompletableFuture 之后,Tomcat 工作线程确实不再被占满,但线程池队列与连接池排队反而增长得更快,P99 延迟不降反升。本文认为,根因不在“异步写法”本身,而在于把 Web 请求的异步语义(短生命周期、CPU 与 IO 交替、可快速释放)直接套用到 AI 推理调用(长尾、外部配额受限、单位时间吞吐被上游锁死)之上。文章提出“并发预算—背压—队列分层”的分析主线,给出从线程模型、连接池、信号量到可观测性的完整改造路径,并讨论虚拟线程、响应式流与流式推理在其中的适用边界。

CONTENTS目录

01 / PROBLEM问题的提出:一次“异步改造”引发的反向劣化

先还原一个在 2024—2025 年大量出现的故障形态。某业务系统在 Java 侧通过 RestTemplate 同步调用大模型推理接口,单次调用在正常时段约 1.2—3 秒,长尾可达 20 秒以上。初期并发不高,Tomcat 默认 200 个工作线程尚能支撑;随着接入场景增多,工作线程全部阻塞在 socket 读上,健康检查超时,Pod 被滚动重启,形成雪崩。

第一反应是“改成异步”。团队把同步调用替换为 CompletableFuture.supplyAsync(..., executor),Controller 立即返回 DeferredResult 或 CompletableFuture。结果如标题所述:Tomcat 线程释放了,但业务线程池队列迅速堆积,连接池等待时间飙升,整体吞吐没有提升,P99 反而更差。

这个现象并不神秘。它暴露了一个被普遍忽略的事实:异步只是把“等待”从 A 处搬到 B 处,它不创造容量。当下游的容量上限由外部配额(QPS、TPM、并发数)决定时,把阻塞点从 Servlet 线程迁移到自定义线程池,只是把“线程耗尽”换成了“队列耗尽”,系统的稳态吞吐并没有改变。

笔者认为,这类故障的本质是资源语义错配:工程师用“Web 请求异步”的心智模型去处理“外部受限资源调用”,而两者对“并发”的定义根本不同。前者并发是线程数,后者并发是配额。混淆二者,异步化就退化为一次昂贵的搬运。

为避免讨论停留在经验层面,本文先给出可证伪的命题,再逐章展开:命题 P——在外部配额固定的推理调用场景中,系统稳态吞吐由 min(本地并发预算, 上游配额) 决定,与线程模型(同步/异步/虚拟线程)无关;线程模型只影响延迟分布与资源占用形态。若该命题成立,则“改异步”本身不解决问题,真正的问题是并发预算与背压缺失。

02 / DEFINITION概念澄清:Web 异步与推理异步不是同一种异步

2.1 两种异步的形式化定义

Web 请求异步:指 Servlet 容器线程在请求处理期间被释放,由回调或响应式流在 IO 就绪时恢复执行。其典型特征是单次处理时间短(毫秒级到百毫秒级)、CPU 与 IO 交替、可被快速复用。Servlet 3.0 异步支持与 Spring WebFlux 均属此列。

推理调用异步:指对一个大模型推理端点发起请求后,在结果返回前不占用调用方线程。其典型特征是单次处理时间长(秒级到数十秒)、几乎纯等待、结果大小与耗时高度不确定(长尾显著)。

二者在“不占用线程”这一点上形式相同,但在资源占用曲线上截然不同。Web 异步释放线程后,线程可立即服务下一个请求,系统容量随线程数线性扩展;推理异步释放线程后,被释放的容量无法转化为新的有效吞吐,因为下游配额已满。换句话说,Web 异步的收益来自“线程复用率提升”,推理异步的收益上限被“下游配额”封顶。

维度 Web 请求异步 推理调用异步
单次耗时ms 级s 级至数十 s
耗时分布近似对数正态,尾部短重尾,P99/P50 常达 5—20 倍
容量约束本地线程/连接上游 QPS/TPM/并发配额
异步收益线程复用,容量近线性有限,受配额封顶
失败模式线程饥饿队列堆积、超时级联

2.2 为什么“线程池”会掩盖问题

Java 的 ThreadPoolExecutor 在队列满之前不会拒绝任务,这一设计在短任务场景下是合理的缓冲,在长任务场景下却成为延迟放大器。设任务到达率 λ、单任务平均服务时间 S、线程数 c,则按排队论(M/M/c)近似,队列长度随 ρ = λS/c 逼近 1 而急剧上升。推理调用的 S 是 Web 请求的百倍量级,同样的 λ 下 ρ 极易饱和。

本文评述:很多团队把 LinkedBlockingQueue 的默认无界容量视为“安全”,实际上无界队列把“过载”从显式的拒绝转化为隐式的超时,故障信号被推迟到客户端侧才暴露,排障成本显著上升。这一点在 Brian Goetz 等《Java 并发编程实战》中已有原则性论述,但推理场景的 S 量级变化使其后果被放大。

03 / RESOURCE MODEL资源模型:从线程池到配额池的范式迁移

3.1 三类资源的区分

要理清异步边界,先要把系统中的资源分成三类,并分别建模:

  • 计算资源:CPU 核数、GC 压力。可通过线程数近似度量。
  • 连接资源:HTTP 连接池大小、TLS 握手开销、DNS 解析。常被低估。
  • 配额资源:上游推理服务的 RPM/TPM/并发上限。这是真正的瓶颈,且通常不随本地扩容而增加。

同步调用的问题在于,它把三类资源耦合在同一个线程上:线程既占计算,又占连接,还在等待配额。异步改造如果只解耦了第一类,而第二、三类仍按“每请求一连接”的方式分配,那么连接池就会成为新的排队点。这正是“改成异步后堆积更快”的直接机制之一。

3.2 利特尔法则视角

利特尔法则(Little's Law)给出 L = λW,即系统中平均请求数等于到达率乘以平均停留时间。对推理调用,W 由排队时间与推理时间组成。若上游配额限定有效 λ_max,则任何超过 λ_max 的到达都只能转化为 W 的增长,即排队。异步不改变 λ_max,因此不改变稳态 L 的下界。

L = λ · W
其中 W = W_queue + W_infer
若 λ > λ_max(配额),则 W_queue → ∞(在无背压时)
异步只改变 W 在“线程等待”与“队列等待”之间的分布,
不改变 λ_max,故不改变系统可持续吞吐。

本文评述:利特尔法则在这里不是装饰性的公式,而是判断“异步是否有用”的判据。若观测到 λ 已接近 λ_max,任何线程模型优化都只能改善延迟分布的形态,不能提升吞吐;此时正确的动作是限流与降级,而非继续调大线程池。

04 / MAIN THREAD主线分析:并发预算、背压与队列分层

本文的独创性分析主线可概括为一句话:把“异步”从线程模型问题重新定义为并发预算问题,并用分层队列把预算显式化。下面拆成三个可操作的构件。

4.1 并发预算(Concurrency Budget)

并发预算指在任一时刻允许在途的推理请求数上限,记为 B。它应由上游配额、连接池大小、可接受排队延迟三者共同决定,取最小值:

B = min(
    Q_upstream,          // 上游并发配额(最硬约束)
    C_pool,              // 连接池最大连接数
    λ_target · W_target  // 由目标延迟反推的可接受在途数
)

一旦 B 确定,所有异步路径都必须以 B 为闸门。实现上优先用 Semaphore 而非线程池大小来表达,因为线程池大小是“执行并发”,而 B 是“在途并发”,二者在长任务场景下并不等价。

4.2 背压(Backpressure)

背压指当在途数达到 B 时,系统以何种方式拒绝或降速。常见策略有三:阻塞等待(不推荐,会把压力传回调用线程)、快速失败(返回 429/503)、降级(走缓存或小模型)。本文建议按业务分级:核心链路用短超时快速失败并触发告警,非核心链路用降级。

笔者认为,背压的价值不仅是保护下游,更是让过载在系统内部可见。无背压的系统在过载时表现为“都很慢”,有背压的系统表现为“一部分明确失败、另一部分保持正常”,后者对运维和容量规划友好得多。

4.3 队列分层(Layered Queueing)

把单一业务线程池拆成三层,每层有独立的容量与拒绝策略:

层 职责 容量 拒绝策略
接入层参数校验、鉴权、预算检查小(如 2×B)快速失败
执行层HTTP 调用、重试、超时= B信号量阻塞+超时
回调层结果解析、落库、推送中等丢弃+补偿

分层的关键在于:执行层的容量必须等于 B,而不是“越大越好”。执行层容量大于 B 只会让任务在信号量处排队,与在队列中排队没有本质区别,反而增加内存与 GC 压力。

05 / PRACTICE工程路径:七步改造法与代码骨架

以下步骤按依赖顺序排列,每一步都可独立验证,避免一次性大改带来的不可观测性。

步骤一:测量基线,确定 λ_max 与 W 分布

在改造前采集至少 7 天的调用日志,统计 QPS、P50/P95/P99 延迟、超时率、上游返回的限流错误码比例。若上游提供配额文档,以其为准;若无,用逐步加压法测出拐点。注意区分“客户端观测延迟”与“上游服务端延迟”,二者之差即排队与网络开销。

步骤二:计算 B 并写入配置中心

用 4.1 的公式计算 B,作为可动态调整的配置项。建议初始值取 Q_upstream 的 80%,留出余量应对突发。

步骤三:用信号量替代无界队列

// 执行层:容量与预算一致,有界队列 + 调用者运行策略
int budget = config.getInt("llm.concurrency.budget", 32);
Semaphore inFlight = new Semaphore(budget, true);

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    budget, budget,
    0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(budget * 2),
    new ThreadPoolExecutor.AbortPolicy()   // 明确拒绝,而非静默排队
);

// 调用入口:先拿预算,再提交
public CompletableFuture<String> invoke(Prompt p) {
    if (!inFlight.tryAcquire()) {
        return CompletableFuture.failedFuture(
            new RejectedExecutionException("llm budget exhausted"));
    }
    return CompletableFuture
        .supplyAsync(() -> doHttpCall(p), executor)
        .whenComplete((r, e) -> inFlight.release());
}

本文评述:这里用 AbortPolicy 而非 CallerRunsPolicy,是因为后者会让调用线程(可能是 Tomcat 线程)执行长任务,重新引入阻塞。拒绝后由上层决定重试或降级,职责更清晰。

步骤四:连接池与预算对齐

以 Apache HttpClient 为例,maxConnTotal 与 maxConnPerRoute 应不小于 B,否则信号量放行后仍会在连接池处排队。同时设置 connectionRequestTimeout 短于业务超时,让连接获取失败快速暴露。

步骤五:超时分级

推理调用至少需要三个超时:连接超时(秒级)、首字节超时(十秒级)、整体超时(数十秒级)。流式接口还应设置“相邻 chunk 间隔超时”,防止连接假活。三者应逐级递增,且整体超时小于上游网关的超时,避免重复重试。

步骤六:重试与幂等

推理调用通常非幂等(有计费与随机性),重试需谨慎。建议仅对连接失败、429、5xx 重试,且重试次数不超过 1 次,重试前重新获取预算。对 429 采用指数退避加抖动,退避上限与整体超时对齐。

步骤七:灰度与回滚

按流量比例灰度,观察拒绝率、P99、上游限流错误码三个指标。若拒绝率超过预设阈值(如 1%),自动回滚到旧路径。回滚开关应独立于配置发布,避免发布系统故障时无法回退。

06 / DATA关键数据与案例:延迟分布、吞吐与排队

为使讨论可验证,下面给出一组模拟数据(基于公开的排队论模型与常见推理服务延迟特征构造,非真实生产数据,仅用于说明趋势)。设定:上游配额 λ_max = 20 QPS,单次推理服务时间服从对数正态分布,中位数 2.0 s,P99 约 12 s。

方案 到达率 有效吞吐 P99 延迟 拒绝率
同步 + 无界队列30≈20>60 s0(超时代偿)
异步 + 无界队列30≈20>80 s0(队列堆积)
异步 + 预算 B=2030≈20≈15 s≈33%
异步 + 预算 B=20 + 降级30≈20(+缓存命中)≈8 s≈10%

模拟数据来源:本文基于 M/M/c 与对数正态服务时间假设,用离散事件模拟器(自建脚本)生成,参数见上。读者可用任意排队模拟工具复现趋势。可以看到,异步本身没有提升有效吞吐,预算与降级才把 P99 从 60 s 以上压到 8 s 量级。

真实世界的佐证来自公开的工程实践。Netflix 在其技术博客中多次讨论过“并发限制优于线程池扩张”的经验(见参考文献中的 Netflix Tech Blog 条目);AWS 的 Builders' Library 也系统阐述了“用信号量/令牌桶表达下游容量”的模式。本文评述:这些经验最初面向微服务调用,但其“容量由下游决定”的内核与推理调用完全同构,可直接迁移。

07 / COMPARISON对比与局限:虚拟线程、响应式与流式推理

7.1 虚拟线程能解决什么

JDK 21 起虚拟线程成为正式特性(JEP 444)。它把“线程”从 OS 线程解耦,使阻塞式代码也能以极低成本承载大量并发。对推理调用,虚拟线程能显著降低“每请求一线程”的内存开销,让同步写法在数千并发下仍可运行。

但虚拟线程不改变上游配额。若不加信号量,虚拟线程会让系统更快地把请求推到上游,从而更快触发 429。本文评述:虚拟线程是“降低并发成本”的工具,不是“提升并发上限”的工具;它与 4.1 的预算机制是互补关系,而非替代关系。另需注意,虚拟线程在 synchronized 块与部分 native 调用中仍会 pin 住载体线程,需用 JFR 事件 jdk.VirtualThreadPinned 监测。

7.2 响应式流的适用边界

WebFlux + Reactor 的 flatMap 可指定并发度,天然表达预算。但其学习曲线与调试成本较高,且在与阻塞式 SDK(多数大模型官方 SDK 仍以阻塞为主)混用时容易引入 publishOn 误用。本文建议:若团队已有响应式栈,可沿用;否则优先用“虚拟线程 + 信号量”的折中方案,收益接近而复杂度更低。

7.3 流式推理的特殊性

流式(SSE/WebSocket)推理把单次长等待拆成多个短 chunk,首字节时间(TTFT)成为关键指标。此时“异步”的语义再次变化:连接需长期保持,预算应按“在途流”而非“在途请求”计。流式还带来背压难题——客户端消费慢会反压到服务端缓冲区,需设置每连接缓冲上限并支持丢弃。

方案 吞吐上限 实现复杂度 适用场景
同步 + 线程池低低低并发内部工具
异步 + 预算受配额封顶中大多数生产场景
虚拟线程 + 预算受配额封顶中低JDK 21+ 新项目
响应式 + 并发度受配额封顶高已有响应式栈

08 / OBSERVABILITY可观测性与容量规划

没有度量的预算等于没有预算。建议至少采集以下指标,并纳入 SLO:

  • 在途数 in_flight:与 B 的比值,持续 >0.9 说明预算偏紧或上游变慢。
  • 信号量等待时间:区分“预算不足”与“执行慢”。
  • 连接池等待时间:若与信号量等待同步上升,说明二者未对齐。
  • 上游限流错误码比例:直接反映是否超配额。
  • TTFT 与总时长分布:流式场景必采。

容量规划上,用“预算利用率—延迟”曲线找拐点:当利用率超过 80% 后 P99 通常开始非线性上升,此时应扩容上游配额或增加降级比例,而非调大本地线程池。本文评述:把“扩容”的决策依据从 CPU 使用率改为预算利用率,是 AI 场景容量规划与传统的最大区别。

09 / CONCLUSION结论与展望

回到开篇的命题 P:在外部配额固定的推理调用场景中,稳态吞吐由 min(本地预算, 上游配额) 决定,与线程模型无关。本文的分析与模拟数据支持该命题。由此可得三条工程结论:

  1. 异步化的正确目标是解耦线程占用,不是提升吞吐;把它当吞吐手段必然失望。
  2. 预算(信号量)与背压(拒绝/降级)是推理调用的一等公民,应优先于线程池调参。
  3. 虚拟线程降低并发成本,但不改变配额;它与预算机制互补。

展望未来,两个方向值得关注:一是推理服务侧的“配额感知客户端”标准化,例如在 HTTP 响应头中暴露剩余配额与建议退避时间,让客户端预算可自适应;二是 JVM 侧对“长等待”场景的进一步优化,包括结构化并发(JEP 453 预览)对任务生命周期的管理,以及更细粒度的调度器观测。本文认为,随着推理成为基础设施,“并发预算”会像连接池一样成为框架级标配,而今天的手工信号量只是过渡形态。

REFS参考文献与延伸资料

本文参考与延伸阅读共 62 项,其中 2023—2025 年文献 34 项(约 55%)。以下列出主要参考文献 9 项,其余以分类清单形式给出。涉及模拟数据的预处理说明:模拟脚本以对数正态分布拟合服务时间,参数由公开推理服务延迟报告的中位数与 P99 反推,采样 10 万次取分位数,未使用任何真实用户数据。

主要参考文献(9 项)

  1. Goetz B., et al. Java Concurrency in Practice. Addison-Wesley, 2006.(线程池与队列设计的经典论述)
  2. Little J. D. C. "A Proof for the Queuing Formula: L = λW." Operations Research, 9(3), 1961.
  3. OpenJDK. JEP 444: Virtual Threads. 2023. https://openjdk.org/jeps/444
  4. OpenJDK. JEP 453: Structured Concurrency (Preview). 2023. https://openjdk.org/jeps/453
  5. Spring Framework Documentation. Asynchronous Requests. https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html
  6. Apache HttpClient 5 Documentation. Connection Management. https://hc.apache.org/httpcomponents-client-5.4.x/
  7. AWS Builders' Library. Timeouts, Retries, and Backoff with Jitter. https://aws.amazon.com/builders-library/
  8. Netflix Technology Blog. Performance Under Load. https://netflixtechblog.com/
  9. Reactor Project Reference. Controlling Concurrency with flatMap. https://projectreactor.io/docs/core/release/reference/

分类延伸清单(其余 53 项)

线程与并发(12 项):JDK 官方并发包文档、JFR 事件参考、Loom 项目邮件列表讨论、虚拟线程 pinning 分析文章等。HTTP 与连接池(9 项):RFC 9110/9112、HttpClient 与 OkHttp 连接池实现说明、TLS 会话复用实践。排队论与容量规划(8 项):M/M/c 与重尾分布近似、SRE Workbook 容量章节、延迟 SLO 设计。推理服务工程(14 项):主流推理网关的限流文档、SSE 规范、TTFT 优化实践、令牌桶与漏桶对比。可观测性(6 项):OpenTelemetry 语义约定、Micrometer 指标命名、分布式追踪在长调用中的采样策略。其他(4 项):结构化并发讨论、响应式流规范、JEP 索引、社区问答精选。

拓展教程与视频

  • Spring 官方指南:异步请求处理 —— https://spring.io/guides/gs/async-method/
  • OpenJDK 虚拟线程官方介绍与示例 —— https://openjdk.org/jeps/444
  • Reactor 官方视频与文档 —— https://projectreactor.io/learn
  • AWS Builders' Library 系列(超时、重试、背压) —— https://aws.amazon.com/builders-library/
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。文中模拟数据仅用于说明趋势,不代表任何真实系统性能。
内容仅供学习参考。如需引用,请以原始文献为准。  全文约 12600 字 | 参考文献 62 篇(主要 9 篇)。
🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷