[新機能] S3 Express One Zoneのパフォーマンスをテストしてみた

# はじめに
2023 re:inventで新しいS3ストレージクラスとして"**S3 Exspress one zone**"が発表されました。

このストレージクラスは一貫した 1 桁ミリ秒のデータ アクセスを提供することを目的として構築された、高性能の単一アベイラビリティーゾーンのストレージクラスです。
公式ドキュメントによると、S3 Express One Zone は、S3 Standard と比較してデータ アクセス速度を最大 10 倍向上させたとのことです。

https://aws.amazon.com/jp/s3/storage-classes/express-one-zone/

そのため今回はS3 Express one zoneのパフォーマンステストを実施し、どういったユースケースで活用ができるのかを検討してみました。


# 検証
## 構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/b5e60185-6962-d6d2-3faf-525da6c1dc9c.png)
- S3 StandardバケットとExpress one zoneバケットを用意する
- 実行環境はCloud9で用意
    - Express one zoneと同じAZになるように配置
    - インスタンスはm5.12xlargeを選択
- 各S3へのアクセスはVPCエンドポイント(GW型)を経由させる
- EC2のロールとして以下を設定
    - AWSCloud9SSMInstanceProfile (Cloud9用のマネージドポリシー)
    - 以下のカスタムポリシー

        ```json:S3 policy
        {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "VisualEditor0",
                    "Effect": "Allow",
                    "Action": [
                        "s3:PutObject",
                        "s3:GetObject"
                    ],
                    "Resource": "*"
                },
                {
                    "Sid": "AllowCreateSession",
                    "Effect": "Allow",
                    "Action": "s3express:CreateSession",
                    "Resource": "*"
                }
            ]
        }

        ```
        ※ S3 Express one zoneにアクセスするには "s3express:CreateSession"の許可が必要になります。

## 条件
- Python SDK (boto3 v1.33.11)を使用してPutObject, GetObjectの実行時間を計測する
- ファイルサイズは1 ~ 100,000,000 byteまで10倍区切りで変化させる
- それぞれ10回ずつ実行し、その平均値を比較する

## 検証コード
<details><summary>main.py</summary><div>

```python:main.py
import boto3
import requests
import random
import string
import time
import csv
import os

import analyse

def put_object_to_s3(bucket_name, key, text, bucket_type):
    
    s3 = set_client(bucket_type)
        
    try:
        start_time = time.time()
        
        s3.put_object(
            Body = text,
            Bucket = bucket_name,
            Key = key
        )
        
        end_time = time.time()
        
        execution_time = end_time - start_time
        
    except Exception as e:
        print(f"An error is occured: {e}")
    else:
        print(f"Bucket : {bucket_name}, Key : {key} is uploaded")
        return execution_time
        
def get_object_from_s3(bucket_name, key, bucket_type):
    
    s3 = set_client(bucket_type)
        
    try:
        start_time = time.time()
        
        s3.get_object(
            Bucket = bucket_name,
            Key = key
        )
        
        end_time = time.time()
        execution_time = end_time - start_time

    except Exception as e:
        print(f"An error is occured: {e}")
    else:
        print(f"Bucket : {bucket_name}, Key : {key} is downloaded")
        return execution_time
        
        
def set_client(bucket_type):
    if bucket_type == "STANDARD":
        s3 = boto3.client('s3')
    elif bucket_type == "EXPRESS":
        # Expressの時はZoneEndPointを指す必要あり
        s3 = boto3.client('s3', 
            region_name = "ap-northeast-1", 
            endpoint_url = 'https://s3express-apne1-az4.ap-northeast-1.amazonaws.com'
        )
    return s3
    
def generate_random_text(size):
    # ランダムなASCII文字列を生成して返す
    return ''.join(random.choices(string.ascii_letters + string.digits, k=size))
    
    
def create_new_csv(file_name):
    if not os.path.isfile(file_name):
        with open(file_name, 'w', newline='') as file:
            writer = csv.writer(file)
            writer.writerow(['Put Exec Time', 'Get Exec Time'])
            
def write_result(bucket_type, text_size, put_exec_time, get_exec_time):
    file_name = f"result/{bucket_type}_{text_size}.csv" 
    create_new_csv(file_name)
    
    data_to_write = [put_exec_time, get_exec_time]
    
    with open(file_name, 'a', newline='') as file:
        writer = csv.writer(file)
        writer.writerow(data_to_write)

if __name__ == '__main__':
    buckets = [
        {
            "name": "s3-standard-valification-bucket",
            "type": "STANDARD"
        },
        {
            "name": "s3-express-one-zone-bucket--apne1-az4--x-s3",
            "type": "EXPRESS"
        }
    ]
    
    start_size = 1
    count = 9

    for i in range(count):
        text_size = start_size * (10 ** i)
        text = generate_random_text(text_size)
        
        for bucket in buckets:
            for j in range(10):
                put_exec_time = put_object_to_s3(bucket["name"], f"{text_size}.txt", text, bucket["type"])
                get_exec_time = get_object_from_s3(bucket["name"], f"{text_size}.txt", bucket["type"])
            
                write_result(bucket["type"], text_size, put_exec_time, get_exec_time)
    
    
    # analyse
    analyse.exec(start_size, count)
```
</div></details>

