S3のCopyObjectによるコピーの並列実行によるコピー時間短縮の検証
- はじめに
- 検証構成
- 環境準備
- 検証-1 AWS CLIとpythonスクラッチプログラムの比較
- 検証-2 インスタンスタイプと並列度の検証
- 検証-3 200万ファイルのランニング検証とS3スロットリング対策
- 全体まとめ
- その他
はじめに
検証用に、S3バケット上に1MBのファイル多量に準備したいという話があり、色々試行錯誤した経緯のメモです。
アプローチとしては、オリジナルファイルをインスタンスから都度アップロードするのはインスタンスに負荷がかかり(特にCPU、ネットワーク)効率が上がらないため、マスターデータをあらかじめS3にアップロードし、S3上にあるマスターファイルをcopy-object APIを利用し、実際のコピー処理をS3にオフロードさせるようにしています。
またCopyObject APIは、S3でのコピー完了を持って応答が帰ってくる*1ため、極力並列でAPIを実行すること処理時間の短縮を図っています。
なお検証結果から導かれるのCopyObjectによるコピーの並列実行のポイントは、以下の通りです。
検証構成
環境構成概要図

データ構成
マスターデータ
マスターデータは同一プレフィクスへのアクセス集中を避けるためファイルごとに、"XXXXXXXXXX-original-data"(XXXXXXXXXXは、ランダムな文字列)というプレフィクスをつけて(フォルダに)格納します。
GETは、 「プレフィクス毎に秒間5,500 回以上の GET/HEAD リクエスト」となるので念の為それを意識した構成としています。(結果として、PUTのスロットリングが先に来ているので、ランダムな文字列は不要かもしれないです)
検証コード
検証コードはこちらです。
github.com
環境準備
(1)AWSリソース準備
(2)インスタンス初期設定
# AWS CLIとboto3(AWS Python SDK)のセットアップ curl -o "get-pip.py" "https://bootstrap.pypa.io/get-pip.py" sudo python get-pip.py sudo pip install boto3 sudo pip install --upgrade awscli # AWS CLI設定 aws configure set aws_access_key_id <作成したIAMユーザのアクセスキー> aws configure set aws_secret_access_key <作成したIAMユーザのシークレットキー> aws configure set region ap-northeast-1 aws configure set output json # 検証用プログラムのダウンロード sudo yum -y install git git clone https://github.com/Noppy/GenerateCsvOfTestFilesList.git
(3)テスト用のマスターファイルの作成とS3アップロード
#マスターファイル用のディレクトリ作成と移動 mkdir master && cd master ; pwd #マスターファイル生成 dd if=/dev/urandom of=test-001MB.dat bs=1024 count=1024 #マスターファイルのS3アップロード Profile=default MasterFileList="test-001MB.dat" Bucket=s3-100million-files-test for src in ${MasterFileList} do hash=$( cat /dev/urandom | base64 | fold -w 10 | sed -e 's/[\/\+\=]/0/g' | head -n 1 ) aws --profile=${Profile} s3 cp ${src} "s3://${Bucket}/${hash}-original-data/" done
(4)CSV生成pythonの設定ファイル作成
# configration JSON用のテンプレート生成 cd ~/GenerateCsvOfTestFilesList #要件に応じて設定 NumberOfFiles="219120" StartDate="2015/1/1" EndDate="2019/12/31" Bucket=s3-100million-files-test #Jsonファイル生成 ./gen_json.sh "${NumberOfFiles}" "${StartDate}" "${EndDate}" "${Bucket}" #作成したJSONファイルの確認 cat config.json
(5)ターゲット用CSV生成
#CSV生成 ./generate_testfiles_list.py #生成したCSVの確認 wc -l list_of_copy_files.csv #行数確認(NumberOfFilesと同じ行数が作成される)
検証-1 AWS CLIとpythonスクラッチプログラムの比較
検証概要
AWS CLI(aws s3 cp)でオブジェクトごとにCLIバケット間コピーした場合と、Pythonで作成したプログラム(clinetで取得して、for文でCopyObjectを実行)した場合の実行速度のを比較します。
検証前提
検証手順
(1)検証用のターゲットリスト生成
NumberOfFiles="10800" StartDate="2015/1/1" EndDate="2015/3/31" Bucket=s3-100million-files-test #JSON作成 ./gen_json.sh "${NumberOfFiles}" "${StartDate}" "${EndDate}" "${Bucket}" #ターゲットリスト作成 ./generate_testfiles_list.py wc -l list_of_copy_files.csv
(2)検証実行
(2)-(a) AWS CLIでの実行
実行し、timeの"real"の時間を取得
time awk -F ',' -e '{print $1; print $2}' ./list_of_copy_files.csv | xargs -P 100 -L 2 aws s3 cp
(2)-(b) Pythonでの実行
test1_result_summary.csvに出力される実行時間を取得
./S3_CopyObject_ParallelExecution.sh 100 test1_result_summary.csv
検証-2 インスタンスタイプと並列度の検証
検証前提
検証手順
(1)ulimitの増加設定
OSのulimitデフォルトでは、ユーザが同時実行可能なプロセス(nproc)は1024、同時オープンファイル(nofile)は4096です。このままでは、多重実行の検証中にリソース不足でエラーが発生する恐れがあるため、ulimitの設定を引き上げます。
sudo -i cat > /etc/security/limits.d/99-s3-test.conf * hard nofile 500000 * soft nofile 500000 * hard nproc 100000 * soft nproc 100000 別コンソールで、sshログインし下記コマンドで設定反映を確認 ulimit -Ha
(2)検証用のターゲットリスト生成
NumberOfFiles="219120" StartDate="2015/1/1" EndDate="2019/12/31" Bucket=s3-100million-files-test #JSON作成 ./gen_json.sh "${NumberOfFiles}" "${StartDate}" "${EndDate}" "${Bucket}" #ターゲットリスト作成 ./generate_testfiles_list.py wc -l list_of_copy_files.csv
(3)検証実行
インスタンスタイプを変更(都度再起動)しながら、下記コマンドを実行します。
検証結果は、test_3_results_summary.csvに集約
nohup ./test_3_s3tos3_python_parallels.sh &
検証結果
(1) 多重度とCopyObject API実行回数
インスタンスタイプをあげても処理時間の平均3000〜3500回/秒あたりで頭打ちとなり、それ以上多重度をあげるとS3のスロットリングが発生します。*3

