别再只跑一次 speedtest:用 dd 把网络测速和稳定性测成一条证据链
先说结论
一次测速软件给出的“最高速度”,只能回答某个时刻、某条线路、某个服务端的一个瞬间。要判断一条链路能不能稳定承载备份、远程桌面、容器拉镜像或 Agent 调用,最好用自己能控制的两端,反复发送固定大小的数据,并同时记录方向、耗时、退出码、波动和错误原因。
dd正好能把数据量钉死,ssh负责把字节送过网络,另一端的dd of=/dev/null负责接住后立即丢弃。本文把
dd | ssh | dd变成一套可复用的网络实验:先解释为什么“跑一次”不够,再展示上传、下载、长稳和故障判读;最后给出 Windows 11、Ubuntu 26.04、macOS 26 三套一键脚本,以及人工执行和 Agent 自动配置两种方法。脚本默认小流量、只写/dev/null,不依赖第三方测速服务。文中的实测数字来自脱敏的隔离实验,不是任何线路的 SLA 承诺。
图 1:原创 SVG 封面。核心思想只有一句话:速度是一个数字,稳定性是一组分布。
一、问题背景:为什么“测速 800 Mbps”仍然会卡
1.1 一次测速像只看了一秒钟的天气
很多人遇到网络问题时,第一反应是打开测速网页,看到一个很大的数字,然后下结论:“带宽没问题,肯定是应用的问题。”这一步并没有错,只是证据不完整。
测速网页通常把数据送到它选择的服务端,路径、并发连接、协议、服务端负载和浏览器实现都由它决定。你看到的是“这台电脑到那个服务端,在这一刻,用那套协议的成绩”。而真正让你烦恼的业务,可能走的是另一条路径:备份去某个存储节点,远程终端走一条长延迟隧道,Agent 的模型请求还要经过代理、TLS 和重试。
打个比方:你在小区门口测到水管瞬时流量很大,不代表把水送到顶楼、连续接水半小时也不会忽大忽小。网络也有“水压”:平均速度、方向不对称、瞬时排队、重传和连接建立时间,都会影响实际体验。
1.2 dd 为什么能帮忙
dd 原本是一个字节搬运工:从 if(input file)读数据,按 bs(block size)分块,读 count 个块,再写到 of(output file)。/dev/zero 会源源不断地产生零字节,/dev/null 会把收到的字节吃掉。因此我们可以把它放在网络管道两端:
图 2:原创图。测试不会在远端留下大文件,但测到的是“本机生产数据 → SSH 加密 → 网络路径 → 远端接收”的端到端时间。
本地 dd if=/dev/zero ── stdin/stdout ──> ssh ──> 远端 dd of=/dev/null
它的优点是简单、可审计、两端都能看见退出码;缺点也要说清楚:这不是裸 TCP 线速仪,它包含 SSH 握手、加密、远端 shell 和两端 CPU 的开销。正因为如此,它更接近“这条 SSH 通道能给我的应用多少吞吐”,而不是宣传页上的理论带宽。
二、先把 dd 的几个开关弄懂:最容易被忽略的是 iflag=fullblock
2.1 四个参数就是四个量杯
if=/dev/zero:数据从哪里来;of=/dev/null:数据到哪里去;bs=1048576:每个块 1 MiB;用数字写法,GNU 和 BSDdd都容易理解;count=32:搬运 32 个完整块,也就是 32 MiB。
status=progress 很适合现场看进度,但它不是所有 BSD 版本都一致支持,所以脚本把诊断输出重定向掉,自己用墙上时钟计算耗时。真正跨管道时还要加 iflag=fullblock:它告诉 dd,一次 read() 只拿到半个块时不要急着把它算成一个完整记录,继续读到块填满。
如果没有这个选项,管道可能出现这样的错觉:本来以为发送了 4 MiB,实际上因为一次只读到 64 KiB,count=4 很快就结束了。数据量变小,速度数字反而变得“漂亮”。这就像规定“数四杯水”,却把四个半杯也当成四杯,账面完成得很快,水桶却没装满。
本地基线也要使用完整块:
dd iflag=fullblock if=/dev/zero bs=1048576 count=32 \
of=/dev/null 2>/dev/null
2.2 四只时钟决定一个结果
一次 dd 传输至少同时包含四段时间:产生/读取数据、建立 SSH、真正在线路上传输、远端进程接收。把它们混成一个数字没关系,但报告里必须说明定义,否则别人无法复现。
图 3:原创图。小 payload 时,握手时间占比很大;大 payload 更接近持续吞吐,但会消耗更多流量。
三、测试设计:先做基线,再测两个方向,最后看分布
3.1 三种测试回答三种问题
图 4:原创图。上传快不代表下载快,单向结果不能替代双向结果。
| 测试 | 数据路径 | 主要回答的问题 |
|---|---|---|
| 本地基线 | dd → /dev/null |
本机产生和丢弃字节的上限是多少? |
| 上传 | 本地 dd → SSH → 远端 dd |
客户端到服务端的路径怎样? |
| 下载 | 远端 dd → SSH → 本地 dd |
服务端到客户端的路径怎样? |
3.2 单位别混:MiB 不是 MB,Mbps 也不是 MiB/s
本文用 MiB(2²⁰ 字节)计算,因为 bs=1048576 很明确。网络设备常用十进制 Mbps(10⁶ bit/s)。换算时先把 MiB/s × 8 得到 Mib/s,再根据需要换成 Mbps;不要把 100 MiB/s 直接写成 100 Mbps。测速报告至少写出 payload、方向、墙上耗时和单位。
3.3 固定 payload,重复 5~10 轮
只跑一轮无法分辨“偶然排队”和“长期波动”。建议从 16 或 32 MiB、5 轮开始;链路允许时再把 payload 提到 64 或 128 MiB。总流量大约是:
总流量 ≈ payload × 轮数 × 方向数
例如 32 MiB × 5 轮 × 2 个方向,就是约 320 MiB(不含 SSH 协议开销)。先征得链路所有者同意,别在共享出口上做“压力测试”。
图 5:原创图。稳定性不是“最后一次成功”,而是整组样本的形状。
四、手工跑一轮:两端都不落盘
4.1 先确认远端真的有 dd
远端只需要一个可登录的 POSIX shell、dd 和 /dev/null。先做只读检查,不要上来就发大流量:
ssh -T -o BatchMode=yes -o Compression=no \
testuser@test-endpoint \
'command -v dd && command -v sh'
首次连接应人工核对主机指纹。代码里的 testuser@test-endpoint 只是占位符,公开文章和脚本里不放真实地址、机器名或密钥。
4.2 上传:本地生产,远端接收
/usr/bin/time -p sh -c \
'dd iflag=fullblock if=/dev/zero bs=1048576 count=64 2>/dev/null |
ssh -T -o BatchMode=yes -o Compression=no \
testuser@test-endpoint \
"dd iflag=fullblock of=/dev/null bs=1048576 count=64 2>/dev/null" \
>/dev/null'
time 打印的 real 就是这一次的墙上耗时。因为块大小固定为 1 MiB,粗略吞吐是 64 ÷ real MiB/s。若退出码不是 0,先看 SSH 错误,不要把失败强行写成“0 MiB/s”。
4.3 下载:远端生产,本地接收
/usr/bin/time -p sh -c \
'ssh -T -o BatchMode=yes -o Compression=no \
testuser@test-endpoint \
"dd iflag=fullblock if=/dev/zero bs=1048576 count=64 2>/dev/null" |
dd iflag=fullblock of=/dev/null bs=1048576 count=64 2>/dev/null'
这两个方向一定要分开跑。很多家庭宽带、VPN、云主机安全策略和 Wi-Fi 节能机制都会造成上下行不对称。
4.4 最小重复循环
不想马上下载完整脚本,可以先用下面的 Bash 片段。它使用 pipefail,任一端失败都会让这一轮失败:
set -o pipefail
for round in 1 2 3 4 5; do
start=$(perl -MTime::HiRes=time -e 'printf "%.6f", time')
dd iflag=fullblock if=/dev/zero bs=1048576 count=32 2>/dev/null |
ssh -T -o BatchMode=yes -o Compression=no \
testuser@test-endpoint \
'dd iflag=fullblock of=/dev/null bs=1048576 count=32 2>/dev/null' \
>/dev/null
rc=$?
end=$(perl -MTime::HiRes=time -e 'printf "%.6f", time')
seconds=$(awk -v s="$start" -v e="$end" 'BEGIN { print e-s }')
rate=$(awk -v s="$seconds" 'BEGIN { print 32/s }')
printf 'upload round=%d rc=%d seconds=%.3f MiB/s=%.1f\n' \
"$round" "$rc" "$seconds" "$rate"
done
五、一次真实实验:平均值不错,但上传波动更大
我在一条经过授权的隔离 SSH 路径上,用 32 MiB payload 各跑 5 轮。地址、用户名和机器信息均已从截图中移除;Compression=no,两端都用 /dev/null,所以结果不代表磁盘性能,也不代表互联网 SLA。
5.1 本地基线:生产者不是瓶颈

