编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

    • README
    • 公共方法论

    • 体系建设篇

    • 资源专项篇

    • 流水线专项

    • 业务专项篇

    • 交付防御篇

      • 崩溃捕获设计实践
      • 包体积与资源治理
      • 应用安全性能权衡
      • 弱网极端环境治理
        • 01.阅读说明
        • 02.贯穿案例
          • 2.1 案例背景
          • 2.2 经验派 6 周折腾
          • 2.3 方法派 10 天闭环
          • 2.4 上线效果
          • 2.5 案例串联全文
        • 03.弱网本质定义
          • 3.1 物理层定义
          • 3.2 工程层定义
          • 3.3 用户感知定义
          • 3.4 弱网三种形态
        • 04.弱网判定原理
          • 4.1 信号量判定误区
          • 4.2 RTT 判定法
          • 4.3 吞吐量判定法
          • 4.4 失败率判定法
          • 4.5 三合一判定模型
        • 05.度量与采集
          • 5.1 三类采集方案
          • 5.2 各方案盲区对照
          • 5.3 跨平台采集对照
        • 06.归因决策树
          • 6.1 五阶段二分法
          • 6.2 重试雪崩归因
          • 6.3 半死连接归因
        • 07.慢速形态全链路
          • 7.1 慢速如何产生
          • 7.2 TCP 重传与窗口塌缩
          • 7.3 应用层表象
          • 7.4 应对核心思路
        • 08.抖动形态全链路
          • 8.1 抖动如何产生
          • 8.2 丢包与乱序投递
          • 8.3 应用层表象
          • 8.4 应对核心思路
        • 09.切换形态全链路
          • 9.1 切换如何产生
          • 9.2 TCP 半死的本质
          • 9.3 应用层表象
          • 9.4 应对核心思路
        • 10.跨端形态对照
          • 10.1 端到端流程对照
          • 10.2 三形态归因速查
          • 10.3 统一启示
        • 11.治理一层超时
          • 11.1 超时本质再认识
          • 11.2 连接超时与读超时
          • 11.3 分级超时策略
          • 11.4 超时的探索性反思
        • 12.治理二层重试
          • 12.1 重试的双刃剑
          • 12.2 指数退避与抖动
          • 12.3 幂等性前置条件
          • 12.4 熔断与重试上限
        • 13.治理三层降级
          • 13.1 降级的设计哲学
          • 13.2 弱网级别映射
          • 13.3 内容降级三档
          • 13.4 进度可见原则
        • 14.治理四层离线
          • 14.1 离线优先思想
          • 14.2 缓存的三种策略
          • 14.3 离线队列设计
          • 14.4 冲突合并难题
        • 15.治理五层架构
          • 15.1 本地优先架构
          • 15.2 协议层升级 HTTP3
          • 15.3 CRDT 与终极一致
          • 15.4 五层治理总览
        • 16.求证实验
          • 16.1 实验一:超时最优值
          • 16.2 实验二:退避算法
          • 16.3 实验三:长短连接
          • 16.4 实验四:缓存命中
          • 16.5 实验五:HTTP3 收益
          • 16.6 五大实验启示
        • 17.实战案例
          • 17.1 地铁失败重试
          • 17.2 iOS 切网半死
          • 17.3 启动期 DNS 阻塞
        • 18.防劣化体系
          • 18.1 三道防线总览
          • 18.2 CI 弱网回归
          • 18.3 线上 SLO
        • 19.跨平台速查
          • 19.1 工具速查
          • 19.2 关键 API 速查
        • 20.总结与延伸
          • 20.1 五条核心原则
          • 20.2 五个常见误区
          • 20.3 延伸阅读
        • 21.一句话总结
  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 性能优化实践
  • 交付防御篇
杨充
2026-05-27
目录

弱网极端环境治理

# 弱网与极端环境治理

📊 学习成本预估 | 难度:⭐⭐⭐⭐(4/5)| 阅读:约 35 分钟 | 实操:3 小时 🔗 前置阅读:卷四·02 | ➡️ 后续延伸:—

# 目录介绍

  • 01.阅读说明
  • 02.贯穿案例
    • 2.1 案例背景
    • 2.2 经验派 6 周折腾
    • 2.3 方法派 10 天闭环
    • 2.4 上线效果
    • 2.5 案例串联全文
  • 03.弱网本质定义
    • 3.1 物理层定义
    • 3.2 工程层定义
    • 3.3 用户感知定义
    • 3.4 弱网三种形态
  • 04.弱网判定原理
    • 4.1 信号量判定误区
    • 4.2 RTT 判定法
    • 4.3 吞吐量判定法
    • 4.4 失败率判定法
    • 4.5 三合一判定模型
  • 05.度量与采集
    • 5.1 三类采集方案
    • 5.2 各方案盲区对照
    • 5.3 跨平台采集对照
  • 06.归因决策树
    • 6.1 五阶段二分法
    • 6.2 重试雪崩归因
    • 6.3 半死连接归因
  • 07.慢速形态全链路 ⭐
    • 7.1 慢速如何产生
    • 7.2 TCP 重传与窗口塌缩
    • 7.3 应用层表象
    • 7.4 应对核心思路
  • 08.抖动形态全链路 ⭐
    • 8.1 抖动如何产生
    • 8.2 丢包与乱序投递
    • 8.3 应用层表象
    • 8.4 应对核心思路
  • 09.切换形态全链路 ⭐
    • 9.1 切换如何产生
    • 9.2 TCP 半死的本质
    • 9.3 应用层表象
    • 9.4 应对核心思路
  • 10.跨端形态对照
    • 10.1 端到端流程对照
    • 10.2 三形态归因速查
    • 10.3 统一启示
  • 11.治理一层超时 ⭐
    • 11.1 超时本质再认识
    • 11.2 连接超时与读超时
    • 11.3 分级超时策略
    • 11.4 超时的探索性反思
  • 12.治理二层重试 ⭐
    • 12.1 重试的双刃剑
    • 12.2 指数退避与抖动
    • 12.3 幂等性前置条件
    • 12.4 熔断与重试上限
  • 13.治理三层降级 ⭐
    • 13.1 降级的设计哲学
    • 13.2 弱网级别映射
    • 13.3 内容降级三档
    • 13.4 进度可见原则
  • 14.治理四层离线 ⭐
    • 14.1 离线优先思想
    • 14.2 缓存的三种策略
    • 14.3 离线队列设计
    • 14.4 冲突合并难题
  • 15.治理五层架构 ⭐
    • 15.1 本地优先架构
    • 15.2 协议层升级 HTTP3
    • 15.3 CRDT 与终极一致
    • 15.4 五层治理总览
  • 16.求证实验 ⭐
    • 16.1 实验一:超时最优值
    • 16.2 实验二:退避算法
    • 16.3 实验三:长短连接
    • 16.4 实验四:缓存命中
    • 16.5 实验五:HTTP3 收益
    • 16.6 五大实验启示
  • 17.实战案例
    • 17.1 地铁失败重试
    • 17.2 iOS 切网半死
    • 17.3 启动期 DNS 阻塞
  • 18.防劣化体系
    • 18.1 三道防线总览
    • 18.2 CI 弱网回归
    • 18.3 线上 SLO
  • 19.跨平台速查
    • 19.1 工具速查
    • 19.2 关键 API 速查
  • 20.总结与延伸
    • 20.1 五条核心原则
    • 20.2 五个常见误区
    • 20.3 延伸阅读
  • 21.一句话总结

# 01.阅读说明

  • 本文卷归属:卷五 · 交付与防御 · 第 4 篇
  • 本文目标层级:L3 专家
  • 适用平台:Android / iOS / Web / 服务端
  • 前置阅读:卷四·02 网络性能分析优化(本章是它在"极端场景"的特化)
  • 横联阅读:卷五·01 崩溃捕获(弱网是崩溃高发场景)
  • 本文核心命题:

    弱网治理是性能体系的"压力测试":实验室完美的产品,在地铁、电梯、信号断点下经常崩溃。 弱网治理 = 弱网识别 + 五层治理(超时 → 重试 → 降级 → 离线 → 架构)+ SLO 防劣化。 "95% 用户良好"和"99.9% 用户在极端环境下不崩",是高品质 App 与普通 App 的分水岭。

8 个反直觉问题(带着这些问题读):

  1. 为什么"信号 4 格"也可能弱网?
  2. TCP 超时默认 10 秒,真实场景应该设多少?
  3. 为什么"重试 3 次"经常加剧而不是缓解问题?
  4. HTTP/2 在弱网下真的比 HTTP/1.1 快吗?
  5. 为什么"地铁里 4G 显示满格但加载不出"?
  6. CDN 切换为什么经常制造卡顿?
  7. 离线缓存到底应该缓存什么?
  8. 为什么"长连接断线"在地铁场景比"短连接超时"更常见?

# 02.贯穿案例

本案例贯穿全文:§03 拿到弱网定义、§04 学会判定、§07-§10 各形态原理、§11-§15 五层治理、§16 用实验复盘。

# 2.1 案例背景

某头部出行 App V8.0 在通勤高峰期收到大量"打车按钮点了没反应"投诉,但后端监控显示接口成功率 99.5%(行业极致)。用户量级与投诉量级完全对不上:

  • 客服每日"无法叫车"投诉 5000+,对应 DAU 1200 万应有 0.5%+。
  • 服务端 API 成功率显示 99.5%,客户端真实成功率只有 87%。
  • 应用商店评分掉到 3.5,评论高频出现"地铁里打不到车"。

研发组:"服务端没问题,应该是用户网络问题。"——这是经典的"服务端视角偏见"。

