寫得比較匆忙,本文主要以整體微服務架構簡述開頭,按照問題排查過程敘述,加以springboot啓動源碼淺析和解決辦法。
如有錯誤請指正
先說版本Spring boot 2.0.4。Spring cloud F版 jdk1.8
我們目前的微服務架構,當用戶登錄後會發放JWT的token令牌給前端,之後的請求都將此token放到http的header中傳入後臺。
但是前端調用時發現之前正常的請求報錯400 bad request。查找資料說是請求頭過長導致。
首先排查權限系統默認的tomcat的header請求,發現默認大小不夠。配置:
server.tomcat.maxhttpheadersize==102400
未解決。
聯想到我們微服務商是通過gateway網關轉發的。準備去調整gateway的server.tomcat.maxhttpheadersize。但是gateway是由webflux開發的使用netty服務器的。沒有這個配置項。
查看資料發現存在另外一個配置server.maxhttpheadersize同樣可以設置header大小。
於是配置server.maxhttpheadersize==102400 啓動,但是發現並不生效。
於是決定debug啓動過程,發現ServerProperties中讀取到了配置102400,但是不生效。
再深入 看netty啓動過程。發現nettry並沒有配置使用ServerProperties配置類。
分析啓動過程,
springboot項目啓動,調用
SpringApplication.run(Application.class, args);
內部調用構造器
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = deduceWebApplicationType();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
這裏通過deduceWebApplicationType();給webApplicaitonType賦值
private WebApplicationType deduceWebApplicationType() {
if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
&& !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_WEB_ENVIRONMENT_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : WEB_ENVIRONMENT_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
這裏根據存在的類判斷是哪種模式,如果是
org.springframework.web.reactive.DispatcherHandler
並且不存在
org.glassfish.jersey.server.ResourceConfi
org.springframework.web.servlet.DispatcherServlet
返回
WebApplicationType.REACTIVE
再往下看到run方法內調用createApplicationContext
context = createApplicationContext();
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, "
+ "please specify an ApplicationContextClass",
ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
這裏根據前面得到的REACTIVE服之context爲
public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
+ "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";
繼承於ReactiveWebServerApplicationContext類。調用createWebServer方法給webServer賦值。
private void createWebServer() {
WebServer localServer = this.webServer;
if (localServer == null) {
this.webServer = getWebServerFactory().getWebServer(getHttpHandler());
}
initPropertySources();
}
因爲我們這裏啓用的netty,所以調用到NettyReactiveWebServerFactory工廠類,而此類中通過
private HttpServer createHttpServer() {
return HttpServer.builder().options((options) -> {
options.listenAddress(getListenAddress());
if (getSsl() != null && getSsl().isEnabled()) {
SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(
getSsl(), getSslStoreProvider());
sslServerCustomizer.customize(options);
}
if (getCompression() != null && getCompression().getEnabled()) {
CompressionCustomizer compressionCustomizer = new CompressionCustomizer(
getCompression());
compressionCustomizer.customize(options);
}
applyCustomizers(options);
}).build();
}
生成httpServer。而在HttpServer總通過builder構建者的options.可以配置請求大小。
public static final class Builder extends ServerOptions.Builder<Builder> {
public static final int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4096;
public static final int DEFAULT_MAX_HEADER_SIZE = 8192;
public static final int DEFAULT_MAX_CHUNK_SIZE = 8192;
public static final boolean DEFAULT_VALIDATE_HEADERS = true;
public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128;
private BiPredicate<HttpServerRequest, HttpServerResponse> compressionPredicate;
private int minCompressionResponseSize = -1;
private int maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH;
private int maxHeaderSize = DEFAULT_MAX_HEADER_SIZE;
private int maxChunkSize = DEFAULT_MAX_CHUNK_SIZE;
private boolean validateHeaders = DEFAULT_VALIDATE_HEADERS;
private int initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE;
public final Builder maxHeaderSize(int value) {
if (value <= 0) {
throw new IllegalArgumentException("maxHeaderSize must be strictly positive");
}
this.maxHeaderSize = value;
return get();
}
這裏HttpServer是final類和不變類,通過構建者聲稱的HttpServer無法被修改。
所以這裏我們想要自己修改這個值,只能將整個NettyReactiveWebServerFactory工廠類在他自動注入前注入,然後運行到autoconfiguration時,就不會再此注入原有的NettyReactiveWebServerFactory類了。
這裏放出重寫的代碼
private HttpServer createHttpServer() {
return HttpServer.builder().options((options) -> {
options.maxHeaderSize(102400);
options.listenAddress(getListenAddress());
if (getSsl() != null && getSsl().isEnabled()) {
SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(
getSsl(), getSslStoreProvider());
sslServerCustomizer.customize(options);
}
if (getCompression() != null && getCompression().getEnabled()) {
CompressionCustomizer compressionCustomizer = new CompressionCustomizer(
getCompression());
compressionCustomizer.customize(options);
}
applyCustomizers(options);
}).build();
}
@Bean
@Order(-1)
public NettyReactiveWebServerFactory nettyReactiveWebServerFactory() {
return new NettyReactiveWebServerFactory();
}
啓動調試,發現102400已經被配置到了netty中。
本地調試啓動權限系統,網關,前端調用。過了。
到這裏解決了spring cloud gateway中netty自定義header請求限制大小問題。
但是,部署到dev環境,請求,失敗。。。
打印日誌調試,發現權限系統和網關配置的都沒問題。
而且報錯請求根本沒進入gateway。
這就又想到我們在k8s集羣內,有個ingress在前面,又去配置ingress-controller的配置項。配置
client-header-buffer-size == 100k
重啓pod。 調用,成功。
其實前面還有一層slb,4層負載,只不過4層負載不到http的應用層,所以沒有配置大小;到ingress是http服務器,雖然協議沒規定一定要限制大小;但是ingress的默認nginx配置了16k。所以不符合需求。這就導致了這次的問題。