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回收很明显,有明显的下降趋势