SpringCloudGateway tcp连接无法回收的分析与修复

本文详细分析了SpringCloudGateway在不同版本下出现TCP连接无法回收的问题,探讨了网关功能、生产环境中的现象以及尝试的两种解决办法。第一种方法是通过配置Nacos来调整HTTP客户端池的参数,但未能解决问题。第二种方法涉及到了TCP的keepalive功能,通过检查发现服务的TCP连接未启用keepalive,导致连接无法被回收。最后,通过自定义NettyServerCustomizer并在代码中设置`ChannelOption.SO_KEEPALIVE`,成功实现了TCP连接的回收。实测表明,修改后的网关能够有效地回收TCP连接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

SpringCloudGateway tcp连接无法回收的分析与修复

scg tcp连接不回收的分析与修复

一、springcloud版本

第一个版本:Hoxton.SR12
第二个版本:2021.0.2
在第一个版本出问题后升级了第二个版本,可是问题还是依旧出现。

二、网关功能

包含数据加解密、websocket转发这两个额外的功能;

三、 生产描述

2,网关基本上每分钟都有人访问,tcp连接数持续增长,到65535左右后,不再接受新的请求,服务就无法访问,只能重启
在这里插入图片描述

四、 解决方式一(tcp连接还是不回收,对我的网关没效果)

https://blog.youkuaiyun.com/weixin_43142697/article/details/122605048,参照了这个里面的修改意见,我在nacos里面增加了网关配置,可是观察了快两个小时,没有一个链接回收掉,以下是我的配置:

// An highlighted block
  cloud:
    gateway:
      httpclient:
        pool:
          max-idle-time: PT1S
          eviction-interval: PT30S
        connect-timeout: 20000
        response-timeout: PT30S

使用 ss -aoen|grep 443|grep ESTAB 命令获取以下图片内容,左侧红圈中是用户ip,隔一段时间就使用该命令查看,发现ip一个不少,不知道是我配的有问题还是什么原因,我也暂时没有时间细究了,等有时间我要再定位下看看
在这左边里插入图片描述

五、解决方式二(验证通过)

1、使用netstat -tnpoa|sed -n -e 2p -e /443/p,看下图,查看这些tcp连接的状态,无一例外都是ESTABLISHED off
先看看这个的英文解释:
keepalive - when the keepalive timer is ON for the socket
on - when the retransmission timer is ON for the socket
off - none of the above is ON
其实很明白,说这些连接当前的监听器既不是keepalive 也不是on,换句话说就是没有用于回收的监听器,连接永远无法回收
在这里插入图片描述
2、再使用ss -aoen|grep 443|grep ESTAB查看有没有监听器Timer,看下图,右侧红圈中只有ino,没有出现timer字段,也就是没有用上tcp的keepalive功能,说明确实链接没有用到监听回收器
在这左边里插入图片描述
3、不禁要疑问下,会不会是代码的问题,不然springcloud gateway不可能犯这种低级错误,但时间紧,也不能再细想了,但有时间我还得看看自己的代码坑。
4、我们看下正常情况下的tcp链接的keepalive功能,可以看到下图中的
Timer:(keepalive,119min,0),说明这几个tcp都是会被服务器探测是否失效的,其中119min是指119分钟后开始探测该链接是否有效,0代表第一次探测失败后的重试探测次数。

5、通过sysctl -a |grep keepalive,可以查看到linux下默认的配置,虽然这里看到配置了keepalive,但是Tcp进程必须额外开启keepalive,才能生效。
在这里插入图片描述
tcp_keepalive_time:表示多长时间后,开始探测TCP链接是否有效,一般系统默认两小时。
tcp_keepalive_probes:表示如果探测失败的话,会继续探测 9 次。
tcp_keepalive_intvl:tcp_keepalive_probes的探测时间间隔为 75 秒。
如果服务的访问量比较大,建议将tcp_keepalive_time按需设置,比如五分钟,十分钟
6、既然需要开启tcp的keepalive功能,就需要对springcloud gateway中关于tcp的源码进行分析了,在网上搜索gateway tcp相关的源码就可以搜索到TcpServerBind这个类,其实tcp相关的代码都在reactor.netty.tcp这个包下,也很好找,我们看下TcpServerBind这个类,可以发现使用了ChannelOption.SO_REUSEADDR:
这个参数表示允许重复使用本地地址和端口,比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。

	static final TcpServerBind INSTANCE = new TcpServerBind();

	final TcpServerConfig config;

	TcpServerBind() {
		Map<ChannelOption<?>, Boolean> childOptions = new HashMap<>(2);
		childOptions.put(ChannelOption.AUTO_READ, false);
		childOptions.put(ChannelOption.TCP_NODELAY, true);
		this.config = new TcpServerConfig(
				// 可以看到gateway
				Collections.singletonMap(ChannelOption.SO_REUSEADDR, true),
				childOptions,
				() -> new InetSocketAddress(DEFAULT_PORT));
	}

	TcpServerBind(TcpServerConfig config) {
		this.config = config;
	}