# 2.2 经验派 6 周折腾

周次 动作 结果
第 1 周 加大服务端超时配置 60s → 120s 无效,用户更早放弃
第 2 周 客户端失败立即重试 3 次 服务端 QPS 翻 3 倍,触发限流,雪崩
第 3 周 全量切换 HTTP/2 长连接断线后感知更慢,部分场景反而劣化
第 4 周 加 CDN 加速静态资源 CDN 切换时制造新卡顿
第 5 周 上"网络检测页"让用户自测 用户根本看不懂"网络延迟 800ms"
第 6 周 关键页面"如遇加载失败请重试" 没人愿意点

复盘:六周折腾错在"以为弱网是用户问题"。真相是:地铁/电梯场景下应用根本没把"弱网"作为一等公民设计——超时配置一刀切、重试无退避、无降级、无离线、无 SLO 区分。

# 2.3 方法派 10 天闭环

Day 1(§04 弱网判定 + §05 度量):上线 RUM 埋点,按"网络质量分级"切分用户群(详见 §04.5)。

Day 2(§06 归因决策树):发现 60% 失败发生在"切换型弱网"(WiFi ↔ 4G),而非传统"慢速弱网"。

Day 3-4(§11+§12 超时与重试治理):所有 API 改为分级超时 + 指数退避 + jitter(见 §11.3、§12.2)。

Day 5-6(§13 降级 + §14 离线):核心叫车流程加离线队列——用户在隧道里点"打车",先入本地队列,恢复信号后自动发送。

Day 7-8(§09 切换形态治理):监听 NWPathMonitor/ConnectivityManager,切网时主动重建连接(见 §09.4)。

Day 9-10(灰度上线 + §18 SLO 监控)。

# 2.4 上线效果

指标 经验派 6 周后 方法派 10 天后 行业基准
客户端真实成功率 87% 97.5% 95%
弱网用户成功率 65% 92% 90%
重试雪崩频次 周均 2 次 0 次 0
平均叫车用时 P95 18s 6s < 10s
应用商店评分 3.5 4.4 4.5

核心洞察:"服务端成功率"不等于"用户成功率"。服务端看 99.5% 是"到达的请求",看不到"没到达的请求"——这部分恰恰是弱网下最痛的。弱网治理本质是"把不可见的失败可见化、把可见的失败可恢复化"。

# 2.5 案例串联全文

  • §03 弱网定义 ▶▶ "信号 4 格也可能是弱网"。
  • §04 判定原理 ▶▶ 三合一判定让弱网用户单独可见。
  • §05-§06 度量与归因 ▶▶ 找到 60% 失败集中在切换型弱网。
  • §07-§09 三形态原理 ▶▶ 每种形态都有自己的物理根因。
  • §11-§15 五层治理 ▶▶ "超时→重试→降级→离线→架构"由浅入深。
  • §16 求证实验 ▶▶ 5 个实验给出每条决策的数据支撑。
  • §17 实战案例 ▶▶ 跨端同构 + iOS/Android 特异。

# 03.弱网本质定义

弱网不是一个布尔值,而是一个谱系。从物理层、工程层、用户感知层,三个角度给出的定义完全不同——理解这种"多层定义"是后面所有治理决策的认知前提。

# 3.1 物理层定义

物理层的弱网 = 信号-噪声比(SNR)低 + 信道竞争激烈:

[基站]                          [手机]
   │                              │
   │── 信号强度(RSRP)─────▶    │
   │                              │
   │   噪声/干扰                  │
   ▼   ↓ ↑ ↓ ↑                  ▼
   误码率 BER ↑ → 物理层重传 ↑ → 实际吞吐 ↓
1
2
3
4
5
6
7

关键事实:

  • 信号强度 ≠ 网络质量。"4 格信号"只能说明 RSRP 高,但 SNR 可能很低(噪声大)。地铁里"4 格但加载不出"就是 SNR 低的典型——大家都在抢信号,干扰互相叠加。
  • 物理层的弱网是连续谱:从"完全无信号"到"完美信号"中间有无数中间态。每个中间态对应不同的实际带宽和丢包率。
  • 物理层的恶化具有"非线性":信号下降 3dB(一半功率),实际吞吐可能下降 90%——因为更高的 MCS(调制编码)已经无法可靠传输,必须回退到更低的 MCS。

探索性思考:为什么"信号 4 格"反而最容易误导用户? 因为 4 格信号让用户心理预期是"网络很好",但实际 SNR 可能差。一旦加载失败,用户会归因为"应用问题"而不是"网络问题"——这就是为什么弱网投诉比断网投诉更多。断网用户知道是自己网络问题,弱网用户以为是应用 bug。这是产品认知上的不对称,必须由应用主动告知"当前网络较差"来修正。

# 3.2 工程层定义

工程层从"应用能否完成业务"角度定义弱网:

指标 正常 一般弱网 严重弱网 离线
RTT < 200ms 200-1000ms > 1000ms ∞
下行带宽 > 1Mbps 100K-1M < 100K 0
丢包率 < 1% 1-5% > 5% 100%
请求成功率 > 99% 90-99% 60-90% 0%

为什么是这些阈值?

  • 200ms RTT:人眼感知的"立即"阈值。低于这个值用户感觉"流畅",高于这个值用户感觉"卡"。
  • 100Kbps:传统语音通话的下限。低于这个值连"一张缩略图"都加载不动。
  • 5% 丢包:TCP 拥塞控制开始"暴跌"的临界点。超过这个值,TCP 吞吐会因为反复重传而急剧塌缩(详见 §07.2)。

# 3.3 用户感知定义

用户感知的弱网 = "操作没有得到立即响应":

用户点击 → ... 等待 ... → 结果出来
         ↑
    超过 2s 用户感觉"卡"
    超过 5s 用户感觉"应用挂了"
    超过 8s 用户开始放弃/重试
1
2
3
4
5

心理学事实(Nielsen 三个时间阈值):

  • 0.1 秒:用户感觉"立即响应"。
  • 1 秒:用户感觉"被系统响应"(思维仍然连续)。
  • 10 秒:用户注意力上限。超过 10 秒用户开始做别的事。

核心结论:8 秒是工程上的"用户耐心上限"——这就是为什么所有超时都应该围绕 8 秒来设计(见 §11.3)。

探索性思考:为什么"加载中"的进度条能延长用户耐心? 因为它把"未知等待"变成了"可预测等待"。用户的焦虑来自"不知道还要等多久",而不是"等待本身"。所以展示进度比加快速度更重要——这是 §13.4 进度可见原则的心理学基础。

# 3.4 弱网三种形态

工程上把弱网细分为三种质性不同的形态——每种形态的物理根因、用户表象、应对策略都完全不同:

形态 物理本质 典型场景 用户表象 应对核心
慢速 持续低带宽/高延迟 地铁 2G/3G、偏远地区 转圈很久 降级 + 缓存
抖动 阵发性丢包 高铁、电梯入口 时通时断 重试 + 退避
切换 物理链路变更 进出门、换基站、WiFi↔4G 部分请求卡死 监听 + 重建

探索性思考:为什么必须区分三种形态而不是统一处理? 因为它们的治理手段会互相打架:

  • 对"慢速"应该增大超时(给慢点的网络机会),但对"切换"应该缩短超时(半死连接早死早超生)。
  • 对"抖动"应该积极重试(短暂故障会恢复),但对"慢速"应该控制重试(多次重试也不会变快)。
  • 对"切换"应该主动重建连接,但对"慢速"重建只会让事情更糟(建连本身就慢)。

不区分形态、一刀切应对,就是 §02 案例中"经验派 6 周失败"的核心病根。

§07-§09 会分别拆解三种形态的完整链路。


# 04.弱网判定原理

弱网判定不是"读一个 API 拿网络状态"那么简单——操作系统给的网络状态信息和真实网络质量经常不一致。本章讲清楚四种判定方法的原理、各自的盲区、以及为什么必须组合使用。

# 4.1 信号量判定误区

最朴素的弱网判定是读系统的"网络类型"和"信号强度":

// Android
val cm = getSystemService(ConnectivityManager::class.java)
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork)
val isWifi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
val signalStrength = capabilities?.signalStrength  // RSRP/RSSI
1
2
3
4
5

这种判定有三个根本性问题:

① 网络类型不代表网络质量:4G 不一定比 3G 快。机房附近的 3G 基站 RTT 30ms,地铁里的 4G 基站 RTT 800ms。

② 信号强度只反映下行链路的"接收功率":不反映上行、不反映丢包、不反映 SNR。地铁里"满格信号"但全车人都在抢上行带宽,应用照样卡。

③ 系统 API 有缓存延迟:连接刚切换时,系统报告的还是旧网络状态,差几秒到几十秒。

探索性思考:为什么操作系统不直接提供"当前网络是否弱"的 API? 因为操作系统没有应用业务上下文。同一个 200ms RTT,对"打车"是良好(不影响体验),对"实时音视频"是严重弱网(音频卡顿)。"弱"是相对应用需求而言的,OS 给不了通用答案。所以判定必须在应用层做,基于应用自己的业务请求。

# 4.2 RTT 判定法

核心思路:连续 N 次业务请求的 RTT 超过阈值 → 判弱网。

对每次 HTTP 请求,记录:
  RTT = response_end - request_start

滑动窗口(如最近 20 次请求):
  if avg(RTT) > 1000ms OR p95(RTT) > 3000ms:
      标记为"高延迟弱网"
1
2
3
4
5
6

优点:直接反映用户感知(用户等的就是 RTT)。

