对方挂了 — 分清六张面孔,定位责任在谁
目标
做出分别区分连接被拒绝、名称解析失败、连接超时、读取超时、缓慢响应、部分响应的判定器和分类表。亲自测量没有超时时尝试永远不会结束,并做出遵守截止时间的重试,对六个对象一次性进行检查。
为什么重要
“对接不通”至少是六种互不相同的事件。这也是为什么我方已是故障,而对方的仪表板却是绿色的——没能握上手的连接根本没有到达对方的应用,所以对方日志里没有任何记录。 区分之所以重要,是因为下一步行动会因此不同。尤其是能否重试会在这里分岔。在连接之前就失败,说明对方没有收到请求,可以再发一次;但读取超时和部分响应,请求可能已经被处理了。如果这个请求是转账,重试就会造成重复扣款。 如果根本不设置超时,情况更糟。请求会一直等到对方答复,在此期间工作线程回不来。对方的缓慢蔓延为我们的故障,最常见的路径就是这个,而如果再叠加上重试,等待时间就会相乘。 评分器不会相信你的说明。评分器会在自己的端口上搭起六种面孔,实际让你的判定器去连接,并核对分类。端口每次运行都会变,所以无法把值背下来填进去。
步骤
- 创建并运行 /root/dep/gen_dep.py,生成 /root/dep/servers.py。
- 创建 /root/dep/classify.py,让它区分 ok、connection_refused、dns_error。
- 分别设置连接和读取的超时,区分 connect_timeout 和 read_timeout。
- 区分部分响应和缓慢响应,并在 /root/dep/timeout.json 中测量没有超时的情形。
- 在 /root/dep/classes.json 中写下六种面孔各自的责任所在和能否重试。
- 用 /root/dep/retry.py 做出遵守截止时间的重试,并写入 /root/dep/retry.json。
- 对六个对象一次性检查,把结果以表格形式保存到 /root/dep/verdict.json。
- 用 /root/dep/summary.json 和 /root/dep/dep_report.md 分四节进行报告。
参考
- 服务器约定:
python3 /root/dep/servers.py --role <ok|slow|hang|partial|backlog> --port P [--delay-ms N] [--ready-file F](占位符为各个参数)只会按其角色运行。ready 文件生成就表示已准备好,要结束就杀掉进程。 - 判定约定:
python3 /root/dep/classify.py --url <주소> [--connect-timeout S] [--read-timeout S] [--slow-ms N] [--out <json>](占位符依次为地址、JSON 文件)输出一个包含 url、class、elapsed_ms、evidence、connect_timeout、read_timeout 的 JSON。 - 六种 class 的名称是
dns_error、connection_refused、connect_timeout、read_timeout、partial_response、slow_response,正常是ok。 - 缓慢响应的标准是
--slow-ms。如果成功了但超过了这个时间,就记为slow_response。如果收到 200 就全部记为成功,就看不出对方越来越慢的趋势。 - 超时:requests 像
timeout=(연결, 읽기)(占位符依次为连接超时、读取超时)这样,把两者分开接收。用同一个值,两种事件就会被糊成一种面孔。如果给的值是--connect-timeout 0以下,要让它在没有超时的情况下一直等待。 - 重试约定:
python3 /root/dep/retry.py --url <주소> --attempts N --deadline-s S [--connect-timeout S] [--read-timeout S] [--out <json>](占位符依次为地址、JSON 文件)输出 attempts_allowed、attempts_made、deadline_s、elapsed_ms、final_class、tries、gave_up。超过截止时间后,即使尝试次数还有剩余也必须停止。 - 责任所在在
우리、상대、경로三者中选一个填写(韩文,依次意为“我方”“对方”“路径”)。能否重试取决于请求是否可能已经被处理——读取超时和部分响应是有风险的。 - 复现材料:名称解析失败用 RFC 2606 保留的
.invalid名称,连接被拒绝用没人监听的端口,连接超时用在等待队列填满后不再 accept 的套接字(backlog 的作用)来制造。 - 常见错误:对超时只给一个值,只要是 200 就全部记为成功,把部分响应记为解析错误,在读取超时之后重试非幂等的请求。
- 不要编写负载测试。每次评分的预算是 60 秒。服务器用完之后一定要杀掉。
搭起对接对方的六种面孔
创建并运行 /root/dep/gen_dep.py,生成 /root/dep/servers.py。ok、slow、hang、partial、backlog 五种角色必须各自只按自己的方式运行。
即使没有互联网,六种面孔也都能用本地套接字复现。把这个脚本原样保存并运行,启动一个 ok 角色,用 curl 调用一次。用完后别忘了杀掉进程。
做出区分面孔的判定器
创建 /root/dep/classify.py,让它把正常情形区分为 ok,把没人监听的端口区分为 connection_refused,把 .invalid 名称区分为 dns_error。输出中必须有 class、elapsed_ms、evidence。
requests 会以异常的类型告知失败。不过连接被拒绝和名称解析失败都会以 ConnectionError 抛出,所以要看消息才能区分。evidence 中请原样保留作为判定依据的消息尾部——之后要贴进报告的就是它。
把连接和读取区分开
区分出 connect_timeout 和 read_timeout。backlog 角色的握手始终没有完成,所以是 connect_timeout;hang 角色握了手却没有响应,所以是 read_timeout。两个超时必须分别设置。
第 2 步的判定器把两个超时糊成了 timeout 一种面孔。requests 会把连接阶段的失败作为 ConnectTimeout、读取阶段的失败作为 ReadTimeout 分别抛出,所以把两个异常分别捕获,就能区分。如果只留一种面孔,就分不清是防火墙问题还是对方延迟。
部分响应、缓慢响应,以及没有超时
对 partial 角色,要区分为 partial_response;对 slow 角色,超过 --slow-ms 时要区分为 slow_response。然后在 /root/dep/timeout.json 中写下 with_timeout、without_timeout(从外部在 5 秒后强制中断时的退出码)和 note。
部分响应要把正文接收完才会暴露——只看头部就记为成功,就会漏掉。没有超时的请求自己不会结束,所以要从外部中断。timeout 5 python3 ... 的退出码是 124,这个事实本身就是证据。
把六种面孔的责任和重试整理成表
在 /root/dep/classes.json 中,为六种面孔(dns_error、connection_refused、connect_timeout、read_timeout、partial_response、slow_response)各自写下 whose(우리、상대、경로,韩文,依次意为“我方”“对方”“路径”)、retry_safe(真/假)、next_step(10 个字符以上)。重试有风险的两种是 read_timeout 和 partial_response。
能否重试取决于“请求是否可能已经被处理”。在连接建立之前就失败,说明对方没有收到请求,所以是安全的;握手之后的失败可能已经被处理,所以有风险。在 next_step 中写下遇到这种面孔时最先要看的地方。
做出遵守截止时间的重试
创建 /root/dep/retry.py,让它同时遵守 --attempts 和 --deadline-s,并把对 hang 角色运行的结果保存到 /root/dep/retry.json。超过截止时间后,即使尝试次数还有剩余也必须停止。
只规定尝试次数的重试,无法承诺最坏情况下要等多久。每次尝试之前先看剩余时间,没有剩余时间就停止。每次尝试也必须有超时,截止时间才有意义——永远不会结束的尝试,连截止时间也救不了。
一次性检查六个对象
把六种面孔全部搭起来,各判定一次,并在 /root/dep/verdict.json 中写入 targets(name、role、class、whose、retry_safe)以及 ours、theirs、path 的数量。六种 class 必须各出现一次。
检查清单不是一行一行手写,而是运行判定器来填写。whose 和 retry_safe 要取自第 5 步的表——如果表和检查清单对不上,两者之中必有一个是过时的。
按责任所在分别报告
在 /root/dep/summary.json 中写入 classes_covered、retry_elapsed_ms、retry_deadline_s、retry_attempts、theirs、path、no_timeout_exit_code,并在 /root/dep/dep_report.md 中分四节进行报告:## 무엇이 안 됐나、## 우리인가 그들인가、## 재시도는 어떻게 했나、## 남은 위험(韩文,依次意为“什么不行”“是我们还是他们”“重试是怎么做的”“剩余风险”)。
在给对方打电话之前先做出这张表,通话就会变短。如果把连接超时记为“对方故障”,对方在自己的日志里什么也找不到——请把这个区分留在报告里。重试的截止时间和实际耗时也要用数字写下。