哪儿来的RST

年前有同事在Tech@邮件组里分享了一个调用接口间歇性超时的案例,案例本身以及解决方案还有待商酌,不过在案例中有几个有意思的现象引起了大家的讨论。作为一个对所有现象如果不得到合理解释不舒服斯基的工程师,当然不会放过这次分析的机会,本文就是对这其中的RST(reset) 包进行了分析,最后也算是说服了自己。

首先还是描述一下案例:A部门调用B部门的一个HTTPS接口,经常出现超时。于是A部门的工程师邀请B部门同事和OPS一起进行调查,检查了B部门应用的access log和nginx的access log,发现接口都没有出现超时,服务端都在超时之前返回了结果。然后OPS在A部门应用的机器上进行tcpdump抓包,最后发现包里有大量的RST包(这就是本文想分析的),然后在机器监控里发现A部门应用的机器有大量的close_wait状态的连接。最后定位到代码上,发现A部门请求接口的代码存在问题,代码如下所示(有删改):

可能有同学看出来,上面的代码存在问题。 apache的httpclient按照上面的用法是有连接池的, EntityUtils.toString虽然『关闭』了连接,但其实只是释放连接到连接池,然后因为这里每个方法都在创建新的DefaultHttpClient ,所以实际上存在连接泄露,这会导致创建了大量的连接。

最后经过搜索, A 部门修改了上面的代码,解决了超时的问题:

添加了一个 httpGet.setHeader("Connection", "close") 。虽然解决了问题,其实这段代码只是关闭了 HTTP 的 keeplive ,相当于废掉了 apache httpclient 的连接池特性,更好的解决办法当然是共享 DefaultHttpClient 的实例,而不是每个方法都去创建。不过解决生产问题的首要原则是解决问题,所以这样也无可厚非了。

对于案例本身就描述到这里了,吸引我的是为什么抓包的时候出现大量的 RST 呢,这个 RST 和超时有什么关系呢?

为了节省篇幅,我将抓包里的纯数据包省略了。可以看到11行到12行相隔了30秒,这是 nginx 的 keeplive timeout ,因为客户端连接泄露,最后 nginx 等到超时了,主动的关闭了连接。然后在16号包的时候因为 client 端发生 gc 触发了 client 端的关闭动作,我们看到17号包理论上应该发出 FIN 包,但实际上发出的是 RST 包,这个就是本文想弄明白的地方。

不过在继续分析之前,我们先说明另外一个问题:有的同学可能好奇,为什么每次在关闭之前都会发一个数据包呢?记忆中 TCP 的四次挥手是没有这个流程的啊。对于 TCP 而言确实没有这样的流程,不过因为本案例中使用的是 HTTPS,HTTPS 简单可以理解为 TCP + TLS + HTTP ,也就是在不安全的 TCP 之上用 TLS 协议建立一个安全的会话通道。而 TLS 规范里规定了在关闭会话的时候需要发出一个叫 close_notify 的包,而上图中关闭时候的数据包就是这个 close_notify :

回到正题,为啥客户端关闭连接的时候不是发出FIN而是发出 RST呢?在Tech@里有同学解释是nginx收到FIN的ACK之后,进入了FIN_WAIT_2状态,然后当client发送close_notify的时候,因为nginx处于FIN_WAIT_2状态,无法回复ACK,所以client端发送了一个RST。这个解释当然有问题:1. client端发送close_notify之后,立即就发送了RST包,中间间隔非常短。client端根本就没有时间确认nginx是否不能回复ACK,我们都知道一般情况下如果对端有问题,不能回复ACK,TCP里都会重发几次。2. 即使是因为nginx进入FIN_WAIT_2状态不能回复ACK,那么回复RST的应该是nginx,而不是client发送RST。

不过上面也是理论上分析,还是要靠实验进行检查是不是这样。那我们就构造一个实验,看看RST是不是因为这个close_notify没有ACK导致的,那我们就要想办法不发送这个close_notity 。在上文已经介绍了, HTTPS = TCP + TLS + HTTP 。那么如果我们能拿到TCP层的裸Socket就好办了,就可以直接调用Socket的close ,而不走TLS的 close ,然后就不会发出close_notity了。研究了一下apache httpclient的代码,发现是可以的:

