项目中使用了springcloud gateway作为网关,上游与负载均衡服务器连接。近期通过监控系统观察,发现网关与上游负载均衡服务器保持的TCP连接有300+,初步怀疑是调用方未释放连接,用如下方法进行分析:
1)周期性采集当前建立的连接及端口数据,首先是每隔10分钟连续采集2两个小时,发现在两个小时之内新出现的端口不到12个,再逐步缩短采样周期,到最后每秒采集一次,分析发现每秒种建立一个连接,同时关闭一个连接,当仍存在300+连接,这些连接对应的端口称为不活跃端口,记录下这300+不活跃端口。
2)为了进一步分析,用whireshark抓包,发现绝大部分情况下都是正常的连接和关闭,但这300+个不活跃端口对应的连接上没有任何数据,这300+个不活跃对应的连接称为不活跃连接。同步赶紧上马接口调用实时监控功能,发现实际的调用数量却非常少(每分钟不足10个)。
3)与上游的负载均衡工程师一起检查,从负载均衡服务器看到的活跃连接也是个位数,并且并未找到在网关上的不活跃端口。也就是说在负载均衡服务器已经已经拆除了与网关上的不活跃连接对应的连接。咨询负载均衡工程师,负载均衡设备对于1超过1个小时的不活跃连接会主动拆除。
经过以上分析,确定是外部系统经过负载均衡设备与网关建立连接后,并未进行任何操作,但网关会一直维护这个连接,导致网关的连接数持续上升。
为解决这个问题,需要首先回顾一下传统的TCP长连接维护机制。
针对长连接的维护,传统的TCP服务采用心跳来维持,比如服务端每分钟发送一个心跳报文,并启动计数器并设置为1,客户端收到后回应一个报文,服务端收到回复报文后重置计数器,如果为收到应答,则一分钟再发送一个心跳报文,同时计数器加1,连续发送三个心跳报文并且未收到映带,则服务端则认为客户端已经失联,会主动拆除这个连接,以避免不必要的资源占用。
我们现在使用的springcloud gateway,显然很难直接修改源码增加以上的心跳机制,所以我又想到了操作系统协议栈的连接保活机制。
TCP协议栈的保活机制与应用层的长连接维护机制类似(当然,应用层的TCP长连接维护机制就是从协议栈的保护机制学习来的'&'),只不过是在协议栈层面完成,这样避免了应用层实现负载的长连接维护,保活机制如下:
1)服务器端判断一个连接在指定的时间内(缺省为2小时)没有任何数据,则发送一个探测报文,并启动定时器
2)如果客户端在正常运行并且网络可达,则客户端则回复一个响应报文,服务端认为客户端正常,则重新开始计时。如果客户端主机崩溃或网络不可达,服务端将收不到应答,定时器超时后(一般为75秒),服务端将再次发送探测报文,如此连续发送若干次(一般为10次),如果均未收到应答,则服务端将主动关闭连接。当然,如果中间有任何一次服务端收到应答,则认为连接正常,不再发送探测报文。
使用如下命令可以查看以上保活时间、发送探测报文的间隔和次数:
#sysctl -a|grep keepalive
net.ipv4.tcp_keepalive_time = 7200(单位为秒)
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75 (单位为秒)
关于保活参数中两个小时的时间设置存在争议,通常人们希望这个值可以小很多,比如分钟级,但保活间隔时间是系统级别的变量,如果改变该值会影响所有使用该功能的用户。所以,Host Requirements RFC提出一个实现方式,保活间隔是可配置的,但缺省不小于两个小时,并且需要应用程序设置才启用。
如果使用协议栈的保活功能,那么缺省的两个小时的时间还是太长,如果缩短这个时间会有什么影响,并无把握。所以还是先想其他办法,从网上看到可以通过以下代码修改网关对长连接的维护办法,以下代码是设置保活时间为3分钟,如果3分钟内连接上没有数据,网关将主动关闭连接:
配置文件:
server:
netty:
idie-timeout: 300
@Configuration
public class NettyConfig {
@Bean
publiWebServerFactoryCustomizer
@Value("${server.netty.idle-timeout}") Duration idleTimeout) {
return factory -> factory.addServerCustomizers(
server -> server.tcpConfiguration(
tcp->tcp.bootstrap(
bootstrap->bootstrap.childHandler(new ChannelInitializer
@Override
protected void initChannel(Channel channel) {
channel.pipeline().addLast(
new IdleStateHandler(0, 0, idleTimeout.toNanos(), NANOSECONDS) {
private final AtomicBoolean closed = new AtomicBoolean();
@Override
protected void channelIdle(
ChannelHandlerContext ctx, IdleStateEvent evt) {
if (closed.compareAndSet(false, true)) {
ctx.close();
}
}
}
);
}
}))));
}
}
系统上线后,通过监控系统发现网关连接数并未持续增长,刚松一口气,线上业务系统频频报错,请求网关失败,赶紧安排网络抓包,然后马上回退恢复业务。然后对网络抓包进行分析,截图如下:
从抓包结果来看,客户端和网关经过3次握手后,建立了连接,但后面的建立SSL的过程中,网关返回了400 Bad Request,所以导致业务系统请求失败(业务系统使用https请求网关),怀疑是上面的代码中的配置覆盖了配置文件中SSL的相关配置,所以导致SSL连接未建立。我们优秀的工程师,本着锲而不舍的精神对gateway进行源码分析,经过对代码的分析,发现确实是这个配置覆盖了原有的SSL配置,导致SSL配置未生效所致,所以对以上代码进行改写,具体如下:
@Configuration
public class NettyConfig {
@Bean
publiWebServerFactoryCustomizer
@Value("${server.netty.idle-timeout}") Duration idleTimeout) {
return factory -> factory.addServerCustomizers(
server -> server.tcpConfiguration(
tcp->tcp.bootstrap(bootstrap->{
//增加如下代码,从而可保持原有配置并追加保活
BootstrapHandlers.updateConfiguration(bootstrap, "IdleStateHandler",
(connectionObserver, channel) ->{
channel.pipeline().addLast(new IdleStateHandler(0, 0,
idleTimeout.toNanos(), NANOSECONDS) {
private final AtomicBoolean closed = new AtomicBoolean();
@Override
protected void channelIdle(ChannelHandlerContext ctx,
IdleStateEvent evt) {
if (closed.compareAndSet(false, true)) {
ctx.close();
}
}
});
});
return bootstrap;
}
)));
}
}
进行测试验证,一切OK!