对RST包的理解
但是我还有一个问题,那就是为什么会有一个RST包呢?如果没有那个RST包,其实问题还不好发现,虽然按照错误日志的时间,挨个查找Redis数据包的信息,能够查询出来,但是RST无疑从一开始就吸引了我的注意,让我能够更加快速的定位问题。
初识RST
那现在问题来了,为什么会有RST包呢?
首先了解一下RST。(可参考TCP/IP详解 卷1 , 18.7 复位报文段)
归纳起来,当以下任一情况发生时,会产生RST包:
到不存在的端口的连接请求
异常终止一个连接
检测半打开连接
jedis与redis的关闭机制
观察RST之前的几个包
931112-ebd5fc72398ace00.png
ws_3.png
使用wireshark的专家信息查看多个RST包,发现RST之前都会有QUIT,OK的交互。那看来应该是框架层面的问题。
再翻看上面GenericObjectPool的相关代码,在borrowObject时如果发生异常,会调用destroyObject()方法,这个destroyObject是延迟到子类实现的,也就是上面说到的JedisPool。
public void destroyObject(final Object obj) throws Exception {
if (obj instanceof Jedis) {
final Jedis jedis = (Jedis) obj;
if (jedis.isConnected()) {
try {
try {
jedis.quit();
} catch (Exception e) {
}
jedis.disconnect();
} catch (Exception e) {
}
}
}
}
最终调用redis.clients.jedis.Connection的disconnect,关闭输入输出流。
public void disconnect() {
if (isConnected()) {
try {
inputStream.close();
outputStream.close();
if (!socket.isClosed()) {
socket.close();
}
} catch (IOException ex) {
throw new JedisConnectionException(ex);
}
}
}
这也就解释了为什么会出现RST包:
客户端请求QUIT,服务端返回OK。(此时客户端在接收完quit返回后,调用了disconnect方法,导致连接断开)紧接着服务端发起TCP挥手,发送FIN包到之前交互的客户端51311端口,但调用完disconnect的客户端已经断开了和服务端的连接。客户端只能通过发送RST,通知服务端“你发送了一个到不存在的端口的关闭请求”。
翻看新版的jedis代码,除了将之前JedisPool中实现的代码挪到了JedisFactory中实现,大致逻辑依然没有改变()
// 2.10 JedisFactory
@Override
public void destroyObject(PooledObject<Jedis> pooledJedis) throws Exception {
final BinaryJedis jedis = pooledJedis.getObject();
if (jedis.isConnected()) {
try {
try {
jedis.quit();
} catch (Exception e) {
}
jedis.disconnect();
} catch (Exception e) {
}
}
}
@Override
public boolean validateObject(PooledObject<Jedis> pooledJedis) {
final BinaryJedis jedis = pooledJedis.getObject();
try {
HostAndPort hostAndPort = this.hostAndPort.get();
String connectionHost = jedis.getClient().getHost();
int connectionPort = jedis.getClient().getPort();
return hostAndPort.getHost().equals(connectionHost)
&& hostAndPort.getPort() == connectionPort && jedis.isConnected()
&& jedis.ping().equals("PONG");
} catch (final Exception e) {
return false;
}
}
而disconnect最终调用的Connection有变化。
public void disconnect() {
if (isConnected()) {
try {
outputStream.flush();
socket.close();
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException(ex);
} finally {
IOUtils.closeQuietly(socket);
}
}
}
由之前的inpusStream.close()和outputStream.close()改成了outputStream.flush()。原因是jedis自定义了带缓冲的RedisOutputStream,在socket.close前要确保缓冲内容写到流中。
客户端使用disconnect确实能够快速释放资源,在调用disconnect时关闭了客户端端口,回收了文件句柄资源。
试想如果在quit后,服务端就已经释放了文件句柄,关闭了socket连接,而客户端不调用disconnect释放资源,就会一直占用资源,在进程结束才会释放。 |