关于Spring Cloud Gateway与下游服务器的连接分析

简介: 背景最近面试了不少同学,有很大一部分简历上会提到网关,我一般都会顺着往下问他们的网关是怎么做的。基本上都是说直接使用的Spring Cloud Gateway或者基于Spring Cloud Gateway二次开发。这种时候我会继续问一个比较基础的问题:Spring Cloud Gateway作为网关,会把接收到的请求转发给下游服务,那么Spring Cloud Gateway跟下游的服务之间保持的是长连还是短连?还是说每次转发的时候都会新建立一个连接吗?

背景

最近面试了不少同学,有很大一部分简历上会提到网关,我一般都会顺着往下问他们的网关是怎么做的。

基本上都是说直接使用的Spring Cloud Gateway或者基于Spring Cloud Gateway二次开发。

这种时候我会继续问一个比较基础的问题:Spring Cloud Gateway作为网关,会把接收到的请求转发给下游服务,那么Spring Cloud Gateway跟下游的服务之间保持的是长连还是短连?还是说每次转发的时候都会新建立一个连接吗?

很遗憾的是,这么基础的问题,很少有面试者完全搞清楚。

所以才有了这篇文章:通过研究Spring Cloud Gateway的源码,来看看Spring Cloud Gateway跟下游服务之间是怎么通信的。

Spring Cloud Gateway

在源码分析之前,需要先了解一下Spring Cloud Gateway

SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

Spring Cloud Gateway是基于Spring WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

Spring Cloud Gateway架构图如下:

网络异常,图片无法展示
|

源码分析

对于基于webflux的应用,入口点都在DispatchHandler.handle()方法:

网络异常,图片无法展示
|

网络异常,图片无法展示
|

最终执行到
SimpleHandlerAdapter.handle() 方法

网络异常,图片无法展示
|

handler()方法中执行的是
FilteringWebHandler.handle()方法

网络异常,图片无法展示
|


FilteringWebHandler.handler()方法的主要逻辑就是依次执行已经形成的全局过滤器globalFilter的filter()方法。

从截图中可以看到,默认会生成9个全局过滤器GatewayFilter对象。

单步调试下去,发现涉及到网络这一块的操作都在倒数第二个过滤器NettyRoutingFilter类中。

现在着重来看一下NettyRoutingFilter.filter()方法:

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
    // ... 一些省略代码
    // 获取httpclient
    Flux<HttpClientResponse> responseFlux = getHttpClient(route, exchange)
            .headers(headers -> {
                headers.add(httpHeaders);
                // Will either be set below, or later by Netty
                headers.remove(HttpHeaders.HOST);
                if (preserveHost) {
                    String host = request.getHeaders().getFirst(HttpHeaders.HOST);
                    headers.add(HttpHeaders.HOST, host);
                }
            }).request(method).uri(url).send((req, nettyOutbound) -> {
                if (log.isTraceEnabled()) {
                    nettyOutbound
                            .withConnection(connection -> log.trace("outbound route: "
                                    + connection.channel().id().asShortText()
                                    + ", inbound: " + exchange.getLogPrefix()));
                }
                // 发送请求
                return nettyOutbound.send(request.getBody().map(this::getByteBuf));
            }).responseConnection((res, connection) -> {
                // 省略代码,下游请求返回之后做的一些处理
                return Mono.just(res);
            });
    Duration responseTimeout = getResponseTimeout(route);
    // 一些省略代码
    return responseFlux.then(chain.filter(exchange));
}

上面代码的逻辑主要就是

  1. 获取通信用的httpclient
  2. 设置headers,method和url
  3. 执行responseConnection()方法发起连接
  4. 连接成功之后执行send()方法传入的lambda方法。
  5. 执行responseConnection()传入的lambda方法。

首先来看一下getHttpClient()方法

protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) {
    // 省略代码,timeout设置
    return httpClient;
}

实际上就是直接返回httpClient对象,那么httpClient是在哪里设置的呢?

public NettyRoutingFilter(HttpClient httpClient,
        ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider,
        HttpClientProperties properties) {
    this.httpClient = httpClient;
    this.headersFiltersProvider = headersFiltersProvider;
    this.properties = properties;
}

可以看到是在生成NettyRoutingFilter对象的时候传入的,那么NettyRoutingFilter对象在哪里生成的呢?

