在写入时实时分割大文件

在写入时实时分割大文件

我有一个程序将生成 4 个大型二进制文件(每个文件 400GB 以上),我需要尽快将其上传到 AWS S3。

我想在文件完全写入之前开始上传;我正在尝试几种方法,我认为可能可行的一种方法是使用split,但我的实现还有很大的改进空间,我想知道是否有人有更合适的技术:

通过将tail -f输出文件通过管道传输到split我可以成功分割文件,但需要tail在文件完成后终止进程,这似乎不是最佳的。这会将文件分割成 1MB 的块(小,用于测试):

tail -f -n +0 file1.bin | split -d -a 5 -b 1M - file1.bin_split_

有人可以提出更好的实时分割文件的解决方案吗?我正在寻找命令行解决方案;我的 shell 是 Bash。

答案1

下面的代码片段可能会保存到文件中并启动为

./script.sh <PATH_TO_FILE_FOR_SPLITTING>

该脚本将不断检查文件的大小,并通过 dd 实用程序将该文件拆分为 1M 个部分。脚本的终止是通过持续检查特定文件是否存在(在这种情况下为/tmp/.finish)来实现的,一旦文件出现,脚本就会自行完成。

现在,您可以利用 inotify 创建第二个脚本,该脚本将在发生 close_write 事件时创建该文件。

#!/usr/bin/env sh

path_to_file="${1}"
splitted=0
STREAM_FINISHED="/tmp/.finish"

while true ; do

  old_size="${actual_size:-0}"
  _actual_size="$(du "${path_to_file}")"
  actual_size="${_actual_size%$path_to_file}"
  actual_size_old_size_diff=$((actual_size-old_size))

  all_parts_size=$((splitted*1024))

  parts_whole_difference=$((actual_size-all_parts_size))

  part_written=0
  if [ "${actual_size_old_size_diff}" -ge 1024 ] || [ "${parts_whole_difference}" -ge 1024 ] ; then

      dd if="${path_to_file}" of="${splitted}".part iflag=skip_bytes skip=$((1048576*splitted)) bs=1048576 count=1

      echo "Part has been written"

      splitted=$((splitted+1))
      part_written=1

  fi

  if [ -f "${STREAM_FINISHED}" ] ; then

    echo "Finishing. Ensure part == whole"

    case "${parts_whole_difference}" in
      0)
        break
      ;;
      *)
        if [ "${part_written}" -eq 0 ] ; then

          echo "Creating last part"

          dd if="${path_to_file}" of="${splitted}".part iflag=skip_bytes skip=$((1048576*splitted))
          break
        fi
      ;;
    esac
  fi
done

答案2

您的问题并没有很清楚为什么您认为需要拆分文件。鉴于您声明的目标是尽快传输文件,并且甚至在写入完成之前就能够开始传输,看来您所需要的只是一个文件传输工具,可以处理其文件的传输大小在传输过程中积极增加。

强大的rsync实用程序可能就是您所需要的,但我很难构建一个测试用例来最终证明这一点。所以接下来就是一种“吊带和腰带”的方法。不仅rsync在文件传输期间执行检查以查看传输期间是否附加了源文件,脚本逻辑本身也会在检测到stat文件的输出在退出时与退出rsync时不同时循环rsync开始了。

第二个冗余是脚本使用该实用程序对传输的文件 mtree执行校验和验证。表面上也这样做,尽管它的验证不太明确。鉴于我们严重依赖的功能,我认为对传输的最高信心来自于通过独立于传输机制本身的方式验证文件。在极少数情况下,当发现异常时,它会再次进行救援,最后一次启用校验和的传递将重新传输未通过校验和的文件。校验和需要时间,但我仍然相信这种方法将是满足您需求的强大解决方案。sha256rsync--appendrsyncmtreersync

需要考虑的一些要点:

  1. 在传输之前分割文件需要时间。您可能会在备份作业本身运行的同时从同一磁盘读取和写入。这种对磁盘带宽的竞争将减慢备份作业本身和不必要的分割过程。

  2. 文件分割会复制磁盘空间。即使现在这不是问题,但将来可能会出现问题,或者对于部署此解决方案的其他读者来说也可能会出现问题。

  3. 该文件必须在远程端重新组装,这将再次消耗磁盘和时间。

  4. 开始传输文件的最快方法似乎很直观,就是尽快开始将其发送到网络接口。分割是不必要的延迟,并且此方法几乎在启动时立即开始传输。

如果您还没有这样做,您应该创建一个ssh密钥,该密钥将允许您从创建转储的计算机安全、方便地登录到您的 AWS 实例。一旦到位,请考虑以下bash脚本:

#!/usr/bin/env bash

# Several large binary files are being generated in src_dir.  They need to
# to be uploaded to a remote host as quickly as possible, even while they're
# still being written.

# This solution doesn't care how many files there are, all files in src_dir
# get copied to dst_dir on dst_host.  Any pre-existing files in dst_dir are
# deleted.

