のぴぴのメモ

自分用のLinuxとかの技術メモ

S3のCopyObjectによるコピーの並列実行によるコピー時間短縮の検証

はじめに

検証用に、S3バケット上に1MBのファイル多量に準備したいという話があり、色々試行錯誤した経緯のメモです。
アプローチとしては、オリジナルファイルをインスタンスから都度アップロードするのはインスタンスに負荷がかかり(特にCPU、ネットワーク)効率が上がらないため、マスターデータをあらかじめS3にアップロードし、S3上にあるマスターファイルをcopy-object APIを利用し、実際のコピー処理をS3にオフロードさせるようにしています。
またCopyObject APIは、S3でのコピー完了を持って応答が帰ってくる*1ため、極力並列でAPIを実行すること処理時間の短縮を図っています。

なお検証結果から導かれるのCopyObjectによるコピーの並列実行のポイントは、以下の通りです。

  • CopyObjectを利用したバケット内でのオブジェクトコピーによりコピー処理を高速化可能
  • S3のプレフィックス単位のAPIロットリング PUT/DELETEの3500回/秒が上限の目安として、多重度を調整
  • (今回の構成では)、多重度800で、インスタンスタイプは8xlarge(32vcpu)が効率的
  • S3のスロットリング は、boto3の再実行回数引き上げで対処
  • 上記対応で、実測 バケット内でのCopyObjectによる1MBファイルのコピー、3500回/秒までの実行を確認

検証構成

環境構成概要図

f:id:nopipi:20191014234500p:plain:w500

データ構成

マスターデータ

マスターデータは同一プレフィクスへのアクセス集中を避けるためファイルごとに、"XXXXXXXXXX-original-data"(XXXXXXXXXXは、ランダムな文字列)というプレフィクスをつけて(フォルダに)格納します。
GETは、 「プレフィクス毎に秒間5,500 回以上の GET/HEAD リクエスト」となるので念の為それを意識した構成としています。(結果として、PUTのスロットリングが先に来ているので、ランダムな文字列は不要かもしれないです)

コピー先ディレクトリ構成

使い勝手の観点から、「/年/月/日/時刻/」というプレフィクス構成としています。

f:id:nopipi:20191015030029p:plain:w300

検証コード

検証コードはこちらです。
github.com

環境準備

(1)AWSリソース準備

  • IAMユーザ
    • EC2インスタンスにセットするIAMユーザ(理由は文末の「その他」参照)
    • IAMユーザには下記のAWS 管理ポリシーを付与
      • AmazonS3FullAccess
      • ReadOnlyAccess
  • KMS
    • マネコンで、キーポリシーの「キーユーザー」に上記IAMユーザを追加
  • S3
  • 検証用S3バケット
    • デフォルト暗号化: KMS暗号化(作成したKMSのCMK利用)
    • バージョニング:なし
    • アクセスログ取得: なし
  • VPC
    • S3のVPCEndpointがあるVPC
  • インスタンス
    • 検証プログラム実行用
    • OS: Amazon Linux2
    • インスタンスタイプ: とりあえず、m5a.largeあたり(テスト内でタイプ変更)