答:在GatewayAutoConfiguration类中生成的,这个类是在引入网关的依赖之后自动引入的。

同样的,HttpClient对象也是在这个类里面生成的。

@Bean
@ConditionalOnMissingBean
public HttpClient gatewayHttpClient(HttpClientProperties properties,
        List<HttpClientCustomizer> customizers) {
    // 配置连接池
    HttpClientProperties.Pool pool = properties.getPool();
    ConnectionProvider connectionProvider;
    if (pool.getType() == DISABLED) {
        connectionProvider = ConnectionProvider.newConnection();
    }
    else if (pool.getType() == FIXED) {
        connectionProvider = ConnectionProvider.fixed(pool.getName(),
                pool.getMaxConnections(), pool.getAcquireTimeout(),
                pool.getMaxIdleTime(), pool.getMaxLifeTime());
    }
    else {
        connectionProvider = ConnectionProvider.elastic(pool.getName(),
                pool.getMaxIdleTime(), pool.getMaxLifeTime());
    }
    HttpClient httpClient = HttpClient.create(connectionProvider)
            // TODO: move customizations to HttpClientCustomizers
            .httpResponseDecoder(spec -> {
                // 省略代码
                return spec;
            }).tcpConfiguration(tcpClient -> {
                // 省略代码
                return tcpClient;
            });
    // 省略代码  ssl设置
    return httpClient;
}

从上面代码可以看出,HttpClient对象自带一个连接池,生成Httpclient的时候首先会配置这个连接池。

可以看到HttpClient提供的连接池的类型:

public enum PoolType {
    /**
     * 弹性的连接池
     */
    ELASTIC,
    /**
     * 固定长度的连接池
     */
    FIXED,
    /**
     * 不使用连接池
     */
    DISABLED
}

默认使用的是第一种 弹性的连接池

private PoolType type = PoolType.ELASTIC;
connectionProvider = ConnectionProvider.elastic(pool.getName(),
            pool.getMaxIdleTime(), pool.getMaxLifeTime());
static ConnectionProvider elastic(String name, @Nullable Duration maxIdleTime, @Nullable Duration maxLifeTime) {
    return builder(name).maxConnections(Integer.MAX_VALUE) //设置最大连接数无限制
                        .pendingAcquireTimeout(Duration.ofMillis(0))
                        .pendingAcquireMaxCount(-1)
                        .maxIdleTime(maxIdleTime)
                        .maxLifeTime(maxLifeTime)
                        .build();
}
static Builder builder(String name) {
    return new Builder(name);
}

在Builder()构造函数中会调用ConnectionPoolSpec()方法:

private ConnectionPoolSpec() {
    if (DEFAULT_POOL_MAX_IDLE_TIME > -1) {
        maxIdleTime(Duration.ofMillis(DEFAULT_POOL_MAX_IDLE_TIME));
    }
    // 支持不同类型的链接保存方式
    // lifo和fifo
    if(LEASING_STRATEGY_LIFO.equals(DEFAULT_POOL_LEASING_STRATEGY)) {
        lifo();
    }
    else {
        fifo();
    }
}

从代码里面可以看到,httpclient自带的连接池还支持两种连接获取方式: lifo(后进先出)和fifo(先进先出) 默认使用的是fifo。

先总结一下,在引入网关的依赖之后,会自动创建一个HttpClient对象,而这个HttpClient对象自带一个连接池,且默认是Elastic连接池,即连接池内的数量会弹性发生变化。 连接池内部默认采用fifo的方式来保存以及使用连接

现在重新回到NettyRoutingFilter.filter()方法中来看下:

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
    // ... 一些省略代码
    // 获取httpclient
    Flux<HttpClientResponse> responseFlux = getHttpClient(route, exchange)
            .headers(headers -> {
                headers.add(httpHeaders);
                // Will either be set below, or later by Netty
                headers.remove(HttpHeaders.HOST);
                if (preserveHost) {
                    String host = request.getHeaders().getFirst(HttpHeaders.HOST);
                    headers.add(HttpHeaders.HOST, host);
                }
            }).request(method).uri(url).send((req, nettyOutbound) -> {
                if (log.isTraceEnabled()) {
                    nettyOutbound
                            .withConnection(connection -> log.trace("outbound route: "
                                    + connection.channel().id().asShortText()
                                    + ", inbound: " + exchange.getLogPrefix()));
                }
                // 发送请求
                return nettyOutbound.send(request.getBody().map(this::getByteBuf));
            }).responseConnection((res, connection) -> {
                // 省略代码,下游请求返回之后做的一些处理
                return Mono.just(res);
            });
    Duration responseTimeout = getResponseTimeout(route);
    // 一些省略代码
    return responseFlux.then(chain.filter(exchange));
}

