HexVM 工程实践

云端 Mac 弱网复现:可回滚的链路整形实战

云端 Mac 弱网复现:可回滚的链路整形实战

远程调试 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,应改用独立测试入口,不要依赖规则顺序碰运气。

把客户端行为变成自动化断言

弱网验收不能只看请求“有没有回来”。测试代码应记录尝试次数、最终错误类型、总等待时间和请求标识。对于写操作,服务端与客户端都应使用稳定的幂等键;发生超时后,客户端先查询结果,再决定是否重试。

可将网络层替换为可观测封装,并检查以下边界:

  1. 单次连接超过阈值后能够取消,不无限等待。
  2. 重试次数有上限,间隔包含退避,不形成请求风暴。
  3. 用户主动取消后,不再由后台任务偷偷重发。
  4. 上传中断时保留明确状态,不把失败显示为完成。
  5. 网络恢复后可以继续操作,但不会重复创建记录。

模拟器测试可通过明确的设备标识运行,避免依赖机器上恰好存在的设备名称。

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 中没有规则,并重新执行基线请求确认延迟与吞吐恢复。

弱网测试至少要验证哪些客户端行为?

至少验证连接超时、有限次数重试、请求取消、离线提示、重复提交防护,以及网络恢复后能否继续操作且不产生重复数据。

独享 Apple Silicon

为下一次构建选择独享云端 Mac

按工作负载选择 Mac mini 机型、租期与区域。每笔订单对应独立物理节点,实际可用性以控制台实时返回为准。

选择云端 Mac 方案