代码之家  ›  专栏  ›  技术社区  ›  Thysce

如何使用分块方法将文件流式传输到tar?

  •  0
  • Thysce  · 技术社区  · 1 年前

    我想通过ssh调用一个tar'ing脚本来备份我的系统,该脚本通过管道返回到启动ssh的主机的stdout,然后存储tar。

    但是,我想对该主机上运行的一些服务执行逻辑转储,但没有足够的磁盘空间将这些巨大的文件转储到磁盘上,然后从tar捕获它们。

    我知道,tar无法处理流(或任何大小未知的文件)。所以我想,我 split 转储在运行到固定大小的块中时,将它们临时存储在磁盘上,并将它们带到 tar 以进行处理,然后在处理下一个块之前删除它们。

    我的脚本如下所示:

    mkfifo filenames
    tar --files-from filenames -cf - &
    TAR_PID=$!
    exec 100>filenames
    
    # tar all relevant host-level directories/files
    echo "/etc" >&100
    echo "/root" >&100
    
    function splitfilter() {
      tee $1
    
      (
        # wait for tar to finish reading the file and delete it after being processed
        inotifywait -e close_nowrite $1
        rm $1
      ) &
      RM_SHELL_PID=$!
    
      # send the filename for processing to tar
      echo $1 >&100
    
      wait $RM_SHELL_PID
    }
    export -f splitfilter
    
    # perform the logical dumps of my services
    dump_program_1 | split -b 4K /var/backup/PREFIX_DUMP_1_ --filter "splitfilter $FILE"
    dump_program_2 | split -b 4K /var/backup/PREFIX_DUMP_2_ --filter "splitfilter $FILE"
    
    exec 100>&-
    wait $TAR_PID
    rm filenames
    

    然而,我不明白为什么这是随机的和无效的。 到目前为止,我观察到了两种不同的失败行为:

    • 焦油没有停止。在脚本的最后,我确实关闭了文件描述符,所以我希望fifo向tar发送EOF信号。这应该会很快结束tar过程,因为它只需要完成最后4k块的处理(如果还没有完成的话)。我无法解释为什么它会随机挂起。最终的存档实际上是完整的(除了tar的EOF标记)。。。
    • tar处理0字节文件。经过一段时间的处理 inotifywait 在tar关闭区块文件进行读取之前,唤醒。因此导致文件被删除,并因此在存档中显示为0字节大小的条目。我通过放一个 sleep 1 之后 echo $1 >&100 呼叫在那之后,前几个块确实被填满了,但运行一段时间后,后面的块又变成了0大小。我觉得这里有时间问题,但目前看不到。

    经过一天的调试,我对这种方法失去了希望,但如果它能可靠地工作,那就太好了:它实际上可以产生流式tars! 不要误解我的意思:它在调试时工作了一两次。我只是不明白为什么不总是

    0 回复  |  直到 1 年前
        1
  •  0
  •   Kaz    1 年前

    tar格式相当简单。我们可以自己用这个流式传输 TXR Lisp 程序

    注意:这不适用于长路径;它只为每个对象输出一个头块。

    备份列表由路径和命令项的混合组成。

    命令被执行,它们的输出被切成4K块,这些块成为编号文件。这些都是在我们前进的过程中删除的,所以不会累积任何内容。

    即使当我们编写自己的tar实现时,我们仍然必须这样做,因为格式要求提前知道每个对象的大小并将其放入标头中。无法将任意长的命令输出作为tar流进行流式传输。

    (defvar backup-list
      '("/etc"
        "/root"
        (:cmd "cat /proc/cpuinfo" "cpuinfo")
        (:cmd "lspci" "lspci")))
    
    (defsymacro splsize 4096) ;; split size for commands
    
    (defsymacro blsize 512) ;; tar block size: written in stone
    
    (typedef tarheader
      (struct tarheader
        (name (array 100 zchar))
        (mode (array 8 zchar))
        (uid (array 8 zchar))
        (gid (array 8 zchar))
        (size (array 12 zchar))
        (mtime (array 12 zchar))
        (chksum (array 8 char))
        (typeflag char)
        (linkname (array 100 zchar))
        (magic (array 6 char))
        (version (array 2 char))
        (uname (array 32 zchar))
        (gname (array 32 zchar))
        (devmajor (array 8 zchar))
        (devminor (array 8 zchar))
        (prefix (array 155 zchar))))
    
    (defmacro octfill (slot expr)
      ^(fmt "~,0*o" (pred (sizeof tarheader.,slot)) ,expr))
    
    ;; Dump an object into the archive.
    ;; Form a correct header, calculate the checksum,
    ;; put out a header block and for regular files,
    ;; put out data blocks.
    (defun tar-dump-object (file-in stream : stat)
      (let* ((file (trim-path-seps file-in))
             (s (or stat (stat file)))
             (tf (ecaseql* (logand s.mode s-ifmt)
                   (s-ifreg #\0)
                   (s-iflnk #\2)
                   (s-ifchr #\3)
                   (s-ifblk #\4)
                   (s-ifdir #\5)
                   (s-ififo #\6)))
             (h (new tarheader
                     name (let* ((n (cond
                                      ((equal "/" file) ".")
                                      ((starts-with "/" file) [file 1..:])
                                      (t file))))
                            (if (eql tf #\5) `@n/` n))
                     mode (octfill mode (logand s.mode #o777))
                     uid (octfill uid s.uid)
                     gid (octfill gid s.gid)
                     size (octfill size (if (eql tf #\0) s.size 0))
                     mtime (octfill mtime s.mtime)
                     chksum (load-time (str 8))
                     typeflag tf
                     linkname (if (eql tf #\2) (readlink file) "")
                     magic "ustar "
                     version " "
                     uname (or (getpwuid s.uid).?name "")
                     gname (or (getgrgid s.gid).?name "")
                     devmajor (if (meql tf #\3 #\4)
                                (octfill devmajor (major s.rdev)) "")
                     devminor (if (meql tf #\3 #\4)
                                (octfill devminor (minor s.rdev)) "")
                     prefix ""))
             (hb (ffi-put h (ffi tarheader)))
             (ck (logand (sum hb) #x1FFFF))
             (bl (make-buf blsize))
             (nb (trunc (+ s.size blsize -1) blsize)))
        (set h.chksum (fmt "~,06o\xdc00 " ck))
        (ffi-put-into bl h (ffi tarheader))
        (put-buf bl 0 stream)
        (if (eql tf #\0)
          (with-stream (in (open-file file "rb"))
            (each ((i 0..nb))
              (fill-buf-adjust bl 0 in)
              (buf-set-length bl blsize)
              (put-buf bl 0 stream))))))
    
    ;; Output two zero-filled blocks to terminate archive.
    (defun tar-finish-archive (: (stream *stdout*))
      (let ((bl (make-buf (* 2 blsize))))
        (put-buf bl 0 stream)))
    
    ;; Dump an object into the archive, recursing
    ;; if it is a directory.
    (defun tar-dump-recursive (path : (stream *stdout*))
      (ftw path (lambda (path type stat . rest)
                  (tar-dump-object path stream stat))))
    
    ;; Dump a command to the archive by capturing the
    ;; output into numbered temporary split files.
    (defun tar-dump-command (command prefix : (stream *stdout*))
      (let ((bl (make-buf splsize))
            (i 0))
        (with-stream (s (open-command command "rb"))
          (while (plusp (fill-buf-adjust bl 0 s))
            (let ((name (pic `@{prefix}0###` (inc i))))
              (file-put-buf name bl)
              (tar-dump-object name stream)
              (remove-path name))))))
    
    ;; main: process the backup list to stream out the archive
    ;; on standard output, then terminate it.
    (each ((item backup-list))
      (match-ecase item
        ((:cmd @cmd @prefix) (tar-dump-command cmd prefix))
        (`@file` (tar-dump-recursive file))))
    
    (tar-finish-archive)
    

    我没有一个回归测试套件;我手动测试了它,方法是归档各种类型的单个对象,并对其与GNU tar之间的十六进制转储进行比较,然后拆包此实现归档的目录树,对原始树进行递归diff。

    但是,我想知道您正在使用的备份服务是否无法处理链接存档。如果它处理链接的归档,那么您只需多次调用 tar 以产生流,并且不存在所有这些过程协调问题。

    对于tar消费者来说,要处理链接的存档,它只需要忽略所有的零块(不将它们视为存档的末尾),而是继续读取。

    如果备份服务是这样的,那么您基本上可以按照以下方式进行:

    (tar cf - /etc
     tar cf - /root
     dump_program_1 | \
       split -b 4K /var/backup/PREFIX_DUMP_1_ \
             --filter "tar cf - $FILE; rm $FILE"
     ...) | ... into backup service ...
    

    我在GNU Tar中看不到任何不写终止零的选项。可以编写一个过滤器来消除这些问题:

    tar cf - file | remove-zero-blocks
    

    尚未写入 remove-zero-blocks 通过面向块的FIFO读取512字节块的过滤器,该FIFO的长度足以覆盖 焦油 它将一个新读取的缓冲区放入FIFO的一端,并写入从另一端溢出的最旧的缓冲区。当遇到EOF时,FIFO被刷新,但所有为零的512字节的尾部块都被省略。

    这应该会击败拒绝忽略零块的备份服务。