局限:

  • 冷启动问题:应用刚启动还没有足够样本就要做判定。
  • RTT 高 ≠ 弱网:可能是服务端慢(不是网络问题)。需要配合 TCP 连接时间区分——tcp_connect_time 高才能确认是网络层问题。

# 4.3 吞吐量判定法

核心思路:实测下载/上传速度低于阈值 → 判弱网。

实现方式:

  • 被动法:从业务请求的"下载字节数 / 下载耗时"反推。优点是零额外开销,缺点是小请求测不准(受 RTT 影响大)。
  • 主动法:定期下载一个固定大小的探测包(如 50KB)测速。优点是结果稳定,缺点是消耗流量和电量。

关键陷阱:

  • TCP 慢启动:每次新连接的前 10 个 RTT 内速度都没起来。测速必须用"已经稳定的长连接"才准。
  • HTTP/2 多路复用:多个流共享带宽,单流的"速度"不等于实际带宽。

# 4.4 失败率判定法

核心思路:滑动窗口内的请求失败率 > 阈值 → 判弱网。

滑动窗口(最近 1 分钟):
  failure_rate = failed_requests / total_requests
  
  if failure_rate > 10%:
      标记为"高失败率弱网"
1
2
3
4
5

优点:与"用户实际可用性"最直接相关。

局限:

  • 样本不足:用户活跃度低时窗口内请求很少,统计不可靠。
  • 服务端故障污染:服务端 5xx 也会拉高失败率,但不是网络问题。需要按 error code 分类——只统计"网络错误"(DNS 失败、连接超时、IO 错误),排除业务错误。

# 4.5 三合一判定模型

单一指标都有盲区,工程实践是三合一加权判定:

弱网评分 = w1·RTT_score + w2·throughput_score + w3·failure_score

其中:
  RTT_score      = min(avg_RTT / 1000ms, 1.0)
  throughput_score = 1.0 - min(measured_bw / 1Mbps, 1.0)
  failure_score  = failure_rate

总评分 ∈ [0, 1]:
  < 0.2   → 正常
  0.2-0.5 → 一般弱网
  0.5-0.8 → 严重弱网
  > 0.8   → 准离线
1
2
3
4
5
6
7
8
9
10
11
12

权重选择的工程经验:w1 = 0.3, w2 = 0.3, w3 = 0.4——失败率权重最高,因为它最贴近用户感知。

判定的时间衰减:

  • 滑动窗口长度:30-60 秒(太短抖动太大,太长反应太慢)。
  • 进入弱网快(连续 3 次失败就标记),退出弱网慢(连续 30 秒良好才解除)——这是经典的"滞回判定",避免在临界点反复切换。

探索性思考:弱网判定的"假阴性"和"假阳性"哪个更可怕? 假阴性(实际弱网但判定为正常)→ 应用不做降级 → 用户体验差 → 投诉。 假阳性(实际正常但判定为弱网)→ 应用启动降级 → 用户看到低画质 → 抱怨"为什么图片这么糊"。

结论:假阳性更可怕——因为正常网络下用户对"降级"是零容忍。所以判定阈值要偏保守,宁可漏判一些弱网,也不能错判正常网络为弱网。


# 05.度量与采集

# 5.1 三类采集方案

   ① 客户端 RUM(被动埋点)
   ② 主动探测(主动测速 / ping)
   ③ 弱网模拟(开发/CI 期)
1
2
3

① 客户端 RUM——埋点记录每次真实业务请求的全链路数据。物理本质:用户真实体验的镜像。DNS / TCP / TLS / 首字节 / 下载各阶段都打点,能精确归因到"哪一阶段慢"。局限根源:只能事后分析,无法主动探测"下一秒会不会弱网"。

② 主动探测——固定周期或事件触发探测包,记录基线 baseline。物理本质:以"额外流量/功耗"换取"网络环境感知"。适用场景:判定弱网形态(慢速/抖动/切换),辅助 RUM 归因。局限根源:探测时机可能没命中真实问题(探测时网络好,业务请求时网络坏)。

③ 弱网模拟——Charles / Network Link Conditioner / tc-netem 等工具人为制造弱网条件。物理本质:可控制的人造环境,用于开发验证。适用场景:开发期单元测试 + CI 回归。局限根源:真实弱网千变万化(特别是"抖动"和"切换"),模拟只是子集。

# 5.2 各方案盲区对照

现象 RUM 主动探测 弱网模拟
真实用户体验 ✅ 部分 ❌
主动感知弱网 ❌ ✅ N/A
复现已知问题 弱 中 ✅
CI 自动化 ❌ ❌ ✅
上线可用 ✅ ✅(成本) ❌

组合定律:① + ② 必须组合(覆盖线上)+ ③ 补全开发期。

# 5.3 跨平台采集对照

平台 RUM 工具 弱网模拟
Android OkHttp EventListener / Interceptor Network Link Conditioner(系统)/ Charles
iOS URLSession.taskMetrics NLC(Xcode)/ Charles
前端 PerformanceObserver / Resource Timing API Chrome DevTools Throttling
服务端 tcpdump / eBPF tc / netem

同构本质:所有平台都遵循"DNS → 连接 → 传输 → 应用"分阶段度量;优化方法跨平台通用。


# 06.归因决策树

# 6.1 五阶段二分法

请求慢/失败
├─ 哪个阶段慢?
│  ├─ DNS(> 1s)→ DNS 解析问题(HTTPDNS / 缓存)
│  ├─ TCP 握手(> 1s)→ 连接复用 / Connection: keep-alive
│  ├─ TLS(> 1s)→ 证书优化 / TLS 1.3 / 0-RTT
│  ├─ 首字节 TTFB(> 1s)→ 服务端性能(不在客户端范畴)
│  └─ 下载(> 5s)→ 带宽不足 / 包大小过大
└─ 是单点失败还是连锁失败?
   ├─ 单点 → 重试 / fallback
   └─ 连锁 → 已经雪崩,需要熔断 + 降级
1
2
3
4
5
6
7
8
9
10

# 6.2 重试雪崩归因

经典链条:某请求因弱网失败 → 客户端立即重试 → 后端瞬时压力 ×3 → 更多请求超时 → 更多客户端重试 → 后端被打挂 → 全部失败。

判定信号:

  • 服务端:QPS 在短时间内倍增,同时失败率不降反升。
  • 客户端:失败率突然飙高(不是渐变)。

根因:客户端没做 jitter(详见 §12.2)。

# 6.3 半死连接归因

经典链条:用户从 WiFi 切到 4G → 旧 TCP 连接物理链路已断 → 但应用层不知道 → 请求 hang 在旧连接上等到 TCP 默认超时(可达 75 秒)→ 用户早已放弃。

判定信号:

  • RTT 突然飙到几十秒(而不是渐变变差)。
  • 同时段出现网络类型变化事件(WiFi → Cellular)。

根因:没监听网络变化主动重建连接(详见 §09.4)。


# 07.慢速形态全链路

本章把"慢速弱网"从信号弱拆到 TCP 窗口塌缩,回答:为什么 SNR 低会导致 TCP 吞吐崩溃 / 应用层看到的"慢"是怎么传过来的 / 为什么"加大超时"治不了慢速弱网。

# 7.1 慢速如何产生

慢速的物理起点是SNR 低(信号噪声比):

SNR 低 → 物理层 BER 高(比特错误率)
     → 物理层主动重传 + 自动 MCS 降级
     → 等效"信道带宽"急剧下降
     → IP 层有效吞吐量塌缩
1
2
3
4

两个关键事实:

  • 物理层会"自适应降速":现代蜂窝网络的 MCS(Modulation and Coding Scheme)会根据 SNR 自动调整。SNR 高时用 64-QAM(每符号 6 比特),SNR 低时用 QPSK(每符号 2 比特)——光这一项就让带宽差 3 倍。
  • 物理层重传是"应用看不见"的:物理层会用 HARQ 反复重传一个数据块,应用层只看到"延迟变高"。这是为什么"信号 4 格的卡顿"看起来很玄学——应用层度量不到的物理层重传。

# 7.2 TCP 重传与窗口塌缩

物理层的延迟+丢包传到 TCP 层后,会触发雪崩式塌缩:

丢包率 1% ──────▶ TCP 吞吐 ≈ 链路带宽(接近理论上限)
丢包率 5% ──────▶ TCP 吞吐 ≈ 链路带宽 × 30%
丢包率 10% ─────▶ TCP 吞吐 ≈ 链路带宽 × 10%
丢包率 20% ─────▶ TCP 吞吐 ≈ 链路带宽 × 3%
1
2
3
4

为什么塌缩如此剧烈?TCP 拥塞控制(Reno/Cubic)的逻辑:

1. 看到丢包 → 认为是"网络拥塞"
2. 将拥塞窗口砍半(cwnd = cwnd / 2)
3. 进入"拥塞避免"阶段,每个 RTT 只加 1 个 MSS
4. 又遇到丢包 → 又砍半 → 再次进入慢启动
1
2
3
4

在 5% 持续丢包的链路上,TCP 永远在"砍半 → 慢慢加 → 又砍半"之间循环,窗口永远到不了能用满带宽的大小。

探索性思考:TCP 为什么不区分"拥塞丢包"和"无线丢包"? 历史原因:TCP 设计于 1980 年代,那时只有有线网络,丢包基本都是路由器队列溢出(=拥塞)。那个时代"丢包 = 拥塞"是正确的。但今天的无线网络丢包很多是物理层错误(=信号差),不是拥塞。TCP 不区分这两种丢包,导致在无线网络下"过度反应"。这正是 QUIC(HTTP/3)的核心改进之一——用 BBR 等新算法基于"带宽-延迟"建模,而不是简单的"丢包=拥塞"。

# 7.3 应用层表象