(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 CLIpythonクラッチプログラムの比較

検証概要

AWS CLI(aws s3 cp)でオブジェクトごとにCLIバケット間コピーした場合と、Pythonで作成したプログラム(clinetで取得して、for文でCopyObjectを実行)した場合の実行速度のを比較します。

検証前提

  • オブジェクト
    • コピーオブジェクト数:10800個(1フォルダに5オブジェクト、2160のフォルダに格納)
    • オブジェクトサイズ: 1MB
  • 実行方法

検証手順

(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

検証結果

結果は以下の通りです。Pythonクラッチプログラムの方が圧倒的に早いです。AWS CLIはオブジェクト都度CLI実行しているので、オーバヘッド分*2やはり処理が重くなります。

実行方法 実行多重度 コピーオブジェクト数 実行時間 秒間実行数
AWS CLI 100 10800 364.8 秒 29.6 回/秒
Python 100 10800 19.6 秒 551.0 回/秒

一方、今回のPythonクラッチプログラムは、AWSのセッション取得後はfor文でひたすらCopyObjectを実行し続ける分、AWS CLIと比較すると圧倒的に早いです。

検証-2 インスタンスタイプと並列度の検証

検証概要

ここでは、検証-1で利用したPythonプログラム、適正な実行多重度と実行するインスタンスの適正なサイズ(vcpu数)を見極めることを目的としています。

検証前提

  • オブジェクト
    • コピーオブジェクト数:219120個(1フォルダに5オブジェクト、43824のフォルダに格納)
    • オブジェクトサイズ: 1MB
  • インスタンス
    • m5a_4xlarge(16vcpu), m5a_8xlarge(32vcpu), m5a_12xlarge(48vcpu)を利用
  • 実行方法

検証手順

(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

f:id:nopipi:20191014230750p:plain:w500

並列度数CopyObject実行数( 回/秒 )
m5a_4xlarge(16vcpu)m5a_8xlarge(32vcpu)m5a_12xlarge(48vcpu)
100654.1695.6672.1
2001259.31320.01328.0
4001767.12407.92407.9
8001660.03043.33844.2
1000N/A2213.33652.0
1500N/A2577.9
(SlowDown発生)
2331.1
(SlowDown発生)
2000N/A2282.5
(SlowDown発生)
2672.2
(SlowDown発生)
(2) 多重度の上限とインスタンスタイプの妥当性
m5a.12xlarge(48vcpu)で、800多重度よりあげてもCPU利用率は80%で頭打ちであることと、前のAPI秒間実行回数結果から、多重度を800以上あげても今回の構成では、S3がネックとなり、それ以上処理能力をあげることができないことがわかります。そのことから多重度の上限は、800あたりであると判断します。

次にその多重度800を実行するのに最適なインスタンスタイプについて、m5a.12xlarge(48vcpu)ではCPUに余力があることから、その一つ下のm5a.8xlarge(32vcpu)がもっとも効率よくインスタンスリソースを利用できていることがわかります。

f:id:nopipi:20191014233524p:plain:w500

f:id:nopipi:20191014233536p:plain:w500

検証考察

(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回(デフォルト))

参考情報

(2) KMSのスロットリング

KMSによる暗号化を利用している場合は、KMSのAPIロットリングにも注意が必要です。
KMSのCMK利用に関するAPI(Decrypt,Encryptなど)は、東京リージョンで「1アカウントあたり5500回/秒(カテゴリのAPIで共有)」になります。S3のPUT/COPY/POST/DELETEのAPIが 3,500 回/秒という目安があるので、他にKMSのAPIを多用するサービスと同時利用していなければそこまで留意しなくても良いと考えます。

参考情報

(3) 多重度とインスタンスの最適化

これまでの内容から、今回の構成でS3のバケット内でCopyObjectを実施するときの多重度とインスタンスタイプの組み合わせを以下とします。

検証-3 200万ファイルのランニング検証とS3スロットリング対策

検証概要

検証-2の10倍の200万ファイルで、安定的にAPIを発行できるかの確認と、S3スロットリング対策を検証します。

検証前提

  • オブジェクト
    • コピーオブジェクト数:2191200個(1フォルダに50オブジェクト、43824のフォルダに格納)
    • オブジェクトサイズ: 1MB
  • 実行方法
  • インスタンス: m5a_8xlarge(32vcpu)

検証 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でリトライすることで全量コピーができるようになりました。

対象数再実行回数実行時間秒間実行回数実行結果(オブジェクト数)
成功失敗合計
219120046923166.5218841927812191200
219120046983139.3219011610842191200
219120046883184.9218967715232191200
2191200105623898.9219120002191200
2191200105563941.0219120002191200
2191200105633892.0219120002191200

まとめ

簡易的ですが、SDKの再実行回数の引き上げでSlowDownを抑えることができようになりました。

全体まとめ

今回のS3のCopyObjectによるコピーの並列実行によるコピー時間短縮の検証のまとめです。

  • AWS CLI(aws s3 cp)はオーバーヘッドが大きいため数十万以上のオブジェクトコピーには向かない
  • S3のプレフィックス単位のAPIロットリング PUT/DELETEの3500回/秒が上限の目安として、多重度を調整
  • (今回の構成では)、多重度800で、インスタンスタイプは8xlarge(32vcpu)が効率的
  • S3のスロットリング は、boto3の再実行回数引き上げで対処(プレフィクスは直ぐには効果が出ない?)

その他

検証で、インスタンスロールでなく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/ロール名"で取得できます