2940 字
约 9 分钟
16
S02-A03 · 网络架构决策:延迟预算、丢包归因与代理链路

"接口慢"这三个字背后至少是四种互斥的原因:多绕了一段路、多握了一次手、丢了包在重传、上游排队。它们的修复手段彼此不通用——扩带宽治不了握手,加机器治不了丢包。所以网络架构的第一件事不是选组件,而是把延迟拆成能单独归因的段,再决定哪一段值得为它引入复杂度。


1. 延迟预算的五段拆法

段 典型量级 归因手段 常见误判
DNS 解析 首次数十至上百分之一秒,缓存命中后趋近 0 dig +trace、curl -w 的 time_namelookup 只看平均值,忽略冷启动与 TTL 抖动
TCP 握手 1 个 RTT time_connect - time_namelookup 把排队等 accept 算进"握手慢"
TLS 握手 TLS 1.2 约 2 RTT,1.3 约 1 RTT,会话恢复可再省 time_appconnect 忘记证书链过大、OCSP 往返
服务端处理 应用自身 P50/P99 服务端埋点、Server-Timing 用客户端总耗时倒推服务端
传输与序列化 与 payload 大小、带宽、压缩比相关 抓包看 bytes_in/out 与分片 把大响应体当"网络慢"

最常被忽略的是第一段和最后一段:DNS 在迁移、灰度、多 CDN 场景下反复变化,却很少进监控;响应体大小极少被当成架构指标。

curl -s -o /dev/null -w \
 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} \
  first=%{time_starttransfer} total=%{time_total} size=%{size_download}\n' \
 https://TARGET/health

五个数一摆,"慢在哪一段"就不再是猜。time_starttransfer - time_appconnect 才是应用时间。


2. 带宽与延迟是两件事

带宽管"一次能搬多少",延迟管"一次往返多久"。二者的判据完全不同:

症状 更可能是 判据
大文件下载慢、小请求快 带宽/拥塞窗口 吞吐随并发线性上升,单流速度受 BDP 限制
小请求也慢,且随距离增加 延迟/往返次数 单次请求耗时 ≈ RTT × 往返数
并发一上来就崩 连接数与排队 accept 队列溢出、TIME_WAIT 堆积
首屏慢但接口快 资源数与串行依赖 请求瀑布图、关键路径长度

一个常被混淆的换算:吞吐 ≈ 窗口 / RTT。窗口没放大时,跨地域的吞吐上限由 RTT 决定,与买多大带宽无关。这也是"上专线不一定快"的原因——专线降低的是丢包与抖动,若窗口与并发没配,RTT 仍在那里。

BDP = 带宽 × RTT
例:100 Mbps × 40 ms ≈ 500 KB 在途数据
→ 单连接窗口小于 BDP 时,吞吐被窗口锁死,而不是被带宽锁死

3. 丢包与重传:1% 为什么比直觉严重

TCP 的丢包不是"重发一次就好"。拥塞控制把丢包当作拥塞信号:一旦判定丢包,窗口回退,之后靠慢启动爬回。经典 Reno 的近似吞吐关系是:

吞吐 ≈ (MSS / RTT) × C / sqrt(p)      C 约 1~1.5,p 为丢包率
p 从 0.1% 升到 1%:sqrt 项放大 3 倍以上,吞吐下探近一个数量级

所以 1% 丢包不是"损失 1% 的包",而是整条连接的稳态吞吐掉一个档位,表现为"偶发几十毫秒的毛刺"和"高峰期整体变慢"。归因手段:

ss -ti dst TARGET            # 看 retrans、cwnd、rtt、rto
nstat -az | grep -i retrans  # 系统级重传计数
sar -n ETCP 1 5              # 每秒新增重传段
mtr -rwzbc 100 TARGET        # 逐跳丢包与抖动(区分链路丢包与端上丢包)

判读要点:ss -ti 里 retrans 持续增长而 cwnd 上不去,是链路问题;cwnd 正常但 rtt 抖动大,是排队(bufferbloat)而不是丢包。


4. 代理链路的五类代价

代价 机制 表现
多一跳 每跳增加 1 个 RTT 与一次排队 内网 P99 抬升,长尾更明显
连接复用失效 代理到上游默认短连接 上游 TIME_WAIT 暴涨、握手开销翻倍
TLS 终结位置 在代理终结则后端明文;透传则无法按路径路由 证书管理分散、mTLS 需重新设计
头部改写 X-Forwarded-*、Host、Content-Length 鉴权、限流、跳转地址错乱
超时叠加 每层各有一套超时 上游已返回 504,下游仍在等

反向代理不等于负载均衡。 代理解决的是"入口收敛、TLS 终结、路径路由";负载均衡解决的是"多实例分摊与健康剔除"。nginx 做反代时若没配 upstream 复用,它同时也是一个放大器:

upstream app {
    server 10.0.0.11:8080;
    server 10.0.0.12:8080;
    keepalive 64;                 # 到上游的空闲长连接池
    keepalive_requests 1000;
    keepalive_timeout 60s;
}
location /api/ {
    proxy_http_version 1.1;       # 1.0 不支持 keep-alive
    proxy_set_header Connection "";  # 清掉 close,否则复用失效
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 30s;
}

这三行(http_version、Connection ""、keepalive)缺任何一行,上游都会退化成短连接,代价体现在上游的连接数与时延上,而不是在代理自己的指标里。


