こんにちは!コロプラバックエンドエンジニアのおかごうです。
皆さんは、S3 を使っていますか?
コロプラではゲームのアセットのストレージとして使っており、S3 に大量のアセットをアップロードしています。ファイルを S3 上に転送する際は AWS CLI の AWS S3 Sync を利用しています。S3 Sync (以後 sync) は rsync と比べて処理速度が遅い(※)ため、S3 の利用に躊躇している方も居るのじゃないでしょうか?
※ rsync はファイルリストを生成して転送ファイルを判定しているが、sync はそのような制御をしていないため
本記事では sync を高速化して使うコツをご紹介します。
TL;DR
- 除外フィルターを利用せずに除外する
- 除外フィルター内も処理が走るため
- 転送の必要のないディレクトリ (.git など) は明示的に除外すると良い
- 並列処理を活用する
- つまりこんな感じ
$ls -a .git android-assets ios-assets $aws configure set default.s3.max_concurrent_requests 100 $aws configure set default.s3.max_queue_size 10000 $cmdList=( "aws s3 sync ./android-assets/ s3://your-bucket/android-assets/;" "aws s3 sync ./ios-assets/ s3://your-bucket/ios-assets/;" ) # ※ GNU 版 xargs $echo "${cmdList[@]}" | xargs -d ';' -I {} -r -P2 sh -c "{}"
背景的な話
弊社における S3 の利用用途
コロプラではゲームのアセットのストレージとして利用しています。
アセットとは画像や3Dモデルなどのゲーム内に用いるリソースのことです。
弊社で sync を実行する際の、特筆すべき特徴をあげると以下になります。
- 既存のファイルが大量にある
- 運用タイトルだと 数万 ~ 数百万ファイルに及ぶ
- 抜け漏れなく正しく転送する必要がある
- ファイル容量は比較的小さい (数百KB程度)
- 一度の実行では、数十 ~ 数百ファイルを実際に転送している
問題点と高速化の結果
長年手がつけられていない問題として、sync速度が遅いことがありました。開発中に待ち時間が多く発生し、イテレーションサイクルに影響を与えていました。しかし、今回ご紹介するコツを適用することで、sync 速度を約50%高速化することができました。
まずはネットに落ちている情報を調べてみる
高速化について調べると大体以下の記事がヒットします。
気になる点が二つあるので詳しく説明します。
除外フィルタ
まずは、公式のQ&Aを見てみましょう。
気になる文言があることがわかります。
注: 除外フィルターと一致フィルターを使用する場合でも、sync コマンドはソースバケット内のすべてのファイルを確認します 。
ここから、「除外フィルターで指定したファイル群が速度に影響を与えていそう」という仮説が生まれます。
並列処理
次に、クラスメソッドの記事を見てみましょう。
要約すると以下のようになります。
- 8KBの容量を持つ9999個のファイルを転送する
- ファイル数が多い場合のパフォーマンスの検証
- AWS CLI S3 Configuration による並列処理設定を変えて実験を行う
- 対象
- max_concurrent_requests
- max_queue_size
- デフォルトの10倍だと性能改善がみられる
- CPU負荷が大きくあがる
- ロードアベレージも 2 ~ 3
- デフォルトの100倍だと性能改善はみられない
- 対象
上記の実験ではファイル全てに転送を行っていますが、弊社のユースケースだと一度の実行で実際に転送するファイルは少ないです。
ここから、「実際に転送が行われる数が少ない場合、CPU負荷/ロードアベレージに余裕がありそう」という仮説が生まれます。
挙動を確かめる
準備
ということで、検証をして挙動を確かめて行きます。
今回の検証では、hoge fuga という2つのディレクトリに8KBのファイルを9999個を作成します。
また、AWS CLI S3 Configurationによる並列化設定に関してはデフォルトの10倍としておきます。
$cd your-work-dir ## 8KBのファイルを9999個あるディレクトリを二つ用意 $mkdir -p hoge seq -f './hoge/file_%04g.txt' 9999 | xargs -I {} sh -c 'dd if=/dev/zero of={} bs=1K count=8' &>/dev/null $mkdir -p fuga seq -f './fuga/file_%04g.txt' 9999 | xargs -I {} sh -c 'dd if=/dev/zero of={} bs=1K count=8' &>/dev/null ## AWS CLI S3 Configurationによる並列化設定は、デフォの10倍としておく $aws --profile configure set s3.max_concurrent_requests 100 $aws --profile configure set s3.max_queue_size 10000 ## 確認 $du -sh ./* 79M ./fuga 79M ./hoge
除外フィルタの確認
仮説: 「除外フィルターで指定したファイル群が速度に影響を与えてそう」
timeコマンドを用いて、以下の条件で処理時間を比べてみます。
① 転送を全く行わない場合
② 転送を全く行わない ∩ 除外フィルターを利用する場合
③ 転送を全く行わない ∩ 除外フィルターを利用せずに除外する場合
## 事前に転送しておく $aws s3 sync ./ s3://test-bucket/ --quiet ## ① 転送を全く行わない場合 $time aws s3 sync ./ s3://test-bucket/ --quiet real 0m8.665s user 0m5.677s sys 0m1.141s ## ② 転送を全く行わない ∩ 除外フィルターを利用する場合 $time aws s3 sync ./ s3://test-bucket/ --exclude "fuga*" --quiet real 0m11.167s user 0m8.240s sys 0m1.133s ## ③ 転送を全く行わない ∩ 除外フィルターを利用せずに除外する場合 $time aws s3 sync ./hoge/ s3://test-bucket/hoge --quiet real 0m4.772s user 0m3.273s sys 0m0.637s
③の除外フィルターを利用せずに除外する場合は処理時間が他の半分ほどになっています。
ここから、転送に必要のないディレクトリ (.git など) は、 除外フィルターを利用しないで除外するのが良いことがわかります。
並列処理の確認
仮説: 「実際に転送が行われる数が少ない場合、CPU負荷/ロードアベレージに余裕がありそう」
topコマンドを用いて、以下の条件でCPU負荷/ロードアベレージを見てみます。
① 全てのファイルを転送する場合
② 転送を全く行わない場合
## ① 全てのファイルを転送する場合 $aws s3 rm s3://test-bucket/ --recursive --quiet $aws s3 sync ./ s3://test-bucket/ ## 別窓で実行 $top top - 20:34:19 up 19 days, 7:10, 11 users, load average: 1.75, 0.80, 0.51 Tasks: 380 total, 1 running, 378 sleeping, 1 stopped, 0 zombie %Cpu(s): 11.8 us, 4.3 sy, 0.0 ni, 83.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 61677624 total, 6229832 free, 3335264 used, 52112528 buff/cache KiB Swap: 0 total, 0 free, 0 used. 57804768 avail Mem ## ② 転送を全く行わない場合 $aws s3 sync ./ s3://test-bucket/ ## 別窓で実行 $top top - 19:57:58 up 19 days, 6:34, 11 users, load average: 0.22, 0.35, 0.46 Tasks: 380 total, 2 running, 378 sleeping, 0 stopped, 0 zombie %Cpu(s): 7.6 us, 1.7 sy, 0.0 ni, 90.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 61677624 total, 6353516 free, 3291424 used, 52032684 buff/cache KiB Swap: 0 total, 0 free, 0 used. 57844860 avail Mem
①と②を比べると、転送を全く行わない場合はCPU負荷/ロードアベレージへの影響が約半分になっています。
また実験を行っている環境ではまだまだ余裕があるので、プロセスも並列化してみます。
$cmdList=( "aws s3 sync ./hoge/ s3://test-bucket/hoge/ --quiet;" "aws s3 sync ./fuga/ s3://test-bucket/fuga/ --quiet;" ) # ※ GNU 版 xargs $echo "${cmdList[@]}" | xargs -d ';' -I {} -r -P2 sh -c "{}" ## 別窓で実行 $top top - 19:53:45 up 19 days, 6:29, 11 users, load average: 0.58, 0.45, 0.51 Tasks: 385 total, 3 running, 382 sleeping, 0 stopped, 0 zombie %Cpu(s): 14.3 us, 3.3 sy, 0.0 ni, 82.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 61677624 total, 6294024 free, 3357632 used, 52025968 buff/cache KiB Swap: 0 total, 0 free, 0 used. 57778656 avail Mem
おおよそプロセス数に比例してCPU負荷/ロードアベレージが上がっています。
ここから、プロセスの並列化も積極的に行えそうなことがわかります。
まとめ
ということで、「除外フィルターを利用せずに除外する」および「並列処理を活用する」ことが高速化に有効であることを紹介しました。
ただし並列処理を行う場合は、環境に合わせた形で調整しつつご利用いただければと思います。
どこかの誰かのお役に立てば光栄です。
ColoplTechについて
コロプラでは、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。
connpassおよびTwitterで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
また、コロプラではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!
興味を持っていただいた方はぜひお気軽にご連絡ください。