7、而我们现在配置另一个参数:Channeloption.SO_KEEPALIVE,对应于套接字选项中的SO_KEEPALIVE,服务器会启动定时器去探测该tcp连接的有效性。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
那在哪里设置这个ChannelOption.SO_KEEPALIVE呢,NettyReactiveWebServerFactory这个类会负责
配置一些启动参数,比如里面的NettyServerCustomizer,字面意思就是netty服务器自定义,那很明显,我可以设置一些自定义属性

	/**
	 * Set {@link NettyServerCustomizer}s that should be applied to the Netty server
	 * builder. Calling this method will replace any existing customizers.
	 * @param serverCustomizers the customizers to set
	 */
	public void setServerCustomizers(Collection<? extends NettyServerCustomizer> serverCustomizers) {
		Assert.notNull(serverCustomizers, "ServerCustomizers must not be null");
		this.serverCustomizers = new LinkedHashSet<>(serverCustomizers);
	}

	/**
	 * Add {@link NettyServerCustomizer}s that should be applied while building the
	 * server.
	 * @param serverCustomizers the customizers to add
	 */
	public void addServerCustomizers(NettyServerCustomizer... serverCustomizers) {
		Assert.notNull(serverCustomizers, "ServerCustomizer must not be null");
		this.serverCustomizers.addAll(Arrays.asList(serverCustomizers));
	}

8、再继续看NettyServerCustomizer 这个接口的实现,可以看到继承了函数,并且入参和出参都是HttpServer,

/**
 * Mapping function that can be used to customize a Reactor Netty server instance.
 *
 * @author Brian Clozel
 * @since 2.1.0
 * @see NettyReactiveWebServerFactory
 */
@FunctionalInterface
public interface NettyServerCustomizer extends Function<HttpServer, HttpServer> {

}

9、再继续看 HttpServer,代码如下,在类里面搜索关键字childOption和tcp,我们很容易定位到以下代码,可以看到注释中其实告诉我们怎么设置childOption了


	/**
	 * Apply a {@link TcpServer} mapping function to update TCP configuration and
	 * return an enriched {@link HttpServer} to use.
	 * <p>
	 * <strong>Note:</strong>
	 * There isn't only one method that replaces this deprecated method.
	 * The configuration that can be done with this deprecated method,
	 * can also be done with the other methods exposed by {@link HttpServer}.
	 * </p>
	 * <p>Examples:</p>
	 * <p>Configuration via the deprecated '.tcpConfiguration(...)' method</p>
	 * <pre>
	 * {@code

	 * HttpServer.tcpConfiguration(tcpServer ->
	 *     tcpServer.attr(...) // configures the channel attributes
	 *              .bindAddress(...) // configures the bind (local) address
	 *              .channelGroup(...) // configures the channel group
	 *              .childAttr(...) // configures the child channel attributes
	 *              .childObserve(...) // configures the child channel connection observer
	 *              .childOption(...) // 	 可用于设置keepalive
	 *              .doOnBound(...) // configures the doOnBound callback
	 *              .doOnChannelInit(...) // configures the channel handler
	 *              .doOnConnection(...) // configures the doOnConnection callback
	 *              .doOnUnbound(...) // configures the doOnUnbound callback
	 *              .handle(...) // configures the I/O handler
	 *              .host(...) // configures the host name
	 *              .metrics(...) // configures the metrics
	 *              .noSSL() // removes SSL configuration
	 *              .observe() // configures the connection observer
	 *              .option(...) // configures the channel options
	 *              .port(...) // configures the port
	 *              .runOn(...) // configures the event loop group
	 *              .secure() // configures the SSL
	 *              .wiretap()) // configures the wire logging
	 * }
	 * </pre>
	 *
	 * <p>Configuration via the other methods exposed by {@link HttpServer}</p>
	 * <pre>
	 * {@code
	 * HttpServer.attr(...) // configures the channel attributes
	 *           .bindAddress(...) // configures the bind (local) address
	 *           .channelGroup(...) // configures the channel group
	 *           .childAttr(...) // configures the child channel attributes
	 *           .childObserve(...) // configures the child channel connection observer
	 *           .childOption(...) // configures the child channel options
	 *           .doOnBound(...) // configures the doOnBound callback
	 *           .doOnChannelInit(...) // configures the channel handler
	 *           .doOnConnection(...) // configures the doOnConnection callback
	 *           .doOnUnbound(...) // configures the doOnUnbound callback
	 *           .handle(...) // configures the I/O handler
	 *           .host(...) // configures the host name
	 *           .metrics(...) // configures the metrics
	 *           .noSSL() // removes SSL configuration
	 *           .observe() // configures the connection observer
	 *           .option(...) // configures the channel options
	 *           .port(...) // configures the port
	 *           .runOn(...) // configures the event loop group
	 *           .secure() // configures the SSL
	 *           .wiretap() // configures the wire logging
	 * }
	 * </pre>
	 *
	 * <p>Wire logging in plain text</p>
	 * <pre>
	 * {@code
	 * HttpServer.wiretap("logger", LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL)
	 * }
	 * </pre>
	 *
	 * @param tcpMapper A {@link TcpServer} mapping function to update TCP configuration and
	 * return an enriched {@link HttpServer} to use.
	 * @return a new {@link HttpServer}
	 * @deprecated Use the other methods exposed by {@link HttpServer} to achieve the same configurations.
	 * This method will be removed in version 1.1.0.
	 */
	@Deprecated
	@SuppressWarnings("ReturnValueIgnored")
	public final HttpServer tcpConfiguration(Function<? super TcpServer, ? extends TcpServer> tcpMapper) {
		Objects.requireNonNull(tcpMapper, "tcpMapper");
		HttpServerTcpConfig tcpServer = new HttpServerTcpConfig(this);
		// ReturnValueIgnored is deliberate
		tcpMapper.apply(tcpServer);
		return tcpServer.httpServer;
	}

