springboot max-http-header-size最大长度的那些事及JVM调优方式
作者:菠萝y
问题
线上程序出现了OOM,程序日志中的输出为
Exception in thread "http-nio-8080-exec-1027" java.lang.OutOfMemoryError: Java heap space
Exception in thread "http-nio-8080-exec-1031" java.lang.OutOfMemoryError: Java heap space
看线程名称应该是tomcat的nio工作线程,线程在处理程序的时候因为无法在堆中分配更多内存出现了OOM,幸好JVM启动参数配置了-XX:+HeapDumpOnOutOfMemoryError,使用MAT打开拿到的hprof文件进行分析。
第一步就是打开Histogram看看占用内存最大的是什么对象:
可以看到byte数组占用了接近JVM配置的最大堆的大小也就是8GB,显然这是OOM的原因。
第二步看一下究竟是哪些byte数组,数组是啥内容:
可以看到很明显这和HTTP请求相关,一个数组大概是10M的大小。
第三步通过查看GC根查看谁持有了数组的引用:
这符合之前的猜测,是tomcat的线程在处理过程中分配了10M的buffer在堆上。
至此,马上可以想到一定是什么参数设置的不合理导致了这种情况,一般而言tomcat不可能为每一个请求分配如此大的buffer。
第四步就是检查代码里是否有tomcat或服务器相关配置,看到有这么一个配置:
max-http-header-size: 10000000
至此,基本已经确定了八九不离十就是这个不合理的最大http请求头参数导致的问题。
关于http header最大长度的那些事
http协议,超文本传输协议,HyperText Transfer Protocol,是互联网上应用最为广泛的一种网络协议,所有的WWW文件都遵守这个标准。
关于http协议消息的格式,大家可以网上自行搜索,这里不再赘述。
本文关注的是其header部分,如下图所示(红框标注部分):
问题原型
有一个web application提供web service,这个web application基于java开发,部署在tomcat容器上。
问题是:当客户端发送一个GET请求,结果得到400的response,意思是说bad request。
检查了这个request的代码实现逻辑,并没有相关input validation的逻辑,并且检查server端日志发现,request请求似乎并没有到达我们自己代码实现逻辑部分。这是为什么呢?
问题解释
遇到这个问题时,第一步就是查看server端日志,但是觉得很tricky的是,最开始并没有发现相关的日志,只是发现request并没有到达我们自己代码实现逻辑部分。
后来,mina同学眼神很好,发现了如下日志:
通过日志note信息发现,该条日志在info级别下只会打印一次,之后都会是debug级别才打印,难怪之前没有注意到这条日志。
从日志信息可知,request的header部分太大,超过了tomcat允许的最大值。
默认情况下,tomcat(8.0版本)允许的http请求header的最大值是8024个字节(8KB)。
那为什么之前没有出现这个问题呢?
原因是,项目迁移到SCP平台上之后,改成JWT token做权限校验,这个JWT token会被添加到request的header,然而JWT token一般来说都很大(平均有6k个字节左右),所以说在增加了JWT token这个header以及其他一些相关的headers之后,整个request的header部分就超过8024个字节,于是就出现了这个问题。
那么如何解决这个问题呢?可以从两个方面考虑:
增加tomcat允许http header最大值。这个配置参数maxHttpHeaderSize可以设置tomcat允许的http header最大值。
减少header的size,比如不要添加无关的header到request。
扩展
在研究这个问题的过程中,其实还有一些其他疑问。首先,一个request的转发流程大致如下:
那么,在这个流程中,为什么request在前面的部分没有出现这个问题,而这个问题出现在最后一个技术栈是java/tomcat的component呢?
原因是,每个web服务器的http header最大长度的默认值不一样,同时随语言、版本不同也会不一样。举个例子tomcat 5的http header size的默认值是4K。
我找到了其他component中对于http header size的默认值的定义:
CF Router是用Go语言实现,Go语言的http处理模块对于它的定义是默认值1MB。
App Router是用Nodejs实现,Nodejs的http处理模块对它的定义是默认值80KB。
以上两个默认值都要远远大于8KB,这也就解释了没什么问题出在最后一个component。
Tomcat修改maxParameterCount配置
问题
java.lang.IllegalStateException: More than the maximum number of request
parameters (GET plus POST) for a single request ([10,000]) were detected.
Any parameters beyond this limit have been ignored.
To change this limit, set the maxParameterCount attribute
on the Connector.
解决方案
以前使用外部Tomcat部署项目的时候,可以通过修改server.xml文件中的Connector节点maxParameterCount属性值解决这个问题。
<Connector port=“8080” redirectPort=“8443” protocol=“HTTP/1.1” maxParameterCount="-1" />
因为SpringBoot使用的是内嵌的Tomcat,无法配置server.xml。经过查看相关API文档并没有发现可以直接在配置文件中配置maxParameterCount属性,那么我们就在代码中进行配置,在SpringBoot的API文档中讲解了通过实现WebServerFactoryCustomizer接口可以对Tomcat进行相关配置。
参考
自定义tomcat配置
创建一个类并实现WebServerFactoryCustomizer接口的customize方法。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.stereotype.Component; /** * 自定义Tomcat容器配置类 * */ @Component public class MyTomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> { public static final int DEFAULT_MAX_PARAMETER_COUNT = 10000; private Logger logger = LoggerFactory.getLogger(getClass()); /** * 单次请求参数最大限制数 */ @Value("${server.tomcat.maxParameterCount}") private int maxParameterCount = DEFAULT_MAX_PARAMETER_COUNT; @Override public void customize(TomcatServletWebServerFactory factory) { if (logger.isDebugEnabled()) { logger.debug("MyTomcatWebServerFactoryCustomizer customize"); } PropertyMapper propertyMapper = PropertyMapper.get(); propertyMapper.from(this::getMaxParameterCount) .when((maxParameterCount) -> maxParameterCount != DEFAULT_MAX_PARAMETER_COUNT) .to((maxParameterCount) -> customizerMaxParameterCount(factory, maxParameterCount)); } /** * 配置内置Tomcat单次请求参数限制 * * @param factory * @param maxParameterCount */ private void customizerMaxParameterCount(TomcatServletWebServerFactory factory, int maxParameterCount) { factory.addConnectorCustomizers( connector -> connector.setMaxParameterCount(maxParameterCount)); } public void setMaxParameterCount(int maxParameterCount) { this.maxParameterCount = maxParameterCount; } public int getMaxParameterCount() { return maxParameterCount; } }
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。