Android 弱网模拟:别只会用均匀分布——三种延迟模型和两种丢包模型的原理与实现

别只会用均匀分布:三种延迟模型和两种丢包模型的原理与实现

WeakNet 技术博客系列 | 第 6 篇


上一篇实现了 5 种操控器,但有一个问题没回答:延迟的抖动值怎么取?丢包的概率怎么定?直接用随机数行不行?答案是行,但效果不好。如果你用 Random.nextInt() 生成抖动,用 Random.nextInt(100) < 30 决定丢包,模拟出来的弱网和真实弱网差得很远。

真实网络的延迟和丢包不是均匀随机的。延迟通常聚集在某个均值附近,偶尔出现大幅飙升;丢包也不是独立发生的,而是成串成片地爆发。要让模拟效果逼真,需要用对数学模型。WeakNet 提供了 3 种延迟模型和 2 种丢包模型,这篇文章把它们挨个讲清楚。

三种延迟模型

延迟模型的职责是生成抖动值(jitter)------在基础延迟 delayMs 上叠加的随机偏移量。三种模型对应三种概率分布,模拟不同类型的网络环境。

UNIFORM -- 均匀分布

kotlin 复制代码
DelayModel.UNIFORM -> Random.nextInt(-jitterMs, jitterMs + 1)

最简单的模型。在 [-jitterMs, +jitterMs] 区间内均匀取值,每个整数出现的概率完全相等。设置 jitterMs=100,抖动值在 -100 到 +100 之间均匀分布。-100、0、+100 出现的概率一模一样。

问题在于:真实网络的抖动不是均匀的。大多数时候延迟稳定在均值附近,偶尔出现大幅偏移。均匀分布把"大幅偏移"和"小幅偏移"赋予了同样的概率,模拟不出这种"平时稳、偶尔崩"的特征。

适用场景:基线测试。简单、可预测,适合验证功能正确性。

GAUSSIAN -- 高斯分布(Box-Muller 变换)

kotlin 复制代码
DelayModel.GAUSSIAN -> {
    // Box-Muller 变换生成标准正态分布
    val u1 = Random.nextDouble(1e-10, 1.0)
    val u2 = Random.nextDouble()
    val g = sqrt(-2.0 * ln(u1)) * cos(2.0 * PI * u2)  // 标准正态 N(0,1)
    (g * jitterMs).toInt().coerceIn(-3 * jitterMs, 3 * jitterMs)
}

高斯分布(正态分布)是自然界最常见的分布。Box-Muller 变换是生成高斯随机数的经典方法,原理不复杂:

取两个 (0, 1) 区间的均匀随机数 u1、u2,通过公式 g = sqrt(-2 * ln(u1)) * cos(2 * PI * u2) 变换,得到一个标准正态分布 N(0,1) 的随机数。这里 ln(u1) 产生指数分布的效果,cos(2*PI*u2) 提供角度均匀性,两者组合恰好抵消了笛卡尔坐标到极坐标变换中的雅可比行列式,得到标准正态。

乘以 jitterMs(标准差 sigma),得到实际的抖动值。最后用 coerceIn(-3*sigma, 3*sigma) 做 3sigma 裁剪------正态分布中 99.7% 的值落在 3sigma 以内,裁剪掉极端尾部防止出现不合理的巨大延迟。

设置 jitterMs=100,大约 68% 的抖动值在 -100 到 +100 之间,95% 在 -200 到 +200 之间,偶尔出现接近 300 的偏移。大部分包的延迟接近基础值,少数包出现较大抖动------比均匀分布真实得多。

适用场景:稳定的弱信号网络。4G 弱信号、WiFi 远距离,延迟围绕均值波动。

LONG_TAIL -- 对数正态分布

kotlin 复制代码
DelayModel.LONG_TAIL -> {
    val u1 = Random.nextDouble(1e-10, 1.0)
    val u2 = Random.nextDouble()
    val normal = sqrt(-2.0 * ln(u1)) * cos(2.0 * PI * u2)
    val logNormal = exp(normal)  // e^N(0,1) = LogNormal(0,1)
    (logNormal * jitterMs).toInt().coerceIn(0, 5 * jitterMs)
}

