自作 API 改修: DynamoDB の Item 取得処理とテーブル設計を見直してレスポンス速度向上

この記事はシリーズものです。

  1. AWSを使って何か作る: API Gateway を使ってAPIを公開する
  2. 本記事

まとめ

  • DynamoDB の Item 取得処理を scan() から query() に変更
  • 検索結果を絞り込むために必要な Attribute をテーブルの Partition Key に設定

以上を行うことで、処理時間が5秒から0.5秒に下がり、レスポンス速度を向上することができた。

# 修正前
> curl -w "speed: %{time_total}\n" -o /dev/null -s https://{END_POINT}?input_text=iaa 
speed: 5.245267

# 修正後
> curl -w "speed: %{time_total}\n" -o /dev/null -s https://{END_POINT}?input_text=iaa 
speed: 0.523382

以下詳細。


開発環境

> aws --version
aws-cli/2.2.5 Python/3.8.8 Darwin/20.6.0 exe/x86_64 prompt/off

API 構成

前提としてAPIの構成をメモ。

tokizuoh.hatenablog.com

ベースは上記の通り。

現状

自作した API に対して GET リクエストを行い、レスポンスが返ってくるまで5秒かかっている。

> curl https://{END_POINT}?input_text=iaa | jq .  
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   751  100   751    0     0    142      0  0:00:05  0:00:05 --:--:--   198
[
  {
    "japanese_word": "近さ",
    "roman_alphabet": "chikasa",
    "vowels": "iaa"
  },
  {
    "japanese_word": "近間",
    "roman_alphabet": "chikama",
    "vowels": "iaa"
  },
  {
    "japanese_word": "ギター",
    "roman_alphabet": "gitaa",
    "vowels": "iaa"
  },
  ...
]

5秒は体感めちゃくちゃ遅い。レスポンス速度を向上したい。

様々なアプローチのやり方があるが、今回は DB 周りの取得処理・設計を見直して速度向上を試みる。

というのも、DynamoDB をよくわからないまま使っているので本記事で少しでも理解を深める。

今回修正する範囲

  1. Lambda → DynamoDB の Item の取得処理
  2. DynamoDBのテーブル設計

1. Lambda → DynamoDB の Item の取得処理 (1)

現在の取得処理

Lambda から DynamoDB の取得処理を見る。

dynamodb_tbl = boto3.resource('dynamodb').Table('{TABLE_NAME}')

def lambda_handler(event, context):
    # クエリ文字列の取得
    input_text = event['queryStringParameters']['input_text']

    response = dynamodb_tbl.scan()
        items = response['Items']
    
    # 以下、for 文でクエリ文字列を使い items の中から欲しいものだけを取得
    ...

scan() は何をしているのだろうか。ドキュメントを見る。

The Scan operation returns one or more items and item attributes by accessing every item in a table or a secondary index.
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Table.scan

テーブル内のアイテム全件取得。Lambda 内ではクエリ文字列を使って条件に沿った Item だけを取得することが目的のため、冗長な利用となっている。

scan() 以外で効率的な取得処理を探すと、query() があるのでドキュメントを見る。

You must provide the name of the partition key attribute and a single value for that attribute. Query returns all items with that partition key value. Optionally, you can provide a sort key attribute and use a comparison operator to refine the search results.
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Table.query

「Partition Key 属性の名前とその属性の単一の値を指定する必要があります。 クエリは、その Partition Key 値を持つすべてのアイテムを返します。 オプションで、Sort Key 属性を指定し、比較演算子を使用して検索結果を絞り込むことができます。」

query() は Partition Key, Sort Key を利用して検索結果を絞り込む。明らかに全件取得より効率が良さそう。現在のテーブルの構成で Partition Key, Sort Key が設定されているか確認する。

2. DynamoDBのテーブル設計

テーブル設計を見る前にどういう Item がテーブルに入っているか確認。

{
    "Items": [
        {
            "japanese_word": {
                "S": ""
            },
            "roman_alphabet": {
                "S": "shou"
            },
            "vowels": {
                "S": "ou"
            },
            "uuid4": {
                "S": "a65b5329-493a-4ef6-9867-9679fdc8fecd"
            }
        },
        ...
    ]
}

現在のdynamoDBのテーブル構成

> aws dynamodb describe-table \                                        
--table-name {TABLE_NAME}
        "TableName": "{TABLE_NAME}",
        "KeySchema": [
            {
                "AttributeName": "uuid4",
                "KeyType": "HASH"
            }
        ],
        ...
    }
}

現在利用しているテーブルには AttributeName が uuid4, KeyType が HASH のものが設定されている。HASH は Partition Keyのことを指す。*1

query() を使うには意味のある Attribute を Partition Key か Sort Key に設定する必要がある。そのため現在のテーブル構成では目的が叶えられない。

APIの目的はクエリ文字列と同じ母音(vowels)を持つ単語のリストを返すことのため、vowels を Partition Key か Sort Key に設定する必要がある。結果として、以下の考えから Partition Key: vowels, Sort Key: uuid4 を設定してテーブルを作り直す。

  • ❌ Partition Key: uuid4, Sort Key: None (現状の構成)
    • query() のために Partition Key に vowels を設定したいので目的が叶えれない
  • ❌ Partition Key: vowels, Sort Key: None
    • APIの目的に並び替えは現状必要ないため Sort Key は不要、としたいところだが、DynamoDBの仕様上、Partition Key のみを含むテーブルでは、2つの項目が同じ Partition Key 値を持つことはできない。*2 そのため、重複する vowels を Partition Key に設定するだけでは不適。
  • ✅ Partition Key: vowels, Sort Key: uuid4
    • 上記の不適要素を解決できる。
    • Sort Key が uuid のためソート不可能だが、Key として人間が使わないので一旦OKとする。Partion Key と Sort Key の組み合わせで一意になれば良いので Sort Key はユニークであれば何でも良さそう。今回は uuid4 にした。

GUIポチポチしてテーブルを作り直した。(ここのあたりコード化したい)

1. Lambda → DynamoDB の Item の取得処理 (2)

テーブル設計を変更して query() を使えるようになったので lambda を修正する。

dynamodb_tbl = boto3.resource('dynamodb').Table('{TABLE_NAME}')

def lambda_handler(event, context):
    # クエリ文字列の取得
    input_text = event['queryStringParameters']['input_text']

    # response = dynamodb_tbl.scan()
    response = dynamodb_tbl.query(
        KeyConditionExpression=Key('vowels').eq(input_text)    
    )

実行すると、Time Totalが表示されなくなった。体感めちゃくちゃ早くなった。

> curl https://{END_POINT}?input_text\=iaa | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   751  100   751    0     0   1449      0 --:--:-- --:--:-- --:--:--  1449
[
  {
    "japanese_word": "いばら",
    "roman_alphabet": "ibara",
    "vowels": "iaa"
  },
  {
    "japanese_word": "イサカ",
    "roman_alphabet": "isaka",
    "vowels": "iaa"
  },
  ...
]

数値でどれくらいレスポンス速度が向上したか確認するために curl -w を使って total_speed を明示的に表示するようにしたところ、以下の結果となった。

# 修正前
> curl -w "speed: %{time_total}\n" -o /dev/null -s https://{END_POINT}?input_text=iaa 
speed: 5.245267

# 修正後
> curl -w "speed: %{time_total}\n" -o /dev/null -s https://{END_POINT}?input_text=iaa 
speed: 0.523382

めちゃくちゃ早くなってる!

処理時間が5秒から0.5秒に下がり、レスポンス速度を向上することができた。

参考