쓰는 동안 대용량 파일을 실시간으로 분할

쓰는 동안 대용량 파일을 실시간으로 분할

가능한 한 빨리 AWS S3에 업로드해야 하는 4개의 대용량 바이너리 파일(각각 400GB 이상)을 생성하는 프로그램이 있습니다.

파일이 완전히 작성되기 전에 업로드를 시작하고 싶습니다. 몇 가지 접근 방식을 시도하고 있으며 효과가 있을 것으로 생각되는 방법 중 하나는 를 사용하는 것입니다 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 이벤트가 발생할 때 파일을 생성할 두 번째 스크립트를 생성할 수 있습니다.

#!/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

두 번째 중복성은 스크립트가 이 유틸리티를 사용하여 전송된 파일에 대한 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에서 20GB 블록 분할 --> 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를 주기적으로 폴링하고 찾은 "부분"을 다운로드하여 백업 파일에 추가합니다. 정확히 20GB가 아닌 것을 다운로드한 후 중지하도록 설정하기에는 너무 게으른 탓에 중지하라고 지시할 때까지 계속됩니다(.downloading 삭제).

그리고 좋은 측정을 위해 첫 번째 부분 세트가 대상 백업 파일에 추가되면 데이터베이스 복구를 시작할 수 있습니다. 복구는 프로세스에서 가장 느린 부분이고, 백업은 그 다음으로 느리고, s3 업로드/다운로드는 가장 느린 부분이기 때문입니다. 가장 빠릅니다. 즉, 백업 속도는 약 500MB/s이고 업로드/다운로드 속도는 최대 700MB/s이며 복구 속도는 약 400MB/s입니다.

오늘은 개발 환경에서 프로세스를 테스트해봤습니다. (백업 1시간 + 업로드 20분 + 다운로드 20분 + 복구 1시간 = 2시간 40분) 소스에서 타겟으로의 복구가 약 1분 만에 완료되었습니다. 한 시간 20분.

마지막으로 주목해야 할 점 - 읽기 MB/초가 그다지 세게 치지 않는 것 같기 tail -f때문에 약간의 캐싱이 있다는 인상을 받았습니다 .aws s3 cp

관련 정보