问题背景:一
个接口方法里,同步调用多个外部 HTTP 服务,在高并发下 JVM 被打爆
业务中一些接口需要接口需要调用多个外部服务,如果在高并发场景下,容易出现以下问题:
- 堆内存爆炸:大量请求导致内存溢出
- 连接数耗尽:数据库连接池、HTTP连接池被耗尽
- 响应超时:外部服务响应慢,导致请求堆积
- 系统崩溃:资源耗尽导致服务不可用
- GC风暴: 大量请求导致创建大量http连接对象,接收返回值创建大量JSONObject、Response等常见临时对象,导致年轻代迅速填满,频繁minor GC(每秒多次),GC时间>业务执行时间,引起GC风暴。
典型特征
- 单个接口调用多个外部服务
- 每个请求需要进行多次数据库查询
- 外部服务响应时间不确定
- 高并发访问(QPS > 100)
”一个看起来正常的接口“,请求量不大的情况下正常运行,访问并发量一上来, jvm炸了。
第一反应:是不是响应体太大?排查后发现:
-
HTTP 响应体并非很大,只有几KB
-
不存在“大 JSON 导致内存暴涨”的问题
并非外部调用的响应体过。
根因分析
不是“数据大”,而是“并发 + 阻塞”
当前场景下,一个用户请求 = N 个外部 HTTP 请求
- 假设:
- QPS = 300
- 每个请求调用 3 个外部服务
- 外部平均耗时 200ms
- 那么瞬间 JVM 内部会出现:
- 300 × 3 = 900 个阻塞中的 HTTP 调用
这些调用
- 占用 Tomcat 工作线程
- 占用 Socket
- 占用堆内对象(Request / Response / JSON)
- 占用 CPU 调度
堆爆不是因为“单个请求大”,而是“同时活着的请求太多”。
1. 内存问题分析
问题表现
java.lang.OutOfMemoryError: Java heap space
根本原因
- HTTP连接未复用:每次请求都创建新的HTTP连接
- 连接未及时释放:连接泄漏导致内存占用持续增长
- 大量对象创建:每个请求创建大量临时对象(JSONObject、List等)
- 线程堆积:同步阻塞调用导致大量线程等待
内存占用计算
单次请求内存占用 ≈ HTTP连接对象 (50KB) × 连接数 + JSON对象 (10KB) × 对象数 + 线程栈 (1MB) × 线程数 + 其他临时对象 (20KB)
1000并发 ≈ 1000 × (50KB + 10KB × 5 + 20KB) = 120MB
实际可能更高(连接泄漏、对象未释放)
2. 连接池问题分析
问题表现
java.sql.SQLNonTransientConnectionException: No operations allowed after connection closed HikariPool-1 - Connection is not available
根本原因
- 连接池配置不当:连接池大小与实际需求不匹配
- 连接泄漏:连接未正确关闭
- 连接超时:网络慢导致连接超时
- 无连接复用:每次都创建新连接
连接数计算
无连接池: 1000并发 × 每个请求3个外部服务 = 3000个连接有连接池(50个连接): 1000并发 → 50个连接复用 = 50个连接
3. 并发控制问题分析
问题表现
- 系统响应变慢
- 资源耗尽
- 服务不可用
根本原因
- 无并发限制:所有请求同时执行
- 资源竞争:多个请求竞争同一资源
- 级联故障:一个服务故障影响整个系统
原因梳理
核心问题总结
| 问题类型 | 具体表现 | 影响程度 | 优先级 |
|---|---|---|---|
| HTTP连接未复用 | 每次请求创建新连接 | ⭐⭐⭐⭐⭐ | P0 |
| 无并发控制 | 所有请求同时执行 | ⭐⭐⭐⭐⭐ | P0 |
| 连接泄漏 | 连接未正确关闭 | ⭐⭐⭐⭐ | P1 |
| 超时控制缺失 | 请求无限等待 | ⭐⭐⭐⭐ | P1 |
| 无重试机制 | 网络抖动导致失败 | ⭐⭐⭐ | P2 |
问题链路图
高并发请求↓ 无并发控制(Semaphore)↓ 大量线程同时执行↓ 每个线程创建新的HTTP连接(无连接池)↓ 连接数爆炸(1000并发 × 3个服务 = 3000连接)↓ 内存占用激增↓ 堆内存溢出(OOM)↓ 系统崩溃
解决方案
方案一:HTTP连接池优化
使用连接池复用HTTP连接,避免每次请求都创建新连接。
1 @Configuration 2 public class HttpClientConfig { 3 @Bean 4 public OkHttpClient okHttpClient() { 5 return new OkHttpClient.Builder() 6 .connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES)) 7 .connectTimeout(10, TimeUnit.SECONDS) 8 .readTimeout(30, TimeUnit.SECONDS) 9 .writeTimeout(30, TimeUnit.SECONDS) 10 .retryOnConnectionFailure(true) 11 .build(); 12 } 13 }
- 连接数减少:从3000个减少到50个(减少98%)
- 内存占用降低:减少连接对象创建
- 性能提升:连接复用,减少握手时间
方案二:Semaphore并发控制
使用信号量限制同时执行的请求数,保护系统资源。
private static final Semaphore querySemaphore = new Semaphore(50, true); private static final int SEMAPHORE_TIMEOUT_SECONDS = 5;public Object query(HttpServletRequest request) {boolean acquired = false;try {acquired = queryMSemaphore.tryAcquire(SEMAPHORE_TIMEOUT_SECONDS, TimeUnit.SECONDS);if (!acquired) {// 返回限流错误return ResFlag.failureServer(dataMap);}// 业务逻辑} finally {if (acquired) {querySemaphore.release();}} }
- 资源保护:限制同时执行的请求数
- 防止雪崩:避免系统资源耗尽
- 优雅降级:超时直接拒绝,不阻塞
方案三:超时控制
设置合理的超时时间,防止请求无限等待。
实现方式
.connectTimeout(10, TimeUnit.SECONDS) // 连接超时 .readTimeout(30, TimeUnit.SECONDS) // 读取超时 .writeTimeout(30, TimeUnit.SECONDS) // 写入超时
- 防止阻塞:超时后立即释放资源
- 快速失败:避免长时间等待
- 资源释放:及时释放连接和线程
方案四:资源正确释放
使用try-with-resources确保资源正确关闭。
实现方式
try (Response response = okHttpClient.newCall(request).execute()) {// 处理响应if (response.isSuccessful() && response.body() != null) {String body = response.body().string();// ... } } // 自动关闭Response,释放连接
- 防止泄漏:确保连接正确关闭
- 内存回收:及时释放资源
- 连接复用:连接返回连接池
优化原则
连接池配置原则
连接池大小 = min(数据库连接池大小 / 每个请求平均占用连接数,HTTP连接池大小 / 每个请求平均占用连接数, 服务器可用内存 / 每个连接内存占用)// 中小型应用 ConnectionPool(20-50, 5, TimeUnit.MINUTES)// 大型应用 ConnectionPool(50-100, 5, TimeUnit.MINUTES)// 超大型应用 ConnectionPool(100-200, 5, TimeUnit.MINUTES)
2. Semaphore并发数计算
Semaphore数量 = min(数据库连接池大小 / 每个请求平均占用连接数,HTTP连接池大小 / 每个请求平均占用连接数,服务器CPU核心数 × 2,服务器可用内存 / 每个请求内存占用)保守策略:30-50(资源有限、外部服务不稳定)
平衡策略:50-80(资源充足、需要更高吞吐量)
激进策略:80-100(资源非常充足、数据库连接池很大)
3. 超时时间配置
// 内网服务 connectTimeout: 3-5秒 readTimeout: 10-15秒// 外网服务 connectTimeout: 10-15秒 readTimeout: 30-60秒// 慢速服务 connectTimeout: 30秒 readTimeout: 60-120秒
4. 错误处理原则
必处理
- ✅ 连接异常
- ✅ 超时异常
- ✅ 资源释放(finally块)
- ✅ 信号量释放(finally块)
注意事项
- Semaphore数量:需要根据实际资源情况调整
- 连接池大小:需要根据并发量调整
- 超时时间:需要根据外部服务响应时间调整
- 资源释放:必须确保在finally块中释放
- 监控告警:需要监控关键指标,及时发现问题