responseConnection()方法中会发起连接操作:

final TcpClient cachedConfiguration;
@SuppressWarnings("unchecked")
Mono<HttpClientOperations> connect() {
    return (Mono<HttpClientOperations>)cachedConfiguration.connect();
}
@Override
public <V> Flux<V> responseConnection(BiFunction<? super HttpClientResponse, ? super Connection, ? extends Publisher<V>> receiver) {
    return connect().flatMapMany(resp -> Flux.from(receiver.apply(resp, resp)));
}

调用的是TcpClient对象的connect()方法,一步步断点下去发现最终调用的是TcpClientConnect.connect()方法.

final ConnectionProvider provider;
@Override
public Mono<? extends Connection> connect(Bootstrap b) {
    if (b.config()
         .group() == null) {
        TcpClientRunOn.configure(b,
                LoopResources.DEFAULT_NATIVE,
                TcpResources.get());
    }
    // 这里的provider实际上就是前面分析的创建HttpClient的时候生成的ConnectProvider对象
    return provider.acquire(b);
}

从代码实现中可以看到,实际上TcpClienConnect是直接从ConnectionProvider获取连接。

看到这里,本文一开始的问题其实已经有解答了:

默认情况下(除非显示设置不使用连接池),网关在把请求转发给下游服务器的时候,是会使用连接池的,而不是每次都重新发起连接。

继续往下分析。

对于Elastic类型的连接池来说,其默认实现为PooledConnectionProvider

// key为远程地址(一般指代一个远程服务),value则对应的ConnectioAllocator
final ConcurrentMap<PoolKey, InstrumentedPool<PooledConnection>> channelPools =
      PlatformDependent.newConcurrentHashMap();
@Override
public Mono<Connection> acquire(Bootstrap b) {
    return Mono.create(sink -> {
        // ...其他省略代码
        SocketAddress remoteAddress = bootstrap.config().remoteAddress();
        PoolKey holder = new PoolKey(remoteAddress, handler != null ? handler.hashCode() : -1);
        // 每个远程地址都可以配置一个PoolFactory,如果没配置则使用默认的PoolFactory
        PoolFactory poolFactory = poolFactoryPerRemoteHost.getOrDefault(remoteAddress, defaultPoolFactory);
        InstrumentedPool<PooledConnection> pool = channelPools.computeIfAbsent(holder, poolKey -> {
            if (log.isDebugEnabled()) {
                log.debug("Creating a new client pool [{}] for [{}]", poolFactory, remoteAddress);
            }
            // newPool是一个连接分配器,实际上就是一个连接池
            InstrumentedPool<PooledConnection> newPool =
                    new PooledConnectionAllocator(bootstrap, poolFactory, opsFactory).pool;
            if (poolFactory.metricsEnabled || BootstrapHandlers.findMetricsSupport(bootstrap) != null) {
                PooledConnectionProviderMetrics.registerMetrics(name,
                        poolKey.hashCode() + "",
                        Metrics.formatSocketAddress(remoteAddress),
                        newPool.metrics());
            }
            return newPool;
        });
        //
        disposableAcquire(new DisposableAcquire(sink, pool, obs, opsFactory, poolFactory.pendingAcquireTimeout, false));
    });
}
static void disposableAcquire(DisposableAcquire disposableAcquire) {
    // accquire一个连接,如果无可用了解则创建,则调用
    Mono<PooledRef<PooledConnection>> mono =
            disposableAcquire.pool.acquire(Duration.ofMillis(disposableAcquire.pendingAcquireTimeout));
    mono.subscribe(disposableAcquire);
}
Publisher<PooledConnection> connectChannel() {
    return Mono.create(sink -> {
        Bootstrap b = bootstrap.clone();
        PooledConnectionInitializer initializer = new PooledConnectionInitializer(sink);
        b.handler(initializer);
        // 创建连接
        ChannelFuture f = b.connect();
        if (f.isDone()) {
            initializer.operationComplete(f);
        } else {
            f.addListener(initializer);
        }
    });
}