src_dir='where/my/files/are'        # slash will be appended
dst_dir='my/AWS/path'               # slash will be appended
dst_host='my.aws.example.com'       # remote host FQDN only

# what rsync syntax shall we use for incremental passes?
# --append is important.

rs_inc='rsync -av -P --append --delete'

# what rsync syntax shall we use for absolute verification (checksum) passes?

rs_chk='rsync -av -c --delete'

# to begin, we must have an empty $dst_dir on $dst_host:

ssh $dst_host rm -vf "$dst_dir"'/*'

# and we need some files to transfer in src_dir

if ! stat $src_dir/* > /dev/null 2>&1
then
        printf 'no files found in %s\n' "$src_dir"
        exit 1
fi

# begin with blank (undefined) src file status

src_stat=

printf 'beginning transfer ...\n'

# loop while the current src file status differs from src_stat

while [[ "$(stat $src_dir/*)" != "$src_stat" ]]
do

        # Before we start rsync, record the src_dir state.

        src_stat="$(stat $src_dir/*)"

        # Then do an incremental rsync of src_dir to dst_host:dst_dir

        $rs_inc "$src_dir"/ $dst_host:"$dst_dir"/

        # Now loop back and check stat again.  If the files have changed
        # while rsync was running, then we'll need to loop again with 
        # another incremental transfer.

done

# Now use mtree(8) to ensure that the src and dst paths contain the same
# files.

printf 'verifying files ...\n'
# It's handy to pause here when testing:
#read -p 'Press Enter ... '

if mtree -ck sha256digest,size,time -p "$src_dir" | ssh $dst_host mtree -p "$dst_dir"
then
        printf 'files in local path %s and remote path %s:%s pass SHA256 test\n' \
                "$src_dir" $dst_host "$dst_dir"
else
        printf 'one or more files in local path %s and remote path %s:%s fail SHA256 test\n' \
                "$src_dir" $dst_host "$dst_dir"
        printf 're-transfering...\n'
        $rs_chk "$src_dir"/* $dst_host:"$dst_dir"/
fi

答案3

感谢大家的意见;许多好的建议和问题使我能够在少数需要使用它的情况下提出一个可行的解决方案。

不过,要回答一堆问题:它是为了迁移到不同的AWS帐户,包括恢复数据库;我受到安全性和架构的限制,希望充分利用我所掌握的工具,而不是将时间和精力投入到可能带来微不足道的好处的更改中;我以缓慢的方式进行,按顺序执行每个步骤,但更快地完成这件事有好处;它是 AWS,所以我可以指定/添加我决定的任何磁盘;我不想直接传输它,以防出现某种中断(即我确实需要将数据放入本地 EBS 作为备用计划); rsync 是不是比较慢?;最后,命名管道是一个选项,但我需要重命名输出文件 - 我不相信它们会给我带来巨大的好处。

无论如何,我想出的解决方案如下:

来源:从 EBS 卷 1 --> EBS 卷 2 备份;从 EBS 卷 2 --> EBS 卷 3 中分割 20GB 块;将块从 EBS Volume3 上传到 S3

目标:从 S3 下载块到 stdout,附加到 EBS 卷 2 中的目标文件;从 EBS 卷 2 恢复到 EBS 卷 1

代码(很抱歉这是一起破解的,但很快就会被扔掉):

tails3.sh


#!/bin/bash

#tails3.sh 
#takes two parameters: file to tail/split, and location to upload it to 
#splits the file to /tmpbackup/ into 20GB chunks. 
#waits while the chunks are still growing 
#sends the final (less than 20GB) chunk on the basis that the tail -f has completed

#i.e. for splitting 3 files simultaneously
#for file in <backup_filename>.00[1-3]; do bash -c "./tails3.sh $file s3://my-bucket/parts/ > /dev/null &"; done
# $1=filename
# $2=bucket/location


set -o pipefail

LOGFILE=$1.log


timestamp() { date +"%Y-%m-%d %H:%M:%S"; }

function log {
    printf "%s - %s\n" "$(timestamp)" "${*}"
    printf "%s - %s\n" "$(timestamp)" "${*}" >> "${LOGFILE}"
}

function closeoff {
  while kill -0 $tailpid 2>/dev/null; do
    kill $tailpid 2>/dev/null
    sleep 1
    log "slept waiting to kill"
  done
}

tail -f -n +0 $1  > >(split -d -a 5 -b 20G - /tmpbackup/$1_splitting_) &

tailpid=$!

inotifywait -e close_write $1 && trap : TERM && closeoff &

log "Starting looking for uploads in 5 seconds"
sleep 5


FINISHED=false
PARTSIZE=21474836480
FILEPREVSIZE=0

until $FINISHED; 
do 
    FILETOTRANSFER=$(ls -1a /tmpbackup/${1}_splitting_* | head -n 1)
    RC=$?
    kill -0 $tailpid >/dev/null 
    STILLRUNNING=$?
    log "RC: ${RC}; Still running: ${STILLRUNNING}"
    if [[ $RC > 0 ]]; then 
        if [[ ${STILLRUNNING} == 0 ]]; then 
            log "tail still running, will try again in 20 seconds"
            sleep 20
        else 
            log "no more files found, tail finished, quitting"
            FINISHED=true
        fi
    else 
        log "File to transfer: ${FILETOTRANSFER}, RC is ${RC}"
        FILEPART=${FILETOTRANSFER: -5}
        FILESIZE=$(stat --format=%s ${FILETOTRANSFER})
        log "on part ${FILEPART} with current size '${FILESIZE}', prev size '${FILEPREVSIZE}'"


        if [[ ${FILESIZE} == ${PARTSIZE} ]] || ([[ ${STILLRUNNING} > 0 ]] && [[ ${FILESIZE} == ${FILEPREVSIZE} ]]); then
            log "filesize: ${FILESIZE} == ${PARTSIZE}'; STILLRUNNING: ${STILLRUNNING}; prev size '${FILEPREVSIZE}'"
            log "Going to mv file ${FILETOTRANSFER} to _uploading_${FILEPART}"
            mv ${FILETOTRANSFER} /tmpbackup/${1}_uploading_${FILEPART}
            log "Going to upload /tmpbackup/${1}_uploading_${FILEPART}"
            aws s3 cp /tmpbackup/${1}_uploading_${FILEPART} ${2}${1}_uploaded_${FILEPART}
            mv /tmpbackup/${1}_uploading_${FILEPART} /tmpbackup/${1}_uploaded_${FILEPART}
            log "aws s3 upload finished" 
        else
            log "Sleeping 30"
            sleep 30
        fi
        FILEPREVSIZE=${FILESIZE}

    fi 

done 

log "Finished"

s3join.sh

#!/bin/bash

#s3join.sh 

#takes two parameters: source filename, plus bucket location 
#i.e. for a 3 part backup: 
#`for i in 001 002 003; do bash -c "./s3join.sh <backup_filename>.$i s3://my-bucket/parts/ > /dev/null &"; done `
#once all files are downloaded into the original, delete the $FILENAME.downloading file to cleanly exit
#you can tell when the generated file matches the size of the original file from the source server 
# $1 = target filename
# $2 = bucket/path

set -o pipefail

FILENAME=$1
BUCKET=$2
LOGFILE=${FILENAME}.log

timestamp() { date +"%Y-%m-%d %H:%M:%S"; }

function die {
    log ${*}
    exit 1
}

function log {
    printf "%s - %s\n" "$(timestamp)" "${*}"
    printf "%s - %s\n" "$(timestamp)" "${*}" >> "${LOGFILE}"
}

touch ${FILENAME}.downloading
i=0 

while [ -f ${FILENAME}.downloading ]; do 
    part=$(printf "%05d" $i)
    log "Looking for ${BUCKET}${FILENAME}_uploading_${part}"
    FILEDETAILS=$(aws s3 ls --summarize ${BUCKET}${FILENAME}_uploaded_${part})
    RC=$?
    if [[ ${RC} = 0 ]]; then 
        log "RC was ${RC} so file is in s3; output was ${FILEDETAILS}; downloading"
        aws s3 cp ${BUCKET}${FILENAME}_uploaded_${part} - >> ${FILENAME}
        ((i=i+1))
    else 
        log "could not find file, sleeping for 30 seconds. remove ${FILENAME}.downloading to quit"
        sleep 30
    fi
done 

使用上述内容,我开始备份,然后立即触发tails3.sh正在生成的备份文件名。这会将这些文件分割到不同的卷。当分割部分达到 20GB(硬编码)时,它们开始上传到 s3。重复此操作,直到所有文件都上传完毕并且tail -f备份文件终止。

在此开始后不久,我在目标服务器上s3join.sh使用源上生成的备份文件名进行触发。然后,该进程定期轮询 s3,下载它找到的任何“部分”并将其附加到备份文件中。这种情况一直持续到我告诉它停止(通过删除 .downloading),因为我懒得在下载任何不完全是 20GB 的文件后将其设置为停止...

而且,为了更好地衡量,一旦第一组部分被附加到目标备份文件中,我就可以开始数据库恢复,因为恢复是该过程中最慢的部分,备份是下一个最慢的部分,而 s3 上传/下载是最快的。即备份速度约为 500MB/s;上传/下载速度高达 ~700MB/s;恢复速度约为 400MB/s。

今天在开发环境上测试流程,本应是(1小时备份+20分钟上传+20分钟下载+1小时恢复=2小时40分钟),在大约1小时20分钟内完成了源到目标的恢复。

最后要注意的一件事 - 我的印象是有一些缓存,tail -f因为aws s3 cp读取 MB/秒似乎没有受到应有的打击。

相关内容