跨境链路慢与不稳定的诊断
服务器带宽有余量,网页能打开,上传一份较大的请求体却经常超时。这时扩容未必有效:客户端接入、两端网络、网关读取请求、目标业务都可能造成停顿。本页以“向受控 API 上传约 500 KB 数据”为主线,说明如何缩小故障范围、比较 TCP 与 QUIC 通道,并证明数据确实被完整接收。
触发场景
- 服务器出口带宽标称足够,单账号限速也不低(例如每人 16 Mbit/s),实际体验却忽快忽慢,而不是持续慢。
- 浏览器大多数时候正常,但通过代理访问中转 API 的客户端经常卡住;网关日志里出现大量
408。 - 服务器负载、内存、进程状态都正常,重启服务只能短暂缓解。
波动是采集时间关联证据的线索,不是丢包的判据。突发负载、连接池等待、DNS 重试和服务限流同样会忽快忽慢。先固定客户端、目标、请求体、时间窗口与配置版本,测试期间停止并发配置变更。
先排除服务器
先检查服务进程、出口与队列,避免把容量问题误当成协议问题。下表保留一次排查中的观察,只用于说明证据如何解释,不能作为其他部署的基准。
| 检查项 | 看什么 | 历史观察及边界 |
|---|---|---|
| 负载与连接 | uptime、ss -s、进程 CPU 与内存 | 负载接近 0,活跃连接不到 20 条;仍需排除单核瓶颈与短时尖峰 |
| 出口速度 | 在服务器上直接下载公网大文件、测到目标的首包时间 | 单线程约 8 MB/s,首包正常 |
| 限速队列 | tc -s qdisc 的字节、积压与丢弃 | 观察窗口内队列无丢包;不能排除队列外限速或其他时段积压 |
| TCP 重传 | nstat 的 TcpRetransSegs / TcpOutSegs | 累计约 2.3%,30 秒抽样约 0.5% |
| 多网卡路由 | ip rule、带源地址的路由查询与两端抓包 | 另一个 UDP 部署出现回程源地址不一致,处理见 Hysteria2 落地 |
TcpRetransSegs / TcpOutSegs 是主机范围的段计数比,不是某条路径的精确丢包率。应比较同一时间窗口内的增量,并关联目标连接。服务器直接下载正常,只证明那个目标、那个方向与时段正常;上传、代理入口和真实业务目标仍需分别验证。
把问题收敛到客户端到服务器
资源和出口观察正常后,继续对照客户端到入口的传输、规则与 DNS、入口网关读取、后端处理。它们不是互斥分支:例如错误 DNS 和丢包可以同时存在。先用客户端连接记录确认请求确实经过预期节点,再关联以下证据。
连接层数据。 用 ss -tin 按客户端或服务端口定位连接,记录 rtt、rttvar、重传和拥塞窗口随时间的变化。曾观察到 RTT 约 80–130 ms、RTT 变化估计约 25–50 ms;rttvar 是 TCP 的 RTT 变化估计,不等于独立测得的抖动分布。ICMP 可能被限速,ping 丢包不能单独证明业务包同样丢失。QUIC 不在 TCP 统计中,需要结合 QUIC 日志和 UDP 抓包。
网关读取证据。 Nginx 可记录请求 ID、$request_time、$upstream_header_time、$status、$http_content_length 与 $request_length,再与错误日志和目标请求 ID 关联。不要记录业务 token 或请求体。
- 成功请求的上游首部耗时正常,不能推导所有失败请求的后端都正常。失败请求没有上游耗时,还要确认是否尚未转发。
- 声明 30–50 万字节、实际记录明显偏小时,需结合
client timed out ... while reading client request body等错误确认读取阶段。$request_length包含请求行、首部和请求体,不是请求体专用计数,也不能把约 16 KB 直接对应成一个 HTTP/2 DATA 帧。 408表示请求等待超时,但仅凭状态无法区分读首部、读请求体或其他产生该响应的组件。client_body_timeout通常限制两次读取之间的等待,不是整个上传的统一总时长。
若错误日志明确停在读请求体、目标端尚未收到完整数据、入口方向连接同时出现重传,才可以将传输停顿作为重点假设。下一步用独立探针复现,避免将业务鉴权或模型响应时间混入上传测量。
TCP 与 QUIC 的差别在哪里
Shadowsocks 加 WebSocket/TLS 的普通流代理在客户端和服务器终止连接,将应用字节转发到另一段连接。它并不必然把原始 TCP 包装进外层 TCP,因此不能直接套用 IP 隧道的 TCP-over-TCP 拥塞失效结论。
TCP 保证连接内字节有序,缺失字节未恢复时,后续字节不能交付。若多项业务确实复用同一条外层 TCP,它们可能一起等待;若每项业务使用独立连接,不能说所有请求都会阻塞。先核对连接数、复用配置和抓包,才能确定影响范围。
QUIC 的可靠流分别交付,可减少不同流之间因字节排序造成的等待,但流仍共享网络路径、连接级拥塞控制和部分资源;同一个丢包也可能涉及多个流。Hysteria2 还可按配置在不同方向选择 Brutal 或其他拥塞控制。它值得作为对照通道,但 UDP 被限制、带宽声明过大或 MTU 异常时也会失败,见 Hysteria2 实践。
诊断流程
图中每次判断只决定下一项实验,不直接给协议定罪。资源与目标正常后,用完整接收证明检查上传;上传完整而业务失败,就回到目标处理与鉴权。只有同目标、同负载的通道对照持续出现差异,才考虑调整传输方案。
用请求体上传做探针
探针使用无业务副作用的专用 HTTPS 端点。端点先完整读取请求体,再返回 JSON 中的 bytes 和 sha256;不能提前返回 401 后仍把它算作上传成功。可使用 Node.js 接收端示例:它只监听回环地址,最大接收 1 MiB,需由已有受控 HTTPS 网关转发到它,不直接作为公网生产服务。网关路径、允许来源与请求体上限须另行配置,测试后停止。
bash
# Linux;另需 curl、openssl、jq、sha256sum
openssl rand -out body.bin 500000
EXPECTED_SHA=$(sha256sum body.bin | cut -d ' ' -f 1)
PROBE_URL='https://probe.example.com/upload-probe'
curl --proxy http://127.0.0.1:7890 --noproxy '' \
--connect-timeout 5 --max-time 25 --fail-with-body \
-H 'Content-Type: application/octet-stream' \
--data-binary @body.bin -o response.json \
-w 'http=%{http_code} upload=%{size_upload} total=%{time_total}\n' \
"$PROBE_URL" &&
jq -e --arg sha "$EXPECTED_SHA" \
'.bytes == 500000 and .sha256 == $sha' response.json命令成功、HTTP 成功且两项校验都相同,才计为完整上传。size_upload 只说明 curl 交给传输层的字节,服务端回传的长度与摘要才补上接收端证据。响应丢失仍可能让已接收的上传显示失败,所以真实非幂等业务不能照此无条件重试。
经代理时用客户端连接页确认当前实际节点。直连对照改用 --proxy '' --noproxy '*',同时确认没有系统透明代理或 TUN 接管;“不使用显式代理”不自动证明物理直连。每条通道交替跑多轮,记录退出码、状态、校验、总耗时和当时节点,比较完成率及成功样本的中位数、P95;超时样本单独计数,不能从耗时统计中悄悄消失。
历史排查曾记录“TCP 12 次中 4 次响应、QUIC 连续 40 余次响应”,但旧探针接受提前返回的 401,缺少完整接收证明。这些数字只能保留为当时响应连通性的观察,不能证明大请求体上传完成,更不能当作当前方案的性能承诺。
测试时容易误判的陷阱
这次排查中,下面几个问题让早期的若干轮测试结果完全无效,写在这里是为了下次先排除。
| 现象 | 原因 | 处理 |
|---|---|---|
| 经代理的测试瞬间失败或全部通过 | 环境变量 no_proxy=* 让 curl 忽略 -x 参数,请求根本没进代理 | 测试时加 --noproxy '',或先 unset no_proxy |
| 对任何外网 IP 的 TCP 握手都只要约 1 ms | 本机或局域网有透明代理接管了连接 | 先确认流量真实路径,再判断延迟 |
代理参数没有传给 curl | zsh 不会按空格拆分未加引号的变量 | 参数直接逐项传入;脚本需要动态参数时使用数组 |
| 内核刚启动时请求全部失败 | 配置含 GEOIP 规则时,首次启动需要下载地理库,耗时可达数十秒 | 等待监听端口就绪后再测 |
| 测试中途连接断开 | 另一个会话同时修改并重启了服务端 | 测试前确认没有并发变更 |
| 结论来回摇摆 | 只测一次、只看峰值 | 固定探针,多轮取完成率与分位数 |
总结与适用边界
这套方法适合能够控制测试目标、关联客户端与服务端时间的网络故障。无法控制目标时,至少保存真实请求 ID、网关错误和目标返回,降低结论强度。通道切换是实验变量之一,资源扩容、超时调整和客户端配置也必须由证据决定。
只有修复后的相同探针、真实上传和长连接都通过,才适合推广到更多使用者。保留经过验证的 TCP 兜底,并验证 UDP 不可达时的行为;切换节点不会自动迁移已经上传一半的业务请求。