对数正态分布的生成方法很简单:先产生一个标准正态数,再取 exp()exp(N(0,1)) 就是对数正态分布 LogNormal(0,1)。

这个分布的关键特征是:中位数是 1,均值约 1.65,但右尾极长。P(X>5) 约 5.4%,P(X>10) 约 1.1%。换句话说,绝大多数时候抖动值很小(靠近 jitterMs),但偶尔会出现 5 倍甚至 10 倍于正常值的巨大延迟尖峰。

还有一个重要特性:值永远为正。exp() 的结果不可能小于 0,所以对数正态分布只会产生正方向的抖动。coerceIn(0, 5*jitterMs) 把上限截断在 5 倍,防止极端值把延迟推到离谱的程度。

这正好模拟了真实弱网的典型行为:平时延迟还行,突然来一个巨大的尖峰。地铁进隧道、电梯关门、基站切换------这些场景的共同特点是"大部分时间可以,偶尔崩一下",对数正态的右偏特征完美匹配。

适用场景:真实弱网环境。地铁、电梯、高铁等移动场景,是三种模型中最贴近现实的。

三种模型对比

模型 分布形态 延迟范围 适用场景
UNIFORM 对称、平坦 -jitter, +jitter 基线测试,简单可预测
GAUSSIAN 对称、钟形 -3sigma, +3sigma 稳定弱信号,延迟围绕均值波动
LONG_TAIL 右偏、长尾 0, 5\*sigma 真实弱网,偶尔出现巨大尖峰

两种丢包模型

丢包模型决定每个数据包是否被丢弃。WeakNet 实现了两种模型,对应两种截然不同的丢包行为。

RANDOM -- 伯努利模型

kotlin 复制代码
LossModel.RANDOM -> Random.nextInt(100) < lossPercent

最直观的丢包方式:每个包独立地以概率 p 被丢弃。设置 lossPercent=30,每个包有 30% 的概率被丢掉,丢不丢和其他任何包无关。

数学上这是 n 重伯努利试验。期望丢包率等于 p,方差等于 p(1-p)。连续丢 3 个包的概率是 p^3------当 p=0.3 时只有 2.7%。

问题在哪?真实网络的丢包不是独立的。丢包倾向于成串发生------进入隧道时连续丢一堆,切基站时又连续丢一堆。伯努利模型下 30% 丢包率的表现是"平均每 3 个包丢 1 个",应用层的纠错机制(TCP 重传、ARQ)很容易应对。但真实场景是"连丢 3 个包然后恢复",这对应用层的冲击大得多。

适用场景:简单的丢包测试。验证应用在丢包环境下的基本表现。

BURST -- Gilbert 模型(两状态马尔可夫链)

kotlin 复制代码
// 状态转移概率
r = 1.0 / 3.0                                    // BAD -> GOOD
p = r * lossPercent / (100.0 - lossPercent)       // GOOD -> BAD

// 状态机
synchronized(lock) {
    val drop = inBadState  // 坏状态:所有包丢弃
    if (inBadState) {
        if (Random.nextDouble() < r) inBadState = false
    } else {
        if (Random.nextDouble() < p) inBadState = true
    }
    drop
}

Gilbert 模型是最经典的突发丢包模型,核心思想是用两状态马尔可夫链描述网络的好与坏:

  • GOOD 状态:正常传输,不丢包
  • BAD 状态:所有包被丢弃
  • 转移概率:GOOD 到 BAD 的概率是 p,BAD 到 GOOD 的概率是 r

参数推导的过程挺有意思。r 固定为 1/3,意思是处于 BAD 状态时,平均经过 3 个包后恢复(几何分布的期望是 1/r = 3)。p 不是随意设置的------它由目标丢包率反推:

稳态下 P(BAD) = p / (p + r)。令 P(BAD) = lossPercent/100,解出 p = r * lossPercent / (100 - lossPercent)

