背景痛点:为什么非得在本地“造”延迟?
做微服务最怕什么?不是 500,而是“时好时坏”的 200。
线上用户反馈“页面卡”,日志里却全是 200 ms 内的响应——真实网络里那一两百毫秒的 RTT(Round-Trip Time)被内网千兆环境抹平了。
结果就是:
- 超时阈值设得过低,移动用户直接触发重试,雪崩;
- 缓存穿透保护在 0 丢包环境测不到,一上 4G 全爆;
- HTTP/2 多路复用“队头阻塞”在局域网根本复现不了。
一句话:不在调试阶段把延迟“演”出来,上线后它就“演”你。
工具对比:Charles 凭什么胜出?
| 工具 | 粒度 | 动态脚本 | 可视化 | 缺点 |
|---|---|---|---|---|
| Fiddler (Win) | 单连接 1 ms 精度 | JScript.NET,语法旧 | 有 | 跨平台靠 Mono,不稳定 |
| tc (Linux) | 0.1 ms 精度 | bash + iproute2 | 无 | 整网卡一刀切,配错直接断网 |
| Charles | 单请求 1 ms 精度 | ES6 + Rhino | 实时瀑布图 | 收费,CLI 弱 |
结论:要“指哪打哪”给某条请求加延迟,同时让产品同学也能秒懂瀑布图,Charles 最趁手。
核心配置:Throttling 面板 5 步走
- 打开Proxy → Throttling Settings
- 勾选Enable Throttling
- 在Throttle Preset选“Custom…”
- 关键字段一次填到位:
- Bandwidth(kbps):下行 20 000 ≈ 20 Mbps,上行 5 000 ≈ 5 Mbps
- Round-trip latency (ms):写 150,表示每条请求固定 +150 ms
- Reliability (%):100 表示不随机丢包,想测 2 % 就填 98
- Stability (%):100 表示不随机断链
- 点Add把需要延迟的 Host 加白,其余流量直通,避免把 IDE 插件也拖慢。
小提示:latency 设 0 时 Charles 仍会给 1 ms 的“软件损耗”,所以真正“零延迟”请关 Throttling。
代码级实践:把延迟写进文件、写进脚本
1. Map Local 静态延迟文件
场景:App 启动接口/config必须卡 300 ms,让骨架屏多飞一会。
步骤:
- 抓一条真实响应,Save Response →
config.json - 在该请求右键Map Local…,Local Path 指向
config_delay.json - 在Tools → Rewrite里加规则:
- 匹配 Path:/config
- 类型:Response
- 动作:Delay 300 ms(Charles 原生支持)
JSON 文件内容就是普通业务报文,无需改结构,延迟由 Rewrite 动作注入,干净可回滚。
2. Scripting 动态延迟(ES6)
场景:A/B 实验,50 % 用户 100 ms,50 % 用户 400 ms,看转化率。
// Charles 4.6+ 内置 Rhino,支持 ES6 语法 function onRequest(context, request) { // 随机数决定桶 const bucket = Math.random() <0.5 ? 'fast' : 'slow'; // 把桶名写进请求头,方便日志关联 request.setHeader('X-Latency-Bucket', bucket); } function onResponse(context, response) { const bucket = response.request.header('X-Latency-Bucket'); if (bucket === 'slow') { // 慢桶:额外再加 300 ms response.setDelay(300); // 单位 ms } else { response.setDelay(100); }脚本保存为latency_ab.js,Tools → Scripting勾选即可。
setDelay只影响当前连接,不会拖慢其他并行请求,比 tc 的 netem 安全得多。
生产环境考量:别让“假”延迟触发“真”重传
TCP 重传阈值
Linux 默认tcp_retries2=15,在 150 ms RTT 网络里约 13 s 才判定断线。
你把 Charles latency 调到 1000 ms 却不改重传,会堆积大量 RTO,导致吞吐量暴跌,测试失真。
建议:- 延迟 < 300 ms 时保持系统默认;
- 延迟 > 500 ms 时,把服务器
net.ipv4.tcp_retries2降到 5(≈ 3 s)。
HTTP 版本差异
- HTTP/1.1:串行请求,延迟线性叠加,200 ms × 6 张图 ≈ 1.2 s 白屏;
- HTTP/2:多路复用,1 条 TCP,延迟只算 1 次 RTT,但队头阻塞仍在 Stream 层;
- HTTP/3 (QUIC):UDP 0-RTT,若 Charles 延迟设在 50 ms 以内,基本无感;> 150 ms 时拥塞控制开始限窗。
结论:别用同一套延迟值去验所有协议,至少分 3 组跑数据。
避坑指南:三条红线别踩
阈值红线
单请求 > 1 s 会触发浏览器“连接已重置”提示,测试变成测心跳,失去意义。
推荐阶梯:- 4G 模拟:150 ms
- 3G 模拟:300 ms
- 弱网模拟:500 ms
再高请直接丢包 5 % 而不是硬拉 latency。
SSL 代理忘了装根证书
延迟规则对 HTTPS 生效的前提是Proxy → SSL Proxying Settings把对应 Host 加进去,否则 Charles 只能旁路转发,Delay 不生效。忘记关 Throttling
曾经把 2 s 延迟留给“拍照上传”接口,结果下班继续刷剧,全公司 Git 拉代码 20 kb/s。
养成习惯:工具栏小乌龟图标变绿 = 延迟开着,用完即关。
延伸思考:给业务做“分层延迟”体检
- 接入层:给 CDN 静态资源加 50 ms,看是否阻塞首屏;
- 逻辑层:给 Gateway /config 加 200 ms,验骨架屏超时;
- 数据层:给慢查询接口加 400 ms + 2 % 丢包,验熔断和降级;
- 端到层:用 Charles 外部代理模式串连 2 台手机,模拟“用户 A → 服务器 → 用户 B”双程延迟,测实时对战体验。
把每一层的“疼痛阈值”记下来,形成自己业务的《延迟基线表》,以后任何版本先跑一遍基线,再谈发版。
写完这篇,我把自用的 150 ms/300 ms 两套配置直接 export 成*.xml,下次需求评审现场 3 分钟就能给产品“演”一次弱网,再也不用“口算”等待时间。
如果你也想从零搭一个会“说话”的 AI,同时让它在真·弱网环境还能低延迟回你,不妨顺路试试这个动手实验:
从0打造个人豆包实时通话AI
我跟着做了一遍,把 Charles 的 200 ms 延迟脚本直接套到 WebRTC 通道上,AI 的应答依旧丝滑,才发现原来“耳朵+大脑+嘴巴”链路也可以这么玩。小白也能 30 分钟跑通,比自己写 tc 命令香多了。