Spring Cloud Gateway 启动流程源码分析
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>配置和启动类</li><li>启动 nettyserver</li><li>创建websever</li><li>重要方法 reactor.netty.transport.ServerTransport#bind</li><li>jvisualvm监控验证</li></ul></div><p>以下分析以 spring-cloud-starter-gateway 4.1.0 源码为分析样本。</p><p class="maodian"></p><h2>配置和启动类</h2>
<p>如果我们要使用 Spring Cloud Gateway,需要在pom里引入如下依赖:</p>
<div class="jb51code"><pre class="brush:plain;"><dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>4.1.0</version>
</dependency></pre></div>
<p>spring-cloud-starter-gateway里的依赖如下:</p>
<div class="jb51code"><pre class="brush:plain;"><dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<optional>true</optional>
</dependency>
</dependencies></pre></div>
<p>看上面引入了 spring-boot-starter-webflux,为后面分析做铺垫;</p>
<p>除了引入依赖,我们还需要有一个启动类,如下:</p>
<div class="jb51code"><pre class="brush:java;">import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@Slf4j
@SpringBootApplication
public class XXGatewayApp {
public static void main(String[] args) {
SpringApplication.run(XXGatewayApp.class, args);
log.info("XX网关启动成功!");
}
}</pre></div>
<p class="maodian"></p><h2>启动 nettyserver</h2>
<p>在主类启动后,是如何启动一个nettyserver的,我们来分析一下;跟踪启动类代码来到如下方法:</p>
<blockquote><p>org.springframework.boot.SpringApplication#run(java.lang.String…)</p></blockquote>
<div class="jb51code"><pre class="brush:java;">public ConfigurableApplicationContext run(String... args) {
Startup startup = SpringApplication.Startup.create();
if (this.registerShutdownHook) {
shutdownHook.enableShutdownHookAddition();
}
... 省略代码...
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = this.printBanner(environment);
// @A1 创建 ConfigurableApplicationContext
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// @A2 触发创建server
this.refreshContext(context);
... 省略代码...
} catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
}
this.handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
return context;
} catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
} else {
this.handleRunFailure(context, ex, (SpringApplicationRunListeners)null);
throw new IllegalStateException(ex);
}
}
}</pre></div>
<p>@A1:这个方法会执行以下逻辑<br />org.springframework.boot.WebApplicationType#deduceFromClasspath<br />计算web容器类型,而最上面的pom依赖引入了 spring-boot-starter-webflux</p>
<div class="jb51code"><pre class="brush:java;"> static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", (ClassLoader)null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", (ClassLoader)null) && !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", (ClassLoader)null)) {
return REACTIVE;
} else {
for(String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
return NONE;
}
}
return SERVLET;
}
}</pre></div>
<p>上面代码根据类路径中加入的依赖,返回REACTIVE,最终返回 org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext 对象</p>
<p class="maodian"></p><h2>创建websever</h2>
<p>接上面 @A2方法,会触发AnnotationConfigReactiveWebServerApplicationContext如下调用:</p>
<p>注:AnnotationConfigReactiveWebServerApplicationContext父类是<br />ReactiveWebServerApplicationContext#createWebServer</p>
<div class="jb51code"><pre class="brush:java;"> private void createWebServer() {
WebServerManager serverManager = this.serverManager;
if (serverManager == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
String webServerFactoryBeanName = this.getWebServerFactoryBeanName();
//@B1 创建ReactiveWebServerFactory ,创建server的核心点
ReactiveWebServerFactory webServerFactory = this.getWebServerFactory(webServerFactoryBeanName);
createWebServer.tag("factory", webServerFactory.getClass().toString());
boolean lazyInit = this.getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
//@B2 WebServerManager构造方法创建server
this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit);
this.getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.serverManager.getWebServer()));
//@B3 很绝的方法
this.getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this.serverManager));
createWebServer.end();
}
this.initPropertySources();
}</pre></div>
<p>@B1 方法很绕,会从ReactiveWebServerFactoryConfiguration里去获得NettyReactiveWebServerFactory的bean定义,而这个bean定义依赖 ReactorResourceFactory ,代码如下:</p>
<div class="jb51code"><pre class="brush:java;"> @Bean
NettyReactiveWebServerFactory nettyReactiveWebServerFactory(ReactorResourceFactory resourceFactory, ObjectProvider<NettyRouteProvider> routes, ObjectProvider<NettyServerCustomizer> serverCustomizers) {
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory();
serverFactory.setResourceFactory(resourceFactory);
Stream var10000 = routes.orderedStream();
Objects.requireNonNull(serverFactory);
var10000.forEach((xva$0) -> serverFactory.addRouteProviders(new NettyRouteProvider[]{xva$0}));
serverFactory.getServerCustomizers().addAll(serverCustomizers.orderedStream().toList());
return serverFactory;
}</pre></div>
<p>也就是说在容器返回NettyReactiveWebServerFactory 对象前会把ReactorResourceFactory 对象初始化完毕;ReactorResourceFactory 这个类实现了InitializingBean,我们看看afterPropertiesSet方法初始化内容:</p>
<div class="jb51code"><pre class="brush:java;"> public void start() {
synchronized(this.lifecycleMonitor) {
if (!this.isRunning()) {
if (!this.useGlobalResources) {
if (this.loopResources == null) {
this.manageLoopResources = true;
this.loopResources = (LoopResources)this.loopResourcesSupplier.get();
}
if (this.connectionProvider == null) {
this.manageConnectionProvider = true;
this.connectionProvider = (ConnectionProvider)this.connectionProviderSupplier.get();
}
} else {
Assert.isTrue(this.loopResources == null && this.connectionProvider == null, "'useGlobalResources' is mutually exclusive with explicitly configured resources");
// @C1
HttpResources httpResources = HttpResources.get();
if (this.globalResourcesConsumer != null) {
this.globalResourcesConsumer.accept(httpResources);
}
this.connectionProvider = httpResources;
this.loopResources = httpResources;
}
this.running = true;
}
}
}</pre></div>
<p>@C1方法最终执行的是 reactor.netty.resources.LoopResources#create(java.lang.String)</p>
<div class="jb51code"><pre class="brush:java;"> static LoopResources create(String prefix) {
if (((String)Objects.requireNonNull(prefix, "prefix")).isEmpty()) {
throw new IllegalArgumentException("Cannot use empty prefix");
} else {
return new DefaultLoopResources(prefix, DEFAULT_IO_SELECT_COUNT, DEFAULT_IO_WORKER_COUNT, true);
}
}</pre></div>
<p>是不是很熟悉了,是创建的reactor.netty.resources.DefaultLoopResources 对象</p>
<p>todo~~</p>
<p>@B2 方法会调用到如下方法:<br />org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory#getWebServer</p>
<div class="jb51code"><pre class="brush:java;"> public WebServer getWebServer(HttpHandler httpHandler) {
// @D1 创建 HttpServer
HttpServer httpServer = this.createHttpServer();
ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter(httpHandler);
NettyWebServer webServer = this.createNettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout, this.getShutdown());
webServer.setRouteProviders(this.routeProviders);
return webServer;
}</pre></div>
<p>@D1 方法会执行到如下方法: org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory#createHttpServer</p>
<div class="jb51code"><pre class="brush:java;"> private HttpServer createHttpServer() {
HttpServer server = HttpServer.create().bindAddress(this::getListenAddress);
if (Ssl.isEnabled(this.getSsl())) {
server = this.customizeSslConfiguration(server);
}
if (this.getCompression() != null && this.getCompression().getEnabled()) {
CompressionCustomizer compressionCustomizer = new CompressionCustomizer(this.getCompression());
server = compressionCustomizer.apply(server);
}
server = server.protocol(this.listProtocols()).forwarded(this.useForwardHeaders);
return this.applyCustomizers(server);
}</pre></div>
<p>继续分析上面:@B3<br />这个地方太绝了,向容器中注入了一个 WebServerStartStopLifecycle,这种类型的类会被框架中触发start方法,触发一些列的start方法,最终执行的是 reactor.netty.http.server.HttpServerBind父类 ----> HttpServer 父类 -----> reactor.netty.transport.ServerTransport的 bindNow方法 ----> bind方法。</p>
<p class="maodian"></p><h2>重要方法 reactor.netty.transport.ServerTransport#bind</h2>
<div class="jb51code"><pre class="brush:java;"> public Mono<? extends DisposableServer> bind() {
CONF config = (CONF)(this.configuration());
Objects.requireNonNull(config.bindAddress(), "bindAddress");
Mono<? extends DisposableServer> mono = Mono.create((sink) -> {
SocketAddress local = (SocketAddress)Objects.requireNonNull((SocketAddress)config.bindAddress().get(), "Bind Address supplier returned null");
if (local instanceof InetSocketAddress) {
InetSocketAddress localInet = (InetSocketAddress)local;
if (localInet.isUnresolved()) {
local = AddressUtils.createResolved(localInet.getHostName(), localInet.getPort());
}
}
boolean isDomainSocket = false;
DisposableBind disposableServer;
if (local instanceof DomainSocketAddress) {
isDomainSocket = true;
disposableServer = new UdsDisposableBind(sink, config, local);
} else {
disposableServer = new InetDisposableBind(sink, config, local);
}
ConnectionObserver childObs = new ChildObserver(config.defaultChildObserver().then(config.childObserver()));
// @E1
Acceptor acceptor = new Acceptor(config.childEventLoopGroup(), config.channelInitializer(childObs, (SocketAddress)null, true), config.childOptions, config.childAttrs, isDomainSocket);
// @E2
TransportConnector.bind(config, new AcceptorInitializer(acceptor), local, isDomainSocket).subscribe(disposableServer);
});
if (config.doOnBind() != null) {
mono = mono.doOnSubscribe((s) -> config.doOnBind().accept(config));
}
return mono;
}</pre></div>
<p>@E1:最终调用 reactor.netty.resources.DefaultLoopResources#cacheNioServerLoops 获得work线程池<br />@E2:最终调用 reactor.netty.resources.DefaultLoopResources#cacheNioSelectLoops 获得boss线程池</p>
<p>问题来了:看reactor.netty.resources.LoopResources源码,如果系统参数里没有配置reactor.netty.ioSelectCount,则boss线程会和work线程池的AtomicReference在cacheNioSelectLoops 方法中返回同一对象;这是不是会造成线程池共用?</p>
<p>可以参看 https://blog.csdn.net/qq_42651904/article/details/134561804 这篇文章理解;</p>
<p>那么你们生产环境会单独设置 reactor.netty.ioSelectCount 参数吗?</p>
<p class="maodian"></p><h2>jvisualvm监控验证</h2>
<ul><li>如果不设置reactor.netty.ioSelectCount 这个启动参数,通过监控网关启动后线程名称是 reactor-http-nio-xx</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309290949.png" /></p>
<ul><li>如果在启动的时候设置这个参数为1,启动的线程不一样 reactor-http-select-xx</li></ul>
<div class="jb51code"><pre class="brush:java;"> public static void main(String[] args) {
System.setProperty("reactor.netty.ioSelectCount", "1");
SpringApplication.run(XXXGatewayApp.class, args);
log.info("XXX网关启动成功!");
}
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309290937.png" /></p>
<p>设想一下,如果代码写得不好在GlobalFilter有阻塞写法,比如数据查询、redis查询,加上高并发请求,那么是不是会影响boss线程“接客”?</p>
<p>后面再通过压测的方式论证一下。</p>
<p>到此这篇关于Spring Cloud Gateway 启动流程源码分析的文章就介绍到这了,更多相关Spring Cloud Gateway 启动流程内容请搜索琼殿技术社区以前的文章或继续浏览下面的相关文章希望大家以后多多支持琼殿技术社区!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>springcloud gateway网关服务启动报错的解决</li><li>spring-cloud-gateway启动踩坑及解决</li><li>springboot集成springCloud中gateway时启动报错的解决</li></ul>
</div>
</div>
<!--endmain--> 感谢楼主的详细分享!这种源码分析帖子对我帮助很大,之前一直对Gateway的启动流程比较模糊,看完清晰多了。
关于你最后提到的问题,我们生产环境确实有设置`reactor.netty.ioSelectCount`参数,设置为1。理由如下:
1. **线程隔离**:默认情况下boss线程和worker线程会共用,如果Gateway的Filter有阻塞操作(比如查Redis、数据库),确实会影响接收新连接的性能
2. **高并发场景**:我们之前遇到过类似问题,高峰期QPS上来后,Gateway响应变慢,排查发现就是线程共用导致的
不过也要注意几点:
- 设置为1会多创建一个select线程,稍微增加一点资源消耗,但对于Gateway这种网关服务来说这点开销可以忽略
- 关键还是要保证Filter是非阻塞的,Gateway基于WebFlux本身就是响应式的
- 如果业务Filter有阻塞操作,建议用`subscribeOn`或者`boundedElastic`调度器去处理
另外想请教下,你们压测结果怎么样?设置这个参数后性能提升明显吗?
頁:
[1]