🎉

AppSyncが捌ける最大リクエスト数を増やすために実践したこと

に公開

弊社では、Amplifyで構築したAppSyncとDynamoDBを使用しているプロジェクトがあります。

AppSyncでは、実装によっては「全然リクエストを捌いてくれない、、」といった困った状況に陥ることもあるので、実装方法には注意が必要です。
実装を工夫することによって、「とても多くのリクエスト捌いてくれる」ようにもなります。

なので、AppSync利用者にとって、次の知識は必須だと思います。

  • AppSyncが1秒間で捌けるリクエスト数がどうやって決まるか
  • 最大リクエスト数を増やすための対策

これらについて、この記事で解説しますので、ぜひご覧ください。

AppSyncの1秒あたりの最大リクエスト数の求め方

AppSyncのサービスクォータ:
https://docs.aws.amazon.com/general/latest/gr/appsync.html#limits_appsync

AppSyncのGraphQL APIには、リクエストトークンという概念があり、「1秒あたりに消費できるリクエストトークン数」が各リージョン毎のアカウントに割り当てられています。(東京リージョンだと、1秒あたり10,000トークン)

GraphQL APIにクエリまたはミューテーションのリクエストをした時にトークンが消費され、消費量は以下の条件で決定されます。

リソース消費量が1,500 KB秒(メモリ使用量×実行時間)以下のリクエストには1トークン、1,500 KB秒を超えるリクエストには追加トークンが割り当てられる

例えば、リソース消費量が2,000 KB秒のリクエストには2トークンが割り当てられます。

つまり、「1リクエストあたり2トークン消費」するとした場合、「1秒あたりの最大リクエスト数は5,000」ということになります。
トークン消費数が少ないほど、最大リクエスト数は増えます。

実際どれだけトークンが消費されたのかを確認したかったら、レスポンスヘッダーのx-amzn-appsync-TokensConsumedで確認できるので、安心です!

自分たちのアプリの利用ユーザー数から、1秒あたりの想定リクエスト数を計算して、そこからトークン消費量をどこまで抑えればよいかを考えることが重要です。

次に、弊社が行ったトークン消費量を減らすための対策を参考までにご紹介します。

リクエストトークンの消費量を減らすために実践したこと

1. DynamoDBからクエリでデータを取得する

DynamoDBからデータを取得する方法として、スキャン(全件走査) と クエリ(索引走査)があります。

DynamoDBには、1回のリクエストで最大1MBのデータしか取得できないという制限があるため、
データ量が多いと、スキャンだと何回もAppSyncにリクエストを投げることになります。
そのため、リクエストトークンの消費量が増えてしまいます。(レイテンシも遅くなります)

データ量が多いなら、クエリを使うべきですが、プライマリキーを検索条件にしないとスキャンしてしまいます。
プライマリキー以外の属性でデータを取得したいケースも多いと思います。
この場合は、GSI(グローバルセカンダリインデックス) を使えば、プライマリキー以外の属性を検索条件にしても、クエリでデータを取得できます。

GSIとは、プライマリキーではない任意の属性でクエリを実行するためのインデックスです。

Amplifyでは、@indexでGSIを作成できます。

schemaサンプル
type Todo @model {
  id: ID!
  name: String!
  status: Int! @index(name: "byStatus")
  description: String
}

AWSマネジメントコンソールでは、テーブルのインデックスタブから作成できます。

このようにして、極力クエリを使ってデータを取得することで、AppSyncへのリクエスト数を減らしリクエストトークンの消費量を減らすことができるようになります。

2. AppSync経由をやめて直接DynamoDBからデータを取得する

APIによっては、データ取得のリクエストを複数回AppSyncに送信せざるをえないものもありました。
対策として、AppSync経由ではなく直接DynamoDBにリクエストを投げるようにしました。
こうすることで、データ取得のために消費されるリクエストトークンを0にすることができます。

pythonサンプル
import boto3
from boto3.dynamodb.types import TypeSerializer


def get_items_with_query(
    table_name,
    key_name,
    key_value,
    index_name,
):
    key_condition = f"{key_name} = :v"
    expression_attribute_values = {":v": TypeSerializer().serialize(key_value)}

    query_params = {
        "TableName": table_name,
        "KeyConditionExpression": key_condition,
        "ExpressionAttributeValues": expression_attribute_values,
        "IndexName": index_name
    }

    items = []
    done = False
    start_key = None
    while not done:
        if start_key:
            query_params["ExclusiveStartKey"] = start_key
        response = boto3.client("dynamodb").query(**query_params)
        items.extend(response.get("Items", []))
        start_key = response.get("LastEvaluatedKey", None)
        done = start_key is None

    return items

3. 複数のミューテーションを1つにまとめてリクエストする

複数回ミューテーションのリクエストを投げて、データ書き込みするAPIもあります。
2の方法と同様にして、DynamoDBに直接データ書き込みすることはできません。(Amplify DataStoreによるデータ同期のために、AppSync経由で書き込みをしないといけない)

そこで、複数のミューテーションを1つにまとめることで、データ書き込みのAppSyncリクエストを1回にするという方針をとりました。
GraphQL APIでは、以下のようにしてミューテーションを1つにまとめられます。

mutationサンプル
mutation MyMutation {
  createTodo1: createTodo(input: {id: 1, name: "sample1", _version: 1}) {
    id
    name
    _version
    createdAt
    updatedAt
  }
  createTodo2: createTodo(input: {id: 2, name: "sample2", _version: 1}) {
    id
    name
    _version
    createdAt
    updatedAt
  }
}

さいごに

消費トークン数を減らす対策として、他にも「リゾルバーを最適化する」ことも考えられます。

また、「消費トークン数を最大限減らすことができたけど、それでも、もっと多くのリクエストを捌きたい・・・」という場合は、AWSに「1秒あたりに消費できるトークン数」の上限引き上げの申請をすることもできますので、ご検討ください。

ただ、AWSの上限引き上げだけに頼って、今回紹介した対策を実施しないと、「レイテンシがとても遅くなる」、「DynamoDBのコストが跳ね上がる」ということも考えられるので、その点はご注意ください。

dotD Tech Blog

Discussion