应用层看到的慢速弱网现象:

  • HTTP 请求 RTT 高且稳定(如稳定的 800ms,不剧烈抖动)。
  • 下载速度低(如 50KB/s)。
  • 首字节 TTFB 正常但下载段慢(说明服务端没问题)。
  • 失败率不一定高(足够耐心的请求最终都能完成)。

# 7.4 应对核心思路

慢速弱网的应对不能靠"等"——上面分析说明 TCP 在持续丢包下吞吐是塌缩的,等多久都达不到正常水平。正确思路是主动降低数据量:

  1. 请求合并:用 1 个请求拿 10 条数据,而不是 10 个请求各拿 1 条(少 9 次 TCP 慢启动)。
  2. 响应压缩:gzip/brotli 压缩响应,能减 60-80% 体积。
  3. 资源降级:图片走低分辨率版本(webp/avif),视频走低码率流。
  4. 延迟非关键请求:埋点、预加载等先排队,让首屏请求独占带宽。
  5. 使用 HTTP/3:QUIC 的 BBR 拥塞控制在丢包链路下吞吐显著优于 TCP(见 §15.2)。

探索性思考:为什么"加大超时"治不了慢速弱网? 加大超时只是让"已经超时的请求多等一会"——但请求慢的根因是带宽塌缩,等再久带宽也不会自己恢复。加大超时的唯一作用是"让用户白等更久",从用户角度反而更差。慢速弱网的本质矛盾是"数据量 vs 带宽"——治理只能从减少数据量这一侧入手。


# 08.抖动形态全链路

本章拆解"抖动型弱网"——它是高铁/电梯等场景的典型,与慢速弱网的治理思路完全相反。回答:抖动的物理根因 / 为什么 TCP 在抖动下表现尤其差 / 为什么"重试 + 退避"是抖动的唯一解药。

# 8.1 抖动如何产生

抖动的物理根因是信道质量的快速波动:

时间轴 →
信号强度:  ▁▁▁▁▆▆▆▁▁▁▁▆▆▆▆▆▁▁▁▁▁▆▆▆
                ↑       ↑        ↑
              抖动     抖动     抖动
1
2
3
4

典型场景:

场景 抖动机制 抖动周期
高铁 基站切换 + 多普勒效应 5-30 秒
电梯 信号反射/遮挡 1-5 秒(进出电梯瞬间)
地下车库 信号穿透损耗 持续 + 间歇恢复
大型活动 短时拥塞峰 秒级

关键特征:抖动是阵发性的,不是持续的。这决定了应对策略——只要等过那个抖动期,请求就能成功。

# 8.2 丢包与乱序投递

抖动期间 IP 层的具体表现:

  • 批量丢包:一段时间内连续丢 N 个包(不是均匀丢包)。
  • 延迟突增:某些包的 RTT 突然飙到几秒。
  • 乱序投递:后发的包先到,先发的包还在路上(基站缓存重排)。

TCP 在这种环境下的反应:

连续丢包 → 触发"快速重传"(收到 3 个重复 ACK)
       → cwnd 减半
       → 进入"拥塞避免"
       → 又遇到丢包 → 又减半
       → cwnd 长期处于小值
1
2
3
4
5

结果:抖动结束后,TCP 不会立即恢复到全速——cwnd 已经被砍到很小,需要几十个 RTT 才能爬回来。这就是为什么"抖动结束后还慢一会"。

探索性思考:为什么 TCP 把"乱序"当作"丢包"信号? 因为 TCP 用"3 次重复 ACK"判定丢包。但乱序场景下,后发包先到也会产生重复 ACK——TCP 错以为丢包,触发不必要的重传和窗口减半。这是 TCP 在无线网络的另一个"水土不服"。

# 8.3 应用层表象

抖动型弱网的应用表象:

  • RTT 剧烈波动(如 50ms ↔ 5000ms 交替)。
  • 失败和成功交替:相邻两次请求结果完全不同。
  • 重试经常成功:失败后 1-2 秒重试有较高成功率。
  • 长连接经常掉:长连接的心跳包丢失即被判定断开。

# 8.4 应对核心思路

抖动场景的治理逻辑与慢速完全相反:

维度 慢速 抖动
重试策略 不要重试(不会变快) 必须重试(会自愈)
超时策略 适度延长 不要延长(早死早超生)
退避策略 不重要 必须退避 + jitter
连接策略 复用长连接 准备好快速重建

核心策略:指数退避 + jitter 是抖动场景的"标准答案"。

第 1 次失败 → 等 1±0.5s 重试
第 2 次失败 → 等 2±1s 重试
第 3 次失败 → 等 4±2s 重试
第 4 次失败 → 等 8±4s 重试,超过则放弃
1
2
3
4

退避的物理意义:给抖动期留出"等过去"的时间。jitter 的作用:避免所有客户端同时重试制造"重试风暴"——详见 §12.2。

探索性思考:为什么不能"无限重试到成功"? 三个原因: ① 服务端保护:如果是服务端故障,无限重试 = DDoS 自家服务。 ② 客户端电量:持续重试消耗电池和流量。 ③ 用户体验:用户已经放弃了,背后还在跑没意义。 重试是手段不是目的——超过 3 次的请求几乎都是"无法恢复的失败",再重试也救不了。


# 09.切换形态全链路

切换型弱网是最隐蔽也最致命的——它不在"信号差"时发生,而在"信号变化"时发生。回答:为什么切换时连接会"半死"/ TCP 默认 75 秒超时的物理根源 / 应用如何提前感知切换。

# 9.1 切换如何产生

切换的本质是底层物理链路变了,但应用层 socket 还在用旧链路:

[场景 A] WiFi → 4G
   用户离开 WiFi 覆盖 → WiFi 信号消失
   ↓
   系统检测到 WiFi 无效 → 自动切到 4G
   ↓
   新的 IP 地址(4G 网卡的 IP)
   ↓
   ❗ 但旧的 TCP 连接还绑定在 WiFi 的 IP 上
   ↓
   旧连接发送的包 → 网络层无路由 → 静默丢弃

[场景 B] 4G 换基站(移动中)
   旧基站 → 新基站,IP 不变但中间路由变了
   ↓
   旧路径上"在飞"的包丢失
   ↓
   连接进入"半死"状态——逻辑上还在,物理上已断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 9.2 TCP 半死的本质

半死连接的"半",是指应用层认为连接正常,TCP/IP 层连接也没收到 RST:

应用层视角:socket 仍然 open,read() 阻塞等数据
   ↓
TCP 层视角:连接状态 ESTABLISHED,没收到任何 FIN/RST
   ↓
IP 层实际:包发出去但没到对端,也没收到 ICMP 错误(路由静默丢弃)
   ↓
结果:TCP 等到 keepalive 探测才能发现连接死了
   ↓
TCP keepalive 默认:2 小时不发包才开始探测,9 次探测每次 75 秒 = 死后 2 小时 + 11 分钟才能感知
1
2
3
4
5
6
7
8
9

Linux 默认 keepalive 参数:

参数 默认值 含义
tcp_keepalive_time 7200 秒 多久没数据才开始探测
tcp_keepalive_intvl 75 秒 探测间隔
tcp_keepalive_probes 9 探测次数上限

这是为什么"半死连接"的请求能 hang 几十秒到几分钟——TCP 根本没有为"快速发现死连接"设计。

探索性思考:TCP 为什么把 keepalive 设这么慢? 因为 TCP 设计目标是"保持连接尽量久"——keepalive 是为"长闲置连接不被中间设备关掉"设计的,不是为"快速发现死连接"设计的。这个语义在移动网络场景完全错配——移动场景需要的恰恰是"快速发现死连接然后重建"。这是为什么所有移动 App 都要自己实现应用层心跳(30 秒一次),不能依赖 TCP keepalive。

# 9.3 应用层表象

切换型弱网的最大特点是症状很怪:

  • 大部分请求秒回,少数请求卡死几十秒——卡死的恰好是"切换前发起、切换后仍在等响应"的那批。
  • 重试就好——新请求会建新连接,走新链路。
  • 重启应用就好——所有 socket 重建。
  • 网络状态显示"良好"——切换后的新网络确实没问题。

这是最让用户困惑的——"看起来什么都正常,但就是有个请求一直转圈"。

# 9.4 应对核心思路

切换场景的治理核心是主动感知 + 主动重建:

第一步:监听系统的网络变化事件

// Android
cm.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) { /* 新网络可用 */ }
    override fun onLost(network: Network) { /* 旧网络断开 */ }
    override fun onCapabilitiesChanged(...) { /* 能力变化 */ }
})
1
2
3
4
5
6
// iOS(用 Network framework,不要用老的 Reachability)
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
    if path.status == .satisfied { /* 网络可用 */ }
}
monitor.start(queue: .global())
1
2
3
4
5
6

第二步:网络变化时主动关闭旧连接池

// OkHttp 示例
client.connectionPool.evictAll()  // 清空连接池,下次请求强制建新连接
1
2

第三步:用应用层心跳替代 TCP keepalive

  • 长连接每 30 秒发一个心跳包。
  • 3 次心跳无响应判定连接死,主动断开重建。

第四步:设短的连接超时

  • 连接超时不要超过 3 秒——半死连接早死早超生。
  • 读超时按业务设(一般 8 秒)。

探索性思考:为什么 HTTP/3 在切换场景天然占优? HTTP/3 基于 QUIC,QUIC 的连接标识不是 <srcIP, srcPort, dstIP, dstPort>,而是 Connection ID(一个 64 位随机数)。这意味着IP 变了,连接还能继续——这就是"连接迁移"特性。WiFi 切 4G,QUIC 连接无感继续,TCP 连接必须重建。这是为什么打车/外卖类应用纷纷切 HTTP/3——它们的用户大部分时间在移动中。


