書き込み中に大容量ファイルをリアルタイムで分割

書き込み中に大容量ファイルをリアルタイムで分割

できるだけ早くAWS S3にアップロードする必要がある4つの大容量バイナリファイル(それぞれ400 GB以上)を作成するプログラムがあります。

ファイルが完全に作成される前にアップロードを開始したいと思います。いくつかのアプローチを試しており、効果があると思われる方法の1つはを使用することですsplit。しかし、私の実装には改善の余地がたくさん残っているので、誰でも知りたいです。より適切な技術があります。

tail -f出力ファイルをパイピングすると、splitファイルを正常に分割できますが、tailファイルが完了したらプロセスを終了する必要があるため、最適ではないようです。これにより、ファイルは1MBチャンクに分割されます(テスト用に小さい)。

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

リアルタイムでファイルを分割するより良いソリューションを提案できる人はいますか?私はコマンドラインソリューションを探しています。私のシェルはBashです。

答え1

以下のコードスニペットはファイルに保存され、次のように実行できます。

./script.sh <PATH_TO_FILE_FOR_SPLITTING>

スクリプトは常にファイルサイズをチェックし、ddユーティリティを使用して1M部分に分割します。スクリプトの終了は、特定のファイル(この場合は/ tmp / .finish)があるかどうかを継続的に確認し、そのファイルが表示されるとスクリプトは自動的に完了します。

inotifyを使用して、close_writeイベントが発生したときにファイルを生成する2番目のスクリプトを生成できるようになりました。

#!/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と異なることを検出するとループを開始します。 。rsyncrsync

2番目の冗長性は、スクリプトがこのユーティリティを使用して送信されたファイルに対して 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アカウントに移行することでした。私はセキュリティとアーキテクチャの制約を受け、何かに時間と労力を費やすのではなく、自由に使用できるツールを最大限に活用したいと思いました。それはわずかな利益の変化をもたらし得る。できる各ステップを順番にゆっくり実行しますが、より速く実行すると利点があるため、決定したディスクを指定/追加できます。中断が発生した場合は直接送信したくありません。 (つまり、バックアップ計画としてデータをローカルEBSに配置する必要があります。)rsyncは遅いですか? ;最後に指定されたパイプはオプションですが、出力ファイルの名前を変更する必要があります。これが私に大きな利点を与えるとは確信していません。

とにかく私が思いついた解決策は次のとおりです。

源泉:EBS ボリューム 1 からバックアップ --> EBS ボリューム 2、EBS ボリューム 2 で 20 GB ブロックを分割 --> EBS ボリューム 3 から 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をポーリングし、見つかった「部分」をダウンロードしてバックアップファイルに追加します。正確に20 GB以外のものをダウンロードして停止するように設定するには、あまりにも怠惰なせいで停止するように指示されるまで続行されます(.downloadingを削除)。

そして、良い測定のために、最初のサブセットがターゲットバックアップファイルに追加されたら、データベースの回復を開始できます。回復はプロセスの最も遅い部分であり、バックアップは次に遅く、s3アップロード/ダウンロードは最も遅い部分であるためです。最速です。つまり、バックアップ速度は約500 MB / s、アップロード/ダウンロード速度は最大700 MB / s、回復速度は約400 MB / sです。

今日は開発環境でプロセスをテストしました。 (バックアップ1時間+アップロード20分+ダウンロード20分+回復1時間=2時間40分)ソースからターゲットへの回復は約1分で完了しました。 1時間20分。

最後に注目すべき点 - 読み取りMB/秒があまり強く打たないようなので、少しのtail -fキャッシュがあるという印象を受けました。aws s3 cp

関連情報