並列度数 | CopyObject実行数( 回/秒 ) | ||
---|---|---|---|
m5a_4xlarge(16vcpu) | m5a_8xlarge(32vcpu) | m5a_12xlarge(48vcpu) | |
100 | 654.1 | 695.6 | 672.1 |
200 | 1259.3 | 1320.0 | 1328.0 |
400 | 1767.1 | 2407.9 | 2407.9 |
800 | 1660.0 | 3043.3 | 3844.2 |
1000 | N/A | 2213.3 | 3652.0 |
1500 | N/A | 2577.9 (SlowDown発生) | 2331.1 (SlowDown発生) |
2000 | N/A | 2282.5 (SlowDown発生) | 2672.2 (SlowDown発生) |
m5a.12xlarge(48vcpu)で、800多重度よりあげてもCPU利用率は80%で頭打ちであることと、前のAPI秒間実行回数結果から、多重度を800以上あげても今回の構成では、S3がネックとなり、それ以上処理能力をあげることができないことがわかります。そのことから多重度の上限は、800あたりであると判断します。
次にその多重度800を実行するのに最適なインスタンスタイプについて、m5a.12xlarge(48vcpu)ではCPUに余力があることから、その一つ下のm5a.8xlarge(32vcpu)がもっとも効率よくインスタンスリソースを利用できていることがわかります。