# 10.跨端形态对照

# 10.1 端到端流程对照

维度 慢速弱网 抖动弱网 切换弱网
物理根因 SNR 低 / MCS 降级 信道质量阵发波动 物理链路变更
持续时间 持续(几分钟-几小时) 阵发(秒-几十秒) 瞬时(毫秒)
TCP 反应 cwnd 长期小 cwnd 反复砍半 半死等 keepalive
应用症状 全部慢 时好时坏 部分请求卡死
应用感知方式 RUM 看 RTT RUM 看失败率 监听网络变化事件
错误手段 加大超时 不退避立即重试 等 TCP 自己发现
正确手段 降级 + 缓存 退避 + jitter 主动监听 + 重建
HTTP/3 收益 中(BBR 拥塞控制) 中(无队头阻塞) 大(连接迁移)

# 10.2 三形态归因速查

抓到一个"网络问题"时,按以下顺序判定形态:

是否最近发生过网络类型变化(WiFi/4G)?
├─ 是 ──▶ 大概率"切换形态"
│        判定:网络变化事件后 30s 内的失败 + 大量请求 hang 在旧连接
└─ 否
   ├─ 失败和成功交替?
   │  ├─ 是 ──▶ 大概率"抖动形态"
   │  │       判定:滑动窗口失败率剧烈波动
   │  └─ 否 ──▶ 大概率"慢速形态"
   │           判定:RTT 持续高、失败率持续中等
1
2
3
4
5
6
7
8
9

# 10.3 统一启示

  • 弱网治理本质是"按形态分别治理":一刀切的"统一超时 + 统一重试"必然顾此失彼。
  • 物理层 → IP 层 → TCP 层 → 应用层是一条因果链:从应用层看到的"慢/失败/卡死"都能追溯到具体的物理根因。
  • HTTP/3 不是银弹但是显著进步:对切换场景效果最大、对慢速次之、对抖动有改善但不如重试机制。
  • 应用层不能依赖 TCP 自带的"快速失败"机制:必须自己做心跳、连接重建、重试退避——TCP 是为"可靠数据流"设计的,不是为"快速发现故障"设计的。

§11-§15 给出五层治理的具体方案。


# 11.治理一层超时

这是治理体系的第一层也是最容易做错的一层。回答:超时到底在治什么 / 为什么连接超时和读超时必须分开 / "超时设多少秒"背后的科学依据。

# 11.1 超时本质再认识

超时的本质是**"快速失败"**——不是为了"等成功",而是为了"早点放弃"。

没超时的世界:请求会等到操作系统或 TCP 内部超时(可达 75 秒+)
有超时的世界:应用层提前判定"够了,放弃,进入下一步(重试/降级/提示)"
1
2

这个区别很关键——超时不是为了让请求成功,而是为了让后续处理(重试/降级)能尽快启动。

反直觉事实:

  • 超时设得长 ≠ 成功率高:5 秒等不到的请求,99% 是 50 秒也等不到的。
  • 超时设得短 ≠ 成功率低:短超时 + 快重试,比长超时 + 慢失败成功率更高。

# 11.2 连接超时与读超时

很多团队配置超时只设一个总超时,这是根本性错误——必须分开:

类型 含义 推荐值 为什么
连接超时 TCP 三次握手最大耗时 3 秒 建连快才能快速识别"网络死了"
TLS 超时 TLS 握手最大耗时 3 秒 同上
读超时 收到响应的最大等待 8 秒 给服务端处理时间
写超时 上传数据的最大耗时 按数据量 大文件单独设

为什么连接超时要短?

  • 切换型弱网下,半死连接的"hang"主要发生在 read 阶段,但 connect 阶段如果也走的旧网络也会 hang。
  • 短连接超时(3 秒)能快速发现"建连不上",立即重试就能切到新网络。
  • 服务端正常的 TCP 三次握手在 1 个 RTT 内完成(< 200ms),3 秒已经是充分大的余量。

为什么读超时要长?

  • 服务端可能在做复杂查询(如下单的库存校验),需要时间。
  • 强切短读超时会导致大量"服务端正常但客户端报错"。

# 11.3 分级超时策略

不同业务有不同的"耐心阈值",必须分级配置:

业务类型 用户耐心 推荐总超时 示例
关键阻塞型 必须等 12 秒 登录、支付
正常交互型 较有耐心 8 秒 列表、详情
辅助型 没耐心 5 秒 搜索建议、点赞
后台型 无感知 30 秒 埋点、心跳、预加载

核心原则:

  • 越交互越短,越后台越长——交互型用户在等,长超时 = 用户白等。
  • 总超时 = 连接超时 + 读超时——分开后整体仍然在 10 秒级别。
  • 重试不计入超时——总超时是"单次请求"的超时,重试是另一回事。

# 11.4 超时的探索性反思

探索性思考 1:为什么主流框架(OkHttp/URLSession)默认超时都是 10 秒? 因为这是"无业务上下文"下的折中值——不能太短(怕误杀慢但能成的请求),不能太长(怕用户等不及)。这个默认值适合 60% 场景,但剩下 40% 必须自己定制。开箱即用的默认值,永远比不上业务定制的精准值。

探索性思考 2:超时报错后,给用户看什么? 三种错误的设计: ① "网络错误,请重试"——最坏。用户不知道什么意思,不知道是不是自己问题。 ② "网络不稳定,正在为您重试..."——中等。但如果重试也失败,用户更慌。 ③ "您的网络较慢,已为您显示缓存数据,最新数据将在网络恢复后自动同步"——最好。说清楚原因 + 给出可用结果 + 承诺后续。 这就是 §13 降级和 §14 离线必须做的事——超时只是入口,真正的用户体验在超时之后。

探索性思考 3:超时配置应该写死还是动态? 写死的优点:可预测、易调试。缺点:网络环境多变,一套配置不能覆盖所有场景。 动态的优点:可根据 RUM 数据自动调整。缺点:实现复杂,A/B 测试成本高。 工程建议:基础超时写死(按业务分级),但重试间隔做动态(根据近期成功率自适应延长/缩短)。


# 12.治理二层重试

重试是最容易做错的治理手段——做对了救命,做错了雪崩。回答:重试为什么是双刃剑 / 退避算法的数学原理 / 哪些请求绝对不能重试。

# 12.1 重试的双刃剑

好的一面:抖动型弱网下,重试能把成功率从 60% 提升到 90%+(详见 §16.2 实验数据)。

坏的一面:

  • 重试雪崩:服务端故障时,所有客户端同时重试 = QPS 翻倍 = 服务器彻底挂掉。
  • 流量浪费:弱网用户的失败请求重试 3 次 = 3 倍流量消耗。
  • 状态污染:非幂等请求(如下单)重试会创建重复订单。
  • 延长用户感知时间:3 次重试 = 等待时间 ×4。

核心矛盾:单个客户端看,重试是个体最优;但所有客户端一起重试时,是集体最差。这是经典的"公地悲剧"——必须从机制设计上避免。

# 12.2 指数退避与抖动

指数退避的核心思想:重试间隔随次数指数增长,给系统留出恢复时间。

第 1 次重试间隔:base = 1s
第 2 次重试间隔:base × 2 = 2s
第 3 次重试间隔:base × 4 = 4s
第 n 次重试间隔:base × 2^(n-1)
1
2
3
4

单纯指数退避还不够——会产生"惊群效应":

[服务端故障] T=0
   ↓
所有客户端同时收到失败
   ↓
T=1s:所有客户端同时第 1 次重试 ── 第 1 个尖峰
   ↓
T=3s:所有客户端同时第 2 次重试 ── 第 2 个尖峰
   ↓
T=7s:所有客户端同时第 3 次重试 ── 第 3 个尖峰
1
2
3
4
5
6
7
8
9

这会反复打挂服务端——刚恢复就被打挂、再恢复再被打挂。

解药:jitter(随机抖动)

实际间隔 = base × 2^(n-1) × (1 + random(-0.5, +0.5))
1

例如基础间隔 4 秒时,实际是 2-6 秒之间的随机值。所有客户端的重试时间被打散,服务端压力变成平稳曲线。

探索性思考:jitter 范围应该是 ±50% 还是 ±100%? ±50% 的优点是"重试时序仍然可预测"(最早 2s 最晚 6s)。 ±100%("full jitter")的优点是"打散更彻底"(0-8s 完全随机),但部分客户端可能 0 秒重试,仍然制造小尖峰。 AWS 的工程实践推荐**"decorrelated jitter"**:next = random(base, prev * 3)——既保证不退化、又最大化随机性。这是云厂商规模下打磨出的最佳实践。

# 12.3 幂等性前置条件

不是所有请求都能重试——必须先满足"幂等性"。

方法 是否幂等 能否重试
GET ✅ 是 安全重试
HEAD ✅ 是 安全重试
PUT ✅ 是 安全重试
DELETE ✅ 是 安全重试
POST ❌ 否 危险:可能创建重复资源
PATCH 看实现 需要业务保证

POST 想要重试的工程方案——幂等键(Idempotency Key):

POST /api/orders
Idempotency-Key: <client-generated UUID>
Content-Type: application/json

{"item_id": 123, "qty": 1}
1
2
3
4
5

服务端约定:相同 Idempotency-Key 的请求只处理一次,后续重复请求直接返回首次的结果。

关键设计:

  • 幂等键由客户端生成(保证重试时是同一个键)。
  • 服务端用 Redis/DB 保存 24h(防止重复)。
  • 客户端"重试"时必须带上同一个键,而不是生成新键。

