S3 のプレフィックスとスケーリングの仕組み
Amazon S3 のパフォーマンスについて 設計パターンのベストプラクティス: Amazon S3 のパフォーマンスの最適化 のページには
Amazon S3 は、高いリクエストレートに自動的にスケールされます。例えば、アプリケーションは、パーティショニングされた Amazon S3 プレフィックスごとに毎秒 3,500 回以上の PUT/COPY/POST/DELETE リクエストまたは 5,500 回以上の GET/HEAD リクエストを達成できます。
との記載があります。
これを読んで、S3 のプレフィックスとは何か、スケーリングはどのように行われるのかが気になったので調べてみました。
2018年7月以前はどうだったのか
現在の仕組みについて見る前に、S3 のパフォーマンスについて大幅なアップデートが行われた 2018年7月以前の仕様について参考として見てみましょう。
アップデート前のドキュメントは当然ながら AWS 公式には残っていないため、クラスメソッドさんの記事を参考にさせていただきました。
こちらの記事によると、アップデート以前は GET リクエストが 800 リクエスト/s、PUT/LIST/DELETE リクエストが 300 リクエスト/s を超える場合にはサポートへの依頼が必要だったようです。
現在は何もしなくても GET/HEAD リクエストが 5,500 リクエスト/s、PUT/POST/DELETE リクエストが 3,500 リクエスト/s までサポートされるため、以前と比べると大幅に性能が向上しています。
アップデート以前は特定のパーティションにリクエストが集中しないようにするため、キーのプレフィックスとしてハッシュ値のようなランダム文字列を追加することが推奨されていました。
現在は AWS のドキュメントからそのような記載はなくなっており、アップデート発表時のアナウンスにも
この S3 リクエストレートのパフォーマンス向上により、オブジェクトプレフィックスをランダム化することでパフォーマンスを向上させるという以前のガイダンスは不要になります。
と書かれています。
では本当にもうプレフィックスをランダム化する必要はないのでしょうか。
プレフィックスとは何か
ドキュメントには
パーティショニングされた Amazon S3 プレフィックスごとに
とあるため、この記述を正しく理解するためには、まずプレフィックスとは何かを理解する必要があります。
re:Post の Amazon S3 リクエストレートについて、プレフィックスとネストされたフォルダの違いは何ですか? S3 バケットではいくつのプレフィックスを持つことができますか? というページには
キープレフィックスは、オブジェクト名の前の完全なパスとなることができる文字列です (バケット名を含む)。例えば、オブジェクト (123.txt) は BucketName/Project/WordFiles/123.txt として保存されます。プレフィックスは「BucketName/Project/WordFiles/123.txt」である場合があります。プレフィックスは、任意の長さにすることができます (オブジェクトキー名全体を含む)。
と書かれています。
若干わかりにくい記述ですが、s3://BucketName/Project/WordFiles/123.txt
というオブジェクトのプレフィックスとしては以下のどれもあり得るということになります。
BucketName/
BucketName/Project/
BucketName/Project/Wo
BucketName/Project/Word
BucketName/Project/WordFiles/123.txt
もちろんこれら以外にも、キーの先頭から任意の長さを切り出した文字列はプレフィックスになりえます。
また、同じページには混同しやすい「フォルダ」との違いについても記載があります。
フォルダは / で区切られた部分を表しているのに対し、プレフィックスにおいては / は単なる文字の一つという扱いのようです。
つまり、プレフィックスは必ずしも / で区切られるわけではないということが重要です。
どのようにスケールされるのか
プレフィックスの意味がわかったところで、どのようにスケールが行われるのかを見ていきます。
公式ドキュメントには「リクエストレートに合わせて自動的にスケールされる」としか書かれていないですが、上記の re:Post 記事に関連情報としてリンクされていた re:Invent 2018 の講演で少し仕組みが紹介されていました。
以下、動画のスクショを引用しながら仕組みを見ていきます。
まず初期状態ではバケット全体で 3,500 PUT/s、5,500 GET/s となります。
リクエストレートが高い状態が続くと /Log
とそれ以外のプレフィックスでパーティションが分割され、それぞれで 3,500 PUT/s、5,500 GET/s となります。
このバケットには /LogFiles/
と /LogErrors/
という2つのフォルダがありますが、パーティションはこの共通部分の /Log
までで切られており、必ずしも / の部分で切られるわけではないことがわかります。
この状態でさらにリクエストレートが高い状態が続くと、より長いプレフィックスでパーティションが分割されることになります。
このパーティションの分割はユーザーの見えないところで自動的に行われます。
どのような基準でパーティションとして分割するプレフィックスを決めているのかは不明ですが、S3 が良い感じにやってくれるようです。
ただし、この分割はすぐに行われるものではなく、動画内では30~60分ほどかかる処理だと述べられていました。
この点は 設計パターンのベストプラクティス: Amazon S3 のパフォーマンスの最適化 にも記載があります。
スケーリングは瞬時にではなく段階的に行われます。Amazon S3 が新たに高くなったリクエストレートに合わせてスケーリングしている間に、503 (Slow Down) エラーが表示される場合があります。
事前に高いリクエストレートになることが分かっている場合には、事前にサポートへ依頼してパーティションの分割を行うことが可能とも動画では述べられていました。
プレフィックスを気にしないといけない具体例
具体的にどのようなときにプレフィックスを気にしないといけないのでしょうか。
ここでは例として、MyBucket
という S3 バケットに YYYYMMDD/ホスト名/ログファイル名
のようなキーで、その日のログファイルを読み書きしている場合を考えてみます。
まず、初期状態では MyBucket
バケット全体で 3,500 PUT/s、5,500 GET/s のレートになります。
■現在のパーティション
MyBucket/ => 3,500 PUT/s 5,500 GET/s
2023/12/01 に各ホストのログファイルがそれなりのリクエストレートで読み書きされ、20231201/
とそれ以外でパーティションが分割されたとします。
■現在のパーティション
MyBucket/ ┬ 20231201/ => 3,500 PUT/s 5,500 GET/s
└ ... => 3,500 PUT/s 5,500 GET/s
さらに、特定のホスト(hostA, hostB)について高いリクエストレートで読み書きされ、それぞれでパーティションが分割されました。
■現在のパーティション
MyBucket/ ┬ 20231201/ ┬ hostA/ => 3,500 PUT/s 5,500 GET/s
│ ├ hostB/ => 3,500 PUT/s 5,500 GET/s
│ └ ... => 3,500 PUT/s 5,500 GET/s
└ ... => 3,500 PUT/s 5,500 GET/s
このとき、MyBucket/20231201/
以下で見ると、合計 10,500 PUT/s 16,500 GET/s と初期状態の最大3倍のリクエスト数に耐えられるようになっています。
ここで日付が変わり、ログファイルの読み書き先が MyBucket/20231202/
以下に変わったとします。
すると、日付が変わった直後は MyBucket/20231202/
に対応するパーティションが作成されていないため、最大リクエストレートは 3,500 PUT/s 5,500 GET/s となってしまいます。
仮にこの時点でこれ以上のリクエストがあった場合には、新しくパーティションが分割されるまでの30~60分間は 503 エラーが発生してしまいます。
このように非常に高いリクエストレートが見込まれるバケットについてはプレフィックスを慎重に設計する必要がありそうです。
プレフィックスの設計はどうすべきか
まず、バケット全体で 3,500 PUT/s 5,500 GET/s に満たないワークロードであれば、プレフィックスの設計を気にする必要はなさそうです。
一方、非常に高いリクエストレートで読み書きされるバケットでプレフィックスとして日付や連番を使うと、上記の例のようにそれが変わる瞬間に性能が低下してしまいます。
これを防ぐには、日付などが変わっても分割されたパーティションが使用されるようにプレフィックスを設計する必要があります。
この方法としては2018年のアップデート以前に推奨されていた、ランダム文字列を先頭に付与することなどがあります。
例えば上記の例で、キーの先頭に 0~9 の数字1文字をランダムに追加すれば、日付が変わったとしても 0/
や 1/
というプレフィックスに対応するパーティションは利用できるため、常時10倍のリクエスト数に耐えられます。
MyBucket/ ┬ 0/ ┬ 20231201/ => 3,500 PUT/s 5,500 GET/s
│ └ ... => 3,500 PUT/s 5,500 GET/s
├ 1/ ┬ 20231201/ => 3,500 PUT/s 5,500 GET/s
│ └ ... => 3,500 PUT/s 5,500 GET/s
...
あるいは、上記の例でのホスト名のように、ある程度分散している値を日付の前に持ってくるのも、ランダム文字列ほどではないですが有効だと思います。
MyBucket/ ┬ hostA/ ┬ 20231201/ => 3,500 PUT/s 5,500 GET/s
│ └ ... => 3,500 PUT/s 5,500 GET/s
├ hostB/ ┬ 20231201/ => 3,500 PUT/s 5,500 GET/s
│ └ ... => 3,500 PUT/s 5,500 GET/s
...
ワークロードに対してどのぐらいパーティションが分割されていれば充分かを考えてプレフィックスを設計する必要があります。
まとめ
長くなってしまいましたが、ざっくりまとめると以下のようになります。
- プレフィックスとはキー名の先頭から任意の長さの部分のこと
- 必ずしも / で区切られるわけではない
- スケーリングは段階的にパーティションが分割されることで自動的に行われる
- 非常に高いリクエストレートが見込まれる場合は、ランダム文字列をキーの先頭に付与するなど、パーティションが充分に分割された状態にする工夫が必要
以上、自分なりに調べてみましたが、間違いなどがあればご指摘いただけると大変助かります。
Discussion