10、源码观察到这里,那就把写好的类贴在这里,代码如下:

@Component
public class NettyServerChannelOptionCustomization extends ReactiveWebServerFactoryCustomizer {

	public NettyServerChannelOptionCustomization(ServerProperties serverProperties) {
		super(serverProperties);
	}

	@SuppressWarnings("deprecation")
	@Override
	public void customize(ConfigurableReactiveWebServerFactory factory) {
		super.customize(factory);
		NettyReactiveWebServerFactory nettyFactory = (NettyReactiveWebServerFactory) factory;
		nettyFactory.setResourceFactory(null);
		nettyFactory.addServerCustomizers(server -> server
				.tcpConfiguration(tcpServer -> tcpServer.childOption(ChannelOption.SO_KEEPALIVE, true)));
	}
}

11、将上面代码放在网关代码里面重新部署上线,tcp趋势图如下,部署一段时间后,用户访问开始激增,并且随着时间推移,用户访问量减缓后,可以看到tcp回收很明显,有明显的下降趋势
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

### SCG 网关 'Connection reset by peer' 错误原因分析 'Spring Cloud Gateway (SCG)' 中出现 `Connection reset by peer` 的错误通常表明客户端尝试向服务器发送数据时,服务器已关闭连接。这种行为可能由多种因素引起。 #### 原因一:后端服务主动断开连接 当后端服务由于某种原因(如超时、资源不足或其他异常情况)主动终止网关之间的 TCP 连接时,后续来自网关的请求可能会触发此错误[^2]。这种情况下的典型日志显示为 `readAddress(..) failed: Connection reset by peer`。 #### 原因二:网络不稳定或中间设备干扰 如果存在防火墙、负载均衡器或其他中间设备,在长时间未活动的情况下可能导致连接被强制中断,从而引发该问题[^1]。 --- ### 解决方案 以下是针对上述问题的具体解决措施: #### 方法一:调整后端服务的连接保持时间 可以通过修改后端服务的相关参数延长其维持空闲连接的时间长度。对于 Undertow 或其他非 Tomcat 容器而言,可以设置如下属性以优化连接管理: ```yaml server: undertow: idle-timeout: 60000 # 单位毫秒,默认值可根据实际需求调整 ``` 此外,还可以增加重试机制以便在网络波动期间提高系统的鲁棒性: ```java @Bean public HttpClient httpClient() { return HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) .responseTimeout(Duration.ofMillis(6000)) .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(60))); } ``` #### 方法二:启用 WebFlux 自动重连功能 Spring Cloud Gateway 使用 Reactor Netty 实现底层通信支持自动恢复失败的 HTTP 请求。只需确保配置文件中启用了必要的选项即可实现这一目标: ```yaml spring: cloud: gateway: httpclient: pool-connections-idle-timeout: PT3S # 配置闲置连接回收周期 max-connection-lifetime: PT1M # 设定最大存活期限减少僵死链接风险 ``` 同时注意检查是否有自定义过滤器影响正常流程逻辑[^1]。 #### 方法三:排查并修复潜在的服务稳定性隐患 定期监控后台应用程序运行状态,及时发现并排除任何可能导致意外退出的因素。必要时可借助工具如 Arthas 对线程堆栈信息深入挖掘定位根本诱因。 --- ### 注意事项 尽管以上建议能够有效缓解大部分场景下遇到的问题,但在某些特殊情况下仍需结合具体业务特点灵活应对。例如涉及大规模分布式部署架构时,则更应关注全局一致性设计原则以及各组件间协作效率等问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值