从代码里面可以看出,ConnectionProvider对每一个远程地址(即下游服的某一个服务器)都缓存了一个连接分配器(ConnectionAllocator),而这个ConnectionAllocator才是真正的连接池,是Project Reactor项目内部实现的一个连接池,就不从源码角度分析,简单来说,就是请求方获取连接的时候,如果池子里面有空闲连接,则直接用现成连接,如果没有的话,则调用PoolFactory创建新的链接。

网络异常,图片无法展示
|

总结一下:

网关内部维持了一个缓存映射,缓存着下游每一个服务地址(ip:port)对应的连接分配器(ConnectionAllocator),而ConnectionAllocator是一个连接池,内部会保存复用已经生成的连接。

当网关转发请求时确认下游目标服务的地址,即可直接从对应的连接池中取出连接复用。

相关文章
|
1月前
|
设计模式 前端开发 Java
Spring MVC——项目创建和建立请求连接
MVC是一种软件架构设计模式,将应用分为模型、视图和控制器三部分。Spring MVC是基于MVC模式的Web框架,通过`@RequestMapping`等注解实现URL路由映射,支持GET和POST请求,并可传递参数。创建Spring MVC项目与Spring Boot类似,使用`@RestController`注解标记控制器类。
34 1
Spring MVC——项目创建和建立请求连接
|
1月前
|
IDE 网络安全 开发工具
IDE之vscode:连接远程服务器代码(亲测OK),与pycharm链接服务器做对比(亲自使用过了),打开文件夹后切换文件夹。
本文介绍了如何使用VS Code通过Remote-SSH插件连接远程服务器进行代码开发,并与PyCharm进行了对比。作者认为VS Code在连接和配置多个服务器时更为简单,推荐使用VS Code。文章详细说明了VS Code的安装、远程插件安装、SSH配置文件编写、服务器连接以及如何在连接后切换文件夹。此外,还提供了使用密钥进行免密登录的方法和解决权限问题的步骤。
356 0
IDE之vscode:连接远程服务器代码(亲测OK),与pycharm链接服务器做对比(亲自使用过了),打开文件夹后切换文件夹。
|
1月前
|
IDE 网络安全 开发工具
IDE之pycharm:专业版本连接远程服务器代码,并配置远程python环境解释器(亲测OK)。
本文介绍了如何在PyCharm专业版中连接远程服务器并配置远程Python环境解释器,以便在服务器上运行代码。
286 0
IDE之pycharm:专业版本连接远程服务器代码,并配置远程python环境解释器(亲测OK)。
|
1月前
|
人工智能 运维 Kubernetes
87cloud案例分析:阿里云国际服务器如何支持在线教育
87cloud案例分析:阿里云国际服务器如何支持在线教育
|
1月前
|
存储 网络协议 Java
【网络】UDP回显服务器和客户端的构造,以及连接流程
【网络】UDP回显服务器和客户端的构造,以及连接流程
51 2
|
1月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
53 2
|
1月前
|
前端开发 Java
学习SpringMVC,建立连接,请求,响应 SpringBoot初学,如何前后端交互(后端版)?最简单的能通过网址访问的后端服务器代码举例
文章介绍了如何使用SpringBoot创建简单的后端服务器来处理HTTP请求,包括建立连接、编写Controller处理请求,并返回响应给前端或网址。
53 0
学习SpringMVC,建立连接,请求,响应 SpringBoot初学,如何前后端交互(后端版)?最简单的能通过网址访问的后端服务器代码举例
|
2月前
|
负载均衡 Java Nacos
SpringCloud基础2——Nacos配置、Feign、Gateway
nacos配置管理、Feign远程调用、Gateway服务网关
SpringCloud基础2——Nacos配置、Feign、Gateway
|
2月前
|
Java 开发者 Spring
Spring Cloud Gateway 中,过滤器的分类有哪些?
Spring Cloud Gateway 中,过滤器的分类有哪些?
48 3
|
2月前
|
负载均衡 Java 网络架构
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
102 5