流式分割/合并命令?

流式分割/合并命令?

splitLinux 中是否存在流媒体版本?

我正在尝试通过 SSH 备份大量数据,但 SSH 的单线程加密限制了传输。这台机器没有硬件 AES 支持,所以我使用 ChaCha 加密,但 cpu 仍然跟不上网络。

所以我想我可以通过将数据流分成两部分,并通过单独的 SSH 连接发送每个数据流,然后在目的地将数据流合并在一起来解决这个问题。这样加密负载就可以在多个 CPU 核心上共享。这看起来是一个足够普遍的想法,它应该已经存在,但我找不到它。

编辑:对于某些数字,我正在从旧计算机备份数据,通过千兆位有线网络备份数百 GB 的数据。我正在从分区复制图像,因为这比在旋转的铁锈驱动器上进行单个文件访问要快,所以从技术上讲,它是随机访问数据,但它太大了,无法如此处理。我尝试压缩它,但这并没有多大帮助。数据不太可压缩。

所以我正在寻找的是一个split(和相应的merge)它将二进制数据流分割成多个流(可能按固定块分割)。

答案1

像这样的东西:

parallel --block -1 --pipepart --recend '' -a bigfile 'ssh dst cat \>{#}'

完成后,cat文件一起:

ssh dst cat $(seq $(parallel --number-of-threads)) '>' bigfile.bak

您需要有空间容纳 2 倍大文件。

答案2

事实上,GNUparallel可以完成解决这个问题所需的大部分样板操作,因为它可以直接按块读取可查找文件并将每个文件提供给并发作业。但是,特别是在旋转硬盘上操作时,我认为建议以固定大小的块而不是整体来parallel管理文件。--block接收方将由每个作业进行处理,只需让它们通过dd seek=+ 复制命令远程传送其特定块即可直接地到目标文件。

这是一个功能齐全的脚本,可以完成所有工作:

#!/bin/sh --

# For improved performance, and even in presence of pubkey authentication,
# it is advisable to use ssh master mode, so that each job spawn by `parallel`
# does not undergo the entire authentication handshake. Otherwise a
# several-hundred-GBs file, even split in chunks of 100MB each, would
# force the whole operation to spend several minutes as a total just on ssh
# authentication handshakes.
trap 'for c in c?; do ssh -S "$c" -O exit .; done' EXIT
for c in $(seq 1 $(parallel --number-of-cores)); do
    ssh -fnNMS "c$c" -o ControlPersist=yes "$@" || exit
done

# let's say 10 megabytes each chunk
csize="$((10*1024*1024))"

parallel --pipepart -a /dev/sr0 --recend '' --block "$csize" '
    ssh -S c{%} localhost "{
        dd bs='"$csize"' seek="$(({#} - 1))" count=1 \
            iflag=fullblock conv=sparse,notrunc # && \
#            head -c '"$csize"'
        } 1<>test 2>/dev/null"
'
# Note this assumes GNU `dd` on the destination side, which supports
# the `iflag=fullblock` operation mode. Lacking that, one would use `dd`
# only as a seeking-capable command, hence employing a `count=0` option in
# place of the `count=1`, operating over the _same_ file-descriptor later used
# by a `head -c` command as in the commented-out line.
# Note also that `conv=sparse` option is not POSIX, though it is supported by
# both GNU and BSD `dd`. In case your `dd` does not support that option then
# just leave it out and `dd` will simply operate somewhat slower (YMMV).

请注意,注释掉的行(尽管不是仅注释行)最好从实际脚本中删除,因为某些 shell 不支持嵌入注释。

您将在发件人机器如:

$ sh script /dev/sda user@receiver-machine

该操作的想法是使用足够大的块大小来使 HDD 速度饱和,而不会使其磁头紧张,同时parallel作业保持对足够相邻的磁盘扇区的稳定读取(假设您直接读取 /dev/sdaX)以使用内核的自己的块设备缓存。如果我们让parallel处理的话,这是不可能获得的全部的serveral-hundred-GBs 文件,因为它的并发作业会请求彼此相距很远的查找和读取操作,因此迫使磁盘来回移动其磁头,同时不断使内核的块设备缓存失效(因此也可能进行换入换出) )。当然,我还假设您的旧机器没有数百 GB 的 RAM。


FWIW 请注意,您也可以在这种情况下使用常规split命令,而且它是非常安全的。只是它的性能不会那么好。我做了一些快速测试,split实际上结果比我偶尔为了比较而制作的替代“样板”纯 shell 脚本还要慢,更不用说使用 GNU 的解决方案了parallel

无论如何,仅作为记录,split在此类情况下,一个自然的配套工具是paste,它本质上执行“循环”读取,可以与 . 执行的循环写入相匹配split -n r/X

以下是 4 分流变速箱的实际示例:

#!/bin/bash --

sfile="${1:?}"; shift

