“对方挂了”的六种意思
一句话总结
“对接不通”至少是六种互不相同的事件,一旦把这六种区分开,责任所在和下一步行动也就随之确定。
为什么需要它
在现场听到“对方挂了”,给对方打电话,对方会回答说自己的仪表板是绿色的。双方都没有说谎,只是我们这边看到的和对方看到的是不同的事件。
即使对方完好无损,也会出现名称找不到的情况。连接被拒绝,说明对方主机还活着,但那个端口上没有任何程序在监听。连接卡住,说明对方没能跟我们握上手,这种情况下,对方的应用日志里不会留下任何记录。读取卡住,说明对方握了手,却给不出响应,这时请求可能已经到达对方并在处理中了。最后这句话是决定性的——能否重试,就在这里分岔。
工作原理
把六种面孔整理成表,是这样的。
얼굴 무슨 일이 일어났나 책임의 위치 재시도
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 与实际收到的字节数比较,一下子就能分清。
实际工作中真正重要的事
- 连接和读取的超时分别设置。用同一个值,两种事件就被糊在一起。
- 能否重试由面孔决定。读取超时和部分响应可能已经被处理。
- 重试要同时设置整体截止时间。只规定次数的重试,无法承诺最坏情况。
- 把我方、路径和对方的责任分开记录。这一行决定了排查的起点。
下一项实验要做什么
用本地套接字搭起对接对方的六种面孔,发出一个请求,做出区分这些面孔的判定器。分别设置连接和读取的超时来区分两种事件,亲自测量没有超时时尝试永远不会结束,并做出遵守截止时间的重试。最后对六个对象一次性检查,把按责任所在分别填写的表汇总成一页纸进行报告。