
S3 の Multi-Region Access Points でクロスリージョン参照性能が改善するか試してみた
はじめに
前回の記事では、Lambda Web Functions を複数リージョンに展開した構成を扱いました。バージニアの S3 を CloudFront 経由で参照するときの遅延には、Origin Shield で対処しました。
Origin Shield は、キャッシュヒット率の向上とネットワーク性能の改善で遅延を抑える対策です。今回は根本対策の候補として、Amazon S3 Multi-Region Access Points(以下、MRAP)を評価しました。
バージニアの S3 を東京(ap-northeast-1)、オレゴン(us-west-2)、フランクフルト(eu-central-1)へレプリケーションして MRAP を作りました。4リージョンの Lambda Event Function(従来の Lambda)から取得する時間を比較しています。比較したのは、S3 API と CloudFront 経由のそれぞれで、取得先を S3 単一バケットにした場合と MRAP にした場合です。更新がレプリカに届くまでの時間と、追加費用の見積もりもあわせて紹介します。
MRAP の仕組み
MRAP は、複数リージョンのバケットをまとめた1つのグローバルなエンドポイントです。リクエストは AWS Global Accelerator を通って AWS のネットワークに入り、ネットワーク遅延に基づいて最寄りのバケットへ振り分けられます。バケット間のデータは、S3 のクロスリージョンレプリケーションで別途そろえます。
利用にあたって押さえておく点は次の3つです。
- リクエストには SigV4A 署名が必要です。
- MRAP のポリシーと各バケットのバケットポリシーの両方で許可されたときにアクセスできます。
- MRAP に含めるバケットは作成後に追加・変更・削除できなくなります。
構築した構成
us-east-1、ap-northeast-1、us-west-2、eu-central-1 の4リージョンにバージョニングを有効にしたバケットを作り、us-east-1 から他の3つへレプリケーションルールを設定しました。次に、us-east-1 のバケットに前回と同じ200ファイル(gzip 圧縮、128〜714バイト)をコピーし、完了から約23秒後に確認すると、3つのバケットすべてにそろっていました。
MRAP は us-west-2 の s3control API で作ります。作成は非同期で、READY になるまで数分かかりました。
MRAP の作成コマンド(mrap-create.json)
aws s3control create-multi-region-access-point \
--region us-west-2 \
--account-id <ACCOUNT_ID> \
--client-token <トークン> \
--details file://mrap-create.json
{
"Name": "oss-mrap-verify",
"PublicAccessBlock": {"BlockPublicAcls": true, "IgnorePublicAcls": true, "BlockPublicPolicy": true, "RestrictPublicBuckets": true},
"Regions": [
{"Bucket": "oss-mrap-us-east-1-<SUFFIX>"},
{"Bucket": "oss-mrap-ap-northeast-1-<SUFFIX>"},
{"Bucket": "oss-mrap-us-west-2-<SUFFIX>"},
{"Bucket": "oss-mrap-eu-central-1-<SUFFIX>"}
]
}
Lambda から MRAP を読む
Node.js 22 ランタイムに同梱の AWS SDK(v3)では SigV4A の実装を読み込めず、そのままでは MRAP から取得できませんでした。@aws-sdk/client-s3 と @aws-sdk/signature-v4a をアプリに同梱しました。コードの先頭で @aws-sdk/signature-v4a を import すると取得できました。
呼び出しは通常の GetObjectCommand で、Bucket にバケット名の代わりに MRAP の ARN を渡します。<MRAP_ALIAS> には、aws s3control get-multi-region-access-point が返す Alias の値(末尾の .mrap を含む)を入れます。
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import "@aws-sdk/signature-v4a";
const s3 = new S3Client({ region: "us-east-1" });
const response = await s3.send(new GetObjectCommand({
Bucket: "arn:aws:s3::<ACCOUNT_ID>:accesspoint/<MRAP_ALIAS>",
Key: "path/to/object"
}));
Lambda の実行ロールには、MRAP の arn:aws:s3::<ACCOUNT_ID>:accesspoint/<MRAP_ALIAS>/object/* と4つのバケットの /* に対する s3:GetObject を許可しました。
CloudFront から MRAP を読む
CloudFront のオリジンに MRAP を指定するときは、S3 オリジンではなく Custom Origin として設定します。ドメイン名は <MRAP_ALIAS>.accesspoint.s3-global.amazonaws.com です。
OAC(Origin Access Control)は、署名プロトコルを sigv4a にして作ります。OAC の種類は、オリジンが Custom Origin でも s3 を指定します。
{
"Name": "oss-mrap-sigv4a-oac",
"SigningProtocol": "sigv4a",
"SigningBehavior": "always",
"OriginAccessControlOriginType": "s3"
}
オリジンの設定
{
"Id": "MRAP-oss-mrap",
"DomainName": "<MRAP_ALIAS>.accesspoint.s3-global.amazonaws.com",
"OriginAccessControlId": "<OAC_ID>",
"CustomOriginConfig": {
"HTTPPort": 80,
"HTTPSPort": 443,
"OriginProtocolPolicy": "https-only",
"OriginSslProtocols": {"Quantity": 1, "Items": ["TLSv1.2"]}
}
}
4つのバケットのバケットポリシーと MRAP のポリシーの両方で、CloudFront のサービスプリンシパルに s3:GetObject を許可しました。いずれも aws:SourceArn でこのディストリビューションに限定しました。
バケットポリシー(4バケット共通)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontViaMRAP",
"Effect": "Allow",
"Principal": {"Service": "cloudfront.amazonaws.com"},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<BUCKET>/*",
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:cloudfront::<ACCOUNT_ID>:distribution/<DIST_ID>"
}
}
}
]
}
キャッシュポリシーなどその他の設定は、前回の S3 オリジンのディストリビューションと同じです。
計測方法
計測は前回と同じ方法です。4リージョンに置いた Event Function(Node.js 22、512MB)が、200ファイルを1件ずつ順に取得し、取得ごとの時間を記録します。8ラウンド(各リージョン・各経路1,600件)を実行しました。
CloudFront 経由はラウンドごとにクエリ文字列 k を変え、エッジにキャッシュがない状態から取得しました。Origin Shield は使っていません。S3 API は AWS SDK for JavaScript v3 で取得しました。S3 単一バケットの場合は us-east-1 のバケット名を、MRAP の場合は MRAP の ARN を Bucket に指定しました。
計測用 Event Function のコード(index.mjs)
// event: { mode: "cf" | "s3" | "mrap", base, bucket, prefix, k, files: [..] }
// cf: https://<ディストリビューション>/<prefix><file>?k=<k> を順に取得する
// s3: bucket を S3 API で直接取得する
// mrap: bucket に MRAP の ARN を渡して S3 API(SigV4A)で取得する
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import "@aws-sdk/signature-v4a";
const now = () => performance.now();
export const handler = async (event) => {
const rows = [];
if (event.mode === "s3" || event.mode === "mrap") {
const s3 = new S3Client({ region: "us-east-1" });
const get = async (key) => {
const r = await s3.send(new GetObjectCommand({ Bucket: event.bucket, Key: key }));
return (await r.Body.transformToByteArray()).length;
};
await get(event.prefix + event.files[0]); // 接続の確立(計測に含めない)
for (const f of event.files) {
const t0 = now();
let status = 200, bytes = 0;
try { bytes = await get(event.prefix + f); } catch (e) { status = e.$metadata?.httpStatusCode ?? 0; }
const t = now() - t0;
rows.push([f, status, +t.toFixed(1), +t.toFixed(1), "", "", bytes]);
}
} else {
const url = (f, k) => `${event.base}/${event.prefix}${f}?k=${k}`;
try { const w = await fetch(url(event.files[0], "warm-" + event.k)); await w.arrayBuffer(); } catch (e) {}
for (const f of event.files) {
const t0 = now();
let status = 0, ttfb = 0, xc = "", pop = "", bytes = 0;
try {
const r = await fetch(url(f, event.k), { signal: AbortSignal.timeout(10000) });
ttfb = now() - t0;
status = r.status; xc = r.headers.get("x-cache") ?? ""; pop = r.headers.get("x-amz-cf-pop") ?? "";
bytes = (await r.arrayBuffer()).byteLength;
} catch (e) { status = -1; xc = String(e.name); }
const t = now() - t0;
rows.push([f, status, +ttfb.toFixed(1), +t.toFixed(1), xc, pop, bytes]);
}
}
return { region: process.env.AWS_REGION, mode: event.mode, k: event.k, columns: ["file", "status", "ttfb_ms", "total_ms", "x_cache", "pop", "bytes"], rows };
};
計測結果
単位は ms です。
S3 API で直接取得
p50
| 呼び出し元 | S3 単一バケット | MRAP | 短縮率(%) |
|---|---|---|---|
| ap-northeast-1 | 173.6 | 30.0 | 83 |
| eu-central-1 | 116.4 | 28.3 | 76 |
| us-west-2 | 89.8 | 37.6 | 58 |
| us-east-1 | 28.6 | 29.8 | − |
p95
| 呼び出し元 | S3 単一バケット | MRAP | 短縮率(%) |
|---|---|---|---|
| ap-northeast-1 | 193.3 | 35.6 | 82 |
| eu-central-1 | 129.7 | 34.3 | 74 |
| us-west-2 | 105.0 | 49.9 | 52 |
| us-east-1 | 42.1 | 44.4 | − |
S3 単一バケットでは us-east-1 から離れるほど取得時間が延びましたが、MRAP ではどのリージョンからも p50 で 28〜38ms に収まりました。us-east-1 からの取得は、S3 単一バケットと MRAP でほぼ同じでした。
CloudFront 経由(エッジにキャッシュなし)
p50
| 呼び出し元 | S3 単一バケット | MRAP | 短縮率(%) |
|---|---|---|---|
| ap-northeast-1 | 492.0 | 38.2 | 92 |
| eu-central-1 | 312.0 | 37.0 | 88 |
| us-west-2 | 233.6 | 55.3 | 76 |
| us-east-1 | 44.7 | 38.7 | − |
p95
| 呼び出し元 | S3 単一バケット | MRAP | 短縮率(%) |
|---|---|---|---|
| ap-northeast-1 | 519.2 | 53.0 | 90 |
| eu-central-1 | 337.4 | 54.5 | 84 |
| us-west-2 | 270.8 | 83.6 | 69 |
| us-east-1 | 78.0 | 76.7 | − |
ap-northeast-1 では p50 で 492ms から 38ms になりました。前回の Origin Shield(p50 159.5ms)と比べても短く、MRAP を S3 API で直接取得した場合(p50 30.0ms)と同程度の値です。
更新が反映されるまでの時間
us-east-1 のバケットの同じキーを5回上書きしました。他の3バケットに新しい内容が届くまでの時間と、ap-northeast-1 の Event Function から MRAP で新しい内容が読めるまでの時間を測りました。レプリケーションは標準の設定で、S3 Replication Time Control は使っていません。
| 回 | 上書きから3バケットすべてに届くまで | 上書きから ap-northeast-1 の MRAP で読めるまで |
|---|---|---|
| 1 | 22.7秒 | 22.3秒 |
| 2 | 21.7秒 | 21.3秒 |
| 3 | 22.6秒 | 22.2秒 |
| 4 | 22.7秒 | 21.7秒 |
| 5 | 22.6秒 | 22.2秒 |
5回とも約22秒でした。この間に ap-northeast-1 から MRAP で読むと、古い内容が HTTP 200 で返りました。
追加費用の見積もり
MRAP を使うと、S3 単一バケットの構成に対して次の費用が追加になります。
- レプリケーション先のバケットへの PUT リクエスト(宛先リージョンの単価)
- リージョン間のデータ転送(1GB あたり $0.02)
- レプリカのストレージ
- MRAP のデータルーティング(1GB あたり $0.0033)
本番で対象にしている著者ページの JSON の構成を想定して計算しました(1,200ファイル、1ファイル約1KB、1日1回全ファイルを上書き、ダウンロードは1日1万回)。バージョニングの旧バージョンは削除する想定で、そのストレージ費用は含めていません。
- レプリケーション PUT:1,200件/日 × 30日 = 3.6万件/月。宛先3リージョンの単価(1,000件あたり東京 $0.0047、オレゴン $0.005、フランクフルト $0.0054)で $0.17 + $0.18 + $0.19 ≒ $0.54/月
- リージョン間のデータ転送:1.2MB × 30日 × 3リージョン ≒ 108MB → $0.002/月
- レプリカのストレージ:1.2MB × 3リージョン → $0.001 未満/月
- MRAP データルーティング:1KB × 1万回 × 30日 = 300MB → $0.001/月
合計で約 $0.54/月で、ほとんどがレプリケーション先への PUT リクエストです。
まとめ
MRAP を使うことで、S3 から離れたリージョンの Lambda でも、同一リージョンの S3 とほぼ同じ水準の参照性能が得られました。
ただ、本番で対象にしている著者ページの JSON は1,200ファイルを毎日すべて更新するため、毎日全ファイルがレプリケーションの対象になります。また、MRAP の導入で管理の複雑化が予想されるため、当面必要な性能が得られる Origin Shield の利用を継続する予定です。
MRAP は認証などの理由で CloudFront を挟まず S3 を直接参照したい場合や、大容量のファイルを Lambda や EC2 から高頻度で参照してクロスリージョン転送の課金が問題になりそうな場合に、あらためて採用を検討したいと思います。












