刚刚解决了GateWay与上游服务的连接不释放问题,又发现网关下游一个服务出现连接数不正常的问题。正常情况下,该服务的连接数应该是个位数,但监控却发现占用的连接数经常接近100个,开始了排查之旅。

        首先怀疑是该服务性能有问题,处理时间长,网关得不到应答所以会一直占用与该服务的连接,经过对SQL的排查发现没有问题,再写脚本直接调用该服务的接口,发现响应时间都在20ms以下,所以性能问题排除。然后,怀疑网关在调用该服务后,未及时释放连接,连续监控服务端口占用情况30分钟,发现所有端口都被关闭,并未出现长期占用连接的情况。最后,每隔1秒记录一下下游服务的端口连接情况,连续监控30分钟,看看每个端口对应连接的持续时间,终于有收获了,发现99%的连接都在60秒以上(含60秒)被释放。这说明在下游服务对连接的维护没有问题。进一步在GateWay检查连接情况,发现大量处于CLOSE_WAIT状态的连接,说明在GateWay未正确的关闭与下游服务的连接。问题定位到GateWay请求下游服务的代码。

        在GateWay使用的是RestTemplate来请求下游服务,关键代码如下:

        RestTemplate restTemplate = restTemplateBuilder.build();

        String result = restTemplate.getForEntity(……);

        使用这种方法问题在于每次调用下游服务时,都会创建一个新的RestTemplate对象。

        进一步分析源码:org.apache.http.impl.client.HttpClientBuilder#build(见下图)。发现RestTemplate默认使用了连接池。代码中连接池的配置参数,包括 keep-alive、最大连接数据等:

        也就是说每次GateWay访问下游服务都会创建一个支持keep-alive的连接池,使用该连接池获取一个连接并且仅进行一次请求后就不再使用,那么为什么会在网关侧出现CLOSE_WAIT状态的端口呢?

        这个问题,需要结合下游服务来一起讨论,因为keep-alive的使用本来就是客户端(GateWay)和服务端(下游服务)配合起来才能正常发挥作用。

        正常情况下,客户端调用服务端时,在HTTP头部会设置Connection:keep-alive,具体如下图所示:

服务端的应答也在HTTP头部设置了Connection:keep-alive,除此之外,服务端的应答在头部还会携带Keep-Alive:timeout=60(单位为秒,可变,这里以60秒为例),具体如下图所示:

服务端返回的timeout是指服务端在关闭该连接前的空闲时间(也就是说连续60秒没有请求,那么服务端会认为该连接不再使用而主动发起关闭)。

        基于Spring-boot的应用timeout时间缺省为60秒,同时还设置了另外一个参数——单个连接处理的请求数量为100个。其具体的连接维护机制就是:如果连续两次请求之间的时间间隔超过60秒,那么服务端就会主动关闭(发送FIN+ACK)连接,反之则重用连接,直到连续调用100次后服务端才主动关闭连接。二者之间的差别在于,后一种情况下,服务端在最后一次HTTP请求返回时在HTTP头部增加Connection:close。

        针对超时的情况(连续60秒无调用),具体报文如下:

客户端接收到服务端的断连报文(FIN+ACK)并不马上处理,那么客户端端口将保持为CLOSE_WAIT状态,直到restTemplate被垃圾回收时,在析构函数中才主动关闭连接,从而完成四次握手关闭连接。

        针对单个连接处理的请求数量达到100个的情况,最后一个应答的HTTP应答头中设置了Connection:close,具体如下图所示:

因为服务端在HTTP应答的头部设置了Connection:close,所以会在收到服务端的断连报文(FIN+ACK)后马上关闭本地连接,并发送断连报文(FIN+ACK),完成四次握手,具体报文如下图所示:

        根据以上分析,原代码中创建RestTemplate对象的方式,虽然创建了连接池,但仅仅使用一个连接进行一次下游服务调用,服务端发现该连接60秒内没有连接,则主动关闭连接。但因为未在应答中设置Connection:close,所以客户端在收到FIN后,并不会马上发送FIN,导致客户端连接处于CLOSE_WAIT状态,没一次调用都会产生一个CLOSE_WAIT的端口,所以我们看到大量CLOSE_WAIT状态的端口。当然,以该种方式创建的RestTemplate对象,在完成HTTP调用后属于可回收对象,所以在垃圾回收时会调用finalize方法,在其中会主动关闭连接,并发送FIN到服务端。所以,虽然CLOSE_WAIT状态的端口总是很多,但又会随着垃圾回收而正常关闭,从而导致没有产生更多的端口占用。但无论如何,RestTemplate这种使用方式也是极其不合理的,因此修改成注入方式,具体代码如下:

        使用注入模式后,才真正利用了RestTempalte的连接池,并保证连接会及时释放。抓包分析,正常情况下,会在一个端口上进行100次调用,然后会经过四次握手正常关闭连接,具体报文如下:

        当然,如果在一个连接上调用不到100次,然后服务端超时,客户端端口仍会进入CLOSE_WAIT状态。如果此时在网关进来一个新的请求,将从连接池取连接,取到连接后发现该连接空闲时间已经超时(客户端空闲超时时间为2秒),此时会先关闭该连接,然后再获取可用连接。

        说到这里必须再分析一下客户端(网关)的连接池维护,客户端有一个配置项validateAfterInactivity,缺省为2秒。也就是说,如果一个连接在2秒内没有被使用,将释放该连接,但并不是马上释放,而是在有新的请求到来时从连接池中取连接才会进行超时判断并且释放已经超时的连接。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