検証考察
(1) S3のスロットリング
この検証から「S3のAPI実行回数、3000〜3500回/秒程度」を越えるとS3スロットリング(SlowDown)が発生していることが読み取れます。この内容から検索すると、S3の開発者ガイドの「ベストプラクティスの設計パターン: Amazon S3 パフォーマンスの最適化」に以下の記載がありました。この内容から今回の検証では「プレフィックスごとに 1 秒あたり 3,500 回以上の PUT/COPY/POST/DELETE リクエスト」に合致している可能性が高いと思われます。
アプリケーションは、Amazon S3 からストレージをアップロードおよび取得する際にリクエストパフォーマンスで 1 秒あたり何千ものトランザクションを簡単に達成できます。Amazon S3 は高いリクエスト率に自動的にスケールされます。たとえば、アプリケーションでバケット内のプレフィックスごとに 1 秒あたり 3,500 回以上の PUT/COPY/POST/DELETE リクエストと 5,500 回以上の GET/HEAD リクエストを達成できます。
S3は負荷に応じて内部で自動スケーリングしますが、自動スケーリングを上手く利用しさらなる改善を図るための方策として以下のものがあります。
具体的対応については、次の検証で検討します。
- プレフィックス分割と、プレフィクス先頭へのランダム文字列挿入
- スロースタートし段階的に多重度をあげる(指数的にあげていく)
- アプリケーションでのリトライ処理の実装(SDKではリトライ回数4回(デフォルト))
参考情報
検証-3 200万ファイルのランニング検証とS3スロットリング対策
検証前提
検証 3-(a) S3スロットリング対策:プレフィクスでの対策
プレフィクスを「年/月/日/時」から、逆にし先頭にハッシュをつけることで分散処理するようにする対策です。
具体的にはS3格納先のプレフィクスを「XXX-時/日/月/年」とします。
手順
(1)検証用のターゲットリスト生成
NumberOfFiles="2191200" StartDate="2015/1/1" EndDate="2019/12/31" Bucket=s3-100million-files-test #JSON作成 ./gen_json.sh "${NumberOfFiles}" "${StartDate}" "${EndDate}" "${Bucket}" #ターゲットリスト作成 ./generate_testfiles_list.py --reverse wc -l list_of_copy_files.csv
(2)検証実行
nohup ./S3_CopyObject_ParallelExecution.sh 800 test_2million_summary.csv &
(3)検証結果
理由を正確には確認できないですが、プレフィクスを変更してもSlowDownの比率は大きく変わりませんでした。
もしかしたらオートスケールするまでのタイムラグがあったのかもしれませんが。
検証 3-(b) 再実行による対策
SlowDownが発生したときに再実行して対処する方策です。
アプリケーションで再実行のロジックを実装する手段もありますが、今回はAWS SDK(boto3)のコンフィグで再実行回数を引き上げて対応しています。
具体的には、client()の引数に設定します。
# Get session config = Config( retries=dict( max_attempts = args.retry ) ) s3 = boto3.client('s3', config=config)
手順
(1)検証用のターゲットリスト生成
#ターゲットリスト作成 ./generate_testfiles_list.py --reverse wc -l list_of_copy_files.csv
(2)検証実行
(2)-(a) 再実行回数デフォルト
nohup ./S3_CopyObject_ParallelExecution.sh 800 test_2million_summary.csv &
(2)-(b) 再実行回数を10回そのまま実行
nohup ./S3_CopyObject_ParallelExecution.sh 800 test_2million_summary.csv 10 &
(3)検証結果
再実行回数を10回に引き上げSDKでリトライすることで全量コピーができるようになりました。
対象数 | 再実行回数 | 実行時間 | 秒間実行回数 | 実行結果(オブジェクト数) | ||
---|---|---|---|---|---|---|
成功 | 失敗 | 合計 | ||||
2191200 | 4 | 692 | 3166.5 | 2188419 | 2781 | 2191200 |
2191200 | 4 | 698 | 3139.3 | 2190116 | 1084 | 2191200 |
2191200 | 4 | 688 | 3184.9 | 2189677 | 1523 | 2191200 |
2191200 | 10 | 562 | 3898.9 | 2191200 | 0 | 2191200 |
2191200 | 10 | 556 | 3941.0 | 2191200 | 0 | 2191200 |
2191200 | 10 | 563 | 3892.0 | 2191200 | 0 | 2191200 |
まとめ
簡易的ですが、SDKの再実行回数の引き上げでSlowDownを抑えることができようになりました。
全体まとめ
今回のS3のCopyObjectによるコピーの並列実行によるコピー時間短縮の検証のまとめです。
その他
検証で、インスタンスロールでなくIAMユーザを利用する理由
EC2インスタンス上に、IAMユーザのアクセスキーとキーポリシーを設定するのは、AWSのベストプラクティス(セキュリティ観点)ではバッドプラクティスとされています。
しかしインスタンスロールの場合、ロールのクレデンシャルをインスタンスのメタデータから取得*4するのですが、今回のようにプロセスを数百起動しメタデータに同時アクセスすると同時アクセスエラーが発生しクレデンシャル取得に失敗します。
本番運用向けであれば、共通機能でクレデンシャルを取得しキャッシュして必要なプロセスからの要求に応じてクレデンシャルを渡すような実装をするのがいいのかもしれないですが実装コストがかかるため、今回はアクセスキーとシークレットキーを利用する方式にしました。アクセスキーとシークレットキーの場合は、"~/.aws/credentials"ファイルに格納されておりファイルへのアクセスであれば同時アクセスがいくらあってもエラーにはならないです。
*1:copy-object APIの記載: "If the copy is successful, you receive a response with information about the copied object."
*2:オーバヘッド:プロセス生成してpythonモジュールをロードし&解析して、AWSのクレデンシャル取得して、認証&セッション取得てという、CopyObject実行までに至る前処理
*3:"ERROR:root:An error occurred (SlowDown) when calling the CopyObject operation (reached max retries: 4): Please reduce your request rate."エラーが発生。S3から"HTTP 503 Slow Down 応答があったことを示唆。
*4:インスタンスロールのクレデンシャルは"http://169.254.169.254/latest/meta-data/iam/security-credentials/ロール名"で取得できます