图 6:真实终端输出的脱敏截图。本地基线约 4,043~5,173 MiB/s,远高于后面的 SSH 路径,说明这次实验主要观察网络和加密通道。
5.2 上传:平均约 38.0 MiB/s,CV 16.7%

图 7:真实终端输出的脱敏截图。五轮为 43.5、33.6、42.8、27.6、42.4 MiB/s;最慢与最快相差约 36.6%。
5.3 下载:平均约 68.9 MiB/s,CV 4.7%

图 8:真实终端输出的脱敏截图。五轮为 70.7、72.4、70.7、63.2、67.8 MiB/s;这条样本的下载方向明显比上传稳定。
5.4 汇总:零失败不等于没有长尾

图 9:真实终端输出的脱敏截图。两方向共 10 轮没有传输失败,但上传 CV 明显高于下载;这只是一个时间窗口,不能替代不同时间段的复测。
同一实验还做了 5 次 ICMP 探测:丢包率为 0%,平均 RTT 约 104 ms,但最大 RTT 突然达到约 519 ms。这个组合很典型:没有丢包,不代表没有排队长尾。远程终端卡顿、语音断续或 Agent 首字延迟变大,往往先表现为尾延迟,而不是 ping 直接丢包。
六、怎样读结果:把“快不快”拆成四个问题
图 10:原创图。像看体检报告一样看测速:一个平均值不能替代方向、波动和错误分类。
- 吞吐:
payload / wall-clock seconds。先比较同一 payload、同一方向,再谈不同链路。 - 波动:记录 min、max、平均值和 CV(标准差 ÷ 平均值)。CV 高说明这组样本不稳定,但不自动说明根因。
- 可靠性:看每轮退出码、SSH stderr 和是否提前结束。
rc=0是必要条件,不是质量证明。 - 体验长尾:把 ping 的 P95/P99、SSH 建连时间、应用首字延迟放到同一时间轴。速度高但长尾大,交互仍会卡。
6.1 常见根因与下一步
- 小 payload 特别慢,大 payload 变快:多半是 SSH 握手和进程启动占比太高;改用更大 payload,或使用持久化连接测持续吞吐。
- 上传和下载差很多:检查路由是否对称、VPN 出口、QoS、Wi-Fi 发射功率和云主机限速。
- 速度忽高忽低但没有失败:看队列、拥塞、CPU steal、加密算法和同一时段的其他流量;不要只调 TCP 缓冲区。
- 突然
Connection refused或超时:先判定端口/防火墙/服务状态,再讨论带宽。错误本身就是证据。 - 结果只有几百 KiB,却显示很高速度:优先怀疑管道短读和缺少
iflag=fullblock。

