记一次 max-http-header-size 配置不当导致的 OOM 问题

一.起因

工作时接手的一个项目线上 Full GC 次数过于频繁,最后 OOM 导致服务挂掉。

通过配置的 -XX:+HeapDumpOnOutOfMemoryError 拿到事故内存快照使用 MAT 进行分析。

二.项目环境

  • Linux (3.10.0-957.21.3.el7.x86_64)
  • JDK 8
  • SpringBoot 2.2.4.RELEASE
  • 内嵌 Tomcat 9.0.3
  • 配置最大堆内存配置 4G

三.MAT 分析

1.查看类实例 Histogram 直方图

记一次 max-http-header-size 配置不当导致的 OOM 问题_第1张图片

首先可以看到 byte 数组占用了大量内存

Shallow Heap(浅堆)代表对象本身占用的内存,包含对象的对象头,实例数据、对齐空间。
Retained Heap(深堆)代表对象本身和对象关联的对象占用的内存,该对象被回收后,能够释放的内存大小。

2.查看 GC 根谁持有了数组的引用

记一次 max-http-header-size 配置不当导致的 OOM 问题_第2张图片

exclude all phantom/weak/soft etc. reference 排除掉 软、弱、虚引用,只剩下强引用。因为除了强引用之外,其他引用都可以被 JVM GC 掉,如果一个对象始终无法被 GC,说明有强引用的存在,导致 GC 过程一直得不到回收,最终内存溢出、

我们排除掉软、弱、虚引用,因为这几种是不会造成内存泄漏的,可以先不用管它,我们只需要看排除后还有没引用存在,有的话 那就是强引用了

记一次 max-http-header-size 配置不当导致的 OOM 问题_第3张图片
发现这和HTTP请求有关,都是Tomcat线程
记一次 max-http-header-size 配置不当导致的 OOM 问题_第4张图片

通过排查可以定位到 Http11InputBufferHttp11OutputBuffer 两个缓冲对象都持有大数组。差不多每一个请求线程就占用 20 M,这显然是不合理的,应该是哪配置不规范导致的。

于是检查项目 yml 配置发现一条设置 max-http-header-size 属性与分配的内存相似。

server:
  max-http-header-size: 1024000

该字段为设置 HTTP 消息头的最大大小。
其 Tomcat 默认设置的大小为 8 * 1024B 大小

记一次 max-http-header-size 配置不当导致的 OOM 问题_第5张图片

四.疑问:为什么设置消息头最大的大小会实际分配对应大的内存

org.apache.coyote.http11.Http11Processor#Http11Processor
记一次 max-http-header-size 配置不当导致的 OOM 问题_第6张图片
可以看到输入输出缓冲对象初始化时携带上了设置了 maxHttpHeaderSize,进一步查看

1.Http11InputBuffer 初始化记一次 max-http-header-size 配置不当导致的 OOM 问题_第7张图片

只是初始化了下 Http11InputBuffer#headerBufferSize 属性值。
具体使用的地方为 org.apache.coyote.http11.Http11InputBuffer#init 方法

在定义缓冲区长度时,maxHttpHeaderSize(1024000) 额外添加了套接字通信的缓冲区大小(8192),所以在 GC 根链路上看到的 byteBuffer 所持数组大小为 10248192

记一次 max-http-header-size 配置不当导致的 OOM 问题_第8张图片

allocate 方法会返回指定大小的 HeapByteBuffer 大小的缓冲区
记一次 max-http-header-size 配置不当导致的 OOM 问题_第9张图片
记一次 max-http-header-size 配置不当导致的 OOM 问题_第10张图片

2.Http11OutputBuffer 初始化

记一次 max-http-header-size 配置不当导致的 OOM 问题_第11张图片
outputBuffer 就在初始化的时候,就创建好了对应大小的缓冲区。此处就直接创建使用 maxHttpHeaderSize 大小。

对应上图 headerBuffer 所持数组 1024000 大小

五.流程分析

Tomcat 接收请求时

  1. NioEndpoint 将请求封装为 NioEndpoint$SocketProcessor 交给 Tomcat ThreadPoolExecutor 执行处理
  2. 执行调用 AbstractProtocol$ConnectionHandler 的 process 方法
  3. 内部使用 HTTP11NioProtocol 处理此请求,会实例化一个 Http11Processor ,最终调用其 process 方法处理
  4. 在 Http11Processor 实例化时会直接初始化一个 [10240000] 大小的 Http11OutputBuffer,处理连接时再初始化 Http11InputBuffer 的大小
  5. 平均每个线程持有 20M 大小的缓存数组,当并发请求时,线程池里的线程数达到200时,超出了设置的最大堆内存 4G 导致 OOM

六.处理

根据该项目实际查看,请求传递参数不多,删除配置设置的请求头大小,使用默认参数即可

后续使用 jmeter 进行压测监控,内存基于稳定,不出现 OOM 问题。

看 Git 提交记录为 init 时就有了,怀疑是 copy 其他项目来着就一直用着了,为后续留下坑。


以上为对该 OOM 问题的一次排查过程记录,还是有很多知识点没有扫到,仍待继续学习。

你可能感兴趣的:(java,spring,tomcat)