Skip to content

跨境链路慢与不稳定的诊断 ​

一台带宽充足的境外代理服务器,多人使用时仍可能出现“网页偶尔慢、长请求和大请求体上传超时”。这类问题不能靠加带宽解决,也不能凭感觉换协议。本页把一次完整排查整理成可复用的方法:先用证据排除服务器,再把问题收敛到客户端到服务器这一段,最后确认协议形态是否放大了丢包,并用固定探针验证修复效果。

触发场景 ​

  • 服务器出口带宽标称足够,单账号限速也不低(例如每人 16 Mbit/s),实际体验却忽快忽慢,而不是持续慢。
  • 浏览器大多数时候正常,但通过代理访问中转 API 的客户端经常卡住;网关日志里出现大量 408。
  • 服务器负载、内存、进程状态都正常,重启服务只能短暂缓解。

“忽快忽慢”是判断的第一个线索:持续慢通常是容量或限速问题,波动慢更像链路丢包或路径抖动。

先排除服务器 ​

下面每一项都应当用命令取得数字,再做结论。

检查项看什么本次结论
负载与连接uptime、ss -s负载接近 0,活跃连接不到 20 条
出口速度在服务器上直接下载公网大文件、测到目标的首包时间单线程约 8 MB/s,首包正常
限速队列tc -s qdisc 中各队列的 dropped每账号限速队列无丢包,限速不是瓶颈
TCP 重传nstat 的 TcpRetransSegs / TcpOutSegs累计约 2.3%,30 秒抽样约 0.5%
多网卡路由ip rule、ip route get <目标> from <本机地址>回包的源地址与出网网卡不一致,见 Hysteria2 落地

累计重传率只能说明“历史上出现过丢包”,不能证明此刻的瓶颈。抽样时要和业务高峰对齐,并且同时记录出站段数,用比例而不是绝对次数下结论。

把问题收敛到客户端到服务器 ​

排除服务器后,问题只可能在三个位置:客户端到服务器的公网路径、协议形态本身、客户端自己的配置。证据来自两处。

连接层数据。 在服务器上用 ss -tin 查看面向客户端的连接,可以读到每条连接的 rtt、retrans、cwnd。本次样本的 RTT 在 80 到 130 ms 之间,抖动(rtt 后的偏差值)达到 25 到 50 ms,重传集中出现在面向客户端的连接上。ICMP ping 的丢包率可能被中间设备限速,不能单独当作丢包证据。

网关耗时日志。 如果入口 Nginx 记录了 request_time、upstream_header_time、status、content_length 和 request_length,就能把“网关慢”和“请求没传完”区分开:

  • 成功请求的 upstream_header_time 与上游模型的响应时间一致,说明后端不慢。
  • 408 集中在少数来源,约占这些来源请求的三成;失败请求的 content_length 在 30 到 50 万字节,而 request_length 只有 16 KB 左右,也就是约一个 HTTP/2 DATA 帧的大小,之后再没有数据到达。
  • 408 的含义是“客户端没有在时限内发完请求体”,不是网关处理慢。

这两组数据合起来指向同一个结论:大请求体上传在很早的阶段就被卡住,之后一直等不到后续数据。

为什么 TCP 套 TCP 在丢包链路上更差 ​

常见的 Shadowsocks 加 v2ray-plugin 的 WebSocket 方案,数据要依次经过 Shadowsocks 加密、WebSocket 封装、TLS 和底层 TCP。三层都跑在同一条 TCP 连接上:

  • 底层任何一个包丢失,整条隧道里的所有请求一起等待重传,这是队头阻塞。
  • 内层应用的 TCP 也有自己的重传和拥塞控制。外层重传期间,内层看到的是延迟突增,容易误判为拥塞而降速;降速后恢复又需要多个往返。
  • 大请求体上传需要连续多个往返才能发完,更容易撞上丢包;浏览器的小请求短而多,受影响反而小。

这解释了“浏览器多数正常、大上传经常超时”的差异,也解释了为什么越测越慢:每次重传都在压低发送窗口。QUIC 基于 UDP,丢包只影响对应的流;Hysteria2 还可以按声明的速率固定发送,不会被丢包持续拉低,所以在同一条路径上表现更稳。方案细节见 Hysteria2 落地与订阅编排。

诊断流程 ​

跨境链路慢与不稳定的诊断流程

四个判断点依次回答:服务器资源是否正常、服务器到目标的出口是否正常、客户端到服务器的链路是否异常、失败是否集中在大请求体上传。前两个为“否”时,在服务器侧处理;第三个为“否”时,回到按层排查(规则、DNS、节点参数);第四个为“是”时,结论才是隧道协议在丢包链路上的队头阻塞。

用请求体上传做探针 ​

排查和验收必须使用同一个可重复的探针,否则无法比较修复前后。大请求体上传最贴近真实故障:

bash
# 生成约 500 KB 的请求体
head -c 500000 /dev/urandom | base64 | head -c 500000 > body.bin

# 向中转服务发起 12 次 POST;没有带密钥时返回 401 也算“请求体已传完”
for i in $(seq 1 12); do
  curl -s -m 25 -X POST -H 'Content-Type: application/json' \
       --data-binary @body.bin -o /dev/null \
       -w "%{http_code} %{time_total}\n" \
       https://relay.example.com/responses
done

判定标准是“25 秒内是否收到服务端的任意响应码”,统计完成次数和耗时分布。本次结果:

通道完成情况单次耗时
直连(TCP)12 次只有 4 次完成,其余超时完成的从 1.5 秒到 15 秒不等
Hysteria2 隧道连续 40 余次全部完成约 1 到 2.5 秒

比较时要使用同一时段、多轮测试,并关注完成率和分位数,不要只看单次峰值速度。

测试时容易误判的陷阱 ​

这次排查中,下面几个问题让早期的若干轮测试结果完全无效,写在这里是为了下次先排除。

现象原因处理
经代理的测试瞬间失败或全部通过环境变量 no_proxy=* 让 curl 忽略 -x 参数,请求根本没进代理测试时加 --noproxy '',或先 unset no_proxy
对任何外网 IP 的 TCP 握手都只要约 1 ms本机或局域网有透明代理接管了连接先确认流量真实路径,再判断延迟
代理参数没有传给 curlzsh 不会按空格拆分未加引号的变量使用数组,或 ${=VAR} 展开
内核刚启动时请求全部失败配置含 GEOIP 规则时,首次启动需要下载地理库,耗时可达数十秒等待监听端口就绪后再测
测试中途连接断开另一个会话同时修改并重启了服务端测试前确认没有并发变更
结论来回摇摆只测一次、只看峰值固定探针,多轮取完成率与分位数

总结与适用边界 ​

  • 这套方法针对“服务器正常、客户端到服务器的路径有丢包或抖动”的场景。如果前两个判断点为“否”,先按表处理服务器问题。
  • 结论不是“换一种协议”,而是三件事一起做:用 QUIC 通道承载主要流量,保留 TCP 通道作为故障切换兜底,用固定探针验证效果。
  • 探针的数字只代表当次网络环境。换运营商、换时段或换地区后需要重新测,不能直接外推。
别急,先让缓存热一下。