5. CDN 的收益边界

CDN 的收益完全由缓存命中率决定,而不是由节点数量决定。命中率上不去时,CDN 只是给每个请求加了一跳。

命中率区间 净收益 说明
高(静态资源、指纹化文件名) 显著降低源站带宽与 RTT 收益主要来自就近与回源减少
中(带参数的列表接口) 视 Vary 与查询串规范化而定 查询串一乱,命中率断崖
低(鉴权、个性化、实时库存) 可能为负 多一跳 + 回源,且缓存一致性风险上升

动态内容的三条路:拆静态与个性化(边缘拼装)、鉴权下沉为短时令牌(Cache-Control: private)、或承认这类请求该走就近部署而非 CDN。Vary: Cookie 一类的头会把命中率打到接近 0,属于典型的"配了 CDN 但没生效"。


6. 跨机房与跨地域的三条路

路径 做法 一致性代价 适用
就近部署 每个地域一套完整服务 写冲突需仲裁或分区归属 读多写少、可接受最终一致
异步化 跨域只传事件,不传请求 延迟可见、需幂等与去重 可容忍秒级~分钟级延迟
数据本地化 数据跟着用户走,跨域只读副本 跨域读可能读到旧值 有数据驻留要求

三条路的共同点是都要在"读旧数据"和"等更久"之间选一个。声称"既强一致又不加延迟"的方案,通常是把延迟藏在了别处(写时同步、或读时补齐)。


7. 连接池与超时预算

代理前后,连接池的语义会变:客户端池化的是"到代理"的连接,代理池化的是"到上游"的连接,两段容量不匹配时,一段空闲一段排队。对齐原则:下游池容量 ≤ 上游可接受并发,且各级超时严格递减。

超时预算(自上而下必须递减,且留重试余量)
网关/客户端 总超时  3000 ms
  └ 代理 read_timeout   2500 ms
      └ 应用 单次下游调用  800 ms × 最多 3 个下游,串行时 ≤ 2400 ms
          └ 重试:仅对幂等请求,最多 1 次,且计入总预算
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofMillis(300))
    .version(HttpClient.Version.HTTP_2)
    .executor(Executors.newFixedThreadPool(32))
    .build();
// 单请求超时用 request.timeout(),不要只靠 connectTimeout

常见错误是把"重试"当免费:一次重试等于把下游压力乘 2,且在超时预算里占了两份时间。重试必须有预算上限,否则尾延迟会被自己放大。


8. 可观测的三层与"平均延迟骗人"

层 看什么 能回答 不能回答
网络层 RTT、重传率、连接数、队列 链路是否健康 哪段代码慢
应用层 P50/P95/P99、错误率 用户体验分布 慢在哪个下游
追踪 span 时长与父子关系 慢点的具体归属 网络排队本身

延迟是长尾分布:P99 翻倍时平均值几乎不动,但受影响请求的比例已从 1% 变成 5%。判据是看分位不看均值,看分位的时间序列不看单点。


9. IPv6 / HTTP2 / HTTP3 的收益前提

技术 真实收益前提 不满足时
IPv6 端到端可达、链路与中间设备支持 双栈复杂度上升,NAT64 引入新瓶颈
HTTP/2 多路复用 + 头部压缩 + 单连接并发 队头阻塞在 TCP 层仍在;单大流仍抢占
HTTP/3 丢包率偏高或移动网络、支持 UDP 通路 中间设备丢 UDP 时回退,收益归零

三者都属于"链路质量差时才显价值"的优化:内网低丢包、同机房调用时,收益通常小于运维复杂度。


排错

现象 常见原因 处理
首字节慢、总耗时快 服务端处理或 accept 排队 看 time_starttransfer - time_appconnect
上游 TIME_WAIT 暴涨 代理未复用上游连接 补 keepalive + Connection "" + HTTP/1.1
偶发几十 ms 毛刺 链路丢包触发重传/窗口回退 ss -ti 看 retrans,mtr 定位跳
上了 CDN 但没变快 命中率低(Vary、查询串、私有头) 看 CDN 命中率与回源比,拆分动静内容
超时层层叠加 各级超时未递减、重试翻倍 重写超时预算表,限制重试次数
鉴权/跳转地址错 X-Forwarded-* 未传或 Host 被改写 显式设置转发头,应用侧信任白名单

边界

场景 不该引入的复杂度 理由
内网同机房、RTT 亚毫秒 CDN、跨域就近、HTTP/3 收益在噪声以下
单体应用、单实例 多层代理与连接池调优 没有多跳就没有叠加
无跨地域需求 数据本地化与冲突仲裁 一致性代价换来不存在的收益
请求量极低 复杂缓存分层 命中率方差大,调试成本高于收益
团队无链路追踪能力 自建全链路采样 数据拿不到反而增加误判

自测

  1. 一次 HTTPS 请求的五个时间段里,哪一段最容易被误算进"服务端慢"?
  2. 为什么"加带宽"解决不了跨地域的小请求慢?
  3. 1% 丢包为什么会带来接近一个数量级的吞吐下探?
  4. nginx 反代到上游时,哪三行配置决定连接能否复用?
  5. CDN 命中率被打到接近 0 最常见的一个响应头是什么?
  6. 超时预算里,"留重试余量"具体指哪两件事?
S02-A03 · 网络架构决策:延迟预算、丢包归因与代理链路
https://www.clxhxhhr.top/posts/6068/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。