探索性思考:为什么 HTTP/3 不解决幂等问题? 因为幂等是业务语义问题,不是协议层问题。无论用什么协议,"创建一个新订单"在业务上都不是幂等的——除非业务层加幂等键。HTTP/3 解决的是连接迁移、队头阻塞等传输层问题,不能解决业务层语义。这提醒我们:协议升级不能替代业务设计。

# 12.4 熔断与重试上限

重试上限:3 次是工程上的"黄金数字"——95% 抖动场景在 3 次内能恢复,超过 3 次的几乎是"不可恢复故障"。

熔断机制:当某个服务的失败率持续高于阈值,主动停止所有新请求(直接返回失败,不进网络)。

熔断器状态机:
   [CLOSED 正常] ── 失败率 > 50% 持续 10s ──▶ [OPEN 熔断]
   [OPEN 熔断]   ── 等待 30s ──▶ [HALF_OPEN 半开]
   [HALF_OPEN]   ── 试探请求成功 ──▶ [CLOSED]
                ── 试探请求失败 ──▶ [OPEN]
1
2
3
4
5

熔断的"反直觉"价值:

  • 熔断主动放弃请求,用户立即看到"网络异常"——比"转圈 8 秒后失败"用户体验好。
  • 熔断保护服务端——客户端不再发请求,服务端有时间恢复。
  • 熔断保护客户端——不用浪费电量和流量做无谓尝试。

代表实现:Netflix Hystrix(Java)、resilience4j、Sentinel。


# 13.治理三层降级

前两层(超时、重试)治的是"请求能否成功",但有些情况下请求注定不能成功——这时候降级才是用户体验的最后救星。回答:降级的设计哲学 / 弱网下应该降什么 / 为什么"进度可见"比"加快速度"更重要。

# 13.1 降级的设计哲学

降级的核心命题:"不完美的可用" >> "完美的不可用"。

理想情况:高清图 + 实时数据 + 完整功能
   ↓ 网络不允许
退而求其次:低清图 + 缓存数据 + 核心功能
   ↓ 还不允许
最低保障:占位图 + 离线提示 + 重连按钮
1
2
3
4
5

降级的两个核心设计原则:

① 优雅退化(Graceful Degradation):完整功能为默认,弱网下逐级退化。 ② 渐进增强(Progressive Enhancement):最小功能为默认,网络好时逐级增强。

两种思想都对,区别在于默认值。移动端建议用"优雅退化"——大部分用户网络是好的,没必要默认低画质。

# 13.2 弱网级别映射

降级的关键是把"网络分级"映射到"体验分级":

网络评分(§4.5) 弱网级别 体验降级
0-0.2 正常 全功能(高清图、视频、实时数据、预加载)
0.2-0.5 一般弱网 关闭预加载、降低图片质量、视频默认低清
0.5-0.8 严重弱网 仅核心功能、文字优先、图片占位(按需加载)
> 0.8 准离线 缓存数据 + 离线提示 + 重连按钮

降级生效要快:判定为弱网后立即生效(不等下一次请求)。判定解除弱网后慢慢恢复(避免反复降级体验跳变)。

# 13.3 内容降级三档

按"对用户体验影响"分档降级:

Tier 1(无感降级):用户基本感觉不到。

  • 图片走 webp 替代 png(小 30-50%)。
  • 关闭预加载、关闭非关键动画。
  • 关闭 vivid 色彩(节省渲染)。
  • 减少埋点频率。

Tier 2(轻感降级):用户能感觉到但能理解。

  • 高清图 → 低清图(用户能看出来"图不太清")。
  • 视频自动播 → 点击播。
  • 个性化推荐 → 默认列表(推荐算法依赖弱网下可能很慢的接口)。

Tier 3(明显降级):必须告知用户。

  • 实时数据 → 缓存数据(必须显示"数据为 5 分钟前")。
  • 长列表 → 短列表("加载更多"按钮)。
  • 多 Tab → 仅核心 Tab(其他 Tab 显示"网络恢复后可用")。

# 13.4 进度可见原则

心理学定律(§03.3 用户感知):用户能容忍"长等待",不能容忍"未知等待"。

弱网下必须做的进度反馈:

等待时长 UI 必须展示
0-1 秒 无需特殊处理
1-3 秒 显示 loading 转圈
3-8 秒 转圈 + 进度文字("加载中...")
> 8 秒 进度百分比 + 取消按钮 + 当前状态(如"已下载 30%")

"假进度条"的合理使用:

  • 真实进度无法获取时(如服务端处理中),可以用预估进度(前 70% 快、后 30% 慢)。
  • 这是心理学上有效的——用户感觉"在进展"比"完全没动"焦虑感低。
  • 但不要假到离谱——进度条到 99% 然后等 30 秒,比一直转圈还糟。

探索性思考:为什么"取消按钮"在弱网下特别重要? 弱网下用户经常想放弃当前操作去做别的(如打开后台缓存的页面)。如果没有取消按钮,用户只能等 loading 自己结束——这种"被绑架"感很差。取消按钮是"用户掌控感"的体现——即使最终结果一样(请求失败),有取消和没取消的用户感受完全不同。


# 14.治理四层离线

前三层(超时、重试、降级)都假设"网络存在但不好",这一层处理**"网络根本没有"**的情况。回答:离线优先思想的内涵 / 缓存到底应该缓存什么 / 离线时用户操作怎么处理。

# 14.1 离线优先思想

离线优先(Offline-First):应用的默认状态是"无网络","有网络"是奖励。

传统思想:在线为正常,离线为异常
离线优先:离线为正常,在线为加成
1
2

这个转变的核心影响:

  • 数据来源:UI 永远从本地读,不直接读网络。网络数据先入本地库再渲染。
  • 操作流程:用户操作立即在本地生效,后台异步同步到服务端。
  • 冲突处理:离线期间的操作要有冲突合并机制(§14.4)。

代表应用:Notion、Linear、Figma、Trello——这些应用打开就能用,断网完全无感。

# 14.2 缓存的三种策略

策略 1:HTTP 缓存(被动)

依靠 HTTP 协议的 Cache-Control / ETag / Last-Modified。

  • 优点:标准化、零代码。
  • 缺点:粒度粗(按 URL)、协商缓存仍需网络。

策略 2:应用层 KV 缓存(主动)

// 简化示意
fun getUserList(): List<User> {
    val cached = cache.get("user_list")
    if (cached != null && !cached.isExpired()) {
        return cached  // 命中
    }
    val fresh = api.fetchUserList()
    cache.put("user_list", fresh, ttl = 5.minutes)
    return fresh
}
1
2
3
4
5
6
7
8
9
10
  • 优点:粒度灵活(按业务 key)、可控 TTL。
  • 缺点:代码侵入、需要维护 cache 一致性。

策略 3:本地数据库(终极)

UI 永远查本地数据库(SQLite/Realm/Room),服务端数据通过同步机制写入本地库。

  • 优点:UI 与网络解耦、查询能力强、支持复杂离线操作。
  • 缺点:架构改造成本高、同步机制复杂。

选型建议:

业务规模 推荐策略
简单应用 1 + 2(HTTP + KV)
中等应用 2 + 3(KV 热数据,DB 持久数据)
复杂离线应用 3 + 同步层(Notion 级别)

# 14.3 离线队列设计

用户离线时的操作不应该"立即报错",而应该入队列:

用户点击"打车" → [离线检查] → 网络可用?
                                ↓ 否
                            写入离线队列(持久化到本地)
                                ↓
                            UI 显示"已下单,将在网络恢复后发送"
                                ↓
                            监听网络恢复
                                ↓
                            自动 flush 队列
1
2
3
4
5
6
7
8
9

离线队列的关键设计:

维度 设计要点
持久化 必须落盘(SQLite/文件),防止应用被杀任务丢失
幂等性 每个任务带客户端生成的 UUID,服务端去重(同 §12.3)
顺序保证 队列严格 FIFO,避免后操作覆盖前操作
失败重试 单任务失败按 §12 指数退避,最多 3 次
过期丢弃 24 小时未发送成功的任务自动丢弃(避免无限堆积)
用户可见 UI 显示"待发送"任务数,给用户掌控感

# 14.4 冲突合并难题

离线最难的是多设备/多客户端冲突:

设备 A 离线:编辑文档 X → "Hello World"
设备 B 离线:编辑文档 X → "Hello Universe"
   ↓
两设备同时上线
   ↓
服务端怎么合并?
1
2
3
4
5
6

三种合并策略:

① Last-Write-Wins:以时间戳最新的为准。简单但会丢失数据(前一次的编辑没了)。

② Manual Resolution:弹窗让用户选择保留哪个版本。安全但打断体验。

③ CRDT(Conflict-free Replicated Data Types):数学上保证可自动合并的数据结构(详见 §15.3)。

探索性思考:为什么 90% 的应用都没做真正的离线? 因为离线优先需要从架构层重构,不是加几行代码能解决的:

  • 数据层要改(从直接调 API 改成读本地库 + 异步同步)。
  • 业务层要改(操作语义改成"先本地后远程")。
  • 冲突合并要设计(业务规则不一致时怎么处理)。
  • 测试要改(要测各种离线-上线切换的场景组合)。

改造成本高 → 大部分团队选择"网络不好就显示加载失败"——这是业务对体验的妥协。真正高品质的应用(Notion/Linear)从第一天就是离线优先架构,这是它们用户体验远超传统 SaaS 的根本原因。


# 15.治理五层架构

这是治理的"终极一层"——当业务对极致弱网体验有刚需时(如 Notion 级别),必须从架构层做长期投入。本章不是给小团队的,而是给"准备 3 年磨一剑"团队的方向。

