远程调试 iOS 客户端时,最难复现的通常不是完全断网,而是“还能连接,但响应很慢”:握手延迟增加、上传速度下降、少量请求丢失,最终触发重复提交、无限重试或界面长期等待。在 HexVM 云端 Mac 上做这类测试,首要原则不是先限速,而是把应用测试流量与 SSH、VNC 等管理流量分开。否则一条过宽的规则就可能切断自己的操作入口。
先定义可验收的弱网场景
不要用“网络差”作为测试条件。它无法复现,也无法判断修复是否有效。先把场景写成固定参数,并为每个场景定义客户端预期。
| 场景 | 下行与上行限制 | 额外延迟 | 丢包率 | 主要验收点 |
|---|---|---|---|---|
| 基线 | 不限制 | 0 ms | 0 | 正常请求耗时与成功率 |
| 高延迟 | 8 Mbit/s | 120 ms | 0 | 加载状态、取消与超时 |
| 低带宽 | 1 Mbit/s | 80 ms | 0 | 上传进度与后台切换 |
| 不稳定链路 | 4 Mbit/s | 100 ms | 2% | 重试上限与幂等处理 |
这里的参数是测试输入,不是对节点网络质量的描述。团队应根据接口超时、文件大小和用户所在网络调整数值,但同一轮回归必须保持参数一致。
弱网测试的目标不是证明请求最终能成功,而是确认失败发生时,客户端能及时结束等待、给出可理解的状态,并且不会产生重复数据。
建立未限速的基线
先在不加载任何整形规则时记录 DNS、路由和接口响应。测试接口应由团队自己控制,并提供不会写入生产数据的探针路径。
export TEST_URL="https://test-endpoint.invalid/health"
scutil --dns | grep 'nameserver\[[0-9]*\]'
route -n get default
networkQuality
curl -sS -o /dev/null \
-w 'code=%{http_code} connect=%{time_connect} tls=%{time_appconnect} total=%{time_total}
' \
"$TEST_URL"
示例域名必须替换成真实的受控测试地址。记录至少三次请求的中位数,不要只保存最好的一次。若基线阶段已经出现 DNS 失败、证书错误或路由异常,应先处理基础问题,不能通过增加重试掩盖。
还要确认目标地址是否由多个 IP 提供服务。只限制其中一个 IP 时,请求可能被解析到另一个地址,造成同一测试忽快忽慢。更稳妥的做法是为弱网验收准备固定入口,并在执行前保存解析结果。
只整形目标接口流量
macOS 的 dnctl 可以创建限速与延迟管道,pf 负责把匹配流量送入管道。规则必须同时限定目标 IP、协议和端口,不能写成覆盖全部出站连接的通配条件。
下面脚本建立一条 8 Mbit/s、120 ms 延迟、2% 丢包的测试管道。TARGET_IP 应填写受控接口的固定地址。
set -euo pipefail
TARGET_IP="${TARGET_IP:?set TARGET_IP first}"
PIPE_ID=310
ANCHOR="com.apple/hexvm-nettest"
cleanup() {
sudo pfctl -a "$ANCHOR" -F all >/dev/null 2>&1 || true
sudo dnctl delete "$PIPE_ID" >/dev/null 2>&1 || true
}
trap cleanup EXIT INT TERM
sudo dnctl pipe "$PIPE_ID" config bw 8Mbit/s delay 120 plr 0.02
printf 'dummynet out quick proto tcp from any to %s port 443 pipe %s
' \
"$TARGET_IP" "$PIPE_ID" |
sudo pfctl -a "$ANCHOR" -f -
sudo pfctl -E >/dev/null
sudo pfctl -a "$ANCHOR" -sr -v
curl -sS -o /dev/null \
-w 'code=%{http_code} connect=%{time_connect} total=%{time_total}
' \
"$TEST_URL"
先用只读健康检查验证规则命中,再运行会写数据的业务测试。不要执行 pfctl -d 作为清理手段,因为机器上可能已有其他防火墙规则。脚本只清空自己的 anchor,并只删除自己的 pipe,避免影响并行任务。
防止远程会话被误伤
执行前保留第二个管理终端,并确认规则中没有管理端口、默认网关或任意目标通配符。若测试目标和管理连接共用同一 IP,应改用独立测试入口,不要依赖规则顺序碰运气。
把客户端行为变成自动化断言
弱网验收不能只看请求“有没有回来”。测试代码应记录尝试次数、最终错误类型、总等待时间和请求标识。对于写操作,服务端与客户端都应使用稳定的幂等键;发生超时后,客户端先查询结果,再决定是否重试。
可将网络层替换为可观测封装,并检查以下边界:
- 单次连接超过阈值后能够取消,不无限等待。
- 重试次数有上限,间隔包含退避,不形成请求风暴。
- 用户主动取消后,不再由后台任务偷偷重发。
- 上传中断时保留明确状态,不把失败显示为完成。
- 网络恢复后可以继续操作,但不会重复创建记录。
模拟器测试可通过明确的设备标识运行,避免依赖机器上恰好存在的设备名称。
export SIMULATOR_UDID="replace-with-booted-simulator-udid"
xcodebuild test \
-scheme NetworkBehaviorTests \
-destination "platform=iOS Simulator,id=${SIMULATOR_UDID}" \
-resultBundlePath build/NetworkBehavior.xcresult
每个网络场景单独保存结果包,并把场景参数写进测试日志。这样失败记录才能回答“在哪种延迟和丢包下失败”,而不是只留下一个无法复现的红色状态。
清理规则并完成回归
脚本正常退出或收到中断信号时会执行清理,但远程会话异常关闭后仍需人工核对。重新连接后运行:
sudo pfctl -a com.apple/hexvm-nettest -sr
sudo dnctl list
curl -sS -o /dev/null \
-w 'code=%{http_code} connect=%{time_connect} total=%{time_total}
' \
"$TEST_URL"
专用 anchor 应为空,列表中不应再有编号为 310 的 pipe。随后重新执行基线请求,确认连接时间与总耗时恢复到测试前的正常区间。
最后做一次无整形回归,重点检查弱网修复是否改变了正常网络下的请求顺序、缓存命中和界面响应。把场景参数、客户端版本、目标接口版本、结果包路径与清理结果放进同一份运行记录。这样,弱网测试才从一次性的手工演示变成可审计、可复现、可安全退出的工程流程。
常见问题
为什么不能直接限制云端 Mac 的全部出站流量?
全局限速或丢包可能同时影响 SSH、VNC、依赖下载和测试流量,导致管理会话中断。应只匹配受控测试接口的目标 IP、协议与端口。
弱网测试结束后怎样确认规则已经清理?
先清空专用 pf anchor,再删除对应的 dnctl pipe,随后检查 anchor 中没有规则,并重新执行基线请求确认延迟与吞吐恢复。
弱网测试至少要验证哪些客户端行为?
至少验证连接超时、有限次数重试、请求取消、离线提示、重复提交防护,以及网络恢复后能否继续操作且不产生重复数据。
为下一次构建选择独享云端 Mac
按工作负载选择 Mac mini 机型、租期与区域。每笔订单对应独立物理节点,实际可用性以控制台实时返回为准。