1. Reset 灵异事件案例
1.1. 场景描述
有一天,业务开发和 DBA 运维跑过来,说凌晨调用 Cetus(数据库中间件)的定时脚本出问题了,Cetus 没有响应回来。当听到这个噩耗,我一脸懵逼。查看了 Cetus 的错误日志,结果没有发现任何有价值的线索。于是我问开发能不能把问题重现一下,因为只要能够重现,问题就容易解决。开发回去试验了多次,问题没有重现,不过开发有了新的发现:执行相同的 SQL 语句,白天 SQL 的响应时间跟凌晨的响应时间不一样。开发认为 SQL 响应很慢的时候,Cetus 会阻塞住会话,不返回结果给客户端。于是请 DBA 运维修改脚本中的 SQL 去模拟 SQL 响应慢的场景,结果 Cetus 返回了结果,没有出现凌晨的死等现象。
一时找不到根本原因,开发发现了 Cetus 有功能方面的问题。因此,开发和 DBA 运维更加坚信是 Cetus 迟迟没有返回响应。事实上,这些问题与 Cetus 响应并不相关。
从第一天的情况来看,问题确实出现了,每一个相关的人都想找到问题原因,会做各种猜测,但并没有找到真正的原因。
第二天,开发反馈半夜的脚本问题又出现了,但白天还是无法重现。DBA 运维反馈说白天试验偶尔会出现,第一次执行会出现,但后续几天 DBA 运维又进行了多次试验,问题再也没有重现。开发着急的抱怨很快就要使用脚本了,我只能建议开发白天使用脚本,避免凌晨出现的问题。因为所有怀疑都聚焦在 Cetus 上面,很难从其它角度去分析问题。
作为 Cetus 开发人员,这么诡异的问题不能轻易放过,否则会影响 Cetus 的后续发展,而且领导也要求问题必须尽快解决。我首先想到的是能不能部署一套 tcpcopy 测试环境,因为如果是 Cetus 程序的问题,复制线上流量到测试环境,可以在测试环境的 Cetus 端输出详细的 debug 日志,用来分析没有返回响应给客户端脚本的原因。这种方案需要搭建一套配套的测试环境,成本太高,最终没有被采用。最终决定用低成本抓包分析的方案,即在凌晨脚本执行的时候,在服务器端进行抓包,分析当时发生了什么,看到问题的真相:要么 Cetus 一直没有发送响应,要么 Cetus 返回了响应,客户端脚本没有收到。只要确认 Cetus 发送了响应,就不是 Cetus 的问题。
第三天,开发反馈凌晨问题没有再次出现,抓包分析也确认问题没出现。经过深入思考,如果是 Cetus 程序的问题,应该不会如此这么诡异:在凌晨多次出现,在白天却很难出现。只能继续等问题再出现,进行抓包分析。
第四天,问题还是没有出现,我忍,继续等。
第五天,问题终于出现,有希望了。
问题分析
抓包文件很多,首先让开发给出问题出现的时间点,在大量的抓包信息里面找出出现问题的 SQL 语句,最终找到的结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KDs2R9xS-1676959369816)(https://static001.geekbang.org/infoq/18/183fb179afd38c9f681dead20ff128fd.png)]
图 1
从上面的抓包文件(服务器抓取)来看,发送 SQL 时间为凌晨 3 点,Cetus 服务过了 630 秒(03:10:30.899249 - 03:00:00.353157)返回 SQL 响应信息给客户端,这说明 Cetus 是返回 SQL 响应的,但过了仅仅 238 微秒(03:10:30.899487 - 03:10:30.899249),服务器的 TCP 层就接收到了 reset 数据包,这么快就返回,reset 数据包非常可疑。需要注意的是,这 reset 数据包,不能直接认为是客户端发的。我们首先要确认 reset 数据包是谁发的,要么是客户端发的,要么是中途设备发的。图 2 展示了这个 TCP 会话的其它时刻的来回往返时间,也即 rtt 时间:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L7NQ3HNp-1676959369817)(https://static001.geekbang.org/infoq/4a/4a79591c6a77ae79616a773565d8cab2.png)]
图 2
在 No.5901 Cetus 返回给客户端 Rsponse OK,而在 No.5907 就捕获到了 ACK,时间差是 214 微秒(02:31:00.073946 - 02:31:00.073732),跟 reset 数据包响应速度差不多,都是在 200 多毫秒。很可惜,通过时间差来判断没有意义,200 多微秒时间太短,客户端发送 reset 是可能的 ,中途设备发送也有可能(一般内网调用都在这个量级左右)。
因为抓包只在服务器端进行,客户端数据包情况并不了解。我们尝试通过分析服务器端的抓包文件,进行逻辑推理找出问题根源。假设客户端发送了 reset,那意味着客户端 TCP 层已经不存在这个连接的 TCP 状态了,TCP 状态从有到无,会通知客户端应用连接异常,客户端脚本在收到这个通知后,会立即报错,但现实是客户端仍然在等待响应回来,因此假设不成立,客户端没有发送 reset。客户端连接还活着,但服务器这边的相应连接已经被 reset 干掉了。“可怜无定河边骨,犹是春闺梦里人”正反映了这个现象:服务器端连接已经不存在了,而客户端连接还在坚守着等待服务器响应的归来。
1.2. 水落石出
reset 到底是谁发的呢?最大怀疑对象是亚马逊的云环境。DBA 运维根据这个抓包分析结果询问亚马逊客服,得到如下信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A8i7ffNz-1676959369818)(https://static001.geekbang.org/infoq/5c/5c6f81f69eac7f61744ba3bc035b29e7.png)]
图 3
客服给出的答复与我们分析的结果一致,是亚马逊的 elb(类似 lvs 负载均衡器)强制干扰了 TCP 会话。图 3 中的回复指出,如果响应超过了 350 秒阈值(抓包显示为 630 秒),亚马逊的 elb 设备就会发送 reset 给响应的一方(本案例里是发送给服务器)。开发部署的客户端脚本,被忽视了,并没有接收到 reset,仍认为服务器连接仍然存活着。官方对此类问题的建议是,利用 TCP keepalive 机制来规避这方面的问题。
得到官方答复以后,问题算是彻底解决。
1.3. 小结
作为 Cetus 开发,不应该受到其他人判断的干扰,而应该从用户反馈的现象去推理分析问题。在 MySQL 响应时间差不多的情况下,问题在凌晨多次出现,而在其它时间不出现,这时就应该怀疑问题跟 Cetus 关系不大,当机立断采用抓包分析排除问题。
在只有单方面的抓包数据的情况下,可以利用已知的信息去推理出更多的隐含信息。
为什么凌晨有时候出现问题,有时候不出现?这是因为出问题的 SQL 是统计类 SQL,会统计前一天的信息,而每一天的数据量是不一样的,因此执行的 SQL 时间是不一样的,有时候超过了亚马逊的 350 秒阈值,有时候没有超过,所以凌晨有时候出现,有时候不出现。为什么会在凌晨出现?这是因为凌晨执行的时候,数据都是重新加载到内存的,速度慢,而第一次执行完以后,数据已经在内存中了,继续执行就会很快,达不到亚马逊的时间阈值。
为什么白天执行没有出现问题?因为白天的时候,开发和 DBA 运维是直接访问 Cetus,并没有通过亚马逊的 elb 设备访问,所以永远也不会出现问题。DBA 运维反馈的偶尔白天出现是误判,干扰了问题的解决。
这个案例充分说明了 reset 数据包发送不当会带来的问题,后续专题会继续讲述 reset 的深度内容。
2. 什么是 reset
下面我们来介绍一下 reset 数据包具体是什么,什么时候会被触发,它与应用的关系,以及如何构造 reset 数据包。
2.1. reset 数据包格式
RFC 对 reset 的定义如下:
TCP uses the RST (Reset) bit in the TCP header to reset a TCP connection.
在抓包文件里面,reset 数据包显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MVf8xj7l-1676959369818)(https://static001.geekbang.org/infoq/df/df25efe6a7ab8129546930c2dffa9ce1.png)]
TCP Flags 中如果 RST 标记为 1,则代表为 reset 数据包,在 wireshark 中 reset 数据包会显示为红色。
查看 Linux 源代码,TCP 构造 reset 数据包的时候,只有 TCP header 信息,无上层应用 payload 信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h7OL51OW-1676959369818)(https://static001.geekbang.org/infoq/45/45f867325d4817c71d86d97675e30b9e.png)]
查看抓包文件里的 reset 数据包,一般 Len=0。
但有时我们也会遇到带有 payload 信息的 reset 数据包,下图中 payload len 为 61。
查看 reset 数据包 payload 的具体内容(下图):
Payload 信息描述了发送 reset 数据包的具体原因。
带有 payload 的 reset 数据包一般只在中途设备程序存在,而 Linux 服务器 TCP 或客户端 TCP 一般不会带有 payload 信息。
2.2. 什么场景下会产生 reset 数据包?
RFC 官方对 reset 数据包的描述如下:
详细信息如下:
RFC 的介绍表明,只要 sequence 序列号在 TCP 窗口范围内,reset 数据包就是有效的。这意味着如果能够监听数据包,就可以轻松利用这些数据包的 TCP sequence 序列号去伪造 reset 数据包,进而达到杀连接的目的。
什么场景下会发送 reset 数据包呢?
根据经验,可能发送 reset 数据包的情况如下:
-
设置 TCP 选项 SO_LINGER 为 0,暴力关闭连接
-
第一次握手数据包遇到服务不存在,TCP 发送 reset 数据包
-
非三次握手数据包到达服务器 TCP,TCP 通过连接信息去找相应的 TCB(TCP 控制块信息),如果没有找到,则发送 reset 数据包给客户端
-
防火墙/路由器干扰
-
中途设备程序超时处理
-
reset 攻击
-
Keepalive 检测
-
操作系统清除连接资源(例如 windows),降低 timewait 数量
-
清除 TCP 资源
下面主要以案例的方式逐个讲述上面内容。
3. 案例分析
3.1. SO_LINGER 为 0 暴力关闭连接
在程序中设置 SO_LINGER 为 0,告诉对端,本方将立即关闭连接,不再接收数据(如下图)。
与之对应的正常关闭连接的流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XjSNYM8z-1676959369822)(https://static001.geekbang.org/infoq/c0/c0c584d9ec3e3d60360ce5f76810c262.png)]
不建议大家通过设置 SO_LINGER 为 0 的方式暴力关闭连接,因为它并不符合 TCP 规范。
3.2. 服务不存在
当客户端试图连接不存在的服务时,TCP 会发送 reset 数据包,提示客户端连接拒绝。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5rv28DSe-1676959369824)(https://static001.geekbang.org/infoq/6b/6bd987d1ff0d718d1c4f12785a2e01ca.png)]
3.3. 路由器干扰
下图展示了路由器干涉了服务器的数据传输,这种干涉会导致应用很难判断问题原因。
3.4. 对端拒绝接收数据
下图展示了客户端先关闭连接后,服务器继续传递数据,结果导致客户端发送 reset 给服务器,告诉服务器数据不能被接收。
3.5. 中途设备程序超时处理
下图展示了由于连接长时间不活跃(约 5 分钟),中途设备程序发送给客户端 reset 数据包,告诉客户端 TCP 会话超时过期。
下图展示了 MySQL 服务器处理 SQL 很慢,600 多秒后才返回响应给客户端,结果途径负载均衡器的时候,负载均衡器根据超时设置,发送了 reset 给服务器,导致了灵异问题(具体见上一讲)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vS0TzKoZ-1676959369826)(https://static001.geekbang.org/infoq/18/183fb179afd38c9f681dead20ff128fd.png)]
3.6. reset 攻击
在能够监听数据包的场合,利用数据包的 sequence 序列号,构造出 reset 数据包并发送出去,使得大量正常连接被非正常关闭,从而引起应用报错。
3.7. Keepalive 检测
下图中,客户端连续三次都没有检测到服务器端 keep-alive ack 数据包返回,于是在 21:38:03 发送了 reset 数据包给服务器端,通知服务器端连接已经被清理掉。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-exnpOcWc-1676959369826)(https://static001.geekbang.org/infoq/e2/e2a47c3a899a5d673dd6a167eeafbde1.png)]
通过 reset 数据包告知对方连接已经关闭的行为,是一种负责任的行为。
3.8. 操作系统清理 TCP 资源
下图展示了 windows 客户端发送了 FIN 数据包给服务器,接着又发送 reset 数据包给服务器。windows 在清理自身资源的同时也清理了服务器资源。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-43BNHhNv-1676959369827)(https://static001.geekbang.org/infoq/47/47b88dd829307cdb7ea36c17f2353ad4.png)]
3.9. 销毁 TCP 资源
利用 reset 数据包,可以清理异常 TCP 状态,比如清理大量 CLOSE_WAIT 状态,这部分内容后续章节会有详细实验。
4. 与应用的关系
下图展示了遇到 reset 数据包,Linux TCP 是如何反映的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZWJTiCP-1676959369827)(https://static001.geekbang.org/infoq/0a/0a1f7fd2710e4b078845c0f87c1d935e.png)]
当 TCP 状态处于 SYN_SENT(图中 TCP_SYN_SENT),如果遇到 reset,则会向上层应用报 connection refused 错误(ECONNREFUSED);当 TCP 状态处于 CLOSE_WAIT 状态,TCP 会向上层报 pipe 错误(EPIPE);当 TCP 状态处于 CLOSED 状态,则不会做任何动作;其它情况下,TCP 会向上层应用报 connection reset 错误(ECONNRESET)。
5. 如何构造 reset 数据包
要想成功构造 reset 数据包,需要满足如下条件:
-
能够监听到 TCP session 会话
-
构建 sequence 要正确
-
tcp checksum 要计算好
-
绕开 TCP 从 IP 层或者数据链路层发送出去
具体实现如下图:
通过构造 reset 数据包,可以解决一些服务器资源占用的问题,如清理 CLOSE_WAIT 状态。后续专题会利用 reset 来清理异常 TCP 状态。