springboot?max-http-header-size最大長(zhǎng)度的那些事及JVM調(diào)優(yōu)方式
問(wèn)題
線上程序出現(xiàn)了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
看線程名稱應(yīng)該是tomcat的nio工作線程,線程在處理程序的時(shí)候因?yàn)闊o(wú)法在堆中分配更多內(nèi)存出現(xiàn)了OOM,幸好JVM啟動(dòng)參數(shù)配置了-XX:+HeapDumpOnOutOfMemoryError,使用MAT打開(kāi)拿到的hprof文件進(jìn)行分析。
第一步就是打開(kāi)Histogram看看占用內(nèi)存最大的是什么對(duì)象:
可以看到byte數(shù)組占用了接近JVM配置的最大堆的大小也就是8GB,顯然這是OOM的原因。
第二步看一下究竟是哪些byte數(shù)組,數(shù)組是啥內(nèi)容:
可以看到很明顯這和HTTP請(qǐng)求相關(guān),一個(gè)數(shù)組大概是10M的大小。
第三步通過(guò)查看GC根查看誰(shuí)持有了數(shù)組的引用:
這符合之前的猜測(cè),是tomcat的線程在處理過(guò)程中分配了10M的buffer在堆上。
至此,馬上可以想到一定是什么參數(shù)設(shè)置的不合理導(dǎo)致了這種情況,一般而言tomcat不可能為每一個(gè)請(qǐng)求分配如此大的buffer。
第四步就是檢查代碼里是否有tomcat或服務(wù)器相關(guān)配置,看到有這么一個(gè)配置:
max-http-header-size: 10000000
至此,基本已經(jīng)確定了八九不離十就是這個(gè)不合理的最大http請(qǐng)求頭參數(shù)導(dǎo)致的問(wèn)題。
關(guān)于http header最大長(zhǎng)度的那些事
http協(xié)議,超文本傳輸協(xié)議,HyperText Transfer Protocol,是互聯(lián)網(wǎng)上應(yīng)用最為廣泛的一種網(wǎng)絡(luò)協(xié)議,所有的WWW文件都遵守這個(gè)標(biāo)準(zhǔn)。
關(guān)于http協(xié)議消息的格式,大家可以網(wǎng)上自行搜索,這里不再贅述。
本文關(guān)注的是其header部分,如下圖所示(紅框標(biāo)注部分):
問(wèn)題原型
有一個(gè)web application提供web service,這個(gè)web application基于java開(kāi)發(fā),部署在tomcat容器上。
問(wèn)題是:當(dāng)客戶端發(fā)送一個(gè)GET請(qǐng)求,結(jié)果得到400的response,意思是說(shuō)bad request。
檢查了這個(gè)request的代碼實(shí)現(xiàn)邏輯,并沒(méi)有相關(guān)input validation的邏輯,并且檢查server端日志發(fā)現(xiàn),request請(qǐng)求似乎并沒(méi)有到達(dá)我們自己代碼實(shí)現(xiàn)邏輯部分。這是為什么呢?
問(wèn)題解釋
遇到這個(gè)問(wèn)題時(shí),第一步就是查看server端日志,但是覺(jué)得很tricky的是,最開(kāi)始并沒(méi)有發(fā)現(xiàn)相關(guān)的日志,只是發(fā)現(xiàn)request并沒(méi)有到達(dá)我們自己代碼實(shí)現(xiàn)邏輯部分。
后來(lái),mina同學(xué)眼神很好,發(fā)現(xiàn)了如下日志:
通過(guò)日志note信息發(fā)現(xiàn),該條日志在info級(jí)別下只會(huì)打印一次,之后都會(huì)是debug級(jí)別才打印,難怪之前沒(méi)有注意到這條日志。
從日志信息可知,request的header部分太大,超過(guò)了tomcat允許的最大值。
默認(rèn)情況下,tomcat(8.0版本)允許的http請(qǐng)求header的最大值是8024個(gè)字節(jié)(8KB)。
那為什么之前沒(méi)有出現(xiàn)這個(gè)問(wèn)題呢?
原因是,項(xiàng)目遷移到SCP平臺(tái)上之后,改成JWT token做權(quán)限校驗(yàn),這個(gè)JWT token會(huì)被添加到request的header,然而JWT token一般來(lái)說(shuō)都很大(平均有6k個(gè)字節(jié)左右),所以說(shuō)在增加了JWT token這個(gè)header以及其他一些相關(guān)的headers之后,整個(gè)request的header部分就超過(guò)8024個(gè)字節(jié),于是就出現(xiàn)了這個(gè)問(wèn)題。
那么如何解決這個(gè)問(wèn)題呢?可以從兩個(gè)方面考慮:
增加tomcat允許http header最大值。這個(gè)配置參數(shù)maxHttpHeaderSize可以設(shè)置tomcat允許的http header最大值。
減少header的size,比如不要添加無(wú)關(guān)的header到request。
擴(kuò)展
在研究這個(gè)問(wèn)題的過(guò)程中,其實(shí)還有一些其他疑問(wèn)。首先,一個(gè)request的轉(zhuǎn)發(fā)流程大致如下:
那么,在這個(gè)流程中,為什么request在前面的部分沒(méi)有出現(xiàn)這個(gè)問(wèn)題,而這個(gè)問(wèn)題出現(xiàn)在最后一個(gè)技術(shù)棧是java/tomcat的component呢?
原因是,每個(gè)web服務(wù)器的http header最大長(zhǎng)度的默認(rèn)值不一樣,同時(shí)隨語(yǔ)言、版本不同也會(huì)不一樣。舉個(gè)例子tomcat 5的http header size的默認(rèn)值是4K。
我找到了其他component中對(duì)于http header size的默認(rèn)值的定義:
CF Router是用Go語(yǔ)言實(shí)現(xiàn),Go語(yǔ)言的http處理模塊對(duì)于它的定義是默認(rèn)值1MB。
App Router是用Nodejs實(shí)現(xiàn),Nodejs的http處理模塊對(duì)它的定義是默認(rèn)值80KB。
以上兩個(gè)默認(rèn)值都要遠(yuǎn)遠(yuǎn)大于8KB,這也就解釋了沒(méi)什么問(wèn)題出在最后一個(gè)component。
Tomcat修改maxParameterCount配置
問(wèn)題
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部署項(xiàng)目的時(shí)候,可以通過(guò)修改server.xml文件中的Connector節(jié)點(diǎn)maxParameterCount屬性值解決這個(gè)問(wèn)題。
<Connector port=“8080” redirectPort=“8443” protocol=“HTTP/1.1” maxParameterCount="-1" />
因?yàn)镾pringBoot使用的是內(nèi)嵌的Tomcat,無(wú)法配置server.xml。經(jīng)過(guò)查看相關(guān)API文檔并沒(méi)有發(fā)現(xiàn)可以直接在配置文件中配置maxParameterCount屬性,那么我們就在代碼中進(jìn)行配置,在SpringBoot的API文檔中講解了通過(guò)實(shí)現(xiàn)WebServerFactoryCustomizer接口可以對(duì)Tomcat進(jìn)行相關(guān)配置。
參考
自定義tomcat配置
創(chuàng)建一個(gè)類并實(shí)現(xiàn)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()); /** * 單次請(qǐng)求參數(shù)最大限制數(shù) */ @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)); } /** * 配置內(nèi)置Tomcat單次請(qǐng)求參數(shù)限制 * * @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; } }
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于spring.factories失效原因分析及解決
這篇文章主要介紹了關(guān)于spring.factories失效原因分析及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07SpringBoot動(dòng)態(tài)Feign服務(wù)調(diào)用詳解
Feign是Netflix公司開(kāi)發(fā)的一個(gè)聲明式的REST調(diào)用客戶端; Ribbon負(fù)載均衡、 Hystrⅸ服務(wù)熔斷是我們Spring Cloud中進(jìn)行微服務(wù)開(kāi)發(fā)非?;A(chǔ)的組件,在使用的過(guò)程中我們也發(fā)現(xiàn)它們一般都是同時(shí)出現(xiàn)的,而且配置也都非常相似2022-12-12Spring5新特性之Reactive響應(yīng)式編程
這篇文章主要介紹了Spring5新特性之Reactive響應(yīng)式編程,響應(yīng)式編程是一種編程范式,通用和專注于數(shù)據(jù)流和變化的,并且是異步的,下文更多詳細(xì)內(nèi)容,需要的小伙伴可以參考一下,希望對(duì)你有所幫助2022-03-03springboot開(kāi)啟mybatis二級(jí)緩存的步驟詳解
這篇文章給大家介紹了springboot開(kāi)啟mybatis二級(jí)緩存的詳細(xì)步驟,文中通過(guò)代碼示例給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-02-02解答為什么 Java 線程沒(méi)有Running狀態(tài)
Java 線程沒(méi)有Running狀態(tài)指的是一個(gè)在 JVM 中執(zhí)行 的線程處于的狀態(tài),本文小編將為大家詳解一二,需要的朋友可以參考下面文章具體內(nèi)容2021-09-09Java中實(shí)現(xiàn)文件上傳下載的三種解決方案(推薦)
這篇文章主要介紹了Java中實(shí)現(xiàn)文件上傳下載的三種解決方案的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07