Tomcat长连接安全处理及Spring Boot OOM调优咨询
问题根因
这类OOM本质不是传统意义上的内存泄漏,是资源边界缺失导致的内存耗尽:默认配置的RestTemplate没有任何超时限制、没有连接数上限,下游响应慢到数分钟时,每个挂起的请求会持续占住Tomcat工作线程、持有请求/响应缓冲对象、HTTP连接资源,入口请求不断堆积,所有关联对象都无法被GC回收,最终堆内存被撑满。
应用层修复(优先级最高,改完即可解决90%以上的同类问题)
- 给RestTemplate配置严格的超时和连接池限制,绝对不能用默认的无限超时实现。推荐替换成Apache HttpClient实现的请求工厂,配置示例如下:
@Bean public RestTemplate restTemplate() { PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); // 全局最大连接数,结合单实例压测值设置,常规场景设200足够 connManager.setMaxTotal(200); // 单下游路由的最大并发连接,针对故障下游单独设置,避免占满全部连接 connManager.setDefaultMaxPerRoute(50); RequestConfig requestConfig = RequestConfig.custom() // 建立TCP连接超时,设3s即可,连不上直接抛错 .setConnectTimeout(3000) // 从连接池获取连接超时,设1s,池满直接报错不排队 .setConnectionRequestTimeout(1000) // 响应读取超时,按业务可接受最大等待时间设置,最长不超过30s,绝对不允许等数分钟 .setSocketTimeout(10000) .build(); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connManager) // 自动回收30s以上的空闲连接,避免僵死连接占资源 .evictIdleConnections(30, TimeUnit.SECONDS) .setDefaultRequestConfig(requestConfig) .build(); return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); }
注意几个核心参数不能妥协:读超时必须卡死上限,连接池大小必须结合下游承载能力设置,连接获取超时必须配置,避免请求在队列里无限堆积。
- 调整RestTemplate响应缓冲逻辑:默认实现会把整个响应体全读到内存再做序列化,如果接口返回体较大,慢响应场景下缓冲内存占用会非常高,大响应体场景可以替换为支持流式处理的请求工厂,避免全量数据缓冲到堆内存。
- 给故障下游加舱壁隔离和熔断。针对慢响应的下游接口单独做并发限制:比如最多只允许30个并发请求调用该接口,超过阈值直接快速失败返回降级结果,不占用Tomcat工作线程。同时配置熔断规则:如果该下游慢调用(比如响应超过5s)占比超过50%,直接熔断一段时间,期间所有请求走降级逻辑,等下游恢复后再通过半开探测逐步恢复调用,避免雪崩。
- 收紧Tomcat配置,不要让请求无限排队。默认Tomcat的acceptCount(请求等待队列长度)是100,当工作线程全被慢请求占满时,队列里堆积的上百个请求每个都会持有Request/Response对象占用内存,很容易触发OOM。配置示例:
server: tomcat: threads: # 最大工作线程结合CPU核数设置,8核实例设200足够,不要盲目调大 max: 200 # 最小空闲线程留10个应对突发流量即可 min-spare: 10 # 请求等待队列长度设为20,比默认值小很多,队列满了直接返回503拒绝请求 accept-count: 20 # Tomcat侧连接超时设2s,避免僵死连接占资源 connection-timeout: 2000ms
核心逻辑是:队列宁可短一点,快速拒绝多余请求,也不要无限堆积把整个进程拖死。
JVM层兜底配置
- 确保JVM正确识别k8s容器的内存限制:Java 8u191+、Java11+默认支持容器内存感知,低版本Java8需要加启动参数
-XX:+UseCGroupMemoryLimitForHeap;堆内存设置为容器内存limit的70%即可,剩余30%留给元空间、直接内存、JVM自身开销,避免堆占满容器内存触发系统OOMKill。 - 配置OOM自动堆转储,方便事后排查问题:加启动参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heapdump.hprof,对应路径提前挂载存储,避免dump文件打满容器根盘。 - 开启GC日志留存,方便观测内存上涨趋势:Java11+用参数
-Xlog:gc*,gc+age=trace,safepoint:file=/var/log/gc.log:utctime,pid,tags:filecount=5,filesize=100m,Java8调整为对应版本的GC日志参数即可。 - 合理限制直接内存大小:加参数
-XX:MaxDirectMemorySize=256m(按实际业务场景调整,一般不超过堆内存的1/4),避免NIO相关的直接内存占用过高耗尽容器内存。
K8s运维层防护
- 配置合理的就绪探针:如果服务出现线程池满、大量请求超时的情况,就绪探针探测失败会自动将实例从Service端点摘除,不再转发新流量进来,避免故障实例被持续压垮。
- 给Pod设置Guaranteed级别的QoS:即CPU和内存的requests值与limits值完全相等,避免节点资源不足时故障实例被优先驱逐。
- 流量入口侧加并发限流:在Ingress或服务网格层给服务设置单实例最大并发阈值,超过阈值直接返回429响应,避免突发流量把实例打垮。
排查这类问题不要上来就定位内存泄漏,优先拿堆dump看占用内存最多的对象,90%以上的场景都是挂起的HTTP请求关联的byte数组、响应对象,根源就是没做超时和并发限制,把资源边界卡死问题自然解决。
内容的提问来源于stack exchange,提问作者darth jemico
相关产品推荐
相关产品推荐

