できるだけ早く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
と異なることを検出するとループを開始します。 。rsync
rsync
2番目の冗長性は、スクリプトがこのユーティリティを使用して送信されたファイルに対して mtree
チェックサムチェックを実行することです。表面的にもこれを行いますが、検証はあまり明確ではありません。私たちが大きく依存する機能を考えると、転送メカニズム自体とは独立した方法でファイルをチェックすることから、転送に対する最高レベルの信頼性が出てくると思います。まれに例外が見つかった場合は再構築され、最後のチェックサムアクティベーションパスはチェックサムに失敗したファイルを再送信します。チェックサムには時間がかかりますが、まだこの方法がお客様のニーズに合った強力なソリューションになると思います。sha256
rsync
--append
rsync
mtree
rsync
考慮すべきいくつかの点:
転送する前にファイルを分割するのに時間がかかります。バックアップジョブ自体の実行中に同じディスクから読み書きできます。このようなディスク帯域幅の競合により、バックアップ操作自体が遅くなり、不要な分割プロセスが遅くなります。
ファイルパーティションはディスク領域をコピーします。今は問題にならなくても将来問題になる可能性があり、このソリューションを展開している他の読者にとっても問題になる可能性があります。
ファイルをリモート側で再組み立てする必要があるため、ディスクと時間がかかります。
ファイル転送を開始する最速の方法は、できるだけ早くネットワークインターフェイスに転送を開始することが直感的なようです。分割は不要な遅延であり、この方法は開始時にほぼ即座に送信を開始します。
まだ実行していない場合は、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