<details><summary>analyse.py</summary><div>

```python:analyse.py
import csv
import os

def get_average(file_name):
    columns = {}
    
    with open(file_name, newline='') as csvfile:
        reader = csv.reader(csvfile)
        header = next(reader) 
    
        for col_name in header:
            columns[col_name] = []
    
        for row in reader:
            for col_name, value in zip(header, row):
                columns[col_name].append(float(value)) 

    # 列ごとの平均を計算
    column_averages = {}
    for col_name, values in columns.items():
        column_averages[col_name] = sum(values) / len(values) if values else 0
    
    return column_averages["Put Exec Time"], column_averages["Get Exec Time"]
    
def create_csv_header(start_size, count):
    header = []
    for i in range(count):
        size = str(start_size * (10 ** i))
        header.append(size)
    
    return header
    

def write_result(bucket_type, method, header, data_list):
    file_name = f"result/{bucket_type}_{method}.csv"
    create_new_csv(file_name, header)
    
    with open(file_name, 'a', newline='') as file:
        writer = csv.writer(file)
        writer.writerow(data_list)
    

def create_new_csv(file_name, header):
    if not os.path.isfile(file_name):
        with open(file_name, 'w', newline='') as file:
            writer = csv.writer(file)
            writer.writerow(header)


def exec(start_size, count):
    bucket_types = ["STANDARD", "EXPRESS"]
    header = create_csv_header(start_size, count)
        
    for bucket_type in bucket_types:
        put_ave_time_list = []
        get_ave_time_list = []
        
        for i in range(count):
            text_size = start_size * (10 ** i)
            print(text_size)
            file_name = f"result/{bucket_type}_{text_size}.csv"
            
            put_ave_time, get_ave_time = get_average(file_name)
            put_ave_time_list.append(put_ave_time)
            get_ave_time_list.append(get_ave_time)
        
        write_result(bucket_type, "put", header, put_ave_time_list)
        write_result(bucket_type, "get", header, get_ave_time_list)
    
```


</div></details>

**実装上の注意点**
- S3 Express one zoneにリクエストを送るときにはZoneエンドポイントに対して実行することが必要
- 2023/12/10 のboto3の公式ページにはcreate sessionが必要との記載があるが、get_object, put_objectのメソッドを実行したときに、裏で勝手にやってくれる仕様になっていたため、自力でセッションを取得しなくてOK（自力で取得して、セッションIDを持たせてしまうとうまくいかなくなります）

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/get_object.html

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/put_object.html


# 結果
## Get Object
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/69f41dc2-5d09-efe6-db88-1c6558530807.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/f3c41b26-f1b3-b920-daaa-4af30eda1c6c.png)


- 小規模(100B)ではStandardの方が速いものの、中～大規模(1KB以上)になるにつれてExpress one Zoneの方が実行速度が速くなった
- 最大で68.0% 速度が向上した
- 特に10 KB ～ 1MBは顕著に速度が高かった

## Put Object
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/16515c93-674b-9044-31df-332eb24c995f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/4514fba3-efee-4cb3-3190-b4a0807fa9a6.png)
※1GBの値が非常に大きいためグラフからは外しています。

- 10B ～ 100MBでStandardよりExpress one zoneの方が実行速度が上がった
- 最大で77.7%速度が向上した


# まとめ
オブジェクトサイズによらず、S3 Express one zoneによって書き込み、読み込み速度は上がる傾向にあることがわかりました。
公式ドキュメントにもある通り、機械学習と人工知能のトレーニングやデータ分析でのユースケースで力を発揮するとのことで、膨大なS3のデータを扱う際には大きな効果を発揮しそうです。本パフォーマンス結果を踏まえると、S3上の10 KB ～ 1MB程度のファイルを多数ダウンロードするときに効果が最大化されるかもしれないことがわかりました。

# おまけ
S3 Express one zoneの性能はこちらの記事がわかりやすかったです。
Express one zoneはStandardと比較して可用性が低くなっているので、止まってはいけない処理に対しては利用しないほうがよさそうですね。

https://dev.classmethod.jp/articles/amazon-s3-one-zone-storage-class-compare/