图 11:真实终端输出的脱敏截图。连接失败不能悄悄转换成 0 MiB/s;把错误分类保留下来,排障才有方向。
七、安全边界:测速脚本最怕“顺手写满磁盘”
7.1 of=/dev/null 和 of=./test.bin 是两回事
网络测速只需要测字节流,不需要留下文件。不要把远端 sink 改成当前目录,也不要在生产盘上加 conv=fsync 伪装网络测试;那会把磁盘写入、文件系统缓存和网络混在一起,还可能把空间打满。若确实要测“网络 + 落盘”,请另写一个有容量上限、可清理、得到授权的磁盘实验。
7.2 四条护栏
图 12:原创图。测试前先问“我是否有权限、最坏会产生多少流量、失败时怎样停”。
- 先获授权:只测自己拥有或明确获准的两端;共享出口不要随意跑长时间大 payload。
- 设流量上限:脚本把单轮 MiB 和轮数都限制在合理范围,先 dry-run,再执行。
- 保留 SSH 安全性:使用密钥和
BatchMode=yes,不要把密码、私钥或跳板凭据写入脚本;首次accept-new后仍应核对指纹。 - 能停下来:保留连接超时和 keepalive;看到异常波动就停止,不要用并发把问题放大。
八、三套一键脚本:Windows 11、Ubuntu 26.04、macOS 26
完整脚本已经随文章放进仓库,可直接下载:
- Ubuntu 26.04:dd-network-test-ubuntu2604.sh
- macOS 26:dd-network-test-macos26.zsh
- Windows 11:dd-network-test-windows11.ps1
三套脚本的行为保持一致:默认 16 MiB、每个方向 5 轮;远端和本地都把字节送进 /dev/null;使用 iflag=fullblock 防止短读;打印每轮 seconds、MiB/s、退出码和最终 CV;任何一轮失败,脚本返回非零退出码。
8.1 Ubuntu 26.04
chmod +x dd-network-test-ubuntu2604.sh
./dd-network-test-ubuntu2604.sh \
--remote-host test-endpoint \
--remote-user testuser \
--size-mib 32 --rounds 5 --direction both
先看计划、不发字节:
./dd-network-test-ubuntu2604.sh --remote-host test-endpoint --dry-run
脚本只使用系统已有的 bash、dd、ssh、awk,计时优先使用系统 Perl,找不到时才回退到 Python 标准库;没有调用第三方测速 API。
8.2 macOS 26
chmod +x dd-network-test-macos26.zsh
./dd-network-test-macos26.zsh \
--remote-host test-endpoint \
--remote-user testuser \
--size-mib 32 --rounds 5 --direction both
macOS 版本使用系统 zsh 和 OpenSSH,不要求安装 WSL 或额外测速软件。若本机 dd 报告不认识 iflag=fullblock,先升级系统或在经过审核的环境里使用兼容的 GNU coreutils;不要直接删掉该选项后把短读当成完整块。
8.3 Windows 11
Windows 原生没有 /dev/zero,所以 PowerShell 脚本用 .NET 生成零字节并写入 Windows 自带的 ssh.exe 标准输入,远端仍由 dd 接收;下载方向则读取 SSH 标准输出后丢弃,不需要 WSL,也不会创建临时文件。
Set-ExecutionPolicy -Scope Process Bypass
& .\dd-network-test-windows11.ps1 `
-RemoteHost test-endpoint `
-RemoteUser testuser `
-SizeMiB 32 -Rounds 5 -Direction both
如果系统没有 OpenSSH Client,先在“可选功能”中启用微软提供的组件;脚本不会替你下载来路不明的 dd.exe。

图 13:真实检查输出的脱敏截图。当前写作环境是 macOS,因此 PowerShell 做了静态结构检查;拿到 Windows 11 后还应执行一次小 payload 的真实回环。
九、人工自动执行 vs Agent 自动配置
9.1 人工自动执行:先 dry-run,再放行流量
- 从上面的链接下载对应脚本,检查文件哈希和内容;
- 填入你有权限测试的 endpoint、账号和 SSH 端口;
- 运行
--dry-run,确认 payload、轮数、方向和总流量; - 手工核对主机指纹,再执行正式测试;
- 保存 stdout、stderr、日期、时区、客户端/服务端系统版本和链路变更,形成可比较的基线。
人工方式的价值是第一次建立“这条链路平时长什么样”的直觉。不要把脚本输出里的地址和用户名直接贴到公开工单或文章里,先做脱敏。
9.2 Agent 自动配置:给它边界清楚的任务单
你可以把下面的提示词交给已有的 Codex、Claude Code、OpenClaw 或其他 Agent。它要求 Agent 先盘点、再预演、最后才发流量:
请在当前机器上建立一份“dd + SSH 网络测速与稳定性基线”,但只操作我明确授权的远端 endpoint。
要求:
1. 先识别操作系统,选择 dd-network-test-windows11.ps1、dd-network-test-ubuntu2604.sh 或 dd-network-test-macos26.zsh;
2. 先检查 ssh、dd、awk/PowerShell 是否存在,打印版本和脚本 SHA-256;
3. 只使用我提供的 RemoteHost、RemoteUser、Port,不猜测、不扫描其他地址;
4. 先执行 dry-run,计算 payload × rounds × directions 的最坏流量,等待我确认;
5. 正式测试使用小 payload、BatchMode、Compression=no、iflag=fullblock,远端和本地都写 /dev/null;
6. 每轮记录方向、秒数、MiB/s、退出码和 stderr,失败立即停止,不把失败伪装成 0 MiB/s;
7. 完成后计算 avg/min/max/CV/失败数,并把原始日志中的 IP、完整主机名、用户名、路径和密钥全部脱敏;
8. 不修改防火墙、SSH 配置、路由、内核参数,不安装第三方测速服务;
9. 报告“测到了什么、没测到什么、下一步需要谁批准”,不要把结果写成 SLA 承诺。
Agent 自动化的边界要写在任务单里:它可以替你重复测量、整理证据和比较历史,但不能替你猜 endpoint、绕过主机指纹确认或擅自扩大流量。
十、常见问题(Q&A)
Q1:dd 测的是互联网带宽吗?
不一定。它测的是你指定的两端之间、通过 SSH 的一条具体路径。要测公网,必须在你拥有或获准使用的公网两端部署接收端;不要把陌生机器当测速服务器。
Q2:为什么浏览器测速比 dd 快很多?
浏览器测速常用多并发连接、专门协议和就近服务端;本文是一条 SSH 单通道,还包含握手和加密。两者回答的问题不同,不能直接比较数字。
Q3:iflag=fullblock 为什么这么重要?
管道读取可能返回短块,count 默认数的是读取记录,不保证总字节数。fullblock 让每个记录尽量填满,避免少发数据却得到虚高速度。
Q4:一定要 root 才能跑吗?
不需要。只要 SSH 账号能执行 dd、访问 /dev/zero 和 /dev/null 即可。更推荐最小权限账号,避免用 root 做普通测速。
Q5:Windows 为什么没有安装一个 dd.exe?
为了减少供应链和兼容风险,脚本使用系统 ssh.exe 加 .NET 字节流,只有远端执行 dd。如果你确实要本地 GNU dd,请从可信的软件源单独审核,不要下载同名未知二进制。
Q6:跑 5 轮都成功,能说网络稳定吗?
只能说这 5 个样本没有失败。还要看速度 CV、RTT 长尾、不同时间段和不同方向;稳定性是长期分布,不是一张绿色截图。
Q7:为什么建议 32 MiB,而不是 1 GiB?
32 MiB 足以压过一次 SSH 握手的相对影响,同时不会在共享链路上制造太多流量。先小后大,按授权和预算逐步增加。
Q8:能不能把 of=/dev/null 改成文件,顺便测磁盘?
可以做另一个实验,但那已经是“网络 + 文件系统 + 磁盘”的混合测试。请单独设容量上限、清理策略和维护窗口,别把它混进网络基线。
Q9:为什么不用 iperf3?
iperf3 更适合测 TCP/UDP 线速、并发和丢包;dd + SSH 的优势是几乎所有 Unix 服务器都已有,测到的路径更像真实 SSH/文件传输。两者可以互补,不必互相替代。
Q10:看到 Connection refused,是不是带宽为零?
不是。它说明目标端口没有接受连接,可能是端口、服务或防火墙问题。保留错误文本,先修可达性,再谈吞吐。
十一、写在最后:把“感觉卡”变成可以复查的证据
dd 最有价值的地方,不是它能打印一个很大的 MiB/s,而是它迫使我们把问题说清楚:谁产生数据、谁接收数据、走哪个方向、发多少、花多久、失败在哪里、波动多大。只要两端是你能控制且获准测试的机器,就能在不依赖第三方测速服务的前提下,建立自己的基线。
我的建议是把这套实验放进变更流程:网络、VPN、路由、云主机规格或 SSH 加密策略变更前后,各跑一组小 payload;把原始日志和脱敏汇总一起存档;当远程桌面或 Agent 变慢时,先复测同一方向,再决定是否改 MTU、拥塞控制或应用重试。先把证据链补齐,再动参数。
参考资料:GNU Coreutils dd 文档、OpenBSD dd 手册、OpenSSH ssh 手册、Microsoft OpenSSH for Windows、Ubuntu dd manpage、RFC 1122:Internet 主机通信要求。文中的命令和数字以写作时的系统版本与隔离实验为准,执行前请根据自己的系统和授权范围复核。