# 15.1 本地优先架构

核心思想:本地是 source of truth,网络是同步通道。

传统架构:
   UI ──▶ API ──▶ DB(服务端)
       ↑___________________| 状态来自服务端

本地优先架构:
   UI ──▶ Local DB(客户端)
              ↓
          Sync Engine ─◀─▶ Server DB
   
   UI 永远不知道"有没有网络"——本地 DB 永远有数据
1
2
3
4
5
6
7
8
9
10

收益:

  • 启动 0 等待(UI 立即从本地拿数据)。
  • 操作 0 延迟(写本地立即返回)。
  • 网络问题对 UI 完全透明。

代价:

  • 同步引擎复杂度极高(要处理冲突、顺序、垃圾回收)。
  • 本地数据可能"过期"——UI 要明确显示"最后同步时间"。
  • 存储成本(数据要在本地完整保留)。

# 15.2 协议层升级 HTTP3

HTTP/3 在弱网场景的核心优势(详见 §09.4 探索性思考):

特性 解决的弱网问题
0-RTT 握手 慢速场景的"建连耗时"
连接迁移 切换场景的"半死连接"
多路复用无队头阻塞 抖动场景的"一包丢全卡"
BBR 拥塞控制 慢速场景的"TCP 吞吐塌缩"

部署 HTTP/3 的工程门槛:

  • 服务端:CDN/网关支持(Cloudflare/Fastly/腾讯云 EdgeOne 都已支持)。
  • 客户端:iOS 15+ / Android 5.0+ 的 Cronet 库 / 各大移动端框架渐进支持。
  • 协议协商:先发 HTTP/3,失败回退 HTTP/2。

收益数据(§16.5 实验):弱网场景成功率提升 5-15pp,弱网首屏耗时缩短 20-40%。

# 15.3 CRDT 与终极一致

CRDT(Conflict-free Replicated Data Types):一类数学上保证可自动合并的数据结构。

经典 CRDT 类型:

类型 用途 代表
G-Counter 单向计数器 点赞数
PN-Counter 双向计数器 库存
LWW-Register 单值 用户名
OR-Set 集合 收藏列表
RGA / Yjs 有序文本 协作文档

核心保证:无论多个副本以什么顺序合并,最终结果都一致——数学上自动消除冲突。

代表应用:

  • Figma:基于 CRDT 实现多人实时设计协作。
  • Linear:基于 CRDT 实现完全离线优先的项目管理。
  • Automerge / Yjs:开源 CRDT 库。

探索性思考:为什么 CRDT 没成为主流? 三个原因: ① 认知门槛高:CRDT 不是"加个库",是"重新设计数据结构"。 ② 元数据膨胀:CRDT 需要保留大量历史元数据(向量时钟、操作记录),存储开销可能是原数据的 5-10 倍。 ③ 业务语义偏差:CRDT 的"自动合并"未必符合业务期望——业务可能想要"用户 A 优先",CRDT 给的是"数学上合理"。

所以 CRDT 适合协作型应用(多人编辑一份内容),不适合事务型应用(订单/支付——这些需要强一致和业务规则)。

# 15.4 五层治理总览

治理层级           核心命题                工具/手段             改造成本
─────────────────────────────────────────────────────────────────
1. 超时          快速失败                配置调优              低
2. 重试          自动恢复                退避 + jitter + 熔断  低
3. 降级          不完美的可用            内容分级 + 进度可见   中
4. 离线          网络无关的可用          本地缓存 + 离线队列   中-高
5. 架构          网络成为奖励            本地优先 + CRDT + H3  高
─────────────────────────────────────────────────────────────────

每升一层 → 体验显著提升 + 改造成本指数级上升
团队应根据业务规模和质量目标,选择停在哪一层
1
2
3
4
5
6
7
8
9
10
11

ROI 建议:

  • MVP 阶段:做完 §11 + §12(超时 + 重试),ROI 极高。
  • 成长期:补 §13(降级)+ §14 局部离线,关键流程极致体验。
  • 成熟期:考虑 §15 架构改造(如启动核心功能离线优先)。
  • 领先期:全面 §15(Notion/Linear 路线)。

探索性思考:什么时候应该投入 §15 架构改造? 当**用户主诉里"打不开"/"卡顿"占比超过 20%**时——这意味着前 4 层已经做到极致仍然不够,业务必须改架构。否则前 4 层做扎实就已经覆盖 95% 场景,§15 是过度工程。架构改造的判断准则永远是"业务必要性",不是"技术先进性"。


# 16.求证实验

# 16.1 实验一:超时最优值

Step 1 — 原始观察:默认 10s 超时是经验值,真实场景到底设多少最合适?

Step 2 — 提出疑问:超时越长成功率越高,但用户感知耗时也越长——平衡点在哪?

Step 3 — 设计实验:用 NLC 模拟 4G/3G/弱网,分别设 3/5/8/15/30 秒超时,统计成功率和用户感知耗时。

Step 4 — 实测数据(同接口 1000 次/档):

超时 成功率 平均耗时 用户感知耗时*
3s 78% 2.5s 3.0s(失败者按 3s 计)
5s 86% 3.8s 4.5s
8s 91% 5.0s 5.5s
15s 92% 5.5s 8.2s
30s 92% 5.6s 11.0s

*用户感知耗时 = 成功者真实耗时 + 失败者超时值,加权平均。

Step 5 — 提炼结论:

8 秒后边际收益接近 0——继续延长只是让失败者白等更久。默认 8 秒;关键 API(登录/支付)放到 12 秒;非关键 API(搜索建议)压到 5 秒。

Step 6 — 边界:

  • 这个数据基于"服务端处理快"的 API。如果服务端本身处理慢(如复杂查询),需单独评估。
  • 连接超时和读超时应分开设(见 §11.2),合并设的话效果差很多。

# 16.2 实验二:退避算法

Step 1 — 原始观察:失败立即重试 vs 退避重试,到底差多少?

Step 2 — 提出疑问:抖动型弱网下,固定间隔和指数退避哪个成功率高?

Step 3 — 设计实验:失败后分别用"立即重试 3 次"vs"指数退避 1s/2s/4s"两种策略,模拟抖动型弱网(周期 5 秒、每周期断 2 秒)。

Step 4 — 实测数据(500 次模拟):

策略 最终成功率 平均累计耗时 重试雪崩风险
立即重试 3 次 65% 3.2s 高
固定间隔 1s 重试 3 次 72% 4.0s 中
指数退避 1s/2s/4s 87% 4.5s 低
指数退避 + jitter 89% 4.7s 极低

Step 5 — 提炼结论:

指数退避比立即重试成功率高 24pp——弱网"抖动"特性是阵发性,固定间隔三次都打在抖动期;指数退避恰好跨越短抖动。所有重试必须用指数退避 + jitter,最多 3 次。

Step 6 — 边界:

  • 退避基础间隔(base)按业务延迟容忍度设:交互型 1s,后台型 2-5s。
  • jitter 推荐 decorrelated 算法(见 §12.2 探索性思考)。

# 16.3 实验三:长短连接

Step 1 — 原始观察:长连接(HTTP/2 keep-alive)理论上更快,弱网下是不是真的更可靠?

Step 2 — 提出疑问:周期性断网场景下,长连接和短连接哪个失败感知更快?

Step 3 — 设计实验:同样 100 次请求,分别用长连接(HTTP/2)和短连接(HTTP/1.1 + close),模拟周期性断网 1 秒。

Step 4 — 实测数据:

策略 成功率 失败者平均感知耗时
长连接(默认 TCP keepalive) 56% 12s(等 TCP 内部超时)
长连接 + 应用层心跳(30s) 78% 5s
短连接 78% 5s(建连失败立即可知)
长连接 + 应用层心跳(10s) 85% 3s

Step 5 — 提炼结论:

长连接的 TCP 半死状态需要 keepalive 探测才能发现,默认 keepalive 间隔很长(2 小时)。弱网/移动场景必须配合应用层心跳 + 连接健康检查,不能依赖 TCP 自身机制。

Step 6 — 边界:

  • 心跳频率与电量是 trade-off:10s 心跳更灵敏但更耗电,30s 更省电但响应慢。
  • QUIC(HTTP/3)的连接迁移特性在切网场景比 TCP 显著优秀(详见 §16.5)。

# 16.4 实验四:缓存命中

Step 1 — 原始观察:加了 HTTP 缓存到底有多大用?弱网下提升多少?

Step 2 — 提出疑问:HTTP 缓存(Cache-Control)和应用层缓存(KV)在不同网络下的命中率和耗时收益是多少?

Step 3 — 设计实验:某 App 首页接口数据 1 周。

Step 4 — 实测数据:

网络 无缓存耗时 HTTP 缓存(命中率 60%) 应用层缓存(命中率 85%)
WiFi 280ms 200ms 50ms
4G 正常 450ms 320ms 50ms
弱网 3500ms 2100ms(仍走协商) 50ms
离线 失败 失败 成功(命中本地)

Step 5 — 提炼结论:

应用层缓存在弱网下收益巨大(耗时降 70x),且离线下仍可用——HTTP 缓存的"协商缓存"在弱网下仍要发请求,应用层缓存彻底跳过网络。核心数据必须做应用层缓存,仅依赖 HTTP 缓存不够。

Step 6 — 边界:

  • 应用层缓存要解决数据一致性(什么时候更新、如何失效)。
  • 不能把"用户私密数据"做长缓存——隐私和安全风险。

# 16.5 实验五:HTTP3 收益

