はじめに
DynamoDBのプロビジョンドキャパシティについて、割り当てることのできるキャパシティはスケジューリングに基づいてスケールすることができるものの、
- 1日のキャパシティ増減回数に制限がある
- その時々の負荷によって柔軟にキャパシティを増減してくれない(増やす・減らすのに少し時間がかかる)
といった性質があり、基本的には山あり谷ありのプロビジョンドキャパシティグラフにならないようにデータアクセス設計やそもそものテーブル設計をすべきです。例えば、下記のような割り当てになってしまうと、必要以上にキャパシティを割り当ててしまっている状況である可能性が高いです。
今回は、このデータアクセス設計について、バッチ処理を修正してキャパシティ消費のグラフを平滑化して、プロビジョンドキャパシティをよりコスト減になるように割り当てることを目的に実施した施策を備忘として残します。
具体例と対処検討
現状把握
まずは現状把握が必要です。DynamoDBの読込・書込キャパシティがどの時間帯に集中しているかを確認します。今回の例では以下のグラフに対して検討を実施します。
今回は、過去3日の推移をできる範囲で分析します。目立つところで、
- 大きい凸が1日に2回ほどある・・・①
- 中ぐらいの凸が1日に6~7回ほどある・・・②
- 小さい凸が1日1時間おき程度にある・・・③
- 中程度の台形の凸が1日に1回ある・・・④
といったところでしょうか。スケジューリングされたプロビジョンドキャパシティもこの消費パターンに応じて設定されていそうです。
グラフにおける山の特定
規則的な時間でキャパシティ消費が増加しているので、該当時間のEC2cronやEventBridge処理を探します。
処理の確認と対処検討
- for文で回してプライマリキーを用いてレコードを特定するような処理
- レコードをQueryで一気に取得する処理
は場合によっては、キャパシティ消費が一気に跳ね上がってしまうので、処理を小さなバッチに分割し、各バッチの間に遅延を入れることで、キャパシティ消費のスパイクを避けることができます。例えば、AWS SDKを用いたQueryのコードについて、以下のように実装すれば、ホットパーティションを避けることができます。実行時間とのトレードオフとなるので、時間がかかる処理を許容できるかを検討します。
import { config } from "../config.mjs";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({ region: config.aws.region });
const docClient = DynamoDBDocumentClient.from(client);
/**
* @param {string} type - タイプ
* @param {string} subId - サブID
* @param {number} [limit=100] - 1回のクエリで取得する最大項目数
* @param {number} [delay=10000] - クエリの間隔(ミリ秒)
* @returns {Promise<Array>} 取得したレコードの配列
*/
const getItemByTypeSubid = async (type, subId, limit = 100, delay = 10000) => {
let items = [];
let lastEvaluatedKey;
const params = {
TableName: xxxxx,
KeyConditionExpression: "#type = :type AND #sub_id = :sub_id",
ExpressionAttributeNames: { "#type": "type", "#sub_id": "sub_id" },
ExpressionAttributeValues: { ":type": type, ":sub_id": subId },
Limit: limit,
};
do {
// lastEvaluatedKeyが存在する場合のみExclusiveStartKeyを設定
if (lastEvaluatedKey) {
params.ExclusiveStartKey = lastEvaluatedKey;
}
const { Items, LastEvaluatedKey } = await docClient.send(new QueryCommand(params));
items.push(...Items);
lastEvaluatedKey = LastEvaluatedKey;
await new Promise((resolve) => setTimeout(resolve, delay));
} while (lastEvaluatedKey);
return items;
};
実施結果
今回の例で、実際どうなったかを最後に紹介して終わりにします。
オレンジ部分の処理に先ほど紹介したコードを適用した結果は以下の通りです。実行時間は5分程度となりましたが、キャパシティ消費がスパイクするのを避けることができました。これを他のスパイクしている処理に適用すれば、適切なスケジューリングでプロビジョンドキャパシティを割り当てることが可能です。
終わりに
今後は、他のスパイクしている処理に本施策を適用して、グラフを平滑化した上で、プロビジョンドキャパシティの割当量やスケジューリングを再検討したいと思います。
コメント