通过自定义SSLSocketFactory ,我们就可以拿到裸Socket了,然后在nginx发起close之后,我们直接调用裸Socket的close ,

看抓包结果:

看最后一行,这次因为是在裸Socket上close ,所以并没有发出close_notify ,但是还是发出了一个RST 。所以前面说的RST是因为close_notify没有得到ACK导致的是不正确的(不要问我12号包和14号包为啥相差17分钟,这中间发生了什么,因为我哄娃睡觉去了)。

到这里那就很奇怪了,对方发我一个 FIN ,然后我 close ,这是多么正常的四次挥手流程,为啥我不是发出一个 FIN ,而是发了一个 RST 呢?这太奇怪了。其实所有的奇怪只是我们对某些知识的欠缺,在 RFC 1122 的 4.2.2.13 里有这么一段描述:

A host MAY implement a

"half-duplex"

TCP close sequence

,

so that an application that has called CLOSE cannot

continue

to read data

from

the connection

.

If

such a host issues a CLOSE call

while

received data

is

still pending

in

TCP

,

or

if

new

data

is

received after CLOSE

is

called

,

its TCP SHOULD send a RST to show that data was lost

.

翻译一下的意思就是:如果你的 receive buffer 里还有数据,还没有被读取出来,然后你发出了 close ,那么这个时候就是非正常关闭,因为数据还没读完呢,你 close ,数据就丢了,这和 TCP 的可靠是相违背的,这个时候 TCP 就应该发一个 RST 告诉对方,你非正常关闭了。

那么其实到这里就清晰了,这个 RST 并不是因为 client 发的那个 alert 导致的,而是 server 关闭 TLS的session 的时候发出的那个 alert 导致的,因为 server 关闭的时候, client 因为连接泄露,连接是空闲的,没有人去读 receive buffer 的数据呢,然后当真正关闭的时候,这个 receive buffer 里还有数据,所以这个时候是一个『非正常』关闭,发出的是 RST ,而不是正常的 FIN 。

不过到这里,仍然是根据规范的理论分析,我们实际再验证一遍,看看是不是这样的:

上面的代码中,我们在调用 Socket 的 close 方法之前,首先将 receive buffer 里的内容全部都读取了,然后再关闭,再来看看抓包结果:

抓包验证读完 receive buffer 之后再 close ,不再发 RST 了。

不过可能有的同学说,这样也不能说明那个 RST 跟 client 发送的 alert 没关系啊。嗯,不错,我们还要加一个测试,这次我们还是让 client 端发送正常的 alert ,但是在 alert 之前我们先读取 receive buffer 。

然后看抓包结果:

这次 client 正常发送了 alert 和 FIN 。可能有的同学说,你这次还不是有 RST 么,不过请看清楚 RST 的方向,这次是 server -> client ,至于 server 为啥发两次 RST 给 client ,就留作自己思考吧。

到这里,这个 RST 的来龙去脉基本上都清楚了,不过还有几个有意思的地方:

1. 加的那个 Connection:close header 有什么影响? 原始邮件中,为了解决连接泄露问题关闭了 keep live ,添加了 Connection close ,而添加 Connection close 后,一般来讲 server 处理完请求会主动关闭连接,那么 server 主动关闭连接有什么后果呢?

所以原邮件中的解决方案会带来更多的 RST 啊。

2. 既然上面一个场景, server 先关闭, client 后关闭会导致 client 发送 RST ,那么如果我在 server 超时之前 client 端先关闭,是不是 server 也会发送 RST 呢?这里就不贴图了,最后验证是不会。

3. 另外有些同学验证 https://www.baidu.com 发现 server close 的时候并不会发送 close_notify ,后来翻了一下这个并不是强制的。

啰嗦的写了好长,不知道有几个人看到了这里?最后附送几个思考题吧

思考题:

1. 为啥设置了 Connection: close header 后有更多的 RST了 ?猜测一下 apache httpclient 的行为。

2. 为啥 nginx 不会出现 RST ?

3. 我用 netty 实现的 httpclient 走上面的测试结论却不一样,即使是原来的场景也没有 RST ,这是为什么呢?

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180308G078VX00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券