Step 1 — 原始观察:§09.4 探索性思考 提到 HTTP/3 在切换场景天然占优,真实数据有多大提升?

Step 2 — 提出疑问:HTTP/3 vs HTTP/2 在三种弱网形态下的成功率和耗时差距是多少?

Step 3 — 设计实验:某 App 灰度 50% 流量切 HTTP/3,A/B 对比 1 周。

Step 4 — 实测数据:

场景 HTTP/2 成功率 HTTP/3 成功率 提升
WiFi 正常 99.5% 99.6% +0.1pp(无显著差异)
4G 正常 99.0% 99.3% +0.3pp
慢速弱网 92.0% 94.5% +2.5pp
抖动弱网 85.0% 89.0% +4.0pp
切换场景 68.0% 89.0% +21.0pp
场景 HTTP/2 P95 耗时 HTTP/3 P95 耗时 缩短
切换场景 12s 4s -67%

Step 5 — 提炼结论:

HTTP/3 在切换场景收益巨大(+21pp 成功率、-67% 耗时),因为 QUIC 的连接迁移让 IP 变化后连接无感继续。对移动场景为主的应用(出行、外卖、社交)必须切 HTTP/3。

Step 6 — 边界:

  • HTTP/3 需要服务端+CDN+客户端协同支持,部署门槛中等。
  • 部分企业内网/老旧防火墙不支持 UDP(HTTP/3 基于 UDP),需准备回退策略。

# 16.6 五大实验启示

   超时最优值       → 8 秒是工程默认               ─┐
                                                     │
   退避算法         → 指数退避 + jitter 必备         │
                                                     │
   长短连接         → 应用层心跳必备                 ├─▶ 弱网治理 = 快速失败 + 智能重试 + 主动感知 + 极致缓存 + 协议升级
                                                     │
   缓存命中         → 应用层缓存 >> HTTP 缓存        │
                                                     │
   HTTP/3 收益      → 切换场景成功率 +21pp           ─┘
1
2
3
4
5
6
7
8
9

统一启示:

  • 不存在"默认配置就好"的弱网治理——所有参数都必须基于业务实测。
  • 重试是双刃剑:做对救命,做错雪崩。
  • 缓存是性价比之王:投入小、收益大、且适用所有场景。
  • 协议升级是大杠杆:HTTP/3 的"连接迁移"对移动场景是质变。
  • 每层治理都要有数据支撑:拍脑袋决定的超时、重试次数大概率错。

# 17.实战案例

# 17.1 地铁失败重试

问题:某出行 App 用户进入地铁后 4G 信号差,请求失败。用户进站后即使信号恢复,应用仍持续显示"加载失败"。

根因分析:

  • 失败处理只做"显示错误 + 提供重试按钮"。
  • 没有监听网络恢复事件,所以信号回来后应用感知不到。
  • 用户离开地铁后不会回到失败页面,体验为"App 完全无响应"。

修法:

// 1. 失败请求入"待重试队列"
val retryQueue = mutableListOf<PendingRequest>()

fun onRequestFailed(request: Request) {
    if (isNetworkError(request)) {
        retryQueue.add(PendingRequest(request, attempts = 0))
    }
}

// 2. 监听网络恢复
cm.registerDefaultNetworkCallback(object : NetworkCallback() {
    override fun onAvailable(network: Network) {
        flushRetryQueue()  // 自动重试队列里的请求
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

教训:弱网场景必须区分"暂时失败"和"永久失败"。前者要在网络恢复后自动重试,后者直接 fallback。

# 17.2 iOS 切网半死

问题:某 iOS App 用户从 WiFi 进电梯切到 4G 后,已有连接进入半死状态,请求 hang 住直到默认 60 秒超时。

根因分析:见 §09.2 TCP 半死的本质——iOS 没主动断开旧网络的 TCP 连接,请求在旧 socket 上等 TCP 内部超时。

修法:

// 1. 用 Network framework 监听网络变化(不要用老的 Reachability)
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
    // 2. 网络变化时主动清空连接池
    URLSession.shared.invalidateAndCancel()
    // 重建 URLSession(下次请求强制建新连接)
}
monitor.start(queue: .global())
1
2
3
4
5
6
7
8

教训:iOS 上必须用 Network framework,老的 Reachability 库不可靠(基于 SCNetworkReachability,事件延迟可达几十秒)。

# 17.3 启动期 DNS 阻塞

问题:某 App 启动期首屏 API 偶发耗时几十秒,用户感觉"App 启动卡死"。

根因分析:

  • 启动期所有 API 都先做 DNS 解析。
  • 弱网下 DNS 解析本身就慢(操作系统 DNS 默认无超时控制,可达 30 秒)。
  • 所有 API 串行等 DNS。

修法:

  • 接入 HTTPDNS:跳过系统 DNS,走自己的 HTTP 接口解析域名(可控超时 3s)。
  • DNS 结果强缓存(应用启动时优先用上次缓存的 IP)。
  • DNS 失败立即用"启动 IP 列表"兜底(每次发版埋几个备用 IP)。

教训:DNS 是弱网下最容易被忽视的瓶颈。系统 DNS 没有应用可控的超时和缓存策略,启动期 API 必须用 HTTPDNS。


# 18.防劣化体系

# 18.1 三道防线总览

   ┌──────────────────────────────────────┐
   │ 开发期 → CI 期 → 上线后                │
   │   编码 lint  自动弱网测试  SLO 监控      │
   └──────────────────────────────────────┘
1
2
3
4
  • 第一道·开发期:编码 lint + 单元测试覆盖弱网/断网场景。
  • 第二道·CI:自动化弱网模拟,关键流程不达标阻断。
  • 第三道·线上:弱网用户细分 SLO 监控。

# 18.2 CI 弱网回归

CI 阶段必须做的弱网回归:

测试场景 模拟方式 期望结果
高延迟(1s+) NLC / tc-netem 关键 API 成功率 > 95%
高丢包(10%) 同上 重试机制有效,成功率 > 80%
完全断网 airplane mode 自动化 显示离线 UI + 重连提示
切网(WiFi↔4G) 脚本切换 连接重建 < 3s
长延迟首字节 mock server delay 进度条正确显示

关键流程必须 100% 覆盖(登录、核心业务接口、支付等)。

# 18.3 线上 SLO

按"网络分级"切分 SLO(不能只看总体):

网络等级 核心 API 成功率 首屏耗时 P95
正常 ≥ 99.5% < 1s
一般弱网 ≥ 95% < 5s
严重弱网 ≥ 85% < 8s
离线 核心功能 100% 可用 N/A

告警规则:

  • 任一网络等级的 SLO 连续 1 小时未达标 → 告警。
  • 弱网用户占比突然飙高 → 告警(可能是某个地区基站故障)。

# 19.跨平台速查

# 19.1 工具速查

平台 弱网模拟 RUM 工具 HTTP/3 库
Android NLC(系统)/ Charles OkHttp EventListener Cronet
iOS NLC(Xcode)/ Charles URLSession.taskMetrics URLSession(iOS 15+ 自动)
前端 Chrome DevTools Throttling PerformanceObserver 浏览器自动协商
服务端 tc / netem tcpdump / eBPF nginx-quic / Cloudflare

# 19.2 关键 API 速查

用途 Android iOS Web
网络变化监听 ConnectivityManager.registerDefaultNetworkCallback NWPathMonitor navigator.connection.onchange
当前网络类型 NetworkCapabilities.hasTransport NWPath.usesInterfaceType navigator.connection.effectiveType
HTTP 拦截 OkHttp Interceptor URLProtocol Service Worker
持久化离线队列 Room / WorkManager Core Data / URLSession 后台 IndexedDB

# 20.总结与延伸

# 20.1 五条核心原则

  1. 快速失败:超时是为了"早点放弃",不是"等到成功"。8 秒是工程默认。
  2. 智能重试:指数退避 + jitter + 幂等键 + 熔断器,缺一不可。
  3. 优雅降级:弱网下展示缓存 + 降级提示比转圈更友好。
  4. 离线优先:核心功能必须能离线工作,"在线"是加成不是必需。
  5. 可见进度:长加载必须有进度提示,"未知等待"是用户体验杀手。

# 20.2 五个常见误区

  1. ❌ "服务端成功率高 = 用户成功率高"——服务端只看到到达的请求,没看到没到达的。
  2. ❌ "信号 4 格 = 网络好"——RSRP 高不代表 SNR 高。
  3. ❌ "超时设大点更安全"——只是让用户白等更久。
  4. ❌ "失败立即重试 3 次"——会触发雪崩。
  5. ❌ "弱网是用户问题,不是应用问题"——99% 的"弱网问题"都能在应用层缓解。

# 20.3 延伸阅读

  • HTTP/3 普及:QUIC 已被各大 App 采用,移动场景效果显著(§16.5 实验数据)。
  • CRDT 数据结构:解决离线协作的冲突合并(§15.3)。
  • AI 网络感知:基于历史预测当前网络状况,提前降级(前沿方向)。
  • eBPF 全链路观测:服务端弱网归因的新工具,能看到 TCP 重传等内核细节。
  • 本地优先架构:Notion / Linear / Figma 的工程博客 (opens new window) 是必读资料。

# 21.一句话总结

弱网与极端环境是 App 的"诚信考验"——做好了,用户在地铁里也夸你;做不好,那 1% 的差体验会被放大成 100% 的差口碑。五层治理(超时 → 重试 → 降级 → 离线 → 架构)由浅入深,团队应根据业务规模和质量目标选择停在哪一层,每一层都要做到极致再考虑下一层。

上次更新: 2026/06/07, 10:26:12
应用安全性能权衡
README

← 应用安全性能权衡 README→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式