设置 lossPercent=30:p = (1/3) * 30/70 = 0.143。稳态验证:P(BAD) = 0.143 / (0.143 + 0.333) = 0.30,精确等于 30%。每次进入 BAD 状态后平均丢 3 个包再恢复------3 个连续丢包,比伯努利模型的"隔三差五丢一个"杀伤力大得多。

为什么 Gilbert 比伯努利好?真实弱网的丢包是突发的。地铁进隧道,信号断 2 秒,这 2 秒内的所有包全部丢失;出了隧道信号恢复,传输正常。伯努利模型可能丢 1 个、传 2 个、丢 1 个------应用层重传就补上了。Gilbert 模型则是连丢 3-4 个,应用层的滑动窗口直接被清空,TCP 退避重传,真实得多。

适用场景:真实弱网模拟。地铁、电梯、高铁等移动场景,必须用突发丢包才能测出应用的真实表现。

延迟施加的位置

延迟不是在操控管线里施加的。管线只负责"限速、乱序、重复、丢包、篡改"五种操作,延迟的计算(calculateJitter)虽然定义在 ManipulationPipeline 中,但实际的 sleep 调用分散在不同位置。原因前面提到过:TCP 是流式协议,逐包延迟会导致累积堆叠。

场景 施加位置 说明
TCP SYN-ACK connectExecutorThread.sleep(delay+jitter) 模拟握手 RTT,只在建连时延迟一次
TCP 数据包 不施加延迟 逐包延迟会累积堆叠,连接卡死
UDP 出方向 udpExecutorapplyDelay() 每包独立延迟,模拟上行弱网
UDP 入方向 udpExecutorapplyDelay() 每包独立延迟,模拟下行弱网
ICMP icmpExecutorThread.sleep(delay+jitter) 模拟 ping RTT

TCP 是流式传输,如果每个包都延迟 100ms,10 个包就是 1 秒累积延迟,连接直接卡死。所以延迟只在 SYN-ACK 阶段施加一次,模拟握手的往返延迟。建连后的数据流不再加延迟,靠上游网络本身的 RTT 和管线中的限速来控制速率。

UDP 和 ICMP 是报文式传输,每个包独立,不存在累积问题。出方向和入方向各加一次延迟,合起来就是 RTT = 2 * delayMs。

写在最后

三种延迟模型和两种丢包模型的选择,直接决定了弱网模拟的逼真程度。均匀分布和伯努利丢包能跑通基本功能测试,但测不出应用在真实弱网下的表现。高斯分布和对数正态分布模拟了延迟的聚集特征,Gilbert 模型模拟了丢包的突发特征------这些才是地铁、电梯、高铁场景的真实写照。WeakNet 的 12 个预设场景里,"地铁"、"电梯"、"丢包地狱"等高强度预设默认使用 LONG_TAIL + BURST 组合,原因就在这里。

数学模型讲完了,下一篇看两个高级场景:网络闪断和 DNS 故障模拟。


项目地址github.com/baithinking...

相关推荐
Zhu7585 小时前
在k8s集群部署MySQL单实例,支持多个主流发行版
android·mysql·kubernetes
AFinalStone8 小时前
Android 7系统异常问题排查(四)Framework层(上)—System Server Watchdog
android·系统异常
宝杰X710 小时前
Android Room3 多平台数据库
android·数据库
huibin14785236912 小时前
热缓解学习记录
android·学习
小田的博客15 小时前
SAP MM 供应商银行主数据更新报错!message R1228!
android·java·服务器
一笑的小酒馆17 小时前
Android智能猫砂盆视频加载慢卡顿问题分析
android
March.s18 小时前
IP 存储核心 iSCSI:原理拆解 + 无报错实战指南
网络·网络协议·tcp/ip
只会cv的小前端18 小时前
七巧低代码服务端脚本使用方法
android·低代码·rxjava
18 小时前
Android 自定义 View 实战:从零打造工业级全向摇杆控件(OmniJoystickView)
android·kotlin·自定义view·摇杆控件
雨白18 小时前
深入理解 Kotlin 协程 (十一):以逸待劳,探秘 select 多路复用与并发安全策略
android·kotlin