问题
当我在 *NIX 中寻找管道缓冲工具时,我看到使用buffer
、mbuffer
或的建议pv
。但是,前两个并不总是在发行版的官方存储库中(例如 Arch),而pv
(从 1.6.0 开始)有一个错误阻止了此功能。在其他几个问题中,我看到提到dd
用作缓冲区,我想探索它,因为dd
它总是在那里。但是,没有一个足够详细以至于真正有意义,所以在这里我要求一种“正确”的方式来使用它。
提到的问题dd
包括https://unix.stackexchange.com/questions/345072/can-dd-be-used-to-add-a-buffer-to-a-pipe和https://unix.stackexchange.com/questions/21918/utility-to-buffer-an-unbounded-amount-of-data-in-a-pipeline
为了方便测试,我在下面提供了一个测试脚本,并附上了一些我自己的实验的注释。代码清单后面会详细说明。运行前请确保您已pv
安装并至少有 256M 内存!
#!/bin/sh
producer() {
while [ 1 ]; do
dd if=/dev/zero bs=64M count=1 iflag=fullblock status=none
sleep 4
done
}
buffer() {
# Works, but long
# Must at least fill 32M before consumer starts
# So, must choose small obs and chain more to look
# more like a proper "buffer"
dd obs=32M status=none | \
dd obs=32M status=none| \
dd obs=32M status=none| \
dd obs=32M status=none
# Doesn't work, producer rate limited
#dd bs=128M status=none
# Doesn't work, producer must fill buffer, then
# must wait until buffer is empty
#dd obs=128M status=none
# Doesn't work, producer rate limited
#dd ibs=128M status=none
# Doesn't work, producer must fill buffer, then
# must wait until buffer is empty
#dd bs=128M status=none iflag=fullblock
}
consumer() {
pv --rate-limit 1M -q | dd of=/dev/null status=none
}
producer | pv -cN produce | buffer | pv -cN consume | consumer
这里,生产者每 4 秒产生 64MB 的数据,缓冲区为 128MB,而消费者以恒定的 1MB/s 速率消费。当然,这意味着缓冲区会很快溢出,但这是为了清楚地显示效果。理想情况下,在缓冲区填满之前(第三次生产时),我们应该看到恒定的 1MB/s 消耗,并且每次生产都产生 64MB 的数据。“正确”的输出如下所示:
produce: 128MiB 0:00:07 [ 0 B/s] [ <=> ]
consume: 7.25MiB 0:00:07 [1.01MiB/s] [ <=> ]
这里,工作解决方案如下:
dd obs=32M status=none | \
dd obs=32M status=none| \
dd obs=32M status=none| \
dd obs=32M status=none
这是通过将所需的 128MB 缓冲区拆分成 4 个块来构建的。是的,每个块都必须填满,然后数据才能传递到下一级,但由于 32MB 小于 64MB 突发,因此它适用于此测试,就好像它是一个真正的缓冲区一样。现在,有一些问题。
- 在实际应用中,我们不会遇到瞬时的数据爆发,因此数据块需要很小,但不能也小。这意味着将会有一个很长的
dd
命令链 如果在达到 32MB 标记之前遇到 EOF 怎么办?该块会丢失吗?我测试dd if=test.txt| dd obs=1M | dd obs=1M | dd of=test2.txt
并比较了结果。结果发现这不是问题。因此,使用它进行备份不会损坏数据。- 它会产生多少开销?
- 是否有更优雅的方式来实现相同的目的,通过巧妙地安排参数?
脚本中还包含其他一些尝试,但它们不起作用,如评论中所述。我尝试使用 FIFO + 后台进程,结果相同。
PS。请注意,在将 A 备份到 B 时,缓冲管道非常有用,尤其是当 A 是具有寻道时间的 HDD 时。所以我会这样做:
tar cSpf - <path> -C <root path> | <a large buffer> | <some parallel compressor> \
| <small buffer if compressor is generally slow and B have seek time> \
| dd bs=<several GB if B is not an SSD> iflag=fullblock oflag=direct of=<archive.tar.??>
答案1
我正在给出自己的答案。它可能不是最好的,但还可以。
警告
这个是经过多次测试后写在前面的。
不要链接太多的 DD 进行缓冲,否则所有 CPU 核心都可能阻塞 IO,即使您有大量内存剩余,您的计算机也会冻结!
如果您的某个外部 USB 驱动器损坏且速度缓慢,并且需要极高的 IO 强度才能进行读/写,那么这种情况会尤其有害。
例子
我基本上用尽了所有 DD 选项的组合。单个 DD 似乎无法完成这项任务,因为它无法执行异步 IO。否则,在 DD 缓冲区链中,必须先填充最大的块,然后它才能开始像 FIFO 一样工作。因此,如果您不关心填充管道时的初始延迟……两个 dd 的链就可以了。我希望其他人可以提供更优雅的解决方案,但这是一个示例用法。
示例 1:将所有文件从碎片严重的 HDD A(响应时间抖动)压缩到碎片严重的 HDD B(抖动),使用 XZ 作为压缩算法(速度慢),并行(如果您实际使用计算机,则会产生抖动)(免责声明:这是我凭空想象写的,所以细节可能有误。使用风险自负):
tar -cpSf - -C /mnt/A . | \
dd obs=1024M | dd obs=1024M | \
xz -T 0 -c | \
dd obs=1024M | dd obs=1024M | \
dd bs=512M iflag=fullblock of=/mnt/B/A.tar.xz
添加pv
以查看速度。这里,xz
仅在从 A 读取 1GB 数据后才开始(除非数据少于 1GB,否则将完成)。类似地,仅在从 xz 输出 1GB 数据后才开始向 B 写入磁盘。此代码在tar
和之间提供 2GB 缓冲区,在和写入xz
之间提供 2GB 。最后的并不是必需的,但我发现较大的(>64M)块大小可以提供更好的平均写入速度,尤其是在 USB 硬盘上。我想如果驱动器 B 正在使用中(未经证实),它也会创建更少的碎片。xz
bs=512M
示例 2. 目标:将一个巨大的文件从碎片严重的磁盘 A 复制到碎片严重的磁盘 B。
dd if=/mnt/A/file obs=<half of cache you want> | dd bs=<half of cache> iflag=fullblock oflag=direct of=/mnt/B/file
这是我能找到的最简单的形式之一。如果文件足够大,则填充缓存所需的初始时间应该可以忽略不计。同时,它异步读取/写入,并希望将足够多的写入组合在一起以获得一定的连续性能。不过,我想 SSD 不会关心块大小。
示例 3。感谢 Kamil Maciorowski,现在我的 中有以下内容.zshrc
:
buffer() {
if [ "$2" -gt 0 ] ; then
dd status=none obs="$1" | buffer "$1" $(($2-1))
else
cat
fi
}
现在,如果您需要 3 个 512M 缓冲区块,请将其链接buffer 512M 3
到管道中。通常,如果您的作业足够大,可以满足您的吞吐量要求(例如,以平均 100MB/s 的速度复制/压缩 100GB 以上的数据),那么较小的块除了可以更快地填充管道外没有任何优势(这无关紧要,因为这个时间很短)。我观察到,如果您放入太多块,CPU 可能会在 IO 上过于繁忙,以至于命令会冻结整个计算机。
现在,示例 1 变成
tar -cpSf - -C /mnt/A . | \
buffer 1024M 2 | \
xz -T 0 -c | \
buffer 1024M 2 | \
dd bs=512M iflag=fullblock of=/mnt/B/A/tar.xz