AppSyncが捌ける最大リクエスト数を増やすために実践したこと
弊社では、Amplifyで構築したAppSyncとDynamoDBを使用しているプロジェクトがあります。
AppSyncでは、実装によっては「全然リクエストを捌いてくれない、、」といった困った状況に陥ることもあるので、実装方法には注意が必要です。
実装を工夫することによって、「とても多くのリクエスト捌いてくれる」ようにもなります。
なので、AppSync利用者にとって、次の知識は必須だと思います。
- AppSyncが1秒間で捌けるリクエスト数がどうやって決まるか
- 最大リクエスト数を増やすための対策
これらについて、この記事で解説しますので、ぜひご覧ください。
AppSyncの1秒あたりの最大リクエスト数の求め方
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を作成できます。
type Todo @model {
id: ID!
name: String!
status: Int! @index(name: "byStatus")
description: String
}
AWSマネジメントコンソールでは、テーブルのインデックスタブから作成できます。

このようにして、極力クエリを使ってデータを取得することで、AppSyncへのリクエスト数を減らしリクエストトークンの消費量を減らすことができるようになります。
2. AppSync経由をやめて直接DynamoDBからデータを取得する
APIによっては、データ取得のリクエストを複数回AppSyncに送信せざるをえないものもありました。
対策として、AppSync経由ではなく直接DynamoDBにリクエストを投げるようにしました。
こうすることで、データ取得のために消費されるリクエストトークンを0にすることができます。
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 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のコストが跳ね上がる」ということも考えられるので、その点はご注意ください。
Discussion