关于Spring Cloud Gateway与下游服务器的连接分析
suiw9 2024-10-28 18:33 33 浏览 0 评论
背景
最近面试了不少同学,有很大一部分简历上会提到网关,我一般都会顺着往下问他们的网关是怎么做的。
基本上都是说直接使用的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));
}
上面代码的逻辑主要就是
- 获取通信用的httpclient
- 设置headers,method和url
- 执行responseConnection()方法发起连接
- 连接成功之后执行send()方法传入的lambda方法。
- 执行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是一个连接池,内部会保存复用已经生成的连接。
当网关转发请求时确认下游目标服务的地址,即可直接从对应的连接池中取出连接复用。
作者:Slogen
链接:https://juejin.cn/post/6981375636971454477
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
相关推荐
- 俄罗斯的 HTTPS 也要被废了?(俄罗斯网站关闭)
-
发布该推文的ScottHelme是一名黑客,SecurityHeaders和ReportUri的创始人、Pluralsight作者、BBC常驻黑客。他表示,CAs现在似乎正在停止为俄罗斯域名颁发...
- 如何强制所有流量使用 HTTPS一网上用户
-
如何强制所有流量使用HTTPS一网上用户使用.htaccess强制流量到https的最常见方法可能是使用.htaccess重定向请求。.htaccess是一个简单的文本文件,简称为“.h...
- https和http的区别(https和http有何区别)
-
“HTTPS和HTTP都是数据传输的应用层协议,区别在于HTTPS比HTTP安全”。区别在哪里,我们接着往下看:...
- 快码住!带你十分钟搞懂HTTP与HTTPS协议及请求的区别
-
什么是协议?网络协议是计算机之间为了实现网络通信从而达成的一种“约定”或“规则”,正是因为这个“规则”的存在,不同厂商的生产设备、及不同操作系统组成的计算机之间,才可以实现通信。简单来说,计算机与网络...
- 简述HTTPS工作原理(简述https原理,以及与http的区别)
-
https是在http协议的基础上加了一层SSL(由网景公司开发),加密由ssl实现,它的目的是为用户提供对网站服务器的身份认证(需要CA),以至于保护交换数据的隐私和完整性,原理如图示。1、客户端发...
- 21、HTTPS 有几次握手和挥手?HTTPS 的原理什么是(高薪 常问)
-
HTTPS是3次握手和4次挥手,和HTTP是一样的。HTTPS的原理...
- 一次安全可靠的通信——HTTPS原理
-
为什么HTTPS协议就比HTTP安全呢?一次安全可靠的通信应该包含什么东西呢,这篇文章我会尝试讲清楚这些细节。Alice与Bob的通信...
- 为什么有的网站没有使用https(为什么有的网站点不开)
-
有的网站没有使用HTTPS的原因可能涉及多个方面,以下是.com、.top域名的一些见解:服务器性能限制:HTTPS使用公钥加密和私钥解密技术,这要求服务器具备足够的计算能力来处理加解密操作。如果服务...
- HTTPS是什么?加密原理和证书。SSL/TLS握手过程
-
秘钥的产生过程非对称加密...
- 图解HTTPS「转」(图解http 完整版 彩色版 pdf)
-
我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取。所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。...
- HTTP 和 HTTPS 有何不同?一文带你全面了解
-
随着互联网时代的高速发展,Web服务器和客户端之间的安全通信需求也越来越高。HTTP和HTTPS是两种广泛使用的Web通信协议。本文将介绍HTTP和HTTPS的区别,并探讨为什么HTTPS已成为We...
- HTTP与HTTPS的区别,详细介绍(http与https有什么区别)
-
HTTP与HTTPS介绍超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的...
- 一文让你轻松掌握 HTTPS(https详解)
-
一文让你轻松掌握HTTPS原文作者:UC国际研发泽原写在最前:欢迎你来到“UC国际技术”公众号,我们将为大家提供与客户端、服务端、算法、测试、数据、前端等相关的高质量技术文章,不限于原创与翻译。...
- 如何在Spring Boot应用程序上启用HTTPS?
-
HTTPS是HTTP的安全版本,旨在提供传输层安全性(TLS)[安全套接字层(SSL)的后继产品],这是地址栏中的挂锁图标,用于在Web服务器和浏览器之间建立加密连接。HTTPS加密每个数据包以安全方...
- 一文彻底搞明白Http以及Https(http0)
-
早期以信息发布为主的Web1.0时代,HTTP已可以满足绝大部分需要。证书费用、服务器的计算资源都比较昂贵,作为HTTP安全扩展的HTTPS,通常只应用在登录、交易等少数环境中。但随着越来越多的重要...
你 发表评论:
欢迎- 一周热门
-
-
Linux:Ubuntu22.04上安装python3.11,简单易上手
-
宝马阿布达比分公司推出独特M4升级套件,整套升级约在20万
-
MATLAB中图片保存的五种方法(一)(matlab中保存图片命令)
-
别再傻傻搞不清楚Workstation Player和Workstation Pro的区别了
-
Linux上使用tinyproxy快速搭建HTTP/HTTPS代理器
-
如何提取、修改、强刷A卡bios a卡刷bios工具
-
Element Plus 的 Dialog 组件实现点击遮罩层不关闭对话框
-
日本组合“岚”将于2020年12月31日停止团体活动
-
SpringCloud OpenFeign 使用 okhttp 发送 HTTP 请求与 HTTP/2 探索
-
tinymce 号称富文本编辑器世界第一,大家同意么?
-
- 最近发表
- 标签列表
-
- dialog.js (57)
- importnew (44)
- windows93网页版 (44)
- yii2框架的优缺点 (45)
- tinyeditor (45)
- qt5.5 (60)
- windowsserver2016镜像下载 (52)
- okhttputils (51)
- android-gif-drawable (53)
- 时间轴插件 (56)
- docker systemd (65)
- slider.js (47)
- android webview缓存 (46)
- pagination.js (59)
- loadjs (62)
- openssl1.0.2 (48)
- velocity模板引擎 (48)
- pcre library (47)
- zabbix微信报警脚本 (63)
- jnetpcap (49)
- pdfrenderer (43)
- fastutil (48)
- uinavigationcontroller (53)
- bitbucket.org (44)
- python websocket-client (47)