1. 理想网络不存在为什么我建议生产级服务都要做延迟与丢包演练先说一个我反复遇到过的情况应用在办公室网络、本地机房、甚至云上同区域测试环境里跑得非常顺接口响应基本在几十毫秒内结束压测也不崩。结果一到用户手里反馈全是页面转圈接口超时视频卡顿。开发团队第一反应是服务端出了问题查日志、查监控、查数据库最后发现服务端负载很低瓶颈全部发生在网络上——用户所在的位置可能有 100ms 以上的 RTT还有时不时出现的丢包与抖动。这个场景几乎是所有网络相关从业者的共同记忆。问题就出在我们默认了网络是可靠的而真实网络恰恰不可靠。公网环境下丢包、延迟波动、带宽竞争、链路切换这些并不是异常态而是常态。如果一套系统只在完美网络里验证过那就等于没验证过它的真实表现。网络韧性这个词听起来有点抽象说白了就是当网络变得糟糕时你的应用是否还能维持可用能不能优雅降级恢复之后能不能迅速回到正常状态。而要验证这个韧性最直接的手段就是主动制造延迟与丢包把坏网络当作一种可以随时开启、精确控制的测试条件。这比祈祷线上出故障时再被动观察要可控得多也更安全。这篇文章不是讲理论而是从实操角度聊聊怎么把延迟和丢包模拟这件事做起来内核层面用什么工具、应用层怎么做、如何在容器和集群环境中注入故障、测试结果应该怎么读。我假设读者是有一定网络基础的后端、运维、性能测试工程师但即使你是刚入门的小白只要会敲 Linux 命令跟着文章走也能把环境搭出来。2. 三大模拟路线内核限流、代理注入、硬件损伤仪怎么选做延迟和丢包模拟业界并没有一招鲜。不同场景适合不同工具选错了轻则效果失真重则直接干扰生产。我根据自己的使用经验把主流路线分成三类各有用武之地。2.1 内核流量控制Linux tc netem 是绝对主力在 Linux 服务器上最常用也最灵活的做法是使用tcTraffic Control配合netemNetwork Emulator模块。netem是内核自带的一个网络模拟组件能直接作用于网卡出方向的流量给数据包附加人为延迟、丢包、重复、乱序、损坏等等效果。它的好处非常明显不需要改造应用代码不需要代理层配置一条命令就能生效还能叠加链路带宽限制一起用。我用到的最基础命令长这样# 在 eth0 出方向增加 100ms 延迟 tc qdisc add dev eth0 root netem delay 100ms就这么一条所有从 eth0 出去的包都会被推迟 100ms 再发送。这个出方向的概念后面会重点讲因为它决定了你模拟的是哪个方向上的网络劣化。tc本身并不难但要真正用好需要理解一点背景每张网卡上都有一个根队列规则root qdiscnetem相当于插入这个根队列里的一个模拟器。如果你之前没有配置过其他 qdisc直接 add 就可以如果需要叠加多种效果通常要用change命令对已有规则做修改而不是每次重新 add。2.2 应用层代理不需要动内核权限也可以注入故障内核方案虽然强大但有些环境下你根本没有 root 权限或者目标系统不是 Linux比如 Windows、macOS 上的本地开发环境。这时候可以走应用层代理的路线代表性的开源工具是Toxiproxy。Toxiproxy 的思路很简单在客户端和目标服务之间插入一个代理代理本身是透明的但它能对经过的流量施加延迟、丢包、限速等毒性规则。客户端其实还是在请求你原来的服务地址只不过这个地址是 Toxiproxy 监听出来的它再转发到真正的后端。这类方案最大的优势是平台无关、权限要求低而且非常适合做自动化测试——它有完整的 HTTP API你可以在测试脚本里动态注入和恢复故障。缺点也很明显只能在应用层生效模拟不了底层网络的特征而且所有流量都要过一层代理本身会引入少量开销实测下来代理自身的延迟大约在几百微秒到几毫秒量级对于绝大多数场景可接受。2.3 硬件损伤仪设备厂商和高端场景的选择第三种是专用的硬件网络损伤仪比如 Spirent、思博伦这些厂商的设备。它们一般串接在物理链路上能精确模拟卫星链路那种极高延迟、极不规则丢包等极端场景。这类设备的精度和可控性远高于软件方案延迟可以精确到微秒甚至亚微秒级别还能模拟物理层的信号劣化。但说实话除非你是做交换机、路由器或者需要严格认证的通信设备研发否则我不建议一上来就买这些设备几万到几十万的硬件成本会对绝大多数项目形成压力。软件方案已经能覆盖 90% 以上的业务测试场景。2.4 选型建议先搞清楚你要模拟什么一句话建议Linux 环境优先用 tc netem生产环境或跨平台自动化测试优先评估 Toxiproxy 这类代理方案需要极致精度或硬件链路兼容性时才考虑硬件损伤仪。工具选型还有个容易被忽视的判断标准你要模拟的是网络链路的变化还是应用交互中的网络效应。前者比如模拟跨洲 RTT 200ms那必须是内核/链路层面的方案才真实后者比如模拟用户请求偶发超时看应用会不会自动重试、熔断那代理方案就够用了而且更好控制。想清楚这一点你就知道自己该用哪类工具。3. netem 快速上手延迟、抖动、丢包的精确控制与效果验证我不打算罗列tc手册只把真正用得到的场景拿出来讲清楚。每个模拟效果我会给出命令、参数解释以及验证方法。注意下面所有命令默认在 Linux 上执行网卡名称按实际环境替换我以eth0为例。3.1 基础延迟模拟固定 RTT 与随机抖动最常用的模式是给一个固定延迟再加一部分随机抖动。固定延迟模拟的是物理距离抖动模拟的是网络中排队和路由波动的效果。# 固定 100ms 延迟加 20ms 的均方差抖动抖动符合正态分布 tc qdisc add dev eth0 root netem delay 100ms 20ms distribution normal这里不要误解成最小 80ms、最大 120ms 均匀分布——distribution normal表示抖动值服从正态分布所以大多数包的延迟集中在 100ms 附近但偶尔也会出现 140ms、150ms 甚至更高的尾巴。这在模拟真实网络时比均匀分布靠谱得多因为互联网上的排队延迟通常更接近重尾分布。如果只想看简单的固定延迟不加分布参数就行。验证方式最直观的是用pingping -c 50 -i 0.2 10.0.0.1看 RTT 的最小值、平均值和最大值的差距。如果固定 100ms 加 20ms 抖动你会看到均值在 100ms 出头最大值能跑到 130ms-150ms。这里有个小细节ping显示的 RTT 是往返时间如果你在发送端网卡上加了 100ms 出方向延迟实际 RTT 大约是 200ms如果你对端没有加延迟千万别被这个弄晕。3.2 丢包模拟单个值与突发模式丢包模拟同样非常简单# 设置 10% 丢包率 tc qdisc change dev eth0 root netem loss 10%这里用的是change而不是add因为链路已经有一个 netem qdisc 了add会报 Exclusive 错误。这是一个非常容易踩的坑很多人看到报错就以为不支持其实只是命令用错了。丢包率 10% 算非常恶劣的网络环境日常公网上 0.1%-1% 的丢包就已经能让 TCP 吞吐明显下降。所以我建议测试时从小比例开始比如loss 1%、loss 5%逐档递增观察应用表现。真正的考验是突发丢包也就是丢包不是均匀分布的而是成片成片地出现。这更接近真实网络里链路拥塞或无线信号干扰时的表现。netem 支持带相关性的丢包模型# 丢包率 5%当发生丢包时下一个包也有 25% 概率被丢弃模拟突发丢包 tc qdisc change dev eth0 root netem loss 5% 25%从实际效果上看这个参数组合会让丢包呈现一阵一阵的特征短时间内连续丢几个包然后恢复一段时间再连续丢一波。TCP 对这种成片丢包非常敏感因为连续丢包往往导致超时重传RTO而不是快速重传延迟会异常上涨。这一点在压测多媒体推流场景时尤其明显。3.3 清理规则什么时候用 del什么时候用 change在开始实验之前你可能会频繁修改规则。务必把这三种命令区分清楚操作类型命令使用场景添加规则tc qdisc add dev eth0 root netem ...网卡上没有根 qdisc 或需要重置整个根规则时修改规则tc qdisc change dev eth0 root netem ...已有 netem 规则只调整参数时删除规则tc qdisc del dev eth0 root结束实验恢复网卡原本状态我见过不少人做完实验忘了删除规则结果带着一堆延迟和丢包跑了好久排查半天才发现是 tc 规则没清理。所以每次实验结束我习惯立刻执行tc qdisc del dev eth0 root并在脚本里加了自动恢复的逻辑。3.4 注意事项不生效多半是网卡名或 root 权限问题tc需要 root 权限普通用户直接执行会报权限错误用sudo。确认网络接口名是对的别把eth0写在有多个网卡的机器上结果操作错了网卡。执行ip link show可以快速列出所有接口。在云主机上有些网卡的队列规则属于托管类型直接add root netem可能会失败视云厂商不同可能需要先删除默认的 qdisc 或者换到子接口上操作。4. 更贴近真实非对称延迟、突发丢包与带宽瓶颈的组合模拟单一效果的延迟模拟很容易上手但真实网络的恶劣往往不是一百毫秒延迟或5%丢包这样干净利落的而是多种问题叠加出现。这一节讲的是组合模拟和更贴近现实的策略。4.1 非对称延迟上行慢和下行慢是两回事很多测试工具默认对上下行施加同样的延迟这其实不符合真实网络。普通的家庭宽带、移动网络上行带宽远小于下行上行方向的延迟和丢包往往更严重。而业务中的表现也很不同下行延迟大影响用户收到数据的速度上行延迟大影响用户操作上传和请求发送的响应感。更细一点即便只看 RTT互联网上的路径也常常是非对称的你去程走的 A 线路回程走的 B 线路两者的延迟不一样。要模拟这种情况不能只在一台机器上配置。最简单可靠的方案是在通信的双方分别配置不同的延迟。假设有 A、B 两台机器A 到 B 方向走 50msB 到 A 方向走 150ms那么在 A 上tc qdisc add dev eth0 root netem delay 50ms在 B 上tc qdisc add dev eth0 root netem delay 150ms即可。这样可以模拟出 RTT 约 200ms 但两个方向差异悬殊的链路。单台机器上想对特定目标地址设置不同的延迟需要用到prio队列和tc filter组合。比如希望发往 192.0.2.10 的包有 200ms 延迟而其他流量不受影响# 创建三个优先级队列把根队列设为 prio tc qdisc add dev eth0 root handle 1: prio # 把目标 IP 的流量分类到第三队列1:3 tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dst 192.0.2.10/32 flowid 1:3 # 在第三队列插入 netem 延迟 tc qdisc add dev eth0 parent 1:3 handle 30: netem delay 200ms这种方式在测试多服务交互的场景时很好用你只想劣化某个下游依赖其他依赖保持正常用来定位问题是否特定于某条链路。不过prio的队列机制有一些细节初次使用建议先在测试机上验证效果确认符合预期再放到真实环境。4.2 丢包的马尔可夫模型更真实的成簇劣化前面提到的loss 5% 25%已经能模拟出一定的突发特征。如果你的内核较新4.13 以上netem 还支持基于四状态马尔可夫模型的丢包模拟参数为loss gemodel。它的原理是模拟网络好状态和坏状态之间的切换这种模型对无线网络尤其贴近# p13 从好状态转坏状态概率p31 从坏状态转好状态概率 tc qdisc change dev eth0 root netem loss gemodel p13 0.5% p31 10%参数理解上可以这样类比网络链路在大部分时间是通顺的好状态偶尔一下进入坏状态p13 越高越容易进入坏状态不会立刻消失p31 越小坏状态持续越久。设置成p13 0.5% p31 10%大概的效果是链路绝大多数时候正常但一旦进入坏状态会连续丢几十个包模拟信号突然被干扰的情况。这个模型我也不是上来就会用后来在模拟移动端弱网时发现它对偶发性卡顿的还原度远高于独立丢包率才开始在项目里推广。如果你的测试场景主要针对移动端用户强烈建议尝试一下这个参数。4.3 组合带宽限制模拟弱网里的排队延迟延迟和丢包之外还有一个绕不开的变量带宽。在带宽受限的情况下即使链路本身没有新增延迟数据包也会在队列里排队导致延迟变大。这个现象有个专门名词叫Bufferbloat缓冲膨胀网络设备为了应对突发流量设置了大缓冲区结果缓冲区里的包排长队延迟飙升。用tc可以组合出带宽 延迟的效果。把根 qdisc 换成 token bucket filter 限制带宽然后在它下面挂 netem# 限制带宽为 1Mbpsburst 32kbit然后在这个限制基础上加 50ms 延迟和 5% 丢包 tc qdisc add dev eth0 root handle 1: tbf rate 1mbit burst 32kbit latency 400ms tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 50ms loss 5%这样的配置模拟的是用户的网络带宽不大1Mbps 大概相当于早期移动网络或非常拥塞的共享 WiFi同时链路本身还有一定的延迟和丢包。在这种组合下你去看应用的加载时间和首字节时间往往会比单独加延迟更让人绝望——大量数据拥塞时即使丢包率不高延迟也会因为排队而显著上升。4.4 实测建议从一档一档组合开始不要一上来就地狱模式组合模拟的效果千变万化我建议按档位渐进测试档位延迟丢包带宽限制适用场景L1 弱网入门50ms0.5%无跨城专线、普通办公网L2 移动网络100ms±20ms1%5Mbps4G 移动网络常见表现L3 跨洲链路200ms2%1Mbps跨洋访问、卫星链路L4 极端弱网300ms±50ms5%-10%512Kbps偏远地区、网络严重拥塞每一档跑一遍业务的核心链路记录响应时间、成功率、用户可感知的卡顿次数。你会发现一个残酷的事实多数系统在 L2 就开始出现体验问题到 L3 基本不可用。如果能在 L2 和 L3 之间保持基本可用已经说明系统的网络韧性相当不错了。5. 从单机到集群容器与混沌工程环境中的故障注入实验设计单机的tc命令学会了接下来是在测试环境里怎么用起来。真实项目里很少会直接在服务器网卡上乱加规则因为影响面太大而且容易影响线上业务。更稳妥的做法是在独立的测试环境、容器环境或混沌工程平台上做。5.1 容器环境下的网络注入权限与网络命名空间容器里做网络模拟最大的两个坎是权限和网络模式。权限方面tc操作需要NET_ADMIN能力。在 Docker 中运行容器时你需要加上docker run --cap-addNET_ADMIN --cap-addNET_SYS_ADMIN -it my-test-image没有这两个权限容器里执行tc会报 RTNETLINK answers: Operation not permitted。网络模式方面如果容器用的是默认 bridge 模式容器内部的eth0其实是一个 veth 虚拟网卡执行的tc只影响容器本身的流量出方向。这意味着:你在容器内加延迟影响的是容器内进程发出的包而宿主机返回给容器的包并不会被影响。如果你需要模拟用户 - 服务端整条链路上的延迟光在服务端容器里设置是不够的还要在客户端侧或网络链路上设置对应的下行延迟。所以在容器环境做延迟模拟我建议划分清楚方向模拟目标在哪里配置影响效果用户上行慢客户端容器/主机出口请求发送变慢用户下行慢服务端容器/主机出口响应返回变慢双向 RTT 大两端都配置不同延迟请求和响应都变慢5.2 集群中的混沌注入Chaos Mesh 与 NetworkChaos如果你的服务已经在 Kubernetes 集群里不用自己去手动改每个 Pod 的tc规则混沌工程工具可以帮你把故障注入变成声明式的资源配置。我常用的是Chaos Mesh的NetworkChaos它做的事情本质上也是对目标 Pod 的网络命名空间施加tc netem规则但通过 Kubernetes CRD 的方式让你可以精确控制目标、时长、动作。一个给指定应用注入延迟与丢包的例子apiVersion: chaos-mesh.org/v1alpha1 kind: NetworkChaos metadata: name: delay-loss-example spec: action: delay mode: one selector: namespaces: - production labelSelectors: app: payment-service delay: latency: 100ms correlation: 50 jitter: 20ms duration: 5m作用范围可以做到只针对某个 deployment只持续 5 分钟到期自动恢复。这对于线上演练比较友好不用人肉盯着恢复。类似的工具还有 AWS 的 SSM Run Command 结合 tc 脚本、或者开源项目比如tc-blackhole-agent等核心思路一样只是操作入口不同。5.3 实验设计基线、盲区、自动恢复缺一不可故障注入本身并不难难的是设计一套安全、可信、可复用的实验流程。我的经验是三个原则第一先有基线对比。在任何延迟和丢包模拟前先跑一遍正常的性能基线接口的 p50/p95/p99 时延、吞吐量、错误率。没有基线你看到的变慢就没有参照物。第二明确实验边界。混沌工程最有价值的地方在于受控的破坏边界要清晰。具体到一台机器、一个 Pod、一种流量类型不要一上来就对整个集群施压。我见过把故障注入到所有节点导致系统全面瘫痪的案例教训就是破坏范围就像炸弹半径先从小范围开始。第三自动恢复与逃生通道。所有模拟效果都要能够自动恢复设置时长是最低要求。线上环境做演练我还建议用脚本定时检查规则是否存在并准备一个一键清空的脚本把所有网卡的 qdisc 恢复默认。执行命令也尽量用 timeout 包裹防止长时间忘记回收。结合上面的思路一个标准的故障注入流程可以这样跑# 1. 记录基线正常状态 curl -w connect%{time_connect} ttfb%{time_starttransfer} total%{time_total}\n https://service.example.com/api/health # 2. 注入故障目标网卡持续 3 分钟 tc qdisc add dev eth0 root netem delay 150ms loss 3% # 3. 压测/业务请求 # ... # 4. 恢复 tc qdisc del dev eth0 root # 5. 再次记录恢复后的状态观察自愈能力6. 读懂测试结果吞吐、重传、尾部延迟里的真实韧性模拟了半天如果不会看结果等于白做。这一节聊聊怎么从测试数据里判断一个系统的网络韧性到底怎么样。6.1 别只看平均延迟尾部延迟才是真恶魔网络延迟的分布往往非常不听话。加了 100ms ± 20ms 的抖动之后你可能看到平均 RTT 是 120ms但 P99 到了 400ms 甚至更高。对于用户来说他感知到的其实是那个某个请求突然卡了 2 秒的尾部体验而不是平均值。所以在测试时重点关注 P95 和 P99 的响应时间以及请求超时的比例。有个很典型的观察应用在 P50 表现很好但 P99 飙升通常是因为应用缺少重试退避或超时设置得太激进。丢包一上来TCP 进入指数退避单次请求的重试时间就可能翻倍增长这种延迟的长尾才是用户投诉的主要来源。6.2 TCP 重传与乱序网络问题导致的连锁反应ping和curl只能看到最终的延迟数字要理解网络劣化对 TCP 的影响建议在压测时抓包看重传率。只用 tcpdump 看一个连接的报文重传情况tcpdump -i eth0 -w capture.pcap然后在 Wireshark 里开启 TCP 分析观察重传包数量丢包之后必然触发重传重传率过高说明应用对丢包很敏感。如果你在应用层已经做了请求级别的重试TCP 重传和应用层重试叠加会让服务端收到重复请求接口的幂等性就要经受考验。DUP ACK 和乱序如果看到大量重复 ACK 但没有对应重传说明链路出现乱序。TCP 对乱序也很敏感因为乱序会触发接收端的空洞等待重传之后才能向上交付数据。连接重置如果 RST 频繁出现可能是中间的 NAT/防火墙因为网络状态异常把连接清了也可能是服务端超时主动断开。这类问题比单纯延迟更致命。我会建议在测试脚本里顺手统计重传率指标比如netstat -s里的TCPLostRetransmit对比正常状态。如果你发现丢包率 1% 时应用的重传率反而升到 8%、10%那说明 TCP 参数比如初始拥塞窗口可能需要调优。6.3 吞吐量的下降规律性能和用户感知之间的鸿沟TCP 吞吐量受丢包影响极其显著。有个 Rough 经验公式叫 Mathis 公式粗略估算带宽延迟积和丢包率的关系如果 RTT 是 100ms丢包率 1%理论最大吞吐大约只有 1.2Mbps 左右如果丢包率降到 0.1%理论吞吐可以到 12MbpsRTT 从 100ms 降到 20ms同样的丢包率下吞吐能有五倍提升。这也是为什么跨地域链路延迟高的时候用户下载大文件会明显变慢——不光是因为 RTT 高还因为丢包率对吞吐的惩罚被 RTT 放大。你可以用iperf3在注入延迟和丢包前后分别做一次吞吐测试看到数值差异之后再来理解应用层的体验变化会更有说服力。# 服务端 iperf3 -s # 客户端注入故障后 iperf3 -c 10.0.0.1 -t 30 -i 3正常情况下 1Gbps 局域网可能能跑到接近线速加了 100ms 延迟和 1% 丢包之后可能连 100Mbps 都跑不到。这种断崖式的下降往往解释了网络看起来没坏但用户体验糟糕的现象。6.4 把韧性测试纳入常规流程从一次演练到自动化门禁到这里延迟与丢包模拟的技术栈基本闭环了配置工具、注入故障、观察现象、读取指标。但如果你只是偶尔做一次演练价值会大打折扣。更推荐的做法是把它固化到发布流水线里——每次核心服务变更自动跑一轮弱网冒烟测试在 L2 档位下验证关键接口仍然满足 SLO。我自己的实践是写了一套简单的自动化脚本部署完新版本后先正常跑一遍接口用例然后对测试环境的网关所在主机注入 100ms 1% 丢包再跑一遍同样的用例对比两次的成功率和响应时间。如果弱网下成功率低于某个阈值直接阻止发布。一开始团队觉得这有点没事找事直到真的在一次发布后发现弱网环境请求雪崩大家才意识到这种门禁的价值。做这件事的门槛并没有想象中那么高底层就是tc的命令加上一个自动化编排脚本。难的是你要想清楚你的系统在什么网络条件下算是不可用这个标准定义得越清晰韧性测试就越容易落地和持续执行。最后分享一个小习惯。我在做延迟和丢包模拟时始终把实验记录和恢复脚本放在同一个目录下每次实验跑完第一件事就是执行恢复。网络韧性不是一次压测出来的而是无数次在受控的坏网络里反复验证出来的。你每次多模拟一种真实场景系统应对真实世界的能力就多一分底气。