ちょっと話題の記事

【AWS】S3のマルチパートアップロードで簡易サスペンド/レジュームを実装してみた

2014.03.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

本日の課題

こんにちは植木和樹です。本日はS3に大きなサイズのファイルをアップロードする際の課題についての話題です。

S3へ大きなファイルをアップロードする時には次のような課題があります。

  • アップロード途中にネットワーク障害などで失敗しても最初からアップロードし直さず途中から再開したい。
  • サイズが大きなファイルは分割して並列アップロードしたい。

S3にはマルチパートアップロードという機能があります。これはアップロードするファイルをいくつかに分割し(断片ファイルと呼ぶことにします)、断片ファイルをそれぞれS3にアップロードした後に、S3側で再びひとつのファイルに結合する機能です。S3にマルチパートアップロードする手順は以下になります。

  1. アップロードするファイルを断片ファイルに分割する。
  2. マルチパートアップロードのセッションを開始する。(create-multipart-upload
  3. 断片ファイルをアップロードする。(upload-part
  4. マルチパートアップロードのセッションを終了する。(complete-multipart-upload) (この操作でS3側で断片ファイルが1つのファイルに結合されます)

本日はこれらの作業をaws-cliを用いて、シェルから実行してみたいと思います。そしてマルチパートアップロードの仕組みを用いたサスペンド/レジュームの方法を検討します。

検証環境

利用したaws-cliのバージョンです。

% aws --version
aws-cli/1.2.13 Python/2.7.6 Darwin/13.0.0

アップロードするファイルを断片ファイルに分割する

今回はサイズ12MBのファイル(12MB.dmp)を作成します。そして3つの断片ファイル(chunk1.dmp 〜 chunk3.dmp)に分割します。断片ファイルのサイズは5MB以上である必要がありますが、最後の断片ファイルは5MB未満でも良いようです(5MB + 5MB + 2MB)。

% dd if=/dev/random of=12MB.dmp bs=1024k count=12

% dd if=12MB.dmp of=chunk1.dmp bs=1024k skip=0 count=5
5+0 records in
5+0 records out
5242880 bytes transferred in 0.004654 secs (1126548799 bytes/sec)

% dd if=12MB.dmp of=chunk2.dmp bs=1024k skip=5 count=5
5+0 records in
5+0 records out
5242880 bytes transferred in 0.004464 secs (1174503688 bytes/sec)

% dd if=12MB.dmp of=chunk3.dmp bs=1024k skip=10 count=5
2+0 records in
2+0 records out
2097152 bytes transferred in 0.005323 secs (393966633 bytes/sec)

マルチパートアップロードのセッションを開始する

まず最初にマルチパートアップロードの開始を宣言します。コマンドを実行するとUploadIdが返ってきます。この後のコマンドで何度も利用するのでシェル変数にUploadIdを代入しておきます。

% aws s3api create-multipart-upload --bucket cm-ueki-bucket --key 12MB.dmp
{
    "UploadId": "7cjLgpyf(中略)nL5Q--",
    "Bucket": "cm-ueki-bucket",
    "Key": "12MB.dmp"
}

% upload_id="7cjLgpyf(中略)nL5Q--"

断片ファイルをアップロードする。

次に各断片ファイルをアップロードします。マルチパートアップロードを開始した時に取得したUploadIdを指定してください。

% aws s3api upload-part --bucket cm-ueki-bucket --key 12MB.dmp --upload-id $upload_id \
  --part-number 1 --body chunk1.dmp
{
    "ETag": "\"53d553901477a9635de3480d498ba4d2\""
}
% aws s3api upload-part --bucket cm-ueki-bucket --key 12MB.dmp --upload-id $upload_id \
  --part-number 2 --body chunk2.dmp
{
    "ETag": "\"318f7bb36040cf94aa8c86045ab9fa29\""
}
% aws s3api upload-part --bucket cm-ueki-bucket --key 12MB.dmp --upload-id $upload_id \
  --part-number 3 --body chunk3.dmp
{
    "ETag": "\"4a220286789d89db85fe44c896c2c7ed\""
}

(コメント)create-multipart-upload時に指定したものと異なる--bucket--key

を指定するとエラーになります。なら--upload-idだけでいいのではないか?と思うのですが・・・どうしてこういう仕様になっているんでしょう?

マルチパートアップロードのセッションを終了する。

すべての断片ファイルをアップロードし終わったら、最後にマルチパートアップロードセッションを終了します。その際に断片ファイルの一覧を指定する必要があります。upload-partする時に指定したpart-numberとコマンド実行時に返ってきたETagを次のようなフォーマットでJSON形式のファイル(multipart.json)に保存します。

$ cat multipart.json
{
  "Parts": [
    {
      "ETag": "\"53d553901477a9635de3480d498ba4d2\"",
      "PartNumber": 1
    },
    {
      "ETag": "\"318f7bb36040cf94aa8c86045ab9fa29\"",
      "PartNumber": 2
    },
    {
      "ETag": "\"4a220286789d89db85fe44c896c2c7ed\"",
      "PartNumber": 3
    }
  ]
}

(コメント)UploadIdでマルチパートアップロードセッションを指定しているので、そのセッションの断片ファイルをすべて結合してくれればいいんですけど、どうして改めて断片ファイルリストを指定する必要があるんでしょうね?

multipart.jsonを作成したらマルチパートアップロードセッションの終了コマンドを実行します。

$ aws s3api complete-multipart-upload --bucket cm-ueki-bucket --key 12MB.dmp --upload-id $upload_id \
  --multipart-upload file://multipart.json
{
    "ETag": "\"e83d29ed8d38f7403bd38dd7bb7edd54-3\"",
    "Bucket": "cm-ueki-bucket",
    "Location": "https://cm-ueki-bucket.s3.amazonaws.com/12MB.dmp",
    "Key": "12MB.dmp"
}

S3のバケットを確認し "12MB.dmp" というファイルができあがっていれば成功です!

% aws s3 ls s3://cm-ueki-bucket/12MB.dmp
2014-03-01 12:11:23   12582912 12MB.dmp

S3マルチパートアップロードを応用したサスペンド/レジューム や 並列アップロード

マルチパートアップロードを使ってサスペンド/レジュームをするには

ネットワークエラーなどで断片ファイルのアップロード処理が異常終了(サスペンド)した場合は、その断片ファイルのアップロードから再施行すればレジュームできます。

いまどこまで断片ファイルがアップロードできているかはlist-partsコマンドで調べることができます。

並列アップロードするには

並列アップロードするにはupload-partを複数スレッドで実行してあげれば良さそうです。しかし並列アップロードを実装する際にはいくつかの課題をクリアしなくてはいけません。

  • 同時にいくつのスレッドで実行するのか?
  • アップロードに失敗したスレッドの再施行処理(断片ファイルごとにアップロード成功/失敗の管理が必要)

これらはS3クライアント側で考慮し実装する必要があります。

シェルスクリプトでサスペンド/レジュームを行うサンプル

少々長いですが、上記で紹介した手順をシェルスクリプトにしてみました。

アップロード処理開始時にlist-multipart-uploadsで開始済みのマルチパートアップロードセッションがあった場合には、アップロード済みの断片ファイルをスキップするようにしています。また上記の通り並列アップロードをシェルスクリプトで行うのはやや手間なので実装していません。

#!/bin/sh

# 切り上げ計算
function ceil() {
  result=$(($1 / $2))
  if [ $(($1 % $2)) -ne 0 ]; then
    result=$(($result + 1))
  fi
  echo $result
}

# シェルの第1引数: S3バケット名
# シェルの第2引数: アップロードするファイル名
bucket=$1
file=$2

chunksize=5 # [MegaBytes]
filesize=$(stat -f "%z" $file)
chunknum=`ceil $filesize $(($chunksize * 1024 * 1024))`

echo "File size: $filesize"
echo "Chunk Number: $chunknum"

# すでにマルチパートアップロード処理が開始されていれば、レジューム処理を行う
upload_ids=$(aws s3api list-multipart-uploads --bucket $bucket --query 'Uploads[*].UploadId' --output text --prefix $file)
for upload_id in $upload_ids; do
  current=$(aws s3api list-parts --bucket $bucket --key $file --upload-id $upload_id | jq "[ .Parts[] | .PartNumber ] | max")
  current=$(($current + 1))
done


# レジューム処理でなければ、マルチパートアップロードのセッションを開始する
current=${current:=1}
if [ -z $upload_id ]; then
  result=$(aws s3api create-multipart-upload --bucket $bucket --key $file)
  upload_id=$(echo $result | jq -r '.UploadId')
else
  echo "Resume upload. PartNumber: $current"
fi


# アップロードするファイルを断片ファイルに分割し、それぞれをアップロードする
echo "UploadId: $upload_id"
chunkfile=$(mktemp $$.chunk)
while [ $current -le $chunknum ]; do
  skip=$(((current - 1) * $chunksize))
  dd if=$file of=$chunkfile bs=1024k skip=$skip count=$chunksize

  echo "Upload $current of $chunknum"
  aws s3api upload-part --bucket $bucket --key $file --part-number $current --upload-id $upload_id --body $chunkfile

  current=$(($current + 1))
done
rm $chunkfile

# アップロードした断片ファイルを指定して、マルチパートアップロードを終了する
multipart=$(aws s3api list-parts --bucket $bucket --key $file --upload-id $upload_id | jq '{ "Parts": [ .Parts[] | { ETag, PartNumber } ] }')
aws s3api complete-multipart-upload --bucket $bucket --key $file --multipart-upload "$multipart" --upload-id $upload_id

echo "Done!"

aws-cli s3 cpコマンドの挙動

aws-cliのaws s3 cpコマンドによるアップロードは、ファイルサイズが8〜9MB以上になると自動的に分割しマルチパート&並列アップロードをしてくれます。同様にダウンロードについてもファイルサイズがある程度(12MB以上?)大きいと分割し並列ダウンロードをしてくれます。

アップロード/ダウンロード操作中にパソコンがスリープ状態になりネットワークが切断されてしまった場合も、スリープが解除されると自動的にダウンロードが再開されるようになっています(注:環境に依存するようです)。しかしCtrl+Cで処理を中断し、再度コマンドを実行した場合はレジュームはしません。どうやら明示的なサスペンド/レジュームはできないようです。

まとめ

  • S3でマルチパートアップロードを使うことでサスペンド/レジュームができる。
  • ただしS3側でサスペンド/レジュームの仕組みを提供しているわけではない。
  • サスペンド/レジュームの仕組みはS3クライアント側で実装する必要がある。
  • 断片ファイルの並列アップロードもS3クライアント側で実装する必要がある。

コマンドラインでファイルのアップロード/ダウンロードをする場合はaws-cliを使うのが良いでしょう。しかし明示的なサスペンド/レジュームには対応していないので、必要な場合はGUIベースのS3クライアント(CloudBerry Explorerなど)を利用するのが良さそうです。

参考資料