TT Lab
开始
学习 学习路径 课程

调试实战

“对方挂了”的六种意思

在 TT Lab 中继续学习

一句话总结

“对接不通”至少是六种互不相同的事件,一旦把这六种区分开,责任所在和下一步行动也就随之确定。

为什么需要它

在现场听到“对方挂了”,给对方打电话,对方会回答说自己的仪表板是绿色的。双方都没有说谎,只是我们这边看到的和对方看到的是不同的事件。

即使对方完好无损,也会出现名称找不到的情况。连接被拒绝,说明对方主机还活着,但那个端口上没有任何程序在监听。连接卡住,说明对方没能跟我们握上手,这种情况下,对方的应用日志里不会留下任何记录。读取卡住,说明对方握了手,却给不出响应,这时请求可能已经到达对方并在处理中了。最后这句话是决定性的——能否重试,就在这里分岔。

工作原理

把六种面孔整理成表,是这样的。

얼굴                무슨 일이 일어났나              책임의 위치   재시도
dns_error          이름을 주소로 못 바꿨다          경로         안전
connection_refused 호스트는 답하는데 포트가 닫혔다   상대         안전
connect_timeout    손잡기(TCP)가 끝나지 않았다      경로         안전
read_timeout       손은 잡았고 답이 안 온다         상대         위험
partial_response   본문이 약속한 길이보다 짧다      상대         위험
slow_response      답은 왔는데 늦었다               상대         안전

该代码块中的韩文说明依次为:dns_error 表示无法把名称解析为地址,责任在路径,重试安全;connection_refused 表示主机有响应但端口已关闭,责任在对端,重试安全;connect_timeout 表示握手(TCP)始终没有完成,责任在路径,重试安全;read_timeout 表示握手已完成但没有收到响应,责任在对端,重试有风险;partial_response 表示正文比约定的长度短,责任在对端,重试有风险;slow_response 表示响应到了但很晚,责任在对端,重试安全。

重试这一列是这张表的核心。在连接建立之前就失败,说明对方没有收到请求,所以再发一次也不会让同一件事发生两次。但读取超时和部分响应,请求可能已经被处理了。如果这个请求是支付或转账,重试就会造成重复扣款。这种情况下,没有幂等键之类的保护,就不能重试。

好在区分的方法并不难。Python 的 requests 可以为连接和读取分别设置不同的超时,把两者以元组形式给出,失败也会分成 ConnectTimeout 和 ReadTimeout 两种到来。如果把两者用同一个值给出,两种事件就会被糊成一种面孔。

如果根本不设置超时,情况更糟。由于没有默认值,请求会一直等到对方答复,或者直到内核放弃连接。在此期间,该工作线程不会回来,请求一旦堆积,我们这边会先倒下。对方的缓慢蔓延为我们的故障,最常见的路径就是这个。

如果再叠加上重试,情况会成倍恶化。对一个永远不会结束的尝试做三次,就要多等三倍的时间。所以重试必须同时有整体截止时间。只规定尝试次数的重试,在最坏情况下要等多久,没有人知道。

在现场相遇的样子

第一,认为在没有互联网的地方无法复现。其实只用本地套接字,六种面孔就都能复现。连接被拒绝,只需要一个没人监听的端口;连接超时,可以用填满等待队列之后不再 accept 的套接字来制造(listen(2) 的 backlog 就是这个位置)。名称解析失败,用 RFC 2606 为此目的保留的 .invalid 名称即可。

第二,把对方的责任和我们的责任混在一起。如果把连接超时报告为“对方故障”,对方在自己的日志里什么也找不到。因为没能握上手的连接,根本没有到达对方的应用。如果报告里有这个区分,排查就会从防火墙和路径方向开始。

第三,只把缓慢的响应记录为成功。如果因为收到了 200 就记为成功,就没有人能看到对方越来越慢。成功也要设置阈值,超过阈值就记成另一种面孔,趋势才看得见。

第四,把部分响应记录为解析错误。JSON 解析器报错了,就以为是响应格式变了。实际上是正文在中途被截断,这说明是对方或中间设备断开了连接。把 Content-Length 与实际收到的字节数比较,一下子就能分清。

实际工作中真正重要的事

下一项实验要做什么

用本地套接字搭起对接对方的六种面孔,发出一个请求,做出区分这些面孔的判定器。分别设置连接和读取的超时来区分两种事件,亲自测量没有超时时尝试永远不会结束,并做出遵守截止时间的重试。最后对六个对象一次性检查,把按责任所在分别填写的表汇总成一页纸进行报告。