# Side note: uncomment the following lines if you don't have ssh-keys to authenticate
# to the sender machine and thus you need to enter passwords. Note that this requires
# master mode enabled and allowed by the ssh client, and connection multiplexing enabled
# and allowed by the ssh server. Both are typically enabled by default in OpenSSH.
#for cnum in {1..4}; do
#    ssh -fnNMS "c$cnum" -o ControlPersist=yes "$@" || exit
#done
#trap 'for cnum in {1..4}; do ssh -S "c$cnum" -O exit -; done' EXIT

LC_CTYPE=C paste -d \\n \
    <(ssh -S c1 "$@" "LC_ALL=C split -n r/1/4 $sfile") \
    <(ssh -S c2 "$@" "LC_ALL=C split -n r/2/4 $sfile") \
    <(ssh -S c3 "$@" "LC_ALL=C split -n r/3/4 $sfile") \
    <(ssh -S c4 "$@" "LC_ALL=C split -n r/4/4 $sfile")

作为脚本制作,您可以在接收者机器如:

$ bash script /dev/sda user@sender-machine > file

之后,您很可能需要调整接收器计算机上创建的大小file(如果有兴趣,可以提供关于为什么经常需要这样做的详细解释)。您可以通过简单的调整:

$ truncate -s <real-size> file

就这样。

可能需要注意一个(主要是理论上的)警告,因为split -n r/..本质上是通过换行符进行拆分,如果输入数据根本没有换行符,那么根本就不会进行拆分,并且全部数据量将通过一个连接传输示例中的四个。

华泰

答案3

手动“分割”

如果(本地)源文件和(远程)目标文件是可查找的,那么一种简单的解决方案是手动计算偏移量和大小并运行两个或多个管道,每个管道传输文件的一个片段。示例(在源端):

dd if=/source/file bs=10M count=1000 skip=0 iflag=fullblock \
| ssh user@server 'dd of=/destination/file bs=10M seek=0 conv=notrunc'

这将传输前 1000 MiB ( 1000x 10M) 的数据。然后使用skip=1000seek=1000传输第二个 1000 MiB。然后skip=2000seek=2000。等等。您可以同时运行两个或多个此类命令。

事实上,传输第一部分的命令不需要skipnor seek(因为0这是两者的默认值);并且不需要传输最后一部分的命令count(因为两者都dd将在 EOF 处终止)。

所以如果你想跑步 ssh同时处理并覆盖整体/source/file,然后选择bs,得到一个计算器并计算每个的公共值count和各自的skip/值seek几乎相等的部分。最后一部分可以稍小或稍大,只需不指定它count即可。

笔记:

  • bs=10M可能不适合您。如果需要,请更改并重新计算。

  • conv=notrunc对于除了最后一个块之外的每个块都很重要。

  • 使用可以瞬间seek=扩展稀疏。/destination/file这可能会导致碎片。为了避免这种情况,请提前fallocate使用/destination/file。请注意,某些(旧的、非 *nix)文件系统不支持在不实际写入内容的情况下扩展文件,因此在写入零时可能需要很长时间seek=fallocate

  • iflag=fullblock不便于携带。一般情况下国旗很重要。当从一个读取常规的 /source/file大概不需要它。但如果你能用它,那就用它。重点是,即使是部分阅读也算在内count;如果没有iflag=fullblock本地人dd可以到达count并很快停止阅读。

    另一种解决方案就像dd skip=… … | head -c … | ssh …没有count=.我想如果dd从不可查找的流中读取,那么skip没有iflag=fullblock可能会跳过比应有的更少的内容。但是当从常规文件读取skip只是移动指针时,它应该是安全的。head -c虽然不便于携带。

  • GNUdd支持iflag=skip_bytes,count_bytesoflag=seek_bytes.这些可以大大简化您的计算。例如,以下命令将分别传输 的前 200 GiB /source/file、接下来的 300 GiB 和其余部分:

    dd if=/source/file bs=10M iflag=count_bytes                      count=200G | ssh user@server 'dd of=/destination/file bs=10M conv=notrunc'
    dd if=/source/file bs=10M iflag=skip_bytes,count_bytes skip=200G count=300G | ssh user@server 'dd of=/destination/file bs=10M conv=notrunc oflag=seek_bytes seek=200G'
    dd if=/source/file bs=10M iflag=skip_bytes             skip=500G            | ssh user@server 'dd of=/destination/file bs=10M              oflag=seek_bytes seek=500G'
    

    当然,为了解决您的问题,您应该并行运行这些命令(例如在单独的终端中)。iflag=fullblock不需要,因为工具计算的是字节,而不是块。请注意,即使更小/destination/file,至少也会增长到。500G/source/file

坏处

此方法不/source/file按顺序读取和写入/destination/file,它不断从一个偏移量跳转到另一个偏移量。如果涉及机械驱动器,则该方法可能执行得很差和/或使驱动器紧张。


替代方法

该方法可以将数据从可搜索或不可搜索的源传输到可搜索或不可搜索的目的地。它不需要提前知道数据的大小。它按顺序读取和写入。

可以创建一个脚本,当在发送方上运行时,该脚本将设置所有内容并在接收方上运行正确的代码。我决定保持解决方案相对简单,因此有一段代码要在发送方上运行,一段代码要在接收方上运行,再加上一些手动管道要做。

在发送机器上,您将需要bashifnembufferhead支持-c。在接收机器上,您将需要相同的工具集。有些要求可以放宽,请查看下面的注释。

将以下代码保存sender在发送机器上,使其可执行:

#!/bin/bash

declare -A fd
mkfifo "$@" || exit 1

for f do
   exec {fd["$f"]}>"$f"
done

while :; do
   for f do
      head -c 5M | ifne -n false >&"${fd["$f"]}" || break 2
   done
done

rm "$@"

将以下代码保存receiver在接收机器上,使其可执行:

#!/bin/bash

declare -A fd
mkfifo "$@" || exit 1

for f do
   exec {fd["$f"]}<"$f"
done

while :; do
   for f do
      <&"${fd["$f"]}" head -c 5M | ifne -n false || break 2
   done
done

rm "$@"

这些脚本非常相似。

在发送机器上运行sender带有任意数量参数的:

</source/file ./sender /path/to/fifoS1 /path/to/fifoS2 /path/to/fifoS3

这将创建 fifos fifoS1, fifoS2, fifoS3。使用更少或更多的 fifo,由您决定。 fifo 的路径可能是相对的。将sender等待从 fifo 读取数据。

在接收机器上运行receiver具有相同数量参数的命令:

>/destination/file ./receiver /location/of/fifoR1 /location/of/fifoR2 /location/of/fifoR3

这将创建 fifos fifoR1, fifoR2, fifoR3。 fifo 的路径可能是相对的。将receiver等待将数据写入 fifo。

在发送计算机上,通过mbuffer,ssh和 remote将每个本地 fifo 连接到相应的远程 fifo mbuffer

</path/to/fifoS1 mbuffer -m 10M | ssh user@server 'mbuffer -q -m 10M >/location/of/fifoR1'
</path/to/fifoS2 mbuffer -m 10M | ssh user@server 'mbuffer -q -m 10M >/location/of/fifoR2'
# and so on

并行运行这些命令。将所有本地 fifo 连接到其远程对应项后,数据将开始流动。如果不明显:提供给 的第 N 个参数receiver是提供给 的第 N 个参数的对应项sender

笔记:

  • sender可以从管道中读取;receiver可以写入管道。

  • 将可查找源传输到可查找目标时,您可以通过管道传输dd skip=… …sender和 管道传输receiver到 来恢复中断的传输dd seek=… …。请记住本答案上一节中讨论的GNU 的局限性dd和能力。dd

  • -cforhead在脚本中就像-bfor split。这种大小的块以循环方式发送到 fifo。如果您决定更改-c 5M,请在两个脚本中使用相同的值;这是至关重要的。还要相应地调整-ms mbuffer

  • mbuffers 及其缓冲区的大小对于性能很重要。

    • 如果发送端的缓冲区太小,那么发送端的sender速度就会减慢,等待其中一个缓慢加密ssh,而其他ssh缓冲区则空闲地等待sender继续传输。我认为对于mbuffer发送端的每个,您应该至少使用一个块的大小。在示例中,我使用了两倍的 ( -m 10M)。
    • 如果接收端的缓冲区太小,并且receiver在移动到下一个管道之前设法耗尽其中一个缓冲区,则其他ssh缓冲区可能会停止,因为mbuffer它们写入的缓冲区将变满。我认为对于mbuffer接收方的每个,您应该至少使用一个块的大小。在示例中,我使用了两倍的 ( -m 10M)。
  • bash当事先不知道 fifo 的数量时,需要处理文件描述符。sh在 fifo 数量固定的情况下,将脚本移植到 pure应该相对容易。

  • headdd可以用支持替换iflag=fullblock

  • mbuffer+管道ssh应在senderreceiver终止后自动终止。您不需要手动终止这些管道。

  • ifne用于检测EOF。您可以ifne从 中删除每个文件sender,但是您需要手动终止脚本并在完成工作后删除 fifo。了解工作何时完成的一个好方法是在前面./sender加上(cat; echo "done" >&2)

    (</source/file cat; echo "done" >&2) | ./sender …
    

    当你看到 时done,等待每个mbuffer都变空并空闲。然后您可以终止sender,例如使用Ctrl+ C

    同样,您可以ifne从其中删除每个项目receiver,并在您相当确定它已完成时手动终止它。请注意,如果receiver通过管道传输到其他命令,那么通常您无法轻易判断它已完成。即使您认为您可以判断,那么Ctrl+C可能也不是最好的主意,因为它也会发送SIGINT到其他命令。

    有了 就容易多了ifne。在 Debian 中ifne是在moreutils包中。

答案4

由于我需要解决一个问题来学习 Rust,所以我决定自己编写一个程序来解决这个问题。它可以在https://github.com/JanKanis/streamsplit。例子:

streamsplit -i ./mybigfile split 2 'ssh remoteserver streamsplit